opentui_rust 0.2.1

High-performance terminal UI rendering engine with alpha blending and diffed buffers
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
//! E2E tests for full render cycle with frame validation.
//!
//! Tests complete render cycle from initialization through multiple frames to cleanup.
//! Verifies diff rendering, ANSI output correctness, and frame sequence behavior.

#![allow(clippy::uninlined_format_args)] // Clarity over style in test code

mod common;

use common::harness::E2EHarness;
use common::mock_terminal::MockTerminal;
use opentui::ansi::AnsiWriter;
use opentui::buffer::{BoxStyle, OptimizedBuffer};
use opentui::grapheme_pool::GraphemePool;
use opentui::renderer::{BufferDiff, Renderer, RendererOptions};
use opentui::style::TextAttributes;
use opentui::{Cell, Rgba, Style};
use opentui_rust as opentui;
use std::io::Write;

/// Test basic render cycle: init -> draw -> present -> modify -> present -> cleanup.
#[test]
fn test_e2e_basic_render_cycle() {
    let mut harness = E2EHarness::new("render_cycle", "basic_cycle", 40, 10);

    harness
        .log()
        .info("init", "Starting basic render cycle test");

    // Step 1: Initialize buffers (simulating Renderer)
    let mut front_buffer = OptimizedBuffer::new(40, 10);
    let mut back_buffer = OptimizedBuffer::new(40, 10);

    harness.log().info("init", "Buffers initialized: 40x10");

    // Step 2: Draw initial content to back buffer
    back_buffer.draw_text(0, 0, "Hello, OpenTUI!", Style::fg(Rgba::GREEN));
    back_buffer.draw_text(0, 1, "Frame 1", Style::fg(Rgba::WHITE));

    harness
        .log()
        .info("draw", "Drew initial content to back buffer");

    // Step 3: Compute diff and verify first frame behavior
    let diff1 = BufferDiff::compute(&front_buffer, &back_buffer);

    harness.log().info(
        "diff",
        format!("First frame diff: {} cells changed", diff1.change_count),
    );

    // First frame should have changes (back buffer has content, front is empty)
    assert!(diff1.change_count > 0, "First frame should have changes");
    assert!(
        !diff1.changed_cells.is_empty(),
        "Changed cells list should not be empty"
    );

    // Verify "Hello, OpenTUI!" is in the changed region (15 characters at row 0)
    let row0_changes = diff1
        .changed_cells
        .iter()
        .filter(|(_, y)| matches!(*y, 0))
        .count();
    assert!(
        row0_changes >= 15,
        "Row 0 should have at least 15 changed cells for 'Hello, OpenTUI!'"
    );

    // Step 4: Swap buffers (simulate present)
    std::mem::swap(&mut front_buffer, &mut back_buffer);
    back_buffer.clear(Rgba::TRANSPARENT);

    // Redraw to back buffer for next frame
    back_buffer.draw_text(0, 0, "Hello, OpenTUI!", Style::fg(Rgba::GREEN));
    back_buffer.draw_text(0, 1, "Frame 2", Style::fg(Rgba::WHITE)); // Changed content

    harness
        .log()
        .info("draw", "Drew Frame 2 content (modified)");

    // Step 5: Compute diff for second frame
    let diff2 = BufferDiff::compute(&front_buffer, &back_buffer);

    harness.log().info(
        "diff",
        format!("Second frame diff: {} cells changed", diff2.change_count),
    );

    // Second frame should only have changes where content differs ("Frame 1" -> "Frame 2")
    // The "Hello, OpenTUI!" line should be unchanged
    assert!(
        diff2.change_count < diff1.change_count,
        "Second frame should have fewer changes than first (only 'Frame 1' -> 'Frame 2')"
    );

    // Verify row 0 has no changes (content unchanged)
    let row0_changes2 = diff2.changed_cells.iter().any(|(_, y)| matches!(*y, 0));
    assert!(
        !row0_changes2,
        "Row 0 should have no changes (content unchanged)"
    );

    // Verify row 1 has changes
    let row1_changes = diff2.changed_cells.iter().any(|(_, y)| matches!(*y, 1));
    assert!(
        row1_changes,
        "Row 1 should have changes (Frame 1 -> Frame 2)"
    );

    harness.dump_buffer("final_state");
    harness.finish(true);
    eprintln!("[TEST] PASS: E2E basic render cycle works");
}

/// Test layered gradient UI with hit testing across layers.
#[test]
#[allow(clippy::too_many_lines)] // E2E test logs each step; clarity over brevity.
#[allow(clippy::cast_precision_loss)] // f32 lerp requires casts; dimensions are small in tests.
fn test_e2e_layered_gradient_hit_test() {
    let width = 40;
    let height = 12;
    let mut harness = E2EHarness::new("render_cycle", "layered_gradient_hit_test", width, height);

    harness
        .log()
        .info("init", "Starting layered gradient + hit-test test");

    let mut renderer = Renderer::new_with_options(
        width,
        height,
        RendererOptions {
            use_alt_screen: false,
            hide_cursor: false,
            enable_mouse: false,
            query_capabilities: false,
        },
    )
    .expect("Renderer creation should succeed with options disabled");

    let top = Rgba::from_hex("#1a1a2e").unwrap_or(Rgba::BLACK);
    let bottom = Rgba::from_hex("#16213e").unwrap_or(Rgba::BLACK);

    harness
        .log()
        .info("render", "Layer 0: vertical gradient background");

    renderer.render_to_layer(0, |buf| {
        let denom = height.saturating_sub(1).max(1) as f32;
        for y in 0..height {
            let t = y as f32 / denom;
            let color = top.lerp(bottom, t);
            buf.fill_rect(0, y, width, 1, color);
        }
    });
    renderer.register_hit_area(0, 0, width, height, 0);

    let panel_x = 6;
    let panel_y = 3;
    let panel_w = 28;
    let panel_h = 6;
    let panel_base = Rgba::from_hex("#0f3460")
        .unwrap_or(Rgba::BLACK)
        .with_alpha(0.85);

    harness
        .log()
        .info("render", "Layer 1: translucent panel + border");

    renderer.render_to_layer(1, |buf| {
        buf.fill_rect(panel_x, panel_y, panel_w, panel_h, panel_base);
        buf.draw_box(
            panel_x,
            panel_y,
            panel_w,
            panel_h,
            BoxStyle::double(Style::fg(Rgba::WHITE)),
        );
        buf.draw_text(panel_x + 2, panel_y + 2, "Panel", Style::fg(Rgba::WHITE));
    });
    renderer.register_hit_area(panel_x, panel_y, panel_w, panel_h, 1);

    let stats_before = renderer.stats().clone();
    harness
        .log()
        .info("present", "Merging layers and presenting frame");
    renderer.present().expect("present should succeed");

    let stats_after = renderer.stats().clone();
    harness.log().info(
        "stats",
        format!(
            "frames {} -> {}, last_frame_cells={}",
            stats_before.frames, stats_after.frames, stats_after.last_frame_cells
        ),
    );

    let bg_hit = renderer.hit_test(1, 1);
    let panel_hit = renderer.hit_test(panel_x + 1, panel_y + 1);
    harness.log().info(
        "hit_test",
        format!("bg_hit={:?}, panel_hit={:?}", bg_hit, panel_hit),
    );

    assert_eq!(bg_hit, Some(0), "Background hit should resolve to layer 0");
    assert_eq!(panel_hit, Some(1), "Panel hit should resolve to layer 1");

    let front = renderer.front_buffer();
    let sample_x = panel_x + 1;
    let sample_y = panel_y + 1;
    let sample_cell = front
        .get(sample_x, sample_y)
        .unwrap_or_else(|| unreachable!("No cell at ({}, {})", sample_x, sample_y));
    let denom = height.saturating_sub(1).max(1) as f32;
    let base_color = top.lerp(bottom, sample_y as f32 / denom);
    let expected = panel_base.blend_over(base_color);

    let color_close = |a: Rgba, b: Rgba| {
        const EPS: f32 = 0.01;
        (a.r - b.r).abs() < EPS
            && (a.g - b.g).abs() < EPS
            && (a.b - b.b).abs() < EPS
            && (a.a - b.a).abs() < EPS
    };

    harness.log().info(
        "color",
        format!(
            "base={:?} overlay={:?} expected={:?} actual={:?}",
            base_color, panel_base, expected, sample_cell.bg
        ),
    );

    assert!(
        color_close(sample_cell.bg, expected),
        "Blended panel bg should match expected composite"
    );

    harness.finish(true);
    eprintln!("[TEST] PASS: E2E layered gradient + hit-test works");
}

/// Test pattern-style background with alpha overlay blending.
#[test]
fn test_e2e_pattern_blend_overlay() {
    let width = 20;
    let height = 8;
    let mut harness = E2EHarness::new("render_cycle", "pattern_blend_overlay", width, height);

    harness
        .log()
        .info("init", "Starting pattern blend overlay test");

    let mut buffer = OptimizedBuffer::new(width, height);
    let dark = Rgba::from_hex("#2d2d2d").unwrap_or(Rgba::BLACK);
    let light = Rgba::from_hex("#3a3a3a").unwrap_or(Rgba::BLACK);

    // Checkerboard pattern
    for y in 0..height {
        for x in 0..width {
            let bg = if matches!((x + y) % 2, 0) {
                dark
            } else {
                light
            };
            buffer.set(x, y, Cell::clear(bg));
        }
    }

    // Semi-transparent overlay rectangle
    let overlay = Rgba::from_hex("#ff6b6b")
        .unwrap_or(Rgba::WHITE)
        .with_alpha(0.5);
    let overlay_x = 4;
    let overlay_y = 2;
    let overlay_w = 10;
    let overlay_h = 3;
    buffer.fill_rect(overlay_x, overlay_y, overlay_w, overlay_h, overlay);

    // Verify a blended sample cell in overlay region
    let sample_x = overlay_x + 1;
    let sample_y = overlay_y + 1;
    let base_bg = if matches!((sample_x + sample_y) % 2, 0) {
        dark
    } else {
        light
    };
    let expected = overlay.blend_over(base_bg);
    let sample = buffer
        .get(sample_x, sample_y)
        .unwrap_or_else(|| unreachable!("No cell at ({}, {})", sample_x, sample_y));

    let color_close = |a: Rgba, b: Rgba| {
        const EPS: f32 = 0.01;
        (a.r - b.r).abs() < EPS
            && (a.g - b.g).abs() < EPS
            && (a.b - b.b).abs() < EPS
            && (a.a - b.a).abs() < EPS
    };

    harness.log().info(
        "blend",
        format!(
            "base={:?} overlay={:?} expected={:?} actual={:?}",
            base_bg, overlay, expected, sample.bg
        ),
    );

    assert!(
        color_close(sample.bg, expected),
        "Overlay blend should match expected composite"
    );

    harness.finish(true);
    eprintln!("[TEST] PASS: E2E pattern blend overlay works");
}

/// Test that first frame outputs full buffer content.
#[test]
fn test_e2e_first_frame_full_output() {
    let mut harness = E2EHarness::new("render_cycle", "first_frame_full", 20, 5);

    harness
        .log()
        .info("init", "Testing first frame full output");

    // Create buffers
    let front_buffer = OptimizedBuffer::new(20, 5);
    let mut back_buffer = OptimizedBuffer::new(20, 5);

    // Draw some content
    back_buffer.draw_text(0, 0, "Line 1", Style::default());
    back_buffer.draw_text(0, 1, "Line 2", Style::default());
    back_buffer.draw_text(0, 2, "Line 3", Style::default());

    // Compute diff
    let diff = BufferDiff::compute(&front_buffer, &back_buffer);

    // First frame: all drawn cells should be in the diff
    let total_drawn_cells = 6 + 6 + 6; // "Line X" = 6 chars each
    assert!(
        diff.change_count >= total_drawn_cells,
        "First frame should include all drawn cells: expected >= {}, got {}",
        total_drawn_cells,
        diff.change_count
    );

    harness.log().info(
        "verify",
        format!(
            "First frame has {} changes (expected >= {})",
            diff.change_count, total_drawn_cells
        ),
    );

    harness.finish(true);
    eprintln!("[TEST] PASS: E2E first frame full output works");
}

/// Test that subsequent frames output only diffs.
#[test]
fn test_e2e_subsequent_frames_diff_only() {
    let mut harness = E2EHarness::new("render_cycle", "diff_only", 30, 5);

    harness
        .log()
        .info("init", "Testing subsequent frames diff-only output");

    // Create initial state
    let mut front_buffer = OptimizedBuffer::new(30, 5);
    let mut back_buffer = OptimizedBuffer::new(30, 5);

    // Fill both buffers with same content initially
    front_buffer.draw_text(0, 0, "Static content here", Style::default());
    front_buffer.draw_text(0, 1, "Counter: 0", Style::default());

    back_buffer.draw_text(0, 0, "Static content here", Style::default());
    back_buffer.draw_text(0, 1, "Counter: 1", Style::default()); // Only this changes

    // Compute diff
    let diff = BufferDiff::compute(&front_buffer, &back_buffer);

    harness.log().info(
        "diff",
        format!("Diff has {} changed cells", diff.change_count),
    );

    // Only the counter digit should change (position 9 on row 1: '0' -> '1')
    assert!(
        diff.change_count <= 3,
        "Only counter digit should change: expected <= 3, got {}",
        diff.change_count
    );

    // Verify static content row has no changes
    assert!(
        !diff.changed_cells.iter().any(|(_, y)| matches!(*y, 0)),
        "Static row should have no changes"
    );

    harness.finish(true);
    eprintln!("[TEST] PASS: E2E subsequent frames diff-only works");
}

/// Test force redraw outputs full buffer.
#[test]
fn test_e2e_force_redraw_full_output() {
    let mut harness = E2EHarness::new("render_cycle", "force_redraw", 20, 5);

    harness
        .log()
        .info("init", "Testing force redraw full output");

    // Create identical buffers
    let mut front_buffer = OptimizedBuffer::new(20, 5);
    let mut back_buffer = OptimizedBuffer::new(20, 5);

    let content = "Same content";
    front_buffer.draw_text(0, 0, content, Style::default());
    back_buffer.draw_text(0, 0, content, Style::default());

    // Normal diff should show no changes
    let diff = BufferDiff::compute(&front_buffer, &back_buffer);
    assert_eq!(
        diff.change_count, 0,
        "Identical buffers should have no diff"
    );

    harness.log().info("verify", "Identical buffers: no diff");

    // Force redraw simulation: treat all cells as changed
    let total_cells = 20 * 5;
    let force_diff = BufferDiff {
        changed_cells: (0..20u32)
            .flat_map(|x| (0..5u32).map(move |y| (x, y)))
            .collect(),
        dirty_regions: vec![],
        change_count: total_cells,
    };

    assert_eq!(
        force_diff.change_count, total_cells,
        "Force redraw should include all {} cells",
        total_cells
    );

    harness.log().info(
        "verify",
        format!("Force redraw: {} cells", force_diff.change_count),
    );

    harness.finish(true);
    eprintln!("[TEST] PASS: E2E force redraw full output works");
}

/// Test clear + draw outputs correctly.
#[test]
fn test_e2e_clear_and_draw() {
    let mut harness = E2EHarness::new("render_cycle", "clear_draw", 20, 5);

    harness.log().info("init", "Testing clear + draw sequence");

    // Initial state with content
    let mut front_buffer = OptimizedBuffer::new(20, 5);
    front_buffer.draw_text(0, 0, "Old content", Style::default());

    // Back buffer after clear + new draw
    let mut back_buffer = OptimizedBuffer::new(20, 5);
    back_buffer.clear(Rgba::BLACK);
    back_buffer.draw_text(0, 0, "New content", Style::fg(Rgba::RED));

    // Compute diff
    let diff = BufferDiff::compute(&front_buffer, &back_buffer);

    harness.log().info(
        "diff",
        format!("Clear + draw diff: {} cells changed", diff.change_count),
    );

    // Should detect changes where old content was cleared and new content drawn
    assert!(diff.change_count > 0, "Clear + draw should produce changes");

    // Verify first cell is different
    assert!(
        diff.changed_cells.iter().any(|(_, y)| matches!(*y, 0)),
        "Row 0 should have changes after clear + draw"
    );

    harness.finish(true);
    eprintln!("[TEST] PASS: E2E clear and draw works");
}

/// Test ANSI cursor positioning sequences.
#[test]
fn test_e2e_ansi_cursor_positioning() {
    let mut harness = E2EHarness::new("render_cycle", "cursor_positioning", 40, 10);

    harness
        .log()
        .info("init", "Testing ANSI cursor positioning");

    // Capture ANSI output
    let mut output: Vec<u8> = Vec::new();
    {
        let mut writer = AnsiWriter::new(&mut output);

        // Move cursor to various positions
        writer.move_cursor(0, 0);
        writer.move_cursor(5, 10);
        writer.move_cursor(9, 39);

        writer.flush().unwrap();
    }

    let output_str = String::from_utf8_lossy(&output);

    harness
        .log()
        .info("ansi", format!("Output length: {} bytes", output.len()));

    // Parse and verify sequences
    let mut mock = MockTerminal::new(40, 10);
    mock.write_all(&output).unwrap();

    let cursor_moves = mock.cursor_moves();

    harness
        .log()
        .info("verify", format!("Cursor moves: {:?}", cursor_moves));

    // Should have cursor position sequences
    assert!(
        output_str.contains("\x1b["),
        "Output should contain CSI sequences"
    );

    harness.finish(true);
    eprintln!("[TEST] PASS: E2E ANSI cursor positioning works");
}

/// Test ANSI color sequences match drawn colors.
#[test]
fn test_e2e_ansi_color_sequences() {
    let mut harness = E2EHarness::new("render_cycle", "color_sequences", 40, 10);

    harness.log().info("init", "Testing ANSI color sequences");

    // Create a cell with specific colors using builder
    let style = Style::builder().fg(Rgba::RED).bg(Rgba::BLUE).build();
    let cell = opentui::cell::Cell::new('X', style);

    // Capture ANSI output
    let mut output: Vec<u8> = Vec::new();
    {
        let mut writer = AnsiWriter::new(&mut output);
        writer.move_cursor(0, 0);
        writer.write_cell(&cell);
        writer.flush().unwrap();
    }

    let output_str = String::from_utf8_lossy(&output);

    harness.log().info(
        "ansi",
        format!("Output: {}", output_str.replace('\x1b', "ESC")),
    );

    // Verify red foreground (255, 0, 0)
    assert!(
        output_str.contains("\x1b[38;2;255;0;0m"),
        "Output should contain red foreground color sequence"
    );

    // Verify blue background (0, 0, 255)
    assert!(
        output_str.contains("\x1b[48;2;0;0;255m"),
        "Output should contain blue background color sequence"
    );

    // Verify the character 'X' is present
    assert!(
        output_str.contains('X'),
        "Output should contain the cell character"
    );

    harness.finish(true);
    eprintln!("[TEST] PASS: E2E ANSI color sequences work");
}

/// Test ANSI text attribute sequences.
#[test]
fn test_e2e_ansi_text_attributes() {
    let mut harness = E2EHarness::new("render_cycle", "text_attributes", 40, 10);

    harness
        .log()
        .info("init", "Testing ANSI text attribute sequences");

    // Create cells with various attributes
    let bold_style = Style::bold();
    let italic_style = Style::italic();
    let underline_style = Style::underline();

    let bold_cell = opentui::cell::Cell::new('B', bold_style);
    let italic_cell = opentui::cell::Cell::new('I', italic_style);
    let underline_cell = opentui::cell::Cell::new('U', underline_style);

    // Capture ANSI output
    let mut output: Vec<u8> = Vec::new();
    {
        let mut writer = AnsiWriter::new(&mut output);

        writer.move_cursor(0, 0);
        writer.write_cell(&bold_cell);

        writer.reset();
        writer.move_cursor(0, 1);
        writer.write_cell(&italic_cell);

        writer.reset();
        writer.move_cursor(0, 2);
        writer.write_cell(&underline_cell);

        writer.flush().unwrap();
    }

    let output_str = String::from_utf8_lossy(&output);
    let readable = output_str.replace('\x1b', "ESC");

    harness.log().info("ansi", format!("Output: {readable}"));

    // Parse sequences using MockTerminal
    let mut mock = MockTerminal::new(40, 10);
    mock.write_all(&output).unwrap();
    let sequences = mock.parse_sequences();

    // Verify we have attribute sequences
    let has_bold = sequences
        .iter()
        .any(|s| matches!(s, common::mock_terminal::AnsiSequence::SetAttributes(a) if a.contains(TextAttributes::BOLD)));
    let has_italic = sequences
        .iter()
        .any(|s| matches!(s, common::mock_terminal::AnsiSequence::SetAttributes(a) if a.contains(TextAttributes::ITALIC)));
    let has_underline = sequences
        .iter()
        .any(|s| matches!(s, common::mock_terminal::AnsiSequence::SetAttributes(a) if a.contains(TextAttributes::UNDERLINE)));

    harness.log().info(
        "verify",
        format!(
            "Attributes found: bold={}, italic={}, underline={}",
            has_bold, has_italic, has_underline
        ),
    );

    // Check raw sequences in output
    assert!(
        output_str.contains("\x1b[1m"),
        "Output should contain bold sequence (CSI 1 m)"
    );
    assert!(
        output_str.contains("\x1b[3m"),
        "Output should contain italic sequence (CSI 3 m)"
    );
    assert!(
        output_str.contains("\x1b[4m"),
        "Output should contain underline sequence (CSI 4 m)"
    );

    harness.finish(true);
    eprintln!("[TEST] PASS: E2E ANSI text attributes work");
}

/// Test reset sequence on cleanup.
#[test]
fn test_e2e_ansi_reset_on_cleanup() {
    let mut harness = E2EHarness::new("render_cycle", "reset_cleanup", 40, 10);

    harness.log().info("init", "Testing ANSI reset on cleanup");

    // Capture ANSI output with styling then reset
    let mut output: Vec<u8> = Vec::new();
    {
        let mut writer = AnsiWriter::new(&mut output);

        // Set some attributes using builder
        let styled = Style::builder().fg(Rgba::RED).bold().underline().build();
        let styled_cell = opentui::cell::Cell::new('S', styled);
        writer.write_cell(&styled_cell);

        // Reset (cleanup)
        writer.reset();

        writer.flush().unwrap();
    }

    let output_str = String::from_utf8_lossy(&output);

    harness.log().info(
        "ansi",
        format!("Output: {}", output_str.replace('\x1b', "ESC")),
    );

    // Verify reset sequence is present (CSI 0 m)
    assert!(
        output_str.contains("\x1b[0m") || output_str.contains("\x1b[m"),
        "Output should contain reset sequence"
    );

    harness.finish(true);
    eprintln!("[TEST] PASS: E2E ANSI reset on cleanup works");
}

/// Test full render cycle with grapheme pool integration.
#[test]
fn test_e2e_render_cycle_with_graphemes() {
    let mut harness = E2EHarness::new("render_cycle", "graphemes", 40, 10);

    harness
        .log()
        .info("init", "Testing render cycle with graphemes");

    let mut grapheme_pool = GraphemePool::new();

    // Allocate some graphemes
    let emoji_id = grapheme_pool.alloc("🎉");
    let family_id = grapheme_pool.alloc("👨‍👩‍👧");

    harness.log().info(
        "pool",
        format!("Allocated grapheme IDs: emoji={emoji_id:?}, family={family_id:?}"),
    );

    // Create buffer and draw
    let mut buffer = OptimizedBuffer::new(40, 10);
    buffer.draw_text(0, 0, "Party: 🎉", Style::default());

    // Verify grapheme is stored correctly
    assert_eq!(
        grapheme_pool.get(emoji_id),
        Some("🎉"),
        "Should retrieve emoji from pool"
    );
    assert_eq!(
        grapheme_pool.get(family_id),
        Some("👨‍👩‍👧"),
        "Should retrieve family emoji from pool"
    );

    // Generate ANSI output with pool
    let mut output: Vec<u8> = Vec::new();
    {
        let mut writer = AnsiWriter::new(&mut output);

        for y in 0..1u32 {
            for x in 0..20u32 {
                if let Some(cell) = buffer.get(x, y) {
                    if !cell.is_continuation() {
                        writer.write_cell_with_pool(cell, &grapheme_pool);
                    }
                }
            }
        }

        writer.flush().unwrap();
    }

    let output_str = String::from_utf8_lossy(&output);

    harness
        .log()
        .info("ansi", format!("Output length: {} bytes", output.len()));

    // Verify content is in output
    assert!(
        output_str.contains("Party"),
        "Output should contain 'Party'"
    );

    harness.finish(true);
    eprintln!("[TEST] PASS: E2E render cycle with graphemes works");
}

/// Test diff threshold for full redraw decision.
#[test]
fn test_e2e_diff_threshold_decision() {
    let mut harness = E2EHarness::new("render_cycle", "diff_threshold", 20, 10);

    harness
        .log()
        .info("init", "Testing diff threshold for full redraw");

    let total_cells = 20 * 10;

    // Create diff with 10% changes (should use diff)
    let small_diff = BufferDiff {
        changed_cells: vec![(0, 0); total_cells / 10],
        dirty_regions: vec![],
        change_count: total_cells / 10,
    };

    assert!(
        !small_diff.should_full_redraw(total_cells),
        "10% changes should use diff rendering"
    );

    harness.log().info("threshold", "10% changes: diff mode");

    // Create diff with 60% changes (should use full redraw)
    let large_diff = BufferDiff {
        changed_cells: vec![(0, 0); total_cells * 6 / 10],
        dirty_regions: vec![],
        change_count: total_cells * 6 / 10,
    };

    assert!(
        large_diff.should_full_redraw(total_cells),
        "60% changes should trigger full redraw"
    );

    harness.log().info("threshold", "60% changes: full redraw");

    // Edge case: exactly 50%
    let half_diff = BufferDiff {
        changed_cells: vec![(0, 0); total_cells / 2],
        dirty_regions: vec![],
        change_count: total_cells / 2,
    };

    harness.log().info(
        "threshold",
        format!(
            "50% changes: full_redraw={}",
            half_diff.should_full_redraw(total_cells)
        ),
    );

    harness.finish(true);
    eprintln!("[TEST] PASS: E2E diff threshold decision works");
}

/// Test JSONL logging format for render cycle events.
#[test]
fn test_e2e_render_cycle_logging() {
    // Define struct at start to avoid items_after_statements lint
    #[derive(serde::Serialize)]
    struct RenderStats {
        frame: u32,
        changed_cells: usize,
        bytes_output: usize,
    }

    let mut harness = E2EHarness::new("render_cycle", "logging", 20, 5);

    harness.log().info("step", "init");
    harness.log().info("step", "draw");
    harness.log().info("step", "present");
    harness.log().info("step", "verify");
    harness.log().info("step", "cleanup");

    // Log structured data
    let stats = RenderStats {
        frame: 1,
        changed_cells: 50,
        bytes_output: 256,
    };

    harness.log().info(
        "present",
        format!(
            "Frame {}: {} cells, {} bytes",
            stats.frame, stats.changed_cells, stats.bytes_output
        ),
    );

    harness.finish(true);
    eprintln!("[TEST] PASS: E2E render cycle logging works");
}