panasyn 0.1.0

A lightweight GPU-accelerated terminal emulator for macOS and Linux.
use std::time::Instant;

use crate::ansi;
use crate::replay::recorder::{ReplayEvent, ReplayEventKind};
use crate::terminal::{Grid, TerminalState};

/// Result of a replay run.
#[derive(Debug, Default)]
pub struct ReplayResult {
    pub events_processed: u64,
    pub total_bytes: u64,
    pub parse_time_ms: f64,
    pub final_grid_rows: usize,
    pub final_grid_cols: usize,
    pub final_cursor: (usize, usize),
    pub scrollback_lines: usize,
    pub snapshots: Vec<(String, String)>, // (label, grid_snapshot)
    pub errors: Vec<String>,
}

/// Run a replay session deterministically (no timing, no GPU).
pub fn run_replay(events: &[ReplayEvent], cols: u16, rows: u16) -> ReplayResult {
    let mut terminal = TerminalState::new(cols as usize, rows as usize, 10_000);
    let mut parser = ansi::Parser::new();
    let mut result = ReplayResult::default();

    for event in events {
        match &event.kind {
            ReplayEventKind::PtyBytes(data) => {
                let t0 = Instant::now();
                parser.advance(data, &mut terminal);
                let dt = t0.elapsed().as_secs_f64() * 1000.0;
                result.parse_time_ms += dt;
                result.events_processed += 1;
                result.total_bytes += data.len() as u64;
            }
            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 snap = grid_snapshot(&terminal.grid);
                result.snapshots.push((label.clone(), snap));
            }
        }
    }

    result.final_grid_rows = terminal.grid.rows();
    result.final_grid_cols = terminal.grid.cols();
    result.final_cursor = (terminal.grid.cursor_row(), terminal.grid.cursor_col());
    result.scrollback_lines = terminal.scrollback.len();
    result
}

/// Render the current grid as a plain-text snapshot for comparison.
fn grid_snapshot(grid: &Grid) -> String {
    let mut out = String::with_capacity(grid.cols * grid.rows + grid.rows);
    for r in 0..grid.rows() {
        for c in 0..grid.cols() {
            if let Some(cell) = grid.cell(r, c) {
                cell.push_text(&mut out);
            } else {
                out.push(' ');
            }
        }
        if r + 1 < grid.rows() {
            out.push('\n');
        }
    }
    out
}

/// Compare two snapshots and return a diff report.
pub fn diff_snapshots(label: &str, expected: &str, actual: &str) -> String {
    if expected == actual {
        return format!("OK: {}", label);
    }
    let exp_lines: Vec<&str> = expected.lines().collect();
    let act_lines: Vec<&str> = actual.lines().collect();
    let mut diff = Vec::new();
    diff.push(format!("DIFF: {}", label));
    for (i, (e, a)) in exp_lines.iter().zip(act_lines.iter()).enumerate() {
        if e != a {
            diff.push(format!("  row {}: expected {:?} got {:?}", i, e, a));
        }
    }
    if exp_lines.len() != act_lines.len() {
        diff.push(format!(
            "  line count mismatch: expected {} got {}",
            exp_lines.len(),
            act_lines.len()
        ));
    }
    diff.join("\n")
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::replay::recorder::*;

    #[test]
    fn test_run_replay_empty() {
        let result = run_replay(&[], 80, 24);
        assert_eq!(result.events_processed, 0);
        assert_eq!(result.total_bytes, 0);
        assert_eq!(result.final_grid_rows, 24);
        assert_eq!(result.final_grid_cols, 80);
        assert_eq!(result.final_cursor, (0, 0));
        assert_eq!(result.scrollback_lines, 0);
    }

    #[test]
    fn test_run_replay_pty_bytes() {
        let events = vec![ReplayEvent::pty_bytes(b"Hello World")];
        let result = run_replay(&events, 80, 24);
        assert_eq!(result.events_processed, 1);
        assert_eq!(result.total_bytes, 11);
        assert_eq!(result.final_cursor, (0, 11));
    }

    #[test]
    fn test_run_replay_resize_changes_grid() {
        let events = vec![ReplayEvent::resize(40, 10)];
        let result = run_replay(&events, 80, 24);
        assert_eq!(result.final_grid_cols, 40);
        assert_eq!(result.final_grid_rows, 10);
    }

    #[test]
    fn test_run_replay_resize_same_dimensions_no_op() {
        let events = vec![ReplayEvent::resize(80, 24)];
        let result = run_replay(&events, 80, 24);
        assert_eq!(result.final_grid_cols, 80);
        assert_eq!(result.final_grid_rows, 24);
    }

    #[test]
    fn test_run_replay_multiple_pty_bytes() {
        let events = vec![
            ReplayEvent::pty_bytes(b"Hello "),
            ReplayEvent::pty_bytes(b"World\n"),
            ReplayEvent::pty_bytes(b"Line 2"),
        ];
        let result = run_replay(&events, 80, 24);
        assert_eq!(result.events_processed, 3);
        assert_eq!(result.total_bytes, 18);
        assert_eq!(result.final_cursor, (1, 6));
    }

    #[test]
    fn test_run_replay_newline_scrollback() {
        let data = b"1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20\n21\n22\n23\n24\n25\n";
        let events = vec![ReplayEvent::pty_bytes(data)];
        let result = run_replay(&events, 80, 24);
        assert!(result.scrollback_lines > 0, "should have scrollback");
        assert_eq!(result.final_cursor.0, 23);
    }

    #[test]
    fn test_snapshot_captures_state() {
        let events = vec![
            ReplayEvent::pty_bytes(b"Hello"),
            ReplayEvent::snapshot("after_hello"),
            ReplayEvent::pty_bytes(b"\nWorld"),
            ReplayEvent::snapshot("after_world"),
        ];
        let result = run_replay(&events, 80, 24);
        assert_eq!(result.snapshots.len(), 2);
        assert_eq!(result.snapshots[0].0, "after_hello");
        assert_eq!(result.snapshots[1].0, "after_world");
        // First snapshot should have "Hello" in it, second should have more
        assert_ne!(result.snapshots[0].1, result.snapshots[1].1);
    }

    #[test]
    fn test_diff_snapshots_identical() {
        let diff = diff_snapshots("test", "hello", "hello");
        assert!(diff.starts_with("OK:"));
    }

    #[test]
    fn test_diff_snapshots_different() {
        let diff = diff_snapshots("test", "hello", "world");
        assert!(diff.starts_with("DIFF:"));
        assert!(diff.contains("row 0"));
    }

    #[test]
    fn test_diff_snapshots_line_count_mismatch() {
        let diff = diff_snapshots("test", "a\nb\nc", "a\nb");
        assert!(diff.contains("line count mismatch"));
    }

    #[test]
    fn test_grid_snapshot_size() {
        let mut terminal = TerminalState::new(5, 3, 10_000);
        let mut parser = ansi::Parser::new();
        parser.advance(b"Hi!", &mut terminal);
        let snap = grid_snapshot(&terminal.grid);
        assert_eq!(snap.lines().count(), 3);
        assert!(snap.starts_with("Hi! "));
    }

    #[test]
    fn test_replay_newlines_create_scrollback() {
        let mut buf = vec![b'\n'; 100];
        buf.extend_from_slice(b"LAST LINE");
        let events = vec![ReplayEvent::pty_bytes(&buf)];
        let result = run_replay(&events, 80, 24);
        assert!(result.scrollback_lines >= 76);
        assert_eq!(result.final_cursor, (23, 9));
    }
}