panasyn 0.1.0

A lightweight GPU-accelerated terminal emulator for macOS and Linux.
use serde::Serialize;

use crate::ansi;
use crate::replay::format;
use crate::replay::recorder::*;
use crate::terminal::TerminalState;

/// A deterministic snapshot of terminal state at a point in replay.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct StateSnapshot {
    pub label: String,
    pub grid_hash: u64,
    pub cursor: (usize, usize),
    pub alt_screen: bool,
}

impl std::fmt::Display for StateSnapshot {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "Snapshot({}): grid_hash={:016x} cursor=({},{}) alt_screen={}",
            self.label, self.grid_hash, self.cursor.0, self.cursor.1, self.alt_screen
        )
    }
}

/// Full regression result from replaying events.
#[derive(Debug, Clone, Serialize)]
pub struct RegressionResult {
    pub snapshots: Vec<StateSnapshot>,
    pub final_grid_hash: u64,
    pub final_cursor: (usize, usize),
    pub scrollback_hash: u64,
    pub alt_screen_state: bool,
    pub parser_state_bits: ParserStateBits,
    pub events_processed: u64,
    pub errors: Vec<String>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
pub struct ParserStateBits {
    pub alt_screen: bool,
    pub app_cursor: bool,
    pub bracketed_paste: bool,
}

impl std::fmt::Display for RegressionResult {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "Regression Result:")?;
        writeln!(f, "  Events processed: {}", self.events_processed)?;
        writeln!(f, "  Grid hash:        {:016x}", self.final_grid_hash)?;
        writeln!(
            f,
            "  Cursor:           ({}, {})",
            self.final_cursor.0, self.final_cursor.1
        )?;
        writeln!(f, "  Scrollback hash:  {:016x}", self.scrollback_hash)?;
        writeln!(f, "  Alt screen:       {}", self.alt_screen_state)?;
        writeln!(f, "  Snapshots:        {}", self.snapshots.len())?;
        for snap in &self.snapshots {
            writeln!(f, "    {}", snap)?;
        }
        if !self.errors.is_empty() {
            writeln!(f, "  Errors:")?;
            for e in &self.errors {
                writeln!(f, "    {}", e)?;
            }
        }
        Ok(())
    }
}

/// Run a deterministic regression replay, computing snapshots at each Snapshot event.
pub fn run_regression(events: &[ReplayEvent], cols: u16, rows: u16) -> RegressionResult {
    let mut terminal = TerminalState::new(cols as usize, rows as usize, 10_000);
    let mut parser = ansi::Parser::new();
    let mut result = RegressionResult {
        snapshots: Vec::new(),
        final_grid_hash: 0,
        final_cursor: (0, 0),
        scrollback_hash: 0,
        alt_screen_state: false,
        parser_state_bits: ParserStateBits::default(),
        events_processed: 0,
        errors: Vec::new(),
    };

    for event in events {
        match &event.kind {
            ReplayEventKind::PtyBytes(data) => {
                parser.advance(data, &mut terminal);
                result.events_processed += 1;
            }
            ReplayEventKind::Resize { cols: c, rows: r } => {
                if *c as usize != terminal.grid.cols || *r as usize != terminal.grid.rows {
                    terminal.resize(*c as usize, *r as usize);
                }
            }
            ReplayEventKind::Snapshot(label) => {
                let snapshot = capture_snapshot(label, &terminal);
                result.snapshots.push(snapshot);
            }
        }
    }

    result.final_grid_hash = format::grid_hash(&terminal.grid);
    result.final_cursor = (terminal.grid.cursor_row(), terminal.grid.cursor_col());
    result.scrollback_hash = format::scrollback_hash(&terminal.scrollback);
    result.alt_screen_state = terminal.in_alt_screen();
    result.parser_state_bits = ParserStateBits {
        alt_screen: terminal.in_alt_screen(),
        app_cursor: terminal.app_cursor(),
        bracketed_paste: terminal.bracketed_paste(),
    };

    result
}

fn capture_snapshot(label: &str, terminal: &TerminalState) -> StateSnapshot {
    StateSnapshot {
        label: label.to_string(),
        grid_hash: format::grid_hash(&terminal.grid),
        cursor: (terminal.grid.cursor_row(), terminal.grid.cursor_col()),
        alt_screen: terminal.in_alt_screen(),
    }
}

/// Compare two regression results and produce a diff report.
pub fn diff_regression(
    _label: &str,
    expected: &RegressionResult,
    actual: &RegressionResult,
) -> Vec<String> {
    let mut diffs = Vec::new();

    if expected.final_grid_hash != actual.final_grid_hash {
        diffs.push(format!(
            "  grid hash mismatch: expected {:016x}, got {:016x}",
            expected.final_grid_hash, actual.final_grid_hash
        ));
    }
    if expected.final_cursor != actual.final_cursor {
        diffs.push(format!(
            "  cursor mismatch: expected ({},{}), got ({},{})",
            expected.final_cursor.0,
            expected.final_cursor.1,
            actual.final_cursor.0,
            actual.final_cursor.1
        ));
    }
    if expected.scrollback_hash != actual.scrollback_hash {
        diffs.push(format!(
            "  scrollback hash mismatch: expected {:016x}, got {:016x}",
            expected.scrollback_hash, actual.scrollback_hash
        ));
    }
    if expected.alt_screen_state != actual.alt_screen_state {
        diffs.push(format!(
            "  alt screen state mismatch: expected {}, got {}",
            expected.alt_screen_state, actual.alt_screen_state
        ));
    }
    if expected.parser_state_bits != actual.parser_state_bits {
        diffs.push(format!(
            "  parser state mismatch: expected {:?}, got {:?}",
            expected.parser_state_bits, actual.parser_state_bits
        ));
    }

    // Compare snapshots pairwise
    let max_snaps = expected.snapshots.len().max(actual.snapshots.len());
    for i in 0..max_snaps {
        match (expected.snapshots.get(i), actual.snapshots.get(i)) {
            (Some(exp), Some(act)) => {
                if exp != act {
                    diffs.push(format!(
                        "  snapshot[{}] mismatch:\n    expected: {}\n    actual:   {}",
                        i, exp, act
                    ));
                }
            }
            (Some(exp), None) => {
                diffs.push(format!(
                    "  snapshot[{}] missing in actual (expected: {})",
                    i, exp
                ));
            }
            (None, Some(act)) => {
                diffs.push(format!("  snapshot[{}] extra in actual: {}", i, act));
            }
            (None, None) => {}
        }
    }

    diffs
}

/// Build a regression test function that can be used with `#[test]`.
/// Returns Ok(()) if all snapshots match, Err with diff messages otherwise.
pub fn assert_regression(
    _label: &str,
    events: &[ReplayEvent],
    expected: &RegressionResult,
    cols: u16,
    rows: u16,
) -> Result<(), Vec<String>> {
    let actual = run_regression(events, cols, rows);
    let diffs = diff_regression(_label, expected, &actual);
    if diffs.is_empty() { Ok(()) } else { Err(diffs) }
}

/// Write a regression result as JSON for storage.
pub fn regression_to_json(result: &RegressionResult) -> Result<String, Box<dyn std::error::Error>> {
    Ok(serde_json::to_string_pretty(result)?)
}