//! Zero-flicker byte goldens: end-to-end `FrameWriter` streams pinned against
//! the live ink oracle.
//!
//! # Oracle & provenance
//!
//! Every `B64_*` constant below is the base64 of a byte stream emitted by ink's
//! REAL built modules — `createIncremental` (`ink/build/log-update.js`),
//! `bsu`/`esu` (`ink/build/write-synchronized.js`), and
//! `ansiEscapes.clearTerminal` — driven through a verbatim copy of ink's
//! `renderInteractiveFrame` decision tree (`ink/src/ink.tsx` ~1037-1102). The
//! durable harness that regenerates this bundle is
//! `tests/oracle/zero_flicker_oracle.mjs`; run it with
//! `INK_DIR=/home/alpha/rewrite/ink node tests/oracle/zero_flicker_oracle.mjs`.
//! Capture config matches the M2-E line-diff goldens (`src/tests.rs`):
//! `logUpdate.create(stream, {incremental:true, showCursor:true})`, no
//! `setCursorPosition` — the pure `&str -> bytes` path.
//!
//! Each test drives `FrameWriter` (the Rust port of `renderInteractiveFrame`)
//! through the IDENTICAL per-frame output strings and asserts the emitted bytes
//! equal the oracle capture. That equality is a cross-language check: a port bug
//! in `LineDiff` / the bsu-esu / clearTerminal framing flips the bytes and fails
//! here. The clearTerminal DECISIONS are additionally cross-checked, transition
//! by transition, against `ink/test/render.tsx`'s exact `t.is(clearTerminalCount,
//! N)` assertions (see the per-scenario provenance comments).
//!
//! # What these issue-450 goldens pin (and what they do NOT)
//!
//! The `issue450_*` byte goldens pin **inkferro's** stream: the REAL
//! `createIncremental` diff bytes plus the REAL `bsu`/`esu` framing, called the
//! way `FrameWriter` (the `renderInteractiveFrame` port) calls them, cross-checked
//! Rust == JS. They are deliberately NOT byte-identical to ink's real fixture
//! stream, for two independent, documented reasons:
//!
//! 1. **Renderer divergence (eraseLineCount).** ink's issue-450 fixtures run
//!    ink's DEFAULT `incrementalRendering: false` (`ink/src/render.ts:211`) =
//!    `createStandard`, which `eraseLines(previousLines.length)` + FULL-rewrites
//!    every frame (verified: a constant-height fullscreen rerender frame emits
//!    `eraseLines(6)` then the whole frame). inkferro-rt is incremental-ONLY by
//!    the M2-E flicker policy (`src/lib.rs`): it selective-diffs, rewriting only
//!    changed lines, so a constant-height fullscreen rerender emits NO
//!    `eraseLines` (eraseLineCount == 0). DECSET atomicity is what makes ink's
//!    full-rewrite flicker-free; inkferro avoids the rewrite entirely. This is an
//!    intentional architectural divergence, not a port bug — so there is no clean
//!    eraseLineCount equality to assert against ink for the constant-height
//!    fixtures, and these tests pin inkferro's eraseLineCount (0 where noted).
//!
//! 2. **Lifecycle (clearTerminalCount).** `FrameWriter` ports
//!    `renderInteractiveFrame` only — not React's mount, throttle, or the
//!    unmount-final-frame + unmount `clearTerminal`. The per-frame clear DECISION
//!    is reproduced exactly (cross-checked vs `render.tsx`'s `t.is`); the extra
//!    unmount clear is caller-driven (`unmount_clear_fires_when_caller_sets_is_unmounting`
//!    proves `FrameWriter` emits it once the caller sets `is_unmounting`).
//!
//! For the record, the real-PTY `cols=100` capture of the ink fixtures (default
//! standard renderer, frameLimit 8 -> frames 0..8, then unmount) measured — these
//! are ink's numbers, NOT what this incremental layer emits:
//!   full-height-rerender:            clearTerminalCount=1, eraseLineCount=8
//!   full-height-rerender-w-marker:   clearTerminalCount=1, eraseLineCount=8
//!   height-minus-one-rerender:       clearTerminalCount=0, eraseLineCount=8
//!   full-height-with-static:         clearTerminalCount=0, eraseLineCount=8
//!   grow-to-fullscreen-rerender:     clearTerminalCount=1, eraseLineCount=8
//!   shrink-from-fullscreen-rerender: clearTerminalCount=1, eraseLineCount=7
//!   shrink-from-overflow-rerender:   clearTerminalCount=1, eraseLineCount=7
//!   static-shrink-from-fullscreen:   clearTerminalCount=1, eraseLineCount=7
//!   initial-overflow (rows 3):       clearTerminalCount=1, eraseLineCount=0
//!   initial-fullscreen (rows 3):     clearTerminalCount=1, eraseLineCount=0
//! ink's per-frame eraseLines come from the standard renderer's full-rewrite (not
//! a mount artifact); ink's single clearTerminal is the unmount clear. The
//! `multiframe_diff_script` golden (#2), by contrast, drives `createIncremental`
//! directly and IS genuinely ink-equivalent byte-for-byte.

use inkferro_rt::{FrameParams, FrameWriter, bsu, esu};

const CLEAR: &str = "\u{001B}[2J\u{001B}[3J\u{001B}[H";
const ERASE_LINES_1: &str = "\u{001B}[2K\u{001B}[G";

// ---------- minimal, dependency-free base64 decoder (std-only crate) ----------

fn b64_decode(s: &str) -> Vec<u8> {
    fn val(c: u8) -> i16 {
        match c {
            b'A'..=b'Z' => (c - b'A') as i16,
            b'a'..=b'z' => (c - b'a' + 26) as i16,
            b'0'..=b'9' => (c - b'0' + 52) as i16,
            b'+' => 62,
            b'/' => 63,
            _ => -1,
        }
    }
    let mut out = Vec::new();
    let mut acc: u32 = 0;
    let mut bits = 0;
    for &c in s.as_bytes() {
        if c == b'=' {
            break;
        }
        let v = val(c);
        if v < 0 {
            continue; // skip whitespace/newlines
        }
        acc = (acc << 6) | v as u32;
        bits += 6;
        if bits >= 8 {
            bits -= 8;
            out.push((acc >> bits) as u8);
        }
    }
    out
}

/// Count non-overlapping occurrences of `needle` in `haystack`.
fn count(haystack: &[u8], needle: &str) -> usize {
    let n = needle.as_bytes();
    if n.is_empty() {
        return 0;
    }
    let mut hits = 0;
    let mut i = 0;
    while i + n.len() <= haystack.len() {
        if &haystack[i..i + n.len()] == n {
            hits += 1;
            i += n.len();
        } else {
            i += 1;
        }
    }
    hits
}

// ----------------------------- frame drivers --------------------------------

/// issue-450 rerender output model — byte-identical to the oracle harness's
/// `rerenderOutput`: Box height={targetHeight} + 3 labelled rows + flexGrow
/// spacer => exactly `height` lines for height >= 3.
fn rerender_output(frame: usize, height: usize) -> String {
    let mut lines = vec!["#450 top".to_owned(), format!("frame {frame}")];
    while lines.len() < height.saturating_sub(1) {
        lines.push(String::new());
    }
    lines.push("#450 bottom".to_owned());
    lines.join("\n")
}

/// Initial-fixture output model — byte-identical to the oracle's `initialOutput`.
fn initial_output(prefix: &str, line_count: usize) -> String {
    (1..=line_count)
        .map(|i| format!("{prefix} line {i}"))
        .collect::<Vec<_>>()
        .join("\n")
}

fn tty_steady_params<'a>(
    output: &'a str,
    viewport: usize,
    static_output: &'a str,
) -> FrameParams<'a> {
    FrameParams {
        is_tty: true,
        viewport_rows: viewport,
        output,
        output_height: output.split('\n').count(),
        static_output,
        is_unmounting: false,
        cursor_dirty: false,
        cursor: None,
        interactive: Some(true),
        is_in_ci: false,
        debug: false,
    }
}

/// Drive a TTY rerender height schedule through a fresh writer; sync on. The
/// per-frame static line appears only on frame 0 when `static_first`.
fn run_rerender(viewport: usize, heights: &[usize], static_first: bool) -> Vec<u8> {
    let mut w = FrameWriter::new();
    let mut all = Vec::new();
    for (frame, &h) in heights.iter().enumerate() {
        let output = rerender_output(frame, h);
        let static_output = if static_first && frame == 0 {
            "#450 static line\n"
        } else {
            ""
        };
        all.extend_from_slice(&w.write_frame(&tty_steady_params(&output, viewport, static_output)));
    }
    all
}

/// `heightForFrame` over frames 0..8 (frameLimit default 8 -> 9 frames).
fn frames9(f: impl Fn(usize, usize) -> usize) -> Vec<usize> {
    (0..9).map(|frame| f(6, frame)).collect()
}

// ===================== #2: multi-frame diff byte golden =====================

// Oracle: tests/oracle/zero_flicker_oracle.mjs scenario "multiframe-diff-script".
// Six frames within viewport 100 (no clear ever fires): bootstrap, content
// change, grow 3->5, shrink 5->3, steady no-op, change+shrink 3->2. The full
// byte stream — every bsu/esu and every diff byte — is pinned against the live
// createIncremental oracle.
const B64_MULTIFRAME: &str = "G1s/MjAyNmhhbHBoYQpiZXRhCmdhbW1hChtbPzIwMjZsG1s/MjAyNmgbWzNBG1tFG1sxR0JFVEEbW0sKG1tFG1s/MjAyNmwbWz8yMDI2aBtbM0EbW0UbW0UbW0UbWzFHZGVsdGEbW0sKG1sxR2Vwc2lsb24bW0sKG1s/MjAyNmwbWz8yMDI2aBtbMksbWzFBG1sySxtbMUEbWzJLG1tHG1szQRtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzJLG1sxQRtbMksbW0cbWzJBG1sxR29uZRtbSwobWzFHdHdvG1tLChtbPzIwMjZs";

fn multiframe_stream() -> Vec<u8> {
    let mut w = FrameWriter::new();
    let mut all = Vec::new();
    for output in [
        "alpha\nbeta\ngamma",
        "alpha\nBETA\ngamma",
        "alpha\nBETA\ngamma\ndelta\nepsilon",
        "alpha\nBETA\ngamma",
        "alpha\nBETA\ngamma",
        "one\ntwo",
    ] {
        all.extend_from_slice(&w.write_frame(&tty_steady_params(output, 100, "")));
    }
    all
}

#[test]
fn multiframe_diff_script_is_byte_identical_to_ink() {
    assert_eq!(multiframe_stream(), b64_decode(B64_MULTIFRAME));
}

#[test]
fn multiframe_exercises_real_erase_and_sync_wraps() {
    let s = multiframe_stream();
    // Five rendering frames (the 5th, a no-op, emits nothing) -> 5 bsu/esu pairs.
    assert_eq!(count(&s, bsu), 5);
    assert_eq!(count(&s, esu), 5);
    // The two shrink frames (5->3 and 3->2) each emit one eraseLines(1) tail.
    assert_eq!(count(&s, ERASE_LINES_1), 2);
    assert_eq!(count(&s, CLEAR), 0);
}

// ============== #1: unchanged frame in steady state -> 0 bytes ===============

// Oracle scenario "steady-unchanged": secondFrameBytes == 0.
const B64_STEADY_FIRST: &str = "G1s/MjAyNmhzdGVhZHkKc3RhdGUKG1s/MjAyNmw=";

#[test]
fn unchanged_frame_writes_zero_bytes_end_to_end() {
    let mut w = FrameWriter::new();
    let p = tty_steady_params("steady\nstate", 100, "");
    let first = w.write_frame(&p);
    assert_eq!(
        first,
        b64_decode(B64_STEADY_FIRST),
        "bootstrap frame matches oracle"
    );
    let second = w.write_frame(&p);
    assert!(
        second.is_empty(),
        "steady-state unchanged frame must write 0 bytes"
    );
}

// ===================== #4: DECSET wrap presence/absence =====================

// Oracle scenarios "decset-sync-on" / "decset-sync-off" / "decset-debug" over
// the SAME 3-frame sequence ['aa\nbb','aa\ncc','aa\ncc\ndd'].
const B64_DECSET_SYNC_ON: &str = "G1s/MjAyNmhhYQpiYgobWz8yMDI2bBtbPzIwMjZoG1syQRtbRRtbMUdjYxtbSwobWz8yMDI2bBtbPzIwMjZoG1syQRtbRRtbRRtbMUdkZBtbSwobWz8yMDI2bA==";
const B64_DECSET_SYNC_OFF: &str = "YWEKYmIKG1syQRtbRRtbMUdjYxtbSwobWzJBG1tFG1tFG1sxR2RkG1tLCg==";
const B64_DECSET_DEBUG: &str = "YWEKYmJhYQpjY2FhCmNjCmRk";

fn decset_stream(sync_interactive: Option<bool>, debug: bool, is_tty: bool) -> Vec<u8> {
    let mut w = FrameWriter::new();
    let mut all = Vec::new();
    for output in ["aa\nbb", "aa\ncc", "aa\ncc\ndd"] {
        let p = FrameParams {
            is_tty,
            viewport_rows: 100,
            output,
            output_height: output.split('\n').count(),
            static_output: "",
            is_unmounting: false,
            cursor_dirty: false,
            cursor: None,
            interactive: sync_interactive,
            is_in_ci: false,
            debug,
        };
        all.extend_from_slice(&w.write_frame(&p));
    }
    all
}

#[test]
fn decset_sync_on_wraps_every_render() {
    let s = decset_stream(Some(true), false, true);
    assert_eq!(s, b64_decode(B64_DECSET_SYNC_ON));
    // Three rendering frames -> three wrapped pairs; the diffs live between them.
    assert_eq!(count(&s, bsu), 3);
    assert_eq!(count(&s, esu), 3);
}

#[test]
fn decset_sync_off_never_wraps() {
    let s = decset_stream(Some(false), false, true);
    assert_eq!(s, b64_decode(B64_DECSET_SYNC_OFF));
    assert_eq!(
        count(&s, bsu),
        0,
        "non-interactive: shouldSynchronize false -> no DECSET"
    );
    assert_eq!(count(&s, esu), 0);
    assert!(!s.is_empty(), "frames still render, just unwrapped");
}

#[test]
fn decset_debug_never_wraps_and_is_plain_full_frames() {
    let s = decset_stream(Some(false), true, false);
    assert_eq!(s, b64_decode(B64_DECSET_DEBUG));
    assert_eq!(count(&s, bsu), 0, "debug mode: never any DECSET");
    assert_eq!(count(&s, esu), 0);
    assert_eq!(
        count(&s, ERASE_LINES_1),
        0,
        "debug: plain full frames, no erase"
    );
    assert_eq!(count(&s, CLEAR), 0);
}

#[test]
fn decset_noop_emits_zero_bytes_no_empty_wrap() {
    // A repeated identical sync frame must emit nothing: no empty BSU/ESU pair
    // (the willRender rule). End-to-end zero-flicker skip.
    let mut w = FrameWriter::new();
    let p = tty_steady_params("aa\nbb", 100, "");
    let _ = w.write_frame(&p);
    let again = w.write_frame(&p);
    assert!(again.is_empty());
    assert_eq!(count(&again, bsu), 0);
}

// ============== #3: issue-450 clearTerminalCount + eraseLineCount =============
//
// One test per fixture. `clear` is the count emitted by FrameWriter for the
// fixture's height schedule; it equals render.tsx's exact clearTerminal-decision
// assertion (cited per case). `erase` is FrameWriter's eraseLines(1) count over
// the same schedule (the fullscreen steady-diff path uses cursorUp+eraseEndLine,
// not eraseLines, so the in-scope count is 0 where noted; the real-PTY
// lifecycle's >0 erases are the documented mount-bootstrap seam above). Every
// stream is additionally cross-checked against its oracle base64 capture.

fn assert_issue450(name: &str, bytes: &[u8], expect_clear: usize) {
    let golden = b64_decode(oracle_b64(name));
    assert_eq!(
        bytes,
        golden.as_slice(),
        "{name}: FrameWriter stream must equal oracle capture"
    );
    // `expect_clear` is the IN-SCOPE clear count: clears that
    // `renderInteractiveFrame` itself decides (leaving-fullscreen / overflow
    // edges). For the full-height fixtures ink's real `clearTerminalCount=1` is
    // the UNMOUNT clear (out of scope) while this layer's count is 0; for the
    // shrink fixtures both ink and this layer clear once at the SAME in-scope
    // frame-2 leaving-fullscreen/overflow edge.
    assert_eq!(
        count(bytes, CLEAR),
        expect_clear,
        "{name}: in-scope clearTerminalCount"
    );
}

// Oracle base64 for the issue-450 schedules (scenario name -> capture).
fn oracle_b64(name: &str) -> &'static str {
    match name {
        "full-height-rerender" => {
            "G1s/MjAyNmgjNDUwIHRvcApmcmFtZSAwCgoKCiM0NTAgYm90dG9tG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgMRtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSAyG1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDMbW0sKG1tFG1tFG1tFG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgNBtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSA1G1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDYbW0sKG1tFG1tFG1tFG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgNxtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSA4G1tLChtbRRtbRRtbRRtbPzIwMjZs"
        }
        "full-height-rerender-with-marker" => {
            "G1s/MjAyNmgjNDUwIHRvcApmcmFtZSAwCgoKCiM0NTAgYm90dG9tG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgMRtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSAyG1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDMbW0sKG1tFG1tFG1tFG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgNBtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSA1G1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDYbW0sKG1tFG1tFG1tFG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgNxtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSA4G1tLChtbRRtbRRtbRRtbPzIwMjZs"
        }
        "height-minus-one-rerender" => {
            "G1s/MjAyNmgjNDUwIHRvcApmcmFtZSAwCgoKIzQ1MCBib3R0b20KG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgMRtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSAyG1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDMbW0sKG1tFG1tFG1tFG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgNBtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSA1G1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDYbW0sKG1tFG1tFG1tFG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgNxtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSA4G1tLChtbRRtbRRtbRRtbPzIwMjZs"
        }
        "grow-to-fullscreen-rerender" => {
            "G1s/MjAyNmgjNDUwIHRvcApmcmFtZSAwCgoKIzQ1MCBib3R0b20KG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgMRtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSAyG1tLChtbRRtbRRtbMUcbW0sKG1sxRyM0NTAgYm90dG9tG1tLG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgMxtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSA0G1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDUbW0sKG1tFG1tFG1tFG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgNhtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSA3G1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDgbW0sKG1tFG1tFG1tFG1s/MjAyNmw="
        }
        "shrink-from-fullscreen-rerender" => {
            "G1s/MjAyNmgjNDUwIHRvcApmcmFtZSAwCgoKCiM0NTAgYm90dG9tG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgMRtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1syShtbM0obW0gjNDUwIHRvcApmcmFtZSAyCgoKIzQ1MCBib3R0b20bWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSAzG1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDQbW0sKG1tFG1tFG1tFG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgNRtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSA2G1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDcbW0sKG1tFG1tFG1tFG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgOBtbSwobW0UbW0UbW0UbWz8yMDI2bA=="
        }
        "shrink-from-overflow-rerender" => {
            "G1s/MjAyNmgjNDUwIHRvcApmcmFtZSAwCgoKCgojNDUwIGJvdHRvbRtbPzIwMjZsG1s/MjAyNmgbWzJKG1szShtbSCM0NTAgdG9wCmZyYW1lIDEKCgojNDUwIGJvdHRvbRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDIbW0sKG1tFG1tFG1tFG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgMxtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSA0G1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDUbW0sKG1tFG1tFG1tFG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgNhtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSA3G1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDgbW0sKG1tFG1tFG1tFG1s/MjAyNmw="
        }
        "static-shrink-from-fullscreen-rerender" => {
            "G1s/MjAyNmgjNDUwIHN0YXRpYyBsaW5lCiM0NTAgdG9wCmZyYW1lIDAKCgoKIzQ1MCBib3R0b20bWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSAxG1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzJKG1szShtbSCM0NTAgc3RhdGljIGxpbmUKIzQ1MCB0b3AKZnJhbWUgMgoKCiM0NTAgYm90dG9tG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgMxtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSA0G1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDUbW0sKG1tFG1tFG1tFG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgNhtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSA3G1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDgbW0sKG1tFG1tFG1tFG1s/MjAyNmw="
        }
        "full-height-with-static-rerender" => {
            "G1s/MjAyNmgjNDUwIHN0YXRpYyBsaW5lCiM0NTAgdG9wCmZyYW1lIDAKCgoKIzQ1MCBib3R0b20bWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSAxG1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDIbW0sKG1tFG1tFG1tFG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgMxtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSA0G1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDUbW0sKG1tFG1tFG1tFG1s/MjAyNmwbWz8yMDI2aBtbNUEbW0UbWzFHZnJhbWUgNhtbSwobW0UbW0UbW0UbWz8yMDI2bBtbPzIwMjZoG1s1QRtbRRtbMUdmcmFtZSA3G1tLChtbRRtbRRtbRRtbPzIwMjZsG1s/MjAyNmgbWzVBG1tFG1sxR2ZyYW1lIDgbW0sKG1tFG1tFG1tFG1s/MjAyNmw="
        }
        "initial-overflow" => {
            "G1s/MjAyNmgjNDUwIGluaXRpYWwgb3ZlcmZsb3cgbGluZSAxCiM0NTAgaW5pdGlhbCBvdmVyZmxvdyBsaW5lIDIKIzQ1MCBpbml0aWFsIG92ZXJmbG93IGxpbmUgMwojNDUwIGluaXRpYWwgb3ZlcmZsb3cgbGluZSA0G1s/MjAyNmw="
        }
        "initial-fullscreen" => {
            "G1s/MjAyNmgjNDUwIGluaXRpYWwgZnVsbHNjcmVlbiBsaW5lIDEKIzQ1MCBpbml0aWFsIGZ1bGxzY3JlZW4gbGluZSAyCiM0NTAgaW5pdGlhbCBmdWxsc2NyZWVuIGxpbmUgMxtbPzIwMjZs"
        }
        other => panic!("unknown oracle scenario {other}"),
    }
}

// render.tsx:478-481 asserts clearTerminalCount<=1 on the real fixture; that one
// real clear is the UNMOUNT clear (out of scope). In-scope there is no
// fullscreen->non-fs edge and no overflow, so this layer clears 0 times.
#[test]
fn issue450_full_height_rerender() {
    assert_issue450(
        "full-height-rerender",
        &run_rerender(6, &frames9(|r, _| r), false),
        0,
    );
}

// render.tsx:553 — full-height rerenders should not clear before unmount.
// Marker fixture shares the height schedule (the marker is a bare unmount
// stdout.write, outside renderInteractiveFrame), so the in-scope stream is
// byte-identical to full-height-rerender.
#[test]
fn issue450_full_height_rerender_with_marker() {
    let s = run_rerender(6, &frames9(|r, _| r), false);
    assert_issue450("full-height-rerender-with-marker", &s, 0);
    assert_eq!(
        s,
        run_rerender(6, &frames9(|r, _| r), false),
        "marker fixture shares the schedule; marker write is an out-of-scope unmount seam"
    );
}

// render.tsx:532 — rows-1 control rerenders avoid clearTerminal.
#[test]
fn issue450_height_minus_one_rerender() {
    assert_issue450(
        "height-minus-one-rerender",
        &run_rerender(6, &frames9(|r, _| r - 1), false),
        0,
    );
}

// render.tsx:570 — grow from rows-1 to full-height should not clear.
#[test]
fn issue450_grow_to_fullscreen_rerender() {
    assert_issue450(
        "grow-to-fullscreen-rerender",
        &run_rerender(6, &frames9(|r, f| if f < 2 { r - 1 } else { r }), false),
        0,
    );
}

// render.tsx:582 — shrink from full-height to rows-1 clears exactly once. This
// IS the in-scope leaving-fullscreen edge at frame 2 (prev>=viewport, next<
// viewport), so ink and this layer agree on the SAME clear (not the unmount one).
#[test]
fn issue450_shrink_from_fullscreen_rerender() {
    assert_issue450(
        "shrink-from-fullscreen-rerender",
        &run_rerender(6, &frames9(|r, f| if f < 2 { r } else { r - 1 }), false),
        1,
    );
}

// render.tsx:594 — shrink from overflow to rows-1 clears exactly once. In-scope
// edge: frame 1 fires on wasOverflowing (prev height 7 > viewport 6). ink and
// this layer agree on this same clear.
#[test]
fn issue450_shrink_from_overflow_rerender() {
    assert_issue450(
        "shrink-from-overflow-rerender",
        &run_rerender(
            6,
            &frames9(|r, f| if f == 0 { r + 1 } else { r - 1 }),
            false,
        ),
        1,
    );
}

// render.tsx:607 — <Static> with shrink from full-height clears exactly once,
// and the static line survives into the stream.
#[test]
fn issue450_static_shrink_from_fullscreen_rerender() {
    let s = run_rerender(6, &frames9(|r, f| if f < 2 { r } else { r - 1 }), true);
    assert_issue450("static-shrink-from-fullscreen-rerender", &s, 1);
    assert!(
        count(&s, "#450 static line") >= 1,
        "static line must survive"
    );
}

// render.tsx:774-781 — full-height rerenders with <Static> clear at most once;
// here zero in-scope clears, with the static line present.
#[test]
fn issue450_full_height_with_static_rerender() {
    let s = run_rerender(6, &frames9(|r, _| r), true);
    assert_issue450("full-height-with-static-rerender", &s, 0);
    assert!(count(&s, "#450 static line") >= 1);
}

// render.tsx:500-503 — initial overflowing frame should not clear (no previous
// frame). Single frame, rows 3, lineCount 4.
#[test]
fn issue450_initial_overflow() {
    let mut w = FrameWriter::new();
    let out = initial_output("#450 initial overflow", 4);
    let s = w.write_frame(&tty_steady_params(&out, 3, ""));
    assert_issue450("initial-overflow", &s, 0);
}

// render.tsx:518-521 — initial full-height frame should not clear. rows 3,
// lineCount 3.
#[test]
fn issue450_initial_fullscreen() {
    let mut w = FrameWriter::new();
    let out = initial_output("#450 initial fullscreen", 3);
    let s = w.write_frame(&tty_steady_params(&out, 3, ""));
    assert_issue450("initial-fullscreen", &s, 0);
}

// render.tsx:725-733 — grow-to-overflow is exercised NON-TTY; ink's
// !interactive branch never drives createIncremental, so NO main-output bytes
// (and zero clearTerminal) are written. frameLimit 1 -> 2 frames, rows 3.
#[test]
fn issue450_grow_to_overflow_non_tty_writes_nothing() {
    let mut w = FrameWriter::new();
    let mut all = Vec::new();
    for (frame, &h) in [2usize, 4].iter().enumerate() {
        let output = rerender_output(frame, h);
        let p = FrameParams {
            is_tty: false,
            viewport_rows: 3,
            output: &output,
            output_height: h,
            static_output: "",
            is_unmounting: false,
            cursor_dirty: false,
            cursor: None,
            interactive: Some(true),
            is_in_ci: false,
            debug: false,
        };
        all.extend_from_slice(&w.write_frame(&p));
    }
    assert_eq!(count(&all, CLEAR), 0, "non-TTY never clears");
    // Non-TTY still emits diff bytes through this layer (FrameWriter has no
    // !interactive short-circuit — that lives in ink.tsx onRender); what it must
    // NOT do is wrap in DECSET (shouldSynchronize is false for non-TTY).
    assert_eq!(count(&all, bsu), 0, "non-TTY: no DECSET wrap");
    assert_eq!(count(&all, esu), 0);
}

// ============ lifecycle seam: unmount clear is caller-driven ============
//
// Proves the single real-PTY clearTerminal (the unmount clear) is reachable
// through FrameWriter once the lifecycle caller sets is_unmounting=true on a
// was-fullscreen teardown — locating the seam in the caller's lifecycle, not in
// FrameWriter. render.tsx exercises this via process teardown (unreachable
// purely); here it is the explicit is_unmounting flag.
#[test]
fn unmount_clear_fires_when_caller_sets_is_unmounting() {
    let mut w = FrameWriter::new();
    // Establish a full-height (fullscreen) previous frame, viewport 6.
    let f0 = rerender_output(0, 6);
    let _ = w.write_frame(&tty_steady_params(&f0, 6, ""));
    // Teardown frame: same height, is_unmounting=true, wasFullscreen -> clear.
    let f1 = rerender_output(1, 6);
    let mut p = tty_steady_params(&f1, 6, "");
    p.is_unmounting = true;
    let s = w.write_frame(&p);
    assert_eq!(
        count(&s, CLEAR),
        1,
        "unmount on a fullscreen frame emits one clearTerminal"
    );
    // And it is DECSET-wrapped (the clear branch always writes -> wrap when sync).
    assert_eq!(count(&s, bsu), 1);
    assert_eq!(count(&s, esu), 1);
}

// ===================== zero-flicker invariant covenant ======================
//
// THE flicker-free covenant: in every sync-ON stream, no erase sequence
// (eraseLines / clearTerminal) may appear OUTSIDE a BSU..ESU-wrapped segment.
// Scan each stream, pair BSU/ESU into intervals (they never nest — one pair per
// rendering frame), and assert every clearTerminal AND every eraseLines(2K)
// offset falls strictly inside some interval.

/// Find all byte offsets where `needle` begins in `hay`.
fn offsets(hay: &[u8], needle: &[u8]) -> Vec<usize> {
    let mut out = Vec::new();
    if needle.is_empty() || hay.len() < needle.len() {
        return out;
    }
    let mut i = 0;
    while i + needle.len() <= hay.len() {
        if &hay[i..i + needle.len()] == needle {
            out.push(i);
            i += needle.len();
        } else {
            i += 1;
        }
    }
    out
}

/// Pair BSU/ESU into [start_after_bsu, esu_start) content intervals. Panics if
/// the wrap is unbalanced or nested (the covenant assumes one flat pair/frame).
fn sync_intervals(stream: &[u8]) -> Vec<(usize, usize)> {
    let bsu_b = bsu.as_bytes();
    let esu_b = esu.as_bytes();
    let bsus = offsets(stream, bsu_b);
    let esus = offsets(stream, esu_b);
    assert_eq!(bsus.len(), esus.len(), "unbalanced BSU/ESU count");
    let mut intervals = Vec::with_capacity(bsus.len());
    for (i, (&b, &e)) in bsus.iter().zip(esus.iter()).enumerate() {
        let content_start = b + bsu_b.len();
        assert!(content_start <= e, "BSU must precede its ESU");
        if i + 1 < bsus.len() {
            assert!(
                e < bsus[i + 1],
                "BSU/ESU intervals must not nest or interleave"
            );
        }
        intervals.push((content_start, e));
    }
    intervals
}

fn assert_all_erases_wrapped(name: &str, stream: &[u8]) {
    let intervals = sync_intervals(stream);
    let inside =
        |pos: usize, len: usize| intervals.iter().any(|&(s, e)| pos >= s && pos + len <= e);
    // clearTerminal occurrences.
    for pos in offsets(stream, CLEAR.as_bytes()) {
        assert!(
            inside(pos, CLEAR.len()),
            "{name}: clearTerminal at byte {pos} is OUTSIDE a BSU/ESU segment (flicker)"
        );
    }
    // eraseLines emit the per-line `ESC[2K`; assert each is wrapped.
    let erase_line = "\u{001B}[2K";
    for pos in offsets(stream, erase_line.as_bytes()) {
        assert!(
            inside(pos, erase_line.len()),
            "{name}: eraseLine (ESC[2K) at byte {pos} is OUTSIDE a BSU/ESU segment (flicker)"
        );
    }
}

#[test]
fn zero_flicker_covenant_holds_on_every_sync_stream() {
    // Every sync-ON stream produced anywhere in this suite.
    assert_all_erases_wrapped("multiframe", &multiframe_stream());
    assert_all_erases_wrapped("decset-sync-on", &decset_stream(Some(true), false, true));

    let schedules: &[(&str, Vec<usize>, bool)] = &[
        ("full-height-rerender", frames9(|r, _| r), false),
        ("height-minus-one-rerender", frames9(|r, _| r - 1), false),
        (
            "grow-to-fullscreen-rerender",
            frames9(|r, f| if f < 2 { r - 1 } else { r }),
            false,
        ),
        (
            "shrink-from-fullscreen-rerender",
            frames9(|r, f| if f < 2 { r } else { r - 1 }),
            false,
        ),
        (
            "shrink-from-overflow-rerender",
            frames9(|r, f| if f == 0 { r + 1 } else { r - 1 }),
            false,
        ),
        (
            "static-shrink-from-fullscreen-rerender",
            frames9(|r, f| if f < 2 { r } else { r - 1 }),
            true,
        ),
        ("full-height-with-static-rerender", frames9(|r, _| r), true),
    ];
    for (name, heights, static_first) in schedules {
        assert_all_erases_wrapped(name, &run_rerender(6, heights, *static_first));
    }
}
