esi 0.7.0-beta.4

A streaming parser and executor for Edge Side Includes
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
use bytes::Bytes;
use esi::{parse, parse_complete};

/// Tests to validate streaming parser behavior and the theory about delimited content
///
/// Theory to test:
/// 1. Streaming parsers return Incomplete when they need more data
/// 2. delimited() is a sequence combinator that propagates errors from its parsers
/// 3. For incomplete delimited tags (missing closing tag), streaming should return Incomplete
/// 4. parse_complete() should only be used when we KNOW we have complete input

#[test]
fn test_streaming_parse_incomplete_choose_opening() {
    // Incomplete: only the opening tag, no content or closing
    let input = b"<esi:choose>";
    let bytes = Bytes::from_static(input);

    let result = parse(&bytes);

    // Should return Incomplete because we're mid-tag (expecting content + closing)
    match result {
        Err(nom::Err::Incomplete(_)) => {
            // EXPECTED: streaming parser correctly signals it needs more data
        }
        Ok((remaining, elements)) => {
            panic!(
                "Expected Incomplete but got Ok with {} elements, remaining: {:?}",
                elements.len(),
                std::str::from_utf8(remaining)
            );
        }
        Err(e) => {
            panic!("Expected Incomplete but got error: {:?}", e);
        }
    }
}

#[test]
fn test_streaming_parse_incomplete_choose_with_partial_content() {
    // Incomplete: opening + partial content, no closing tag
    let input = b"<esi:choose>\n    <esi:when test=\"";
    let bytes = Bytes::from_static(input);

    let result = parse(&bytes);

    // With streaming parsers, incomplete input MUST return Incomplete
    match result {
        Err(nom::Err::Incomplete(_)) => {
            // EXPECTED: streaming parser correctly signals it needs more data
        }
        Err(nom::Err::Error(e)) => {
            panic!(
                "Incomplete input returned Error({:?}) instead of Incomplete. \
                This indicates a parser bug - incomplete input should return Incomplete.",
                e.code
            );
        }
        Ok((remaining, elements)) => {
            panic!(
                "Expected Incomplete but got Ok with {} elements and {} bytes remaining. \
                Incomplete input should return Incomplete, not partial results.",
                elements.len(),
                remaining.len()
            );
        }
        Err(e) => {
            panic!("Expected Incomplete but got: {:?}", e);
        }
    }
}

#[test]
fn test_streaming_parse_complete_choose() {
    // Complete choose block
    let input = b"<esi:choose>\n    <esi:when test=\"true\">content</esi:when>\n</esi:choose>";
    let bytes = Bytes::from_static(input);

    let result = parse(&bytes);

    match result {
        Ok((remaining, elements)) => {
            assert_eq!(remaining, b"", "Should consume all input");
            assert_eq!(elements.len(), 1, "Should parse one Choose element");
        }
        Err(nom::Err::Incomplete(_)) => {
            // This is also acceptable for streaming - it might want more to be sure
            // Some parsers are cautious and return Incomplete even for complete-looking input
        }
        Err(e) => {
            panic!("Expected success or Incomplete, got error: {:?}", e);
        }
    }
}

#[test]
fn test_parse_complete_vs_parse_on_incomplete_input() {
    // Incomplete input: missing closing tag
    let input = b"<esi:choose>\n    <esi:when test=\"true\">content</esi:when>";
    let bytes = Bytes::from_static(input);

    // Test with streaming parser
    let streaming_result = parse(&bytes);

    // Test with complete parser
    let complete_result = parse_complete(&bytes);

    // Streaming should return Incomplete
    assert!(
        matches!(streaming_result, Err(nom::Err::Incomplete(_))),
        "Streaming parser should return Incomplete for incomplete input, got: {:?}",
        streaming_result
            .as_ref()
            .map(|(r, e)| (r.len(), e.len()))
            .map_err(|e| format!("{:?}", e))
    );

    // Complete parser should handle it (treats Incomplete as EOF)
    match complete_result {
        Ok((_remaining, elements)) => {
            // parse_complete treats Incomplete as "done parsing"
            assert!(
                !elements.is_empty(),
                "Should parse at least partial content"
            );
        }
        Err(e) => {
            panic!("parse_complete unexpectedly failed: {:?}", e);
        }
    }
}

#[test]
fn test_delimited_propagates_incomplete() {
    // Test that delimited() correctly propagates Incomplete from inner parser
    // This validates the theory about delimited being a sequence combinator

    use nom::bytes::streaming::tag;
    use nom::error::Error;
    use nom::sequence::delimited;
    use nom::Parser;

    // Incomplete: has opening and closing tags but incomplete content in middle
    let input = b"<start>incomplete";

    // Try to parse with delimited - should get Incomplete from the closing tag parser
    let result: nom::IResult<&[u8], &[u8], Error<&[u8]>> = delimited(
        tag(&b"<start>"[..]),
        nom::bytes::streaming::take_while1(|c| c != b'<' && c != b'>'),
        tag(&b"<end>"[..]),
    )
    .parse(input);

    assert!(
        matches!(result, Err(nom::Err::Incomplete(_))),
        "delimited() should propagate Incomplete from closing tag parser, got: {:?}",
        result
    );
}

#[test]
fn test_delimited_with_parse_complete_middle() {
    // This test validates that parse_complete inside delimited() will cause
    // delimited() to return Incomplete when the closing tag is missing.
    // While the original test used nom combinators directly, we can test
    // the same concept by ensuring incomplete input returns Incomplete.

    use bytes::Bytes;

    // Test case: incomplete closing tag
    let input = Bytes::from_static(b"<esi:choose><esi:when test=\"true\">yes</esi:when>");
    //                                                               ↑ Missing </esi:choose>

    // parse() should return Incomplete because closing tag is missing
    let result = parse(&input);

    assert!(
        matches!(result, Err(nom::Err::Incomplete(_))),
        "Expected Incomplete from missing closing tag, got: {:?}",
        result
    );
}

#[test]
fn test_parse_complete_doesnt_know_boundaries() {
    // This test demonstrates that parse_complete correctly stops at ESI closing tags
    // even though it doesn't know the boundaries upfront. This works because ESI
    // closing tags are not valid content elements, so the parser naturally stops.

    let input = b"<esi:when test=\"true\">yes</esi:when></esi:choose>more content";
    //                                                   ^^^^^^^^^^^^^^
    //                                                   Not valid ESI content, parser stops here

    let bytes = Bytes::from_static(input);
    let result = parse_complete(&bytes);

    match result {
        Ok((remaining, elements)) => {
            // parse_complete should stop when it hits unrecognized syntax
            let remaining_str = std::str::from_utf8(remaining).unwrap_or("");
            assert!(
                remaining_str.starts_with("</esi:choose>"),
                "parse_complete should stop before closing tag, but remaining is: {:?}",
                remaining_str
            );
            assert!(!elements.is_empty(), "Should parse at least one element");
        }
        Err(e) => {
            panic!("parse_complete unexpectedly failed: {:?}", e);
        }
    }
}

#[test]
fn test_why_it_works_parse_fails_early() {
    // This test demonstrates why parse_complete works with delimited():
    // parse() uses streaming combinators that naturally stop at ESI closing tags
    // because they're not valid top-level content elements.

    let input = b"<esi:when test=\"true\">content</esi:when></esi:choose>";
    //                                                       ^^^^^^^^^^^^^^ This is NOT valid ESI content

    let bytes = Bytes::from_static(input);
    let streaming_result = parse(&bytes);

    match streaming_result {
        Ok((remaining, _elements)) => {
            // Streaming parse should stop when it hits unrecognized syntax
            let remaining_str = std::str::from_utf8(remaining).unwrap_or("");
            assert!(
                remaining_str.starts_with("</esi:choose>"),
                "Streaming parser should leave closing tag unparsed, but remaining is: {:?}",
                remaining_str
            );
        }
        Err(nom::Err::Incomplete(_)) => {
            // Also acceptable - parser might be cautious
        }
        Err(e) => {
            panic!("Streaming parser unexpectedly failed with error: {:?}", e);
        }
    }
}

#[test]
fn test_the_magic_sequence() {
    // This test validates that streaming parse correctly returns Incomplete
    // when parsing incomplete nested ESI tags, preventing data corruption.

    use nom::bytes::streaming::tag;
    use nom::Parser;

    let input = b"<esi:choose><esi:when test=\"true\">yes</esi:whe";
    //            ^-----------^                              ^------^
    //            Opening tag   Content                      Incomplete closing tag

    // Manually simulate what delimited() does:

    // Step 1: Opening tag
    let step1 = tag::<_, _, nom::error::Error<&[u8]>>(&b"<esi:choose>"[..]).parse(input);
    let (after_open, _) = step1.expect("Opening tag should succeed");

    // Step 2: Content with streaming parse
    let bytes2 = Bytes::copy_from_slice(after_open);
    let step2 = parse(&bytes2);

    // CRITICAL: parse() MUST return Incomplete here to prevent data corruption.
    // The <esi:when> tag is incomplete, so accepting it would corrupt data.
    assert!(
        matches!(step2, Err(nom::Err::Incomplete(_))),
        "Expected Incomplete from streaming parse on incomplete <esi:when> tag, got: {:?}",
        step2
    );
}

#[test]
fn test_parse_complete_on_actually_complete_input() {
    // parse_complete should work on actually complete input
    let input = b"<esi:include src=\"/test\"/>";
    let bytes = Bytes::from_static(input);
    let result = parse_complete(&bytes);

    match result {
        Ok((remaining, elements)) => {
            assert!(
                remaining.is_empty(),
                "Complete input should be fully consumed, but {} bytes remain",
                remaining.len()
            );
            assert!(
                !elements.is_empty(),
                "Should have parsed at least one element"
            );
        }
        Err(e) => {
            panic!("Should parse complete input successfully: {:?}", e);
        }
    }
}

#[test]
fn test_streaming_incremental_parsing() {
    // Simulate real streaming scenario: data arrives in chunks

    // Chunk 1: Opening tag only - should return Incomplete
    let chunk1 = b"<esi:choose>";
    let bytes1 = Bytes::from_static(chunk1);
    let result1 = parse(&bytes1);
    assert!(
        matches!(result1, Err(nom::Err::Incomplete(_))),
        "Opening tag only should return Incomplete"
    );

    // Chunk 2: Opening + incomplete when tag - should return Incomplete
    let chunk2 = b"<esi:choose>\n    <esi:when test=\"true\">";
    let bytes2 = Bytes::from_static(chunk2);
    let result2 = parse(&bytes2);
    assert!(
        matches!(result2, Err(nom::Err::Incomplete(_))),
        "Incomplete when tag should return Incomplete"
    );

    // Chunk 3: Complete input - should parse successfully
    let chunk3 = b"<esi:choose>\n    <esi:when test=\"true\">content</esi:when>\n</esi:choose>";
    let bytes3 = Bytes::from_static(chunk3);
    let result3 = parse(&bytes3);

    match result3 {
        Ok((remaining, elements)) => {
            assert_eq!(remaining, b"", "Complete input should be fully consumed");
            assert!(!elements.is_empty(), "Should have parsed elements");
        }
        Err(nom::Err::Incomplete(_)) => {
            // Also acceptable - streaming parser being cautious
        }
        Err(e) => {
            panic!("Complete input failed with error: {:?}", e);
        }
    }
}

#[test]
fn test_theory_parse_complete_used_for_delimited_content() {
    // This tests the theory: content inside delimited tags should use parse_complete
    // because we know the boundaries (the closing tag)

    // Simulate what esi_choose does internally:
    // It has: delimited(tag("<esi:choose>"), parse_complete, tag("</esi:choose>"))

    use nom::bytes::streaming::tag;
    use nom::sequence::delimited;
    use nom::Parser;

    // Complete content between tags
    let input: &[u8] = b"<esi:choose><esi:when test=\"true\">yes</esi:when></esi:choose>";

    // Extract just the content between the tags - use slices not arrays
    let result: nom::IResult<&[u8], &[u8], nom::error::Error<&[u8]>> = delimited(
        tag(&b"<esi:choose>"[..]),
        tag(&b"<esi:when test=\"true\">yes</esi:when>"[..]), // Simplified - just checking structure
        tag(&b"</esi:choose>"[..]),
    )
    .parse(input);

    match result {
        Ok((remaining, _content)) => {
            assert_eq!(remaining, &b""[..], "Should consume entire input");
            println!("✓ delimited correctly parses complete content");
        }
        Err(e) => {
            panic!("delimited failed on complete content: {:?}", e);
        }
    }
}

#[test]
fn test_incomplete_vs_error() {
    // Important distinction: Incomplete means "need more data" vs Error means "invalid syntax"

    // Case 1: Incomplete - valid so far, just need more
    let incomplete = b"<esi:assign name=\"x\" value=\"";
    let bytes1 = Bytes::from_static(incomplete);
    let result = parse(&bytes1);
    assert!(
        matches!(result, Err(nom::Err::Incomplete(_))),
        "Incomplete input should return Incomplete, got: {:?}",
        result
    );

    // Case 2: Invalid syntax - might be treated as HTML/text (not an error)
    let invalid = b"<esi:invalid:tag:name>";
    let bytes2 = Bytes::from_static(invalid);
    let result2 = parse(&bytes2);
    // Invalid ESI tags might be treated as HTML, which is valid behavior
    assert!(
        matches!(
            result2,
            Ok(_) | Err(nom::Err::Error(_)) | Err(nom::Err::Incomplete(_))
        ),
        "Invalid ESI syntax should be handled gracefully"
    );
}

#[test]
fn test_all_incomplete_tag_cutoff_positions() {
    // Comprehensive test for all positions where streaming input could be cut off
    // This ensures the parser returns Incomplete (not Error) for all partial valid inputs

    let test_cases = vec![
        // Cut off in tag name
        ("<", "Just opening bracket"),
        ("<e", "Partial tag name 'e'"),
        ("<esi", "Partial tag name 'esi'"),
        ("<esi:", "Tag name with colon"),
        ("<esi:inc", "Partial 'include'"),
        ("<esi:include", "Complete tag name, no closing"),
        // Cut off in attributes
        ("<esi:include ", "After tag name with space"),
        ("<esi:include s", "Partial attribute name"),
        ("<esi:include src", "Complete attribute name, no ="),
        ("<esi:include src=", "After equals, no quote"),
        ("<esi:include src=\"", "After opening quote"),
        ("<esi:include src=\"/path", "Partial attribute value"),
        (
            "<esi:include src=\"/path/to/file",
            "Complete value, no closing quote",
        ),
        (
            "<esi:include src=\"/path/to/file\"",
            "After closing quote, no >",
        ),
        // Self-closing tag variants
        ("<esi:include src=\"/file\"/", "Self-closing, no >"),
        // Cut off in closing tags
        ("<esi:choose></", "Closing tag start"),
        ("<esi:choose></e", "Partial closing tag name"),
        ("<esi:choose></esi:", "Closing tag with colon"),
        ("<esi:choose></esi:cho", "Partial closing tag 'choose'"),
        (
            "<esi:choose></esi:choose",
            "Complete closing tag name, no >",
        ),
        // Other ESI tags
        ("<esi:assign", "Assign tag incomplete"),
        ("<esi:assign name", "Assign with partial attribute"),
        ("<esi:assign name=", "Assign with attribute name and ="),
        ("<esi:assign name=\"", "Assign with attribute value started"),
        (
            "<esi:assign name=\"x\"",
            "Assign with one attribute, no closing",
        ),
        (
            "<esi:assign name=\"x\" value",
            "Assign with second attribute partial",
        ),
        (
            "<esi:assign name=\"x\" value=",
            "Assign with second attribute =",
        ),
        (
            "<esi:assign name=\"x\" value=\"",
            "Assign with second value started",
        ),
        // When tag with test attribute
        ("<esi:when", "When tag incomplete"),
        ("<esi:when test", "When with attribute name"),
        ("<esi:when test=", "When with ="),
        ("<esi:when test=\"", "When with test value started"),
        ("<esi:when test=\"$(HTTP", "When with partial expression"),
        // Try/Attempt/Except tags
        ("<esi:try", "Try tag incomplete"),
        ("<esi:attempt", "Attempt tag incomplete"),
        ("<esi:except", "Except tag incomplete"),
        // Comment and remove
        ("<esi:comment", "Comment tag incomplete"),
        ("<esi:remove", "Remove tag incomplete"),
        // Vars tag
        ("<esi:vars", "Vars tag incomplete"),
        ("$(", "Expression start"),
        ("$(HTTP", "Partial expression"),
        ("$(HTTP_", "Partial variable name"),
    ];

    for (input, description) in test_cases {
        let bytes = Bytes::copy_from_slice(input.as_bytes());
        let result = parse(&bytes);
        assert!(
            matches!(result, Err(nom::Err::Incomplete(_))),
            "Test case '{}' ({}): Expected Incomplete, got: {:?}",
            input,
            description,
            result
        );
    }
}

#[test]
fn test_incomplete_nested_tags() {
    // Test incomplete input with nested tags at various positions

    let test_cases = vec![
        // Choose with incomplete when
        ("<esi:choose><esi:when", "Choose with partial when tag"),
        ("<esi:choose><esi:when test", "Choose with when missing ="),
        (
            "<esi:choose><esi:when test=",
            "Choose with when missing quote",
        ),
        (
            "<esi:choose><esi:when test=\"true",
            "Choose with when missing closing quote",
        ),
        (
            "<esi:choose><esi:when test=\"true\"",
            "Choose with when missing >",
        ),
        (
            "<esi:choose><esi:when test=\"true\">",
            "Choose with when tag open, no content",
        ),
        (
            "<esi:choose><esi:when test=\"true\">content",
            "Choose with when content, no closing tag",
        ),
        (
            "<esi:choose><esi:when test=\"true\">content</esi:when",
            "Choose with when partial closing tag",
        ),
        (
            "<esi:choose><esi:when test=\"true\">content</esi:when>",
            "Choose with complete when, no otherwise/closing",
        ),
        (
            "<esi:choose><esi:when test=\"true\">yes</esi:when><esi:otherwise",
            "Choose with otherwise partial",
        ),
        // Try with incomplete attempt/except
        ("<esi:try><esi:attempt", "Try with partial attempt"),
        (
            "<esi:try><esi:attempt>",
            "Try with attempt open, no content",
        ),
        (
            "<esi:try><esi:attempt>content",
            "Try with attempt content, no closing",
        ),
        (
            "<esi:try><esi:attempt>content</esi:attempt",
            "Try with attempt partial closing",
        ),
        (
            "<esi:try><esi:attempt>content</esi:attempt><esi:except",
            "Try with except partial",
        ),
        // Comment with incomplete content
        ("<esi:comment text", "Comment with partial attribute"),
        (
            "<esi:comment text=\"",
            "Comment with attribute value started",
        ),
        // Remove with incomplete content
        ("<esi:remove>", "Remove tag open, no content"),
        ("<esi:remove>content", "Remove with content, no closing"),
        (
            "<esi:remove>content</esi:remove",
            "Remove with partial closing tag",
        ),
    ];

    for (input, description) in test_cases {
        let bytes = Bytes::copy_from_slice(input.as_bytes());
        let result = parse(&bytes);
        assert!(
            matches!(result, Err(nom::Err::Incomplete(_))),
            "Test case '{}' ({}): Expected Incomplete, got: {:?}",
            input,
            description,
            result
        );
    }
}

#[test]
fn test_incomplete_with_whitespace_and_newlines() {
    // Test that incomplete tags inside delimited tags work correctly even with whitespace

    let test_cases = vec![
        // These all have the incomplete tag inside a delimited context (choose)
        // so they should return Incomplete
        ("<esi:choose>\n", "Choose with newline, no content"),
        ("<esi:choose>\n  ", "Choose with newline and spaces"),
        (
            "<esi:choose>\n  <esi:when",
            "Choose with formatted partial when",
        ),
        (
            "<esi:choose>\n  <esi:when test=\"true\">\n    ",
            "Choose with when and content whitespace",
        ),
    ];

    for (input, description) in test_cases {
        let bytes = Bytes::copy_from_slice(input.as_bytes());
        let result = parse(&bytes);
        assert!(
            matches!(result, Err(nom::Err::Incomplete(_))),
            "Test case '{}' ({}): Expected Incomplete, got: {:?}",
            input,
            description,
            result
        );
    }

    // Leading whitespace is actually valid content, so these parse the whitespace as Text
    // and leave the incomplete tag for the next parse call. This is correct streaming behavior.
    let whitespace_cases = vec![
        ("  <esi:include", "Leading whitespace with partial tag"),
        ("\n\n<esi:assign", "Leading newlines with partial tag"),
    ];

    for (input, description) in whitespace_cases {
        let bytes = Bytes::copy_from_slice(input.as_bytes());
        let result = parse(&bytes);
        // Should either return Incomplete OR return Ok with the whitespace parsed as Text
        // and the incomplete tag remaining
        match result {
            Err(nom::Err::Incomplete(_)) => {
                // This is fine - parser detected incomplete tag
            }
            Ok((remaining, elements)) => {
                // Also fine - parser consumed whitespace as Text, incomplete tag is in remaining
                assert!(
                    !elements.is_empty() && !remaining.is_empty(),
                    "Test case '{}' ({}): If Ok, should have parsed Text and have remaining incomplete tag",
                    input,
                    description
                );
            }
            other => {
                panic!(
                    "Test case '{}' ({}): Expected Incomplete or Ok with partial parse, got: {:?}",
                    input, description, other
                );
            }
        }
    }
}

#[test]
fn test_incomplete_html_and_script_tags() {
    // Test incomplete HTML tags and script tags
    //
    // Important distinctions:
    // - <script> tags REQUIRE closing tags - incomplete script returns Incomplete
    // - Other HTML tags like <div> are treated as complete (parsed as Text, not Html)
    // - Partial tags (missing >) always return Incomplete

    let test_cases = vec![
        // Partial HTML opening tags (no closing >)
        ("<div", "Partial div tag"),
        ("<div class", "Div with partial attribute"),
        ("<div class=", "Div with attribute equals"),
        ("<div class=\"", "Div with attribute value start"),
        ("<div class=\"container", "Div with partial attribute value"),
        (
            "<div class=\"container\"",
            "Div with complete attribute, no >",
        ),
        // Partial HTML closing tags (no closing >)
        ("</div", "Partial closing div tag"),
        ("</di", "Partial closing tag name"),
        ("</", "Closing tag start only"),
        // Script tags - MUST have closing </script> tag
        ("<script", "Partial script opening tag"),
        ("<script>", "Script opening tag, REQUIRES closing"),
        (
            "<script>console.log('test')",
            "Script with content, no closing tag",
        ),
        (
            "<script>console.log('test')</script",
            "Script with partial closing tag",
        ),
        (
            "<script>console.log('test')</scri",
            "Script with partial closing tag name",
        ),
        (
            "<script>console.log('test')</scr",
            "Script with partial closing tag",
        ),
        (
            "<script>console.log('test')</sc",
            "Script with partial closing tag",
        ),
        (
            "<script>console.log('test')</s",
            "Script with partial closing tag",
        ),
        (
            "<script>console.log('test')<",
            "Script with closing tag start",
        ),
        // Script tags with attributes
        ("<script type", "Script with partial attribute"),
        ("<script type=", "Script with attribute equals"),
        ("<script type=\"", "Script with attribute value start"),
        (
            "<script type=\"text/javascript",
            "Script with partial attribute value",
        ),
        (
            "<script type=\"text/javascript\"",
            "Script with complete attribute, no >",
        ),
        (
            "<script type=\"text/javascript\">",
            "Script with attribute, REQUIRES closing",
        ),
        (
            "<script type=\"text/javascript\">code",
            "Script with attribute and partial content",
        ),
        // Self-closing HTML tags (no closing >)
        ("<br", "Partial br tag"),
        ("<br/", "Self-closing br, no >"),
        ("<img src", "Img with partial attribute"),
        ("<img src=\"", "Img with attribute value start"),
        (
            "<img src=\"/path/to/image.jpg",
            "Img with partial attribute value",
        ),
        (
            "<img src=\"/path/to/image.jpg\"",
            "Img with complete attribute, no >",
        ),
        ("<img src=\"/path/to/image.jpg\"/", "Img self-closing, no >"),
    ];

    for (input, description) in test_cases {
        let bytes = Bytes::copy_from_slice(input.as_bytes());
        let result = parse(&bytes);
        assert!(
            matches!(result, Err(nom::Err::Incomplete(_))),
            "Test case '{}' ({}): Expected Incomplete, got: {:?}",
            input,
            description,
            result
        );
    }

    // Note about non-script HTML tags:
    // Tags like "<div>" without closing are treated as complete and parsed as Text,
    // not as Html elements. This is correct behavior - the parser treats unknown
    // or unclosed non-script tags as text content rather than HTML structure.
}

#[test]
fn test_streaming_handles_incomplete_attributes() {
    // This test validates that streaming parsers correctly handle incomplete attribute values.
    // With the fix to parse_delimited, incomplete attributes now return Incomplete correctly.

    use nom::bytes::streaming::{is_not, tag};
    use nom::sequence::delimited;
    use nom::Parser;

    // Test input: opening quote, some content, but NO closing quote
    let input: &[u8] = b"\"incomplete_attribute_value";

    // Test 1: is_not() alone should return Incomplete
    let content_only = &input[1..]; // Skip the opening quote
    let is_not_result = is_not::<_, _, nom::error::Error<&[u8]>>(&b"\""[..]).parse(content_only);
    assert!(
        matches!(is_not_result, Err(nom::Err::Incomplete(_))),
        "is_not() should return Incomplete when it doesn't find the delimiter"
    );

    // Test 2: delimited() should also return Incomplete
    let delimited_result: nom::IResult<&[u8], &[u8], nom::error::Error<&[u8]>> =
        delimited(tag(&b"\""[..]), is_not(&b"\""[..]), tag(&b"\""[..])).parse(input);
    assert!(
        matches!(delimited_result, Err(nom::Err::Incomplete(_))),
        "delimited() should return Incomplete for missing closing delimiter, got: {:?}",
        delimited_result
    );

    // Test 3: With complete input, parsing should succeed
    let complete_input: &[u8] = b"\"incomplete_attribute_value\"";
    let retry_result: nom::IResult<&[u8], &[u8], nom::error::Error<&[u8]>> =
        delimited(tag(&b"\""[..]), is_not(&b"\""[..]), tag(&b"\""[..])).parse(complete_input);
    assert!(retry_result.is_ok(), "Should succeed with complete input");

    // Test 4: ESI parser with incomplete attribute should return Incomplete
    let esi_input: &[u8] = b"<esi:choose>\n    <esi:when test=\"";
    let esi_bytes = Bytes::copy_from_slice(esi_input);
    let esi_result = parse(&esi_bytes);
    assert!(
        matches!(esi_result, Err(nom::Err::Incomplete(_))),
        "ESI parser should return Incomplete for incomplete attribute, got: {:?}",
        esi_result
    );
}

// ============================================================================
// STREAMING PARSER IMPLEMENTATION SUMMARY
// ============================================================================
//
// ARCHITECTURE:
//
// The ESI parser uses a proper streaming architecture with two key functions:
//
// 1. parse() - Streaming document parser
//    - Uses nom::bytes::streaming combinators
//    - Returns Incomplete when it needs more data
//    - Used for top-level document parsing
//
// 2. parse_delimited() - Streaming content parser for delimited tags
//    - Used inside delimited() for tags like <esi:choose>...</esi:choose>
//    - Calls parse() in a loop to process content
//    - ALWAYS propagates Incomplete when encountered
//    - Prevents data corruption by never accepting incomplete nested tags
//
// HOW IT WORKS:
//
// For delimited tags like <esi:choose>, the structure is:
//   delimited(tag("<esi:choose>"), parse_delimited, tag("</esi:choose>"))
//
// Flow with incomplete input like "<esi:choose><esi:when test=\"":
// 1. Opening tag matches successfully
// 2. parse_delimited is called on the content
// 3. parse_delimited calls parse() which detects incomplete <esi:when> tag
// 4. parse() returns Incomplete
// 5. parse_delimited PROPAGATES Incomplete (critical fix!)
// 6. delimited() propagates Incomplete to caller
// 7. No data corruption - incomplete tags are never accepted
//
// THE FIX:
//
// Previously, parse_delimited would return Ok with partial results when it
// encountered Incomplete after parsing some content. This caused delimited()
// to try matching the closing tag on unconsumed input, which failed with Error.
//
// The fix: parse_delimited now ALWAYS propagates Incomplete. In streaming mode,
// we can't know if incomplete data is the closing tag or more content, so we
// must always ask for more data.
//
// WHY parse() NATURALLY RESPECTS BOUNDARIES:
//
// Input: b"<esi:when...>content</esi:when></esi:choose>"
//
// When parse() encounters "</esi:choose>":
// - It's NOT a valid top-level ESI tag
// - Parser naturally stops and leaves it unparsed
// - This creates an implicit boundary that delimited() can use
// - ESI grammar is unambiguous, so this works reliably
//
// ============================================================================
// TEST COVERAGE:
//
// These tests validate the streaming parser implementation:
//
// 1. test_streaming_parse_incomplete_choose_opening
//    - Validates: Incomplete opening tag returns Incomplete
//
// 2. test_streaming_parse_incomplete_choose_with_partial_content
//    - Validates: Incomplete nested tags return Incomplete (prevents corruption)
//    - This was the bug that got fixed!
//
// 3. test_streaming_parse_complete_choose
//    - Validates: Complete input parses successfully
//
// 4. test_parse_complete_vs_parse_on_incomplete_input
//    - Validates: Distinction between parse() and parse_complete()
//    - parse() returns Incomplete, parse_complete() treats it as EOF
//
// 5. test_delimited_propagates_incomplete
//    - Validates: delimited() propagates Incomplete from missing closing tag
//
// 6. test_the_magic_sequence
//    - Validates: parse_delimited correctly detects incomplete nested tags
//
// 7. test_streaming_incremental_parsing
//    - Validates: Progressive data chunks work correctly
//
// 8. test_streaming_handles_incomplete_attributes
//    - Validates: Incomplete attributes return Incomplete correctly
//
// 9. test_incomplete_vs_error
//    - Validates: Distinction between Incomplete and Error
//
// 10. test_why_it_works_parse_fails_early
//     - Validates: Parser naturally stops at ESI closing tags
//
// 11. test_parse_complete_doesnt_know_boundaries
//     - Validates: parse_complete stops at ESI closing tags
//
// 12. test_delimited_with_parse_complete_middle
//     - Historical test for old pattern (now uses parse_delimited)
//
// 13. test_theory_parse_complete_used_for_delimited_content
//     - Validates: delimited() structure works correctly
//
// 14. test_parse_complete_on_actually_complete_input
//     - Validates: parse_complete() handles complete input correctly
//
// ============================================================================
// STREAMING BEST PRACTICES:
//
// For applications using this parser:
//
// 1. Buffer input when parse() returns Incomplete
// 2. Read more data from the stream
// 3. Combine buffered + new data and retry
// 4. Only treat as error after timeout or max buffer size
//
// The parser correctly returns Incomplete for all cases where more data
// might make the input valid. This prevents data corruption from incomplete
// ESI tags being accepted as complete.
//
// ============================================================================