Skip to main content

ralph_workflow/json_parser/
printer.rs

1//! Printer abstraction for testable output.
2//!
3//! This module provides a trait-based abstraction for output destinations,
4//! allowing parsers to write to stdout, stderr, or test collectors without
5//! changing their core logic.
6
7use std::cell::RefCell;
8use std::io::{self, IsTerminal, Stdout};
9use std::rc::Rc;
10
11#[cfg(any(test, feature = "test-utils"))]
12use std::io::Stderr;
13
14/// Trait for output destinations in parsers.
15///
16/// This trait allows parsers to write to different output destinations
17/// (stdout, stderr, or test collectors) without hardcoding the specific
18/// destination. This makes parsers testable by allowing output capture.
19pub trait Printable: std::io::Write {
20    /// Check if this printer is connected to a terminal.
21    ///
22    /// This is used to determine whether to use terminal-specific features
23    /// like colors and carriage return-based updates.
24    fn is_terminal(&self) -> bool;
25}
26
27/// Printer that writes to stdout.
28#[derive(Debug)]
29pub struct StdoutPrinter {
30    stdout: Stdout,
31    is_terminal: bool,
32}
33
34impl StdoutPrinter {
35    /// Create a new stdout printer.
36    pub fn new() -> Self {
37        let is_terminal = std::io::stdout().is_terminal();
38        Self {
39            stdout: std::io::stdout(),
40            is_terminal,
41        }
42    }
43}
44
45impl Default for StdoutPrinter {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl std::io::Write for StdoutPrinter {
52    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
53        self.stdout.write(buf)
54    }
55
56    fn flush(&mut self) -> io::Result<()> {
57        self.stdout.flush()
58    }
59}
60
61impl Printable for StdoutPrinter {
62    fn is_terminal(&self) -> bool {
63        self.is_terminal
64    }
65}
66
67/// Printer that writes to stderr.
68#[derive(Debug)]
69#[cfg(any(test, feature = "test-utils"))]
70pub struct StderrPrinter {
71    stderr: Stderr,
72    is_terminal: bool,
73}
74
75#[cfg(any(test, feature = "test-utils"))]
76impl StderrPrinter {
77    /// Create a new stderr printer.
78    pub fn new() -> Self {
79        let is_terminal = std::io::stderr().is_terminal();
80        Self {
81            stderr: std::io::stderr(),
82            is_terminal,
83        }
84    }
85}
86
87#[cfg(any(test, feature = "test-utils"))]
88impl Default for StderrPrinter {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94#[cfg(any(test, feature = "test-utils"))]
95impl std::io::Write for StderrPrinter {
96    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
97        self.stderr.write(buf)
98    }
99
100    fn flush(&mut self) -> io::Result<()> {
101        self.stderr.flush()
102    }
103}
104
105#[cfg(any(test, feature = "test-utils"))]
106impl Printable for StderrPrinter {
107    fn is_terminal(&self) -> bool {
108        self.is_terminal
109    }
110}
111
112/// Test printer that captures output for assertion.
113///
114/// This printer stores all output in memory for testing purposes.
115/// It provides methods to retrieve and inspect the captured output.
116#[cfg(any(test, feature = "test-utils"))]
117#[derive(Debug, Default)]
118pub struct TestPrinter {
119    /// Captured output lines.
120    output: RefCell<Vec<String>>,
121    /// Buffer for incomplete lines.
122    buffer: RefCell<String>,
123}
124
125#[cfg(any(test, feature = "test-utils"))]
126impl TestPrinter {
127    /// Create a new test printer.
128    pub fn new() -> Self {
129        Self::default()
130    }
131
132    /// Get all captured output as a single string.
133    pub fn get_output(&self) -> String {
134        let mut result = self.buffer.borrow().clone();
135        for line in self.output.borrow().iter() {
136            result.push_str(line);
137        }
138        result
139    }
140
141    /// Get captured output lines.
142    pub fn get_lines(&self) -> Vec<String> {
143        let mut result: Vec<String> = self.output.borrow().clone();
144        let buffer = self.buffer.borrow();
145        if !buffer.is_empty() {
146            result.push(buffer.clone());
147        }
148        result
149    }
150
151    /// Clear all captured output.
152    pub fn clear(&self) {
153        self.output.borrow_mut().clear();
154        self.buffer.borrow_mut().clear();
155    }
156
157    /// Check if a specific line exists in the output.
158    pub fn has_line(&self, line: &str) -> bool {
159        self.get_lines().iter().any(|l| l.contains(line))
160    }
161
162    /// Get the number of times a specific pattern appears in output.
163    pub fn count_pattern(&self, pattern: &str) -> usize {
164        self.get_lines()
165            .iter()
166            .filter(|l| l.contains(pattern))
167            .count()
168    }
169
170    /// Check if there are duplicate consecutive lines in output.
171    pub fn has_duplicate_consecutive_lines(&self) -> bool {
172        let lines = self.get_lines();
173        for i in 1..lines.len() {
174            if lines[i] == lines[i - 1] && !lines[i].is_empty() {
175                return true;
176            }
177        }
178        false
179    }
180
181    /// Find and return all duplicate consecutive lines.
182    pub fn find_duplicate_consecutive_lines(&self) -> Vec<(usize, String)> {
183        let mut duplicates = Vec::new();
184        let lines = self.get_lines();
185        for i in 1..lines.len() {
186            if lines[i] == lines[i - 1] && !lines[i].is_empty() {
187                duplicates.push((i - 1, lines[i - 1].clone()));
188            }
189        }
190        duplicates
191    }
192
193    /// Get statistics about the output.
194    ///
195    /// Returns a tuple of (`line_count`, `char_count`).
196    pub fn get_stats(&self) -> (usize, usize) {
197        let lines = self.get_lines();
198        let char_count: usize = lines.iter().map(String::len).sum();
199        (lines.len(), char_count)
200    }
201}
202
203#[cfg(any(test, feature = "test-utils"))]
204impl std::io::Write for TestPrinter {
205    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
206        let s =
207            std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
208        let mut buffer = self.buffer.borrow_mut();
209        buffer.push_str(s);
210
211        // Process complete lines
212        while let Some(newline_pos) = buffer.find('\n') {
213            let line = buffer.drain(..=newline_pos).collect::<String>();
214            self.output.borrow_mut().push(line);
215        }
216
217        Ok(buf.len())
218    }
219
220    fn flush(&mut self) -> io::Result<()> {
221        // Flush any remaining buffer content
222        let mut buffer = self.buffer.borrow_mut();
223        if !buffer.is_empty() {
224            self.output.borrow_mut().push(buffer.clone());
225            buffer.clear();
226        }
227        Ok(())
228    }
229}
230
231#[cfg(any(test, feature = "test-utils"))]
232impl Printable for TestPrinter {
233    fn is_terminal(&self) -> bool {
234        // Test printer is never a terminal
235        false
236    }
237}
238
239/// Record of a single `write()` call for streaming analysis.
240///
241/// Captures the content and timestamp of each write operation,
242/// allowing tests to verify incremental streaming behavior.
243#[cfg(any(test, feature = "test-utils"))]
244#[derive(Debug, Clone)]
245pub struct WriteCall {
246    /// The content written in this call.
247    pub content: String,
248    /// Timestamp when write occurred.
249    pub timestamp: std::time::Instant,
250}
251
252/// Record of a flush() call with metadata.
253#[cfg(any(test, feature = "test-utils"))]
254#[derive(Debug, Clone)]
255pub struct FlushCall {
256    /// Index of the last write before this flush (None if no writes yet).
257    pub last_write_index: Option<usize>,
258    /// Timestamp when flush occurred.
259    pub timestamp: std::time::Instant,
260}
261
262/// Test printer that captures EVERY `write()` call for streaming verification.
263///
264/// Unlike [`TestPrinter`] which processes complete lines, this tracks:
265/// - Each individual `write()` call as a separate record
266/// - Each `flush()` call for verifying real-time output behavior
267/// - Content progression over time
268/// - Timing between writes for streaming analysis
269///
270/// Use this to verify that streaming produces incremental output
271/// (multiple small writes) rather than batched output (one large write).
272///
273/// # Example
274///
275/// ```ignore
276/// use ralph_workflow::json_parser::printer::{StreamingTestPrinter, Printable};
277/// use std::io::Write;
278///
279/// let mut printer = StreamingTestPrinter::new();
280/// printer.write_all(b"Hello").unwrap();
281/// printer.flush().unwrap();
282/// printer.write_all(b" World").unwrap();
283/// printer.flush().unwrap();
284///
285/// assert_eq!(printer.write_count(), 2);
286/// assert_eq!(printer.flush_count(), 2);
287/// assert!(printer.get_full_output().contains("Hello World"));
288/// ```
289#[cfg(any(test, feature = "test-utils"))]
290#[derive(Debug)]
291pub struct StreamingTestPrinter {
292    /// Each individual write() call recorded.
293    write_calls: RefCell<Vec<WriteCall>>,
294    /// Each flush() call recorded.
295    flush_calls: RefCell<Vec<FlushCall>>,
296    /// Simulated terminal status for testing different terminal modes.
297    simulated_is_terminal: bool,
298}
299
300#[cfg(any(test, feature = "test-utils"))]
301impl StreamingTestPrinter {
302    /// Create a new streaming test printer (simulates non-terminal).
303    pub fn new() -> Self {
304        Self {
305            write_calls: RefCell::new(Vec::new()),
306            flush_calls: RefCell::new(Vec::new()),
307            simulated_is_terminal: false,
308        }
309    }
310
311    /// Create a new streaming test printer with specified terminal simulation.
312    ///
313    /// # Arguments
314    /// * `is_terminal` - Whether to simulate being connected to a terminal
315    pub fn new_with_terminal(is_terminal: bool) -> Self {
316        Self {
317            write_calls: RefCell::new(Vec::new()),
318            flush_calls: RefCell::new(Vec::new()),
319            simulated_is_terminal: is_terminal,
320        }
321    }
322
323    /// Get all write calls for inspection.
324    pub fn get_write_calls(&self) -> Vec<WriteCall> {
325        self.write_calls.borrow().clone()
326    }
327
328    /// Get the number of write() calls made.
329    pub fn write_count(&self) -> usize {
330        self.write_calls.borrow().len()
331    }
332
333    /// Get the full output (all writes concatenated).
334    pub fn get_full_output(&self) -> String {
335        self.write_calls
336            .borrow()
337            .iter()
338            .map(|w| w.content.clone())
339            .collect()
340    }
341
342    /// Get the content at a specific write index.
343    pub fn get_content_at_write(&self, index: usize) -> Option<String> {
344        self.write_calls
345            .borrow()
346            .get(index)
347            .map(|w| w.content.clone())
348    }
349
350    /// Verify that multiple incremental writes occurred.
351    ///
352    /// # Arguments
353    /// * `min_expected` - Minimum number of writes expected
354    ///
355    /// # Returns
356    /// `Ok(())` if at least `min_expected` writes occurred, `Err` with details otherwise.
357    pub fn verify_incremental_writes(&self, min_expected: usize) -> Result<(), String> {
358        let count = self.write_count();
359        if count >= min_expected {
360            Ok(())
361        } else {
362            Err(format!(
363                "Expected at least {} incremental writes, but only {} occurred. \
364                 This suggests output is batched rather than streamed.",
365                min_expected, count
366            ))
367        }
368    }
369
370    /// Check if the output contains a specific ANSI escape sequence.
371    pub fn contains_escape_sequence(&self, seq: &str) -> bool {
372        self.get_full_output().contains(seq)
373    }
374
375    /// Check if any ANSI escape sequences are present in the output.
376    pub fn has_any_escape_sequences(&self) -> bool {
377        self.get_full_output().contains('\x1b')
378    }
379
380    /// Strip ANSI escape sequences from a string.
381    ///
382    /// Uses a simple state machine approach to remove all ANSI codes.
383    pub fn strip_ansi(s: &str) -> String {
384        let mut result = String::with_capacity(s.len());
385        let mut chars = s.chars().peekable();
386
387        while let Some(c) = chars.next() {
388            if c == '\x1b' {
389                // Skip escape sequence: ESC [ ... letter
390                if chars.peek() == Some(&'[') {
391                    chars.next(); // consume '['
392                                  // Skip until we hit a letter (the terminator)
393                    while let Some(&next) = chars.peek() {
394                        chars.next();
395                        if next.is_ascii_alphabetic() {
396                            break;
397                        }
398                    }
399                }
400            } else {
401                result.push(c);
402            }
403        }
404        result
405    }
406
407    /// Get the content progression across all writes (ANSI stripped).
408    ///
409    /// Returns a vector of accumulated content at each write point,
410    /// useful for verifying that content grows incrementally.
411    pub fn get_content_progression(&self) -> Vec<String> {
412        let mut accumulated = String::new();
413        let mut progression = Vec::new();
414
415        for call in self.write_calls.borrow().iter() {
416            accumulated.push_str(&call.content);
417            // Strip ANSI and control characters for content comparison
418            let clean = Self::strip_ansi(&accumulated)
419                .replace('\r', "")
420                .replace('\n', " ")
421                .trim()
422                .to_string();
423            if !clean.is_empty() {
424                progression.push(clean);
425            }
426        }
427        progression
428    }
429
430    /// Clear all recorded write and flush calls.
431    pub fn clear(&self) {
432        self.write_calls.borrow_mut().clear();
433        self.flush_calls.borrow_mut().clear();
434    }
435
436    /// Get all flush calls for inspection.
437    pub fn get_flush_calls(&self) -> Vec<FlushCall> {
438        self.flush_calls.borrow().clone()
439    }
440
441    /// Get the number of flush() calls made.
442    pub fn flush_count(&self) -> usize {
443        self.flush_calls.borrow().len()
444    }
445
446    /// Verify that flush was called after writes occurred.
447    ///
448    /// This is the critical test for real-time streaming behavior:
449    /// if flush isn't called, output buffers and appears "all at once".
450    ///
451    /// # Returns
452    /// `Ok(())` if at least one flush occurred after writes, `Err` with details otherwise.
453    pub fn verify_flush_after_writes(&self) -> Result<(), String> {
454        let writes = self.write_calls.borrow();
455        let flushes = self.flush_calls.borrow();
456
457        if writes.is_empty() {
458            return Err("No writes occurred".to_string());
459        }
460
461        if flushes.is_empty() {
462            return Err(format!(
463                "No flush() calls occurred after {} write(s). \
464                 This means output is buffered and will appear 'all at once' \
465                 instead of streaming incrementally.",
466                writes.len()
467            ));
468        }
469
470        Ok(())
471    }
472
473    /// Verify that flush was called at least `min_expected` times.
474    ///
475    /// For true streaming, flush should be called after each delta event
476    /// to push content to the user's terminal immediately.
477    pub fn verify_flush_count(&self, min_expected: usize) -> Result<(), String> {
478        let count = self.flush_count();
479        if count >= min_expected {
480            Ok(())
481        } else {
482            Err(format!(
483                "Expected at least {} flush() calls, but only {} occurred. \
484                 This suggests output is not being flushed frequently enough for streaming.",
485                min_expected, count
486            ))
487        }
488    }
489}
490
491#[cfg(any(test, feature = "test-utils"))]
492impl Default for StreamingTestPrinter {
493    fn default() -> Self {
494        Self::new()
495    }
496}
497
498#[cfg(any(test, feature = "test-utils"))]
499impl std::io::Write for StreamingTestPrinter {
500    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
501        let content =
502            std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
503
504        self.write_calls.borrow_mut().push(WriteCall {
505            content: content.to_string(),
506            timestamp: std::time::Instant::now(),
507        });
508
509        Ok(buf.len())
510    }
511
512    fn flush(&mut self) -> io::Result<()> {
513        let last_write_index = if self.write_calls.borrow().is_empty() {
514            None
515        } else {
516            Some(self.write_calls.borrow().len() - 1)
517        };
518        self.flush_calls.borrow_mut().push(FlushCall {
519            last_write_index,
520            timestamp: std::time::Instant::now(),
521        });
522        Ok(())
523    }
524}
525
526#[cfg(any(test, feature = "test-utils"))]
527impl Printable for StreamingTestPrinter {
528    fn is_terminal(&self) -> bool {
529        self.simulated_is_terminal
530    }
531}
532
533/// A virtual terminal that simulates real terminal behavior for testing.
534///
535/// Unlike [`TestPrinter`] which just collects raw output, this accurately simulates
536/// how a real terminal renders text, including:
537///
538/// - **Cursor positioning**: Tracks row and column
539/// - **Carriage return (`\r`)**: Moves cursor to column 0 (doesn't erase)
540/// - **Newline (`\n`)**: Moves cursor to next row, column 0
541/// - **ANSI clear line (`\x1b[2K`)**: Erases entire current line
542/// - **ANSI cursor up (`\x1b[1A`)**: Moves cursor up one row
543/// - **ANSI cursor down (`\x1b[1B`)**: Moves cursor down one row
544/// - **Text overwriting**: Writing after `\r` replaces previous content
545///
546/// This allows tests to verify what the user actually SEES, not just what was written.
547///
548/// # Example
549///
550/// ```ignore
551/// use ralph_workflow::json_parser::printer::VirtualTerminal;
552/// use std::io::Write;
553///
554/// let mut term = VirtualTerminal::new();
555/// write!(term, "Hello").unwrap();
556/// write!(term, "\rWorld").unwrap();  // Overwrites "Hello"
557/// assert_eq!(term.get_visible_output(), "World");
558/// ```
559#[cfg(any(test, feature = "test-utils"))]
560#[derive(Debug)]
561pub struct VirtualTerminal {
562    /// The terminal buffer - each element is a line (row)
563    lines: RefCell<Vec<String>>,
564    /// Current cursor row (0-indexed)
565    cursor_row: RefCell<usize>,
566    /// Current cursor column (0-indexed)
567    cursor_col: RefCell<usize>,
568    /// Whether to simulate terminal mode (affects is_terminal())
569    simulated_is_terminal: bool,
570    /// Raw write history for debugging
571    write_history: RefCell<Vec<String>>,
572}
573
574#[cfg(any(test, feature = "test-utils"))]
575impl VirtualTerminal {
576    /// Create a new virtual terminal (simulates being a TTY by default).
577    pub fn new() -> Self {
578        Self {
579            lines: RefCell::new(vec![String::new()]),
580            cursor_row: RefCell::new(0),
581            cursor_col: RefCell::new(0),
582            simulated_is_terminal: true,
583            write_history: RefCell::new(Vec::new()),
584        }
585    }
586
587    /// Create a new virtual terminal with specified terminal simulation.
588    pub fn new_with_terminal(is_terminal: bool) -> Self {
589        Self {
590            lines: RefCell::new(vec![String::new()]),
591            cursor_row: RefCell::new(0),
592            cursor_col: RefCell::new(0),
593            simulated_is_terminal: is_terminal,
594            write_history: RefCell::new(Vec::new()),
595        }
596    }
597
598    /// Get the visible output as the user would see it.
599    ///
600    /// This returns the final rendered state of the terminal, with all
601    /// ANSI sequences processed and overwrites applied.
602    pub fn get_visible_output(&self) -> String {
603        let lines = self.lines.borrow();
604        // Join non-empty lines, trimming trailing whitespace from each
605        lines
606            .iter()
607            .map(|line| line.trim_end().to_string())
608            .collect::<Vec<_>>()
609            .join("\n")
610    }
611
612    /// Get visible lines (non-empty lines only).
613    pub fn get_visible_lines(&self) -> Vec<String> {
614        self.lines
615            .borrow()
616            .iter()
617            .map(|line| line.trim_end().to_string())
618            .filter(|line| !line.is_empty())
619            .collect()
620    }
621
622    /// Get the raw write history for debugging.
623    pub fn get_write_history(&self) -> Vec<String> {
624        self.write_history.borrow().clone()
625    }
626
627    /// Get current cursor position as (row, col).
628    pub fn cursor_position(&self) -> (usize, usize) {
629        (*self.cursor_row.borrow(), *self.cursor_col.borrow())
630    }
631
632    /// Clear the terminal.
633    pub fn clear(&self) {
634        self.lines.borrow_mut().clear();
635        self.lines.borrow_mut().push(String::new());
636        *self.cursor_row.borrow_mut() = 0;
637        *self.cursor_col.borrow_mut() = 0;
638        self.write_history.borrow_mut().clear();
639    }
640
641    /// Ensure the current row exists in the buffer.
642    fn ensure_row_exists(&self) {
643        let row = *self.cursor_row.borrow();
644        let mut lines = self.lines.borrow_mut();
645        while lines.len() <= row {
646            lines.push(String::new());
647        }
648    }
649
650    /// Write a character at the current cursor position.
651    /// Write a string of regular characters at the current cursor position.
652    /// This is more efficient than write_char for multiple characters.
653    fn write_str(&self, s: &str) {
654        if s.is_empty() {
655            return;
656        }
657
658        self.ensure_row_exists();
659        let row = *self.cursor_row.borrow();
660        let col = *self.cursor_col.borrow();
661        let mut lines = self.lines.borrow_mut();
662        let line = &mut lines[row];
663
664        // Extend the line with spaces if needed
665        while line.chars().count() < col {
666            line.push(' ');
667        }
668
669        // Build new line: prefix + new content + suffix
670        let prefix: String = line.chars().take(col).collect();
671        let suffix: String = line.chars().skip(col + s.chars().count()).collect();
672        *line = format!("{}{}{}", prefix, s, suffix);
673
674        // Move cursor right
675        *self.cursor_col.borrow_mut() = col + s.chars().count();
676    }
677
678    /// Clear the current line.
679    fn clear_line(&self) {
680        self.ensure_row_exists();
681        let row = *self.cursor_row.borrow();
682        let mut lines = self.lines.borrow_mut();
683        lines[row].clear();
684        // Note: cursor position is NOT changed by clear line
685    }
686
687    /// Move cursor up n rows.
688    fn cursor_up(&self, n: usize) {
689        let mut row = self.cursor_row.borrow_mut();
690        *row = row.saturating_sub(n);
691    }
692
693    /// Move cursor down n rows.
694    fn cursor_down(&self, n: usize) {
695        *self.cursor_row.borrow_mut() += n;
696        self.ensure_row_exists();
697    }
698
699    /// Process a string, interpreting control characters and ANSI sequences.
700    fn process_string(&self, s: &str) {
701        let mut chars = s.chars().peekable();
702        let mut text_buffer = String::new();
703
704        // Flush accumulated text to the terminal
705        let flush_text = |term: &Self, buf: &mut String| {
706            if !buf.is_empty() {
707                term.write_str(buf);
708                buf.clear();
709            }
710        };
711
712        while let Some(c) = chars.next() {
713            match c {
714                '\r' => {
715                    flush_text(self, &mut text_buffer);
716                    // Carriage return: move to column 0
717                    *self.cursor_col.borrow_mut() = 0;
718                }
719                '\n' => {
720                    flush_text(self, &mut text_buffer);
721                    // Newline: move to next row, column 0
722                    *self.cursor_row.borrow_mut() += 1;
723                    *self.cursor_col.borrow_mut() = 0;
724                    self.ensure_row_exists();
725                }
726                '\x1b' => {
727                    flush_text(self, &mut text_buffer);
728                    // ANSI escape sequence
729                    if chars.peek() == Some(&'[') {
730                        chars.next(); // consume '['
731
732                        // Parse the numeric parameter (if any)
733                        let mut param = String::new();
734                        while let Some(&c) = chars.peek() {
735                            if c.is_ascii_digit() {
736                                param.push(c);
737                                chars.next();
738                            } else {
739                                break;
740                            }
741                        }
742
743                        // Get the command character
744                        if let Some(cmd) = chars.next() {
745                            let n: usize = param.parse().unwrap_or(1);
746                            match cmd {
747                                'A' => self.cursor_up(n),   // Cursor up
748                                'B' => self.cursor_down(n), // Cursor down
749                                'K' => {
750                                    // Erase in line
751                                    // \x1b[K or \x1b[0K - erase from cursor to end
752                                    // \x1b[1K - erase from start to cursor
753                                    // \x1b[2K - erase entire line
754                                    let mode: usize = param.parse().unwrap_or(0);
755                                    if mode == 2 {
756                                        self.clear_line();
757                                    }
758                                    // For now, we only implement mode 2 (full line clear)
759                                    // which is what the streaming code uses
760                                }
761                                'm' => {
762                                    // SGR (Select Graphic Rendition) - colors/styles
763                                    // We ignore these as they don't affect text content
764                                }
765                                _ => {
766                                    // Unknown command, ignore
767                                }
768                            }
769                        }
770                    }
771                }
772                _ => {
773                    // Regular character: buffer it for batch writing
774                    text_buffer.push(c);
775                }
776            }
777        }
778
779        // Flush any remaining text
780        flush_text(self, &mut text_buffer);
781    }
782
783    /// Check for duplicate visible lines (useful for detecting rendering bugs).
784    pub fn has_duplicate_lines(&self) -> bool {
785        let lines = self.get_visible_lines();
786        for i in 1..lines.len() {
787            if !lines[i].is_empty() && lines[i] == lines[i - 1] {
788                return true;
789            }
790        }
791        false
792    }
793
794    /// Count occurrences of a pattern in the visible output.
795    pub fn count_visible_pattern(&self, pattern: &str) -> usize {
796        self.get_visible_output().matches(pattern).count()
797    }
798}
799
800#[cfg(any(test, feature = "test-utils"))]
801impl Default for VirtualTerminal {
802    fn default() -> Self {
803        Self::new()
804    }
805}
806
807#[cfg(any(test, feature = "test-utils"))]
808impl std::io::Write for VirtualTerminal {
809    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
810        let s =
811            std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
812
813        // Record raw write for debugging
814        self.write_history.borrow_mut().push(s.to_string());
815
816        // Process the string through the terminal emulator
817        self.process_string(s);
818
819        Ok(buf.len())
820    }
821
822    fn flush(&mut self) -> io::Result<()> {
823        // Virtual terminal doesn't need flushing - content is immediately available
824        Ok(())
825    }
826}
827
828#[cfg(any(test, feature = "test-utils"))]
829impl Printable for VirtualTerminal {
830    fn is_terminal(&self) -> bool {
831        self.simulated_is_terminal
832    }
833}
834
835/// Shared printer reference for use in parsers.
836///
837/// This type alias represents a shared, mutable reference to a printer
838/// that can be used across parser methods.
839pub type SharedPrinter = Rc<RefCell<dyn Printable>>;
840
841/// Create a shared stdout printer.
842pub fn shared_stdout() -> SharedPrinter {
843    Rc::new(RefCell::new(StdoutPrinter::new()))
844}
845
846#[cfg(test)]
847mod tests {
848    use super::*;
849    use std::io::Write;
850
851    #[test]
852    fn test_stdout_printer() {
853        let mut printer = StdoutPrinter::new();
854        // Just ensure it compiles and works
855        let result = printer.write_all(b"test\n");
856        assert!(result.is_ok());
857        assert!(printer.flush().is_ok());
858
859        // Verify is_terminal() method is accessible
860        let _is_term = printer.is_terminal();
861    }
862
863    #[cfg(test)]
864    #[test]
865    fn test_printable_trait_is_terminal() {
866        let printer = StdoutPrinter::new();
867        // Test that the Printable trait's is_terminal method works
868        let _should_use_colors = printer.is_terminal();
869    }
870
871    #[test]
872    #[cfg(any(test, feature = "test-utils"))]
873    fn test_stderr_printer() {
874        let mut printer = StderrPrinter::new();
875        // Just ensure it compiles and works
876        let result = printer.write_all(b"test\n");
877        assert!(result.is_ok());
878        assert!(printer.flush().is_ok());
879    }
880
881    #[test]
882    #[cfg(any(test, feature = "test-utils"))]
883    fn test_printer_captures_output() {
884        let mut printer = TestPrinter::new();
885
886        printer
887            .write_all(b"Hello World\n")
888            .expect("Failed to write");
889        printer.flush().expect("Failed to flush");
890
891        let output = printer.get_output();
892        assert!(output.contains("Hello World"));
893    }
894
895    #[test]
896    #[cfg(any(test, feature = "test-utils"))]
897    fn test_printer_get_lines() {
898        let mut printer = TestPrinter::new();
899
900        printer.write_all(b"Line 1\nLine 2\n").unwrap();
901        printer.flush().unwrap();
902
903        let lines = printer.get_lines();
904        assert_eq!(lines.len(), 2);
905        assert!(lines[0].contains("Line 1"));
906        assert!(lines[1].contains("Line 2"));
907    }
908
909    #[test]
910    #[cfg(any(test, feature = "test-utils"))]
911    fn test_printer_clear() {
912        let mut printer = TestPrinter::new();
913
914        printer.write_all(b"Before\n").unwrap();
915        printer.flush().unwrap();
916
917        assert!(!printer.get_output().is_empty());
918
919        printer.clear();
920        assert!(printer.get_output().is_empty());
921    }
922
923    #[cfg(any(test, feature = "test-utils"))]
924    #[test]
925    fn test_printer_has_line() {
926        let mut printer = TestPrinter::new();
927
928        printer.write_all(b"Hello World\n").unwrap();
929        printer.flush().unwrap();
930
931        assert!(printer.has_line("Hello"));
932        assert!(printer.has_line("World"));
933        assert!(!printer.has_line("Goodbye"));
934    }
935
936    #[cfg(any(test, feature = "test-utils"))]
937    #[test]
938    fn test_printer_count_pattern() {
939        let mut printer = TestPrinter::new();
940
941        printer.write_all(b"test\nmore test\ntest again\n").unwrap();
942        printer.flush().unwrap();
943
944        assert_eq!(printer.count_pattern("test"), 3);
945    }
946
947    #[cfg(any(test, feature = "test-utils"))]
948    #[test]
949    fn test_printer_detects_duplicates() {
950        let mut printer = TestPrinter::new();
951
952        printer.write_all(b"Line 1\nLine 1\nLine 2\n").unwrap();
953        printer.flush().unwrap();
954
955        assert!(printer.has_duplicate_consecutive_lines());
956    }
957
958    #[cfg(any(test, feature = "test-utils"))]
959    #[test]
960    fn test_printer_finds_duplicates() {
961        let mut printer = TestPrinter::new();
962
963        printer
964            .write_all(b"Line 1\nLine 1\nLine 2\nLine 3\nLine 3\n")
965            .unwrap();
966        printer.flush().unwrap();
967
968        let duplicates = printer.find_duplicate_consecutive_lines();
969        assert_eq!(duplicates.len(), 2);
970        assert_eq!(duplicates[0].0, 0); // First duplicate at line 0-1
971        assert_eq!(duplicates[0].1, "Line 1\n");
972        assert_eq!(duplicates[1].0, 3); // Second duplicate at line 3-4
973        assert_eq!(duplicates[1].1, "Line 3\n");
974    }
975
976    #[cfg(any(test, feature = "test-utils"))]
977    #[test]
978    fn test_printer_no_false_positives() {
979        let mut printer = TestPrinter::new();
980
981        printer.write_all(b"Line 1\nLine 2\nLine 3\n").unwrap();
982        printer.flush().unwrap();
983
984        assert!(!printer.has_duplicate_consecutive_lines());
985    }
986
987    #[cfg(any(test, feature = "test-utils"))]
988    #[test]
989    fn test_printer_buffer_handling() {
990        let mut printer = TestPrinter::new();
991
992        // Write without newline - buffer should hold it
993        printer.write_all(b"Partial").unwrap();
994
995        // Without flush, content is in buffer but accessible via get_output/get_lines
996        // The TestPrinter stores partial content in buffer which is included in get_output
997        assert!(printer.get_output().contains("Partial"));
998
999        // Add newline to complete the line
1000        printer.write_all(b" content\n").unwrap();
1001        printer.flush().unwrap();
1002
1003        // Now should have the complete content
1004        assert!(printer.has_line("Partial content"));
1005
1006        // Verify the complete output
1007        let output = printer.get_output();
1008        assert!(output.contains("Partial content\n"));
1009    }
1010
1011    #[cfg(any(test, feature = "test-utils"))]
1012    #[test]
1013    fn test_printer_get_stats() {
1014        let mut printer = TestPrinter::new();
1015
1016        printer.write_all(b"Line 1\nLine 2\n").unwrap();
1017        printer.flush().unwrap();
1018
1019        let (line_count, char_count) = printer.get_stats();
1020        assert_eq!(line_count, 2);
1021        assert!(char_count > 0);
1022    }
1023
1024    #[test]
1025    fn test_shared_stdout() {
1026        let printer = shared_stdout();
1027        // Verify the function creates a valid SharedPrinter
1028        let _borrowed = printer.borrow();
1029    }
1030
1031    #[cfg(any(test, feature = "test-utils"))]
1032    #[test]
1033    fn test_streaming_printer_captures_individual_writes() {
1034        let mut printer = StreamingTestPrinter::new();
1035
1036        printer.write_all(b"Hello").unwrap();
1037        printer.write_all(b" ").unwrap();
1038        printer.write_all(b"World").unwrap();
1039
1040        assert_eq!(printer.write_count(), 3);
1041        assert_eq!(printer.get_full_output(), "Hello World");
1042    }
1043
1044    #[cfg(any(test, feature = "test-utils"))]
1045    #[test]
1046    fn test_streaming_printer_verify_incremental_writes() {
1047        let mut printer = StreamingTestPrinter::new();
1048
1049        printer.write_all(b"A").unwrap();
1050        printer.write_all(b"B").unwrap();
1051        printer.write_all(b"C").unwrap();
1052        printer.write_all(b"D").unwrap();
1053
1054        assert!(printer.verify_incremental_writes(4).is_ok());
1055        assert!(printer.verify_incremental_writes(5).is_err());
1056    }
1057
1058    #[cfg(any(test, feature = "test-utils"))]
1059    #[test]
1060    fn test_streaming_printer_detects_escape_sequences() {
1061        let mut printer = StreamingTestPrinter::new();
1062
1063        printer.write_all(b"Normal text").unwrap();
1064        assert!(!printer.has_any_escape_sequences());
1065
1066        printer.clear();
1067        printer.write_all(b"\x1b[2K\rUpdated").unwrap();
1068        assert!(printer.has_any_escape_sequences());
1069        assert!(printer.contains_escape_sequence("\x1b[2K"));
1070    }
1071
1072    #[cfg(any(test, feature = "test-utils"))]
1073    #[test]
1074    fn test_streaming_printer_strip_ansi() {
1075        let input = "\x1b[2K\r\x1b[1mBold\x1b[0m text\x1b[1A";
1076        let stripped = StreamingTestPrinter::strip_ansi(input);
1077        assert_eq!(stripped, "\rBold text");
1078    }
1079
1080    #[cfg(any(test, feature = "test-utils"))]
1081    #[test]
1082    fn test_streaming_printer_content_progression() {
1083        let mut printer = StreamingTestPrinter::new();
1084
1085        printer.write_all(b"[agent] Hello\n").unwrap();
1086        printer
1087            .write_all(b"\x1b[2K\r[agent] Hello World\n")
1088            .unwrap();
1089
1090        let progression = printer.get_content_progression();
1091        assert!(progression.len() >= 1);
1092        // Later entries should contain more content
1093        if progression.len() >= 2 {
1094            assert!(progression[1].len() >= progression[0].len());
1095        }
1096    }
1097
1098    #[cfg(any(test, feature = "test-utils"))]
1099    #[test]
1100    fn test_streaming_printer_terminal_simulation() {
1101        let printer_non_term = StreamingTestPrinter::new();
1102        assert!(!printer_non_term.is_terminal());
1103
1104        let printer_term = StreamingTestPrinter::new_with_terminal(true);
1105        assert!(printer_term.is_terminal());
1106    }
1107
1108    #[cfg(any(test, feature = "test-utils"))]
1109    #[test]
1110    fn test_streaming_printer_get_content_at_write() {
1111        let mut printer = StreamingTestPrinter::new();
1112
1113        printer.write_all(b"First").unwrap();
1114        printer.write_all(b"Second").unwrap();
1115        printer.write_all(b"Third").unwrap();
1116
1117        assert_eq!(printer.get_content_at_write(0), Some("First".to_string()));
1118        assert_eq!(printer.get_content_at_write(1), Some("Second".to_string()));
1119        assert_eq!(printer.get_content_at_write(2), Some("Third".to_string()));
1120        assert_eq!(printer.get_content_at_write(3), None);
1121    }
1122
1123    #[cfg(any(test, feature = "test-utils"))]
1124    #[test]
1125    fn test_streaming_printer_clear() {
1126        let mut printer = StreamingTestPrinter::new();
1127
1128        printer.write_all(b"Some content").unwrap();
1129        assert_eq!(printer.write_count(), 1);
1130
1131        printer.clear();
1132        assert_eq!(printer.write_count(), 0);
1133        assert!(printer.get_full_output().is_empty());
1134    }
1135
1136    // =========================================================================
1137    // VirtualTerminal Tests
1138    // =========================================================================
1139
1140    #[cfg(any(test, feature = "test-utils"))]
1141    #[test]
1142    fn test_virtual_terminal_simple_text() {
1143        let mut term = VirtualTerminal::new();
1144        write!(term, "Hello World").unwrap();
1145        assert_eq!(term.get_visible_output(), "Hello World");
1146    }
1147
1148    #[cfg(any(test, feature = "test-utils"))]
1149    #[test]
1150    fn test_virtual_terminal_newlines() {
1151        let mut term = VirtualTerminal::new();
1152        write!(term, "Line 1\nLine 2\nLine 3").unwrap();
1153        assert_eq!(term.get_visible_output(), "Line 1\nLine 2\nLine 3");
1154    }
1155
1156    #[cfg(any(test, feature = "test-utils"))]
1157    #[test]
1158    fn test_virtual_terminal_carriage_return_overwrites() {
1159        let mut term = VirtualTerminal::new();
1160        // Write "Hello", then \r moves to start, then "World" overwrites
1161        write!(term, "Hello\rWorld").unwrap();
1162        assert_eq!(term.get_visible_output(), "World");
1163    }
1164
1165    #[cfg(any(test, feature = "test-utils"))]
1166    #[test]
1167    fn test_virtual_terminal_carriage_return_partial_overwrite() {
1168        let mut term = VirtualTerminal::new();
1169        // "Hello World" then \r moves to start, "Hi" overwrites first 2 chars
1170        write!(term, "Hello World\rHi").unwrap();
1171        // Result: "Hillo World" (only first 2 chars overwritten)
1172        assert_eq!(term.get_visible_output(), "Hillo World");
1173    }
1174
1175    #[cfg(any(test, feature = "test-utils"))]
1176    #[test]
1177    fn test_virtual_terminal_ansi_clear_line() {
1178        let mut term = VirtualTerminal::new();
1179        // Write text, clear line, write new text
1180        write!(term, "Old text\x1b[2K\rNew text").unwrap();
1181        assert_eq!(term.get_visible_output(), "New text");
1182    }
1183
1184    #[cfg(any(test, feature = "test-utils"))]
1185    #[test]
1186    fn test_virtual_terminal_cursor_up() {
1187        let mut term = VirtualTerminal::new();
1188        // Line 1, newline, Line 2, cursor up, overwrite Line 1
1189        write!(term, "Line 1\nLine 2\x1b[1A\rOverwritten").unwrap();
1190        let lines = term.get_visible_lines();
1191        assert_eq!(lines.len(), 2);
1192        assert_eq!(lines[0], "Overwritten");
1193        assert_eq!(lines[1], "Line 2");
1194    }
1195
1196    #[cfg(any(test, feature = "test-utils"))]
1197    #[test]
1198    fn test_virtual_terminal_cursor_down() {
1199        let mut term = VirtualTerminal::new();
1200        // Write on row 0, move down, write on row 1
1201        write!(term, "Row 0\x1b[1B\rRow 1").unwrap();
1202        let output = term.get_visible_output();
1203        assert!(output.contains("Row 0"));
1204        assert!(output.contains("Row 1"));
1205    }
1206
1207    #[cfg(any(test, feature = "test-utils"))]
1208    #[test]
1209    fn test_virtual_terminal_streaming_simulation() {
1210        // Simulate the actual streaming pattern used by parsers:
1211        // 1. Write "[agent] Hello" + newline + cursor up
1212        // 2. Clear line + carriage return + write "[agent] Hello World" + newline + cursor up
1213        // 3. Cursor down at end
1214        let mut term = VirtualTerminal::new();
1215
1216        // First delta
1217        write!(term, "[agent] Hello\n\x1b[1A").unwrap();
1218        assert_eq!(term.get_visible_lines(), vec!["[agent] Hello"]);
1219
1220        // Second delta (updates in place)
1221        write!(term, "\x1b[2K\r[agent] Hello World\n\x1b[1A").unwrap();
1222        assert_eq!(term.get_visible_lines(), vec!["[agent] Hello World"]);
1223
1224        // Completion (cursor down)
1225        write!(term, "\x1b[1B\n").unwrap();
1226        // Should still show the final content
1227        assert!(term.get_visible_output().contains("[agent] Hello World"));
1228    }
1229
1230    #[cfg(any(test, feature = "test-utils"))]
1231    #[test]
1232    fn test_virtual_terminal_no_duplicate_lines_in_streaming() {
1233        let mut term = VirtualTerminal::new();
1234
1235        // Simulate streaming with in-place updates
1236        write!(term, "[agent] A\n\x1b[1A").unwrap();
1237        write!(term, "\x1b[2K\r[agent] AB\n\x1b[1A").unwrap();
1238        write!(term, "\x1b[2K\r[agent] ABC\n\x1b[1A").unwrap();
1239        write!(term, "\x1b[1B\n").unwrap();
1240
1241        // Should NOT have duplicate lines
1242        assert!(
1243            !term.has_duplicate_lines(),
1244            "Virtual terminal should not show duplicate lines after streaming. Got: {:?}",
1245            term.get_visible_lines()
1246        );
1247
1248        // Final content should be the complete message
1249        assert!(term.get_visible_output().contains("[agent] ABC"));
1250    }
1251
1252    #[cfg(any(test, feature = "test-utils"))]
1253    #[test]
1254    fn test_virtual_terminal_ignores_color_codes() {
1255        let mut term = VirtualTerminal::new();
1256        // Write with color codes (SGR sequences)
1257        write!(term, "\x1b[32mGreen\x1b[0m Normal").unwrap();
1258        assert_eq!(term.get_visible_output(), "Green Normal");
1259    }
1260
1261    #[cfg(any(test, feature = "test-utils"))]
1262    #[test]
1263    fn test_virtual_terminal_is_terminal() {
1264        let term_tty = VirtualTerminal::new();
1265        assert!(term_tty.is_terminal());
1266
1267        let term_non_tty = VirtualTerminal::new_with_terminal(false);
1268        assert!(!term_non_tty.is_terminal());
1269    }
1270
1271    #[cfg(any(test, feature = "test-utils"))]
1272    #[test]
1273    fn test_virtual_terminal_cursor_position() {
1274        let mut term = VirtualTerminal::new();
1275
1276        assert_eq!(term.cursor_position(), (0, 0));
1277
1278        write!(term, "Hello").unwrap();
1279        assert_eq!(term.cursor_position(), (0, 5));
1280
1281        write!(term, "\n").unwrap();
1282        assert_eq!(term.cursor_position(), (1, 0));
1283
1284        write!(term, "World").unwrap();
1285        assert_eq!(term.cursor_position(), (1, 5));
1286
1287        write!(term, "\r").unwrap();
1288        assert_eq!(term.cursor_position(), (1, 0));
1289    }
1290
1291    #[cfg(any(test, feature = "test-utils"))]
1292    #[test]
1293    fn test_virtual_terminal_count_pattern() {
1294        let mut term = VirtualTerminal::new();
1295        write!(term, "Hello World\nHello Again\nGoodbye").unwrap();
1296        assert_eq!(term.count_visible_pattern("Hello"), 2);
1297        assert_eq!(term.count_visible_pattern("Goodbye"), 1);
1298        assert_eq!(term.count_visible_pattern("NotFound"), 0);
1299    }
1300
1301    #[cfg(any(test, feature = "test-utils"))]
1302    #[test]
1303    fn test_virtual_terminal_clear() {
1304        let mut term = VirtualTerminal::new();
1305        write!(term, "Some content\nMore content").unwrap();
1306        assert!(!term.get_visible_output().is_empty());
1307
1308        term.clear();
1309        assert!(term.get_visible_output().is_empty());
1310        assert_eq!(term.cursor_position(), (0, 0));
1311    }
1312
1313    #[cfg(any(test, feature = "test-utils"))]
1314    #[test]
1315    fn test_virtual_terminal_write_history() {
1316        let mut term = VirtualTerminal::new();
1317        write!(term, "First").unwrap();
1318        write!(term, "Second").unwrap();
1319        write!(term, "Third").unwrap();
1320
1321        let history = term.get_write_history();
1322        assert_eq!(history.len(), 3);
1323        assert_eq!(history[0], "First");
1324        assert_eq!(history[1], "Second");
1325        assert_eq!(history[2], "Third");
1326    }
1327}