Skip to main content

ftui_runtime/
simulator.rs

1#![forbid(unsafe_code)]
2
3//! Deterministic program simulator for testing.
4//!
5//! `ProgramSimulator` runs a [`Model`] without a real terminal, enabling
6//! deterministic snapshot testing, event injection, and frame capture.
7//!
8//! # Example
9//!
10//! ```ignore
11//! use ftui_runtime::simulator::ProgramSimulator;
12//!
13//! let mut sim = ProgramSimulator::new(Counter { value: 0 });
14//! sim.init();
15//! sim.send(Msg::Increment);
16//! assert_eq!(sim.model().value, 1);
17//!
18//! let buf = sim.capture_frame(80, 24);
19//! // Assert on buffer contents...
20//! ```
21
22use crate::program::{Cmd, Model};
23use crate::state_persistence::StateRegistry;
24use ftui_core::event::Event;
25use ftui_render::buffer::Buffer;
26use ftui_render::frame::Frame;
27use ftui_render::grapheme_pool::GraphemePool;
28use std::sync::Arc;
29use std::time::Duration;
30
31/// Record of a command that was executed during simulation.
32#[derive(Debug, Clone)]
33pub enum CmdRecord {
34    /// No-op command.
35    None,
36    /// Quit command.
37    Quit,
38    /// Message sent to model (not stored, just noted).
39    Msg,
40    /// Batch of commands.
41    Batch(usize),
42    /// Sequence of commands.
43    Sequence(usize),
44    /// Tick scheduled.
45    Tick(Duration),
46    /// Log message emitted.
47    Log(String),
48    /// Background task executed synchronously.
49    Task,
50    /// Mouse capture toggle (no-op in simulator).
51    MouseCapture(bool),
52}
53
54/// Deterministic simulator for [`Model`] testing.
55///
56/// Runs model logic without any terminal or IO dependencies. Events can be
57/// injected, messages sent directly, and frames captured for snapshot testing.
58pub struct ProgramSimulator<M: Model> {
59    /// The application model.
60    model: M,
61    /// Grapheme pool for frame creation.
62    pool: GraphemePool,
63    /// Captured frame buffers.
64    frames: Vec<Buffer>,
65    /// Record of all executed commands.
66    command_log: Vec<CmdRecord>,
67    /// Whether the simulated program is still running.
68    running: bool,
69    /// Current tick rate (if any).
70    tick_rate: Option<Duration>,
71    /// Log messages emitted via Cmd::Log.
72    logs: Vec<String>,
73    /// Optional state registry for persistence integration.
74    state_registry: Option<Arc<StateRegistry>>,
75}
76
77impl<M: Model> ProgramSimulator<M> {
78    /// Create a new simulator with the given model.
79    ///
80    /// The model is not initialized until [`init`](Self::init) is called.
81    pub fn new(model: M) -> Self {
82        Self {
83            model,
84            pool: GraphemePool::new(),
85            frames: Vec::new(),
86            command_log: Vec::new(),
87            running: true,
88            tick_rate: None,
89            logs: Vec::new(),
90            state_registry: None,
91        }
92    }
93
94    /// Create a new simulator with the given model and persistence registry.
95    ///
96    /// When provided, `Cmd::SaveState`/`Cmd::RestoreState` will flush/load
97    /// through the registry, mirroring runtime behavior.
98    pub fn with_registry(model: M, registry: Arc<StateRegistry>) -> Self {
99        let mut sim = Self::new(model);
100        sim.state_registry = Some(registry);
101        sim
102    }
103
104    /// Initialize the model by calling `Model::init()` and executing returned commands.
105    ///
106    /// Should be called once before injecting events or capturing frames.
107    pub fn init(&mut self) {
108        let cmd = self.model.init();
109        self.execute_cmd(cmd);
110    }
111
112    /// Inject terminal events into the model.
113    ///
114    /// Each event is converted to a message via `From<Event>` and dispatched
115    /// through `Model::update()`. Commands returned from update are executed.
116    pub fn inject_events(&mut self, events: &[Event]) {
117        for event in events {
118            if !self.running {
119                break;
120            }
121            let msg = M::Message::from(event.clone());
122            let cmd = self.model.update(msg);
123            self.execute_cmd(cmd);
124        }
125    }
126
127    /// Inject a single terminal event into the model.
128    ///
129    /// The event is converted to a message via `From<Event>` and dispatched
130    /// through `Model::update()`. Commands returned from update are executed.
131    pub fn inject_event(&mut self, event: Event) {
132        self.inject_events(&[event]);
133    }
134
135    /// Send a specific message to the model.
136    ///
137    /// The message is dispatched through `Model::update()` and returned
138    /// commands are executed.
139    pub fn send(&mut self, msg: M::Message) {
140        if !self.running {
141            return;
142        }
143        let cmd = self.model.update(msg);
144        self.execute_cmd(cmd);
145    }
146
147    /// Capture the current frame at the given dimensions.
148    ///
149    /// Calls `Model::view()` to render into a fresh buffer and stores the
150    /// result. Returns a reference to the captured buffer.
151    pub fn capture_frame(&mut self, width: u16, height: u16) -> &Buffer {
152        let mut frame = Frame::new(width, height, &mut self.pool);
153        self.model.view(&mut frame);
154        self.frames.push(frame.buffer);
155        self.frames.last().expect("frame just pushed")
156    }
157
158    /// Get all captured frame buffers.
159    pub fn frames(&self) -> &[Buffer] {
160        &self.frames
161    }
162
163    /// Get the most recently captured frame buffer, if any.
164    pub fn last_frame(&self) -> Option<&Buffer> {
165        self.frames.last()
166    }
167
168    /// Get the number of captured frames.
169    pub fn frame_count(&self) -> usize {
170        self.frames.len()
171    }
172
173    /// Get a reference to the model.
174    #[inline]
175    pub fn model(&self) -> &M {
176        &self.model
177    }
178
179    /// Get a mutable reference to the model.
180    #[inline]
181    pub fn model_mut(&mut self) -> &mut M {
182        &mut self.model
183    }
184
185    /// Access the simulator grapheme pool used to render captured frames.
186    #[inline]
187    pub fn pool(&self) -> &GraphemePool {
188        &self.pool
189    }
190
191    /// Check if the simulated program is still running.
192    ///
193    /// Returns `false` after a `Cmd::Quit` has been executed.
194    #[inline]
195    pub fn is_running(&self) -> bool {
196        self.running
197    }
198
199    /// Get the current tick rate (if any).
200    #[inline]
201    pub fn tick_rate(&self) -> Option<Duration> {
202        self.tick_rate
203    }
204
205    /// Get all log messages emitted via `Cmd::Log`.
206    #[inline]
207    pub fn logs(&self) -> &[String] {
208        &self.logs
209    }
210
211    /// Get the command execution log.
212    #[inline]
213    pub fn command_log(&self) -> &[CmdRecord] {
214        &self.command_log
215    }
216
217    /// Clear all captured frames.
218    pub fn clear_frames(&mut self) {
219        self.frames.clear();
220    }
221
222    /// Clear all logs.
223    pub fn clear_logs(&mut self) {
224        self.logs.clear();
225    }
226
227    /// Execute a command without IO.
228    ///
229    /// Cmd::Msg recurses through update; Cmd::Log records the text;
230    /// IO-dependent operations are simulated (no real terminal writes).
231    /// Save/Restore use the configured registry when present.
232    fn execute_cmd(&mut self, cmd: Cmd<M::Message>) {
233        match cmd {
234            Cmd::None => {
235                self.command_log.push(CmdRecord::None);
236            }
237            Cmd::Quit => {
238                self.running = false;
239                self.command_log.push(CmdRecord::Quit);
240            }
241            Cmd::Msg(m) => {
242                self.command_log.push(CmdRecord::Msg);
243                let cmd = self.model.update(m);
244                self.execute_cmd(cmd);
245            }
246            Cmd::Batch(cmds) => {
247                let count = cmds.len();
248                self.command_log.push(CmdRecord::Batch(count));
249                for c in cmds {
250                    self.execute_cmd(c);
251                    if !self.running {
252                        break;
253                    }
254                }
255            }
256            Cmd::Sequence(cmds) => {
257                let count = cmds.len();
258                self.command_log.push(CmdRecord::Sequence(count));
259                for c in cmds {
260                    self.execute_cmd(c);
261                    if !self.running {
262                        break;
263                    }
264                }
265            }
266            Cmd::Tick(duration) => {
267                self.tick_rate = Some(duration);
268                self.command_log.push(CmdRecord::Tick(duration));
269            }
270            Cmd::Log(text) => {
271                self.command_log.push(CmdRecord::Log(text.clone()));
272                self.logs.push(text);
273            }
274            Cmd::SetMouseCapture(enabled) => {
275                self.command_log.push(CmdRecord::MouseCapture(enabled));
276            }
277            Cmd::Task(_, f) => {
278                self.command_log.push(CmdRecord::Task);
279                let msg = f();
280                let cmd = self.model.update(msg);
281                self.execute_cmd(cmd);
282            }
283            Cmd::SaveState => {
284                if let Some(registry) = &self.state_registry {
285                    let _ = registry.flush();
286                }
287            }
288            Cmd::RestoreState => {
289                if let Some(registry) = &self.state_registry {
290                    let _ = registry.load();
291                }
292            }
293        }
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use ftui_core::event::{KeyCode, KeyEvent, KeyEventKind, Modifiers};
301    use std::cell::RefCell;
302    use std::sync::Arc;
303
304    // ---------- Test model ----------
305
306    struct Counter {
307        value: i32,
308        initialized: bool,
309    }
310
311    #[derive(Debug)]
312    enum CounterMsg {
313        Increment,
314        Decrement,
315        Reset,
316        Quit,
317        LogValue,
318        BatchIncrement(usize),
319    }
320
321    impl From<Event> for CounterMsg {
322        fn from(event: Event) -> Self {
323            match event {
324                Event::Key(k) if k.code == KeyCode::Char('+') => CounterMsg::Increment,
325                Event::Key(k) if k.code == KeyCode::Char('-') => CounterMsg::Decrement,
326                Event::Key(k) if k.code == KeyCode::Char('r') => CounterMsg::Reset,
327                Event::Key(k) if k.code == KeyCode::Char('q') => CounterMsg::Quit,
328                _ => CounterMsg::Increment,
329            }
330        }
331    }
332
333    impl Model for Counter {
334        type Message = CounterMsg;
335
336        fn init(&mut self) -> Cmd<Self::Message> {
337            self.initialized = true;
338            Cmd::none()
339        }
340
341        fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
342            match msg {
343                CounterMsg::Increment => {
344                    self.value += 1;
345                    Cmd::none()
346                }
347                CounterMsg::Decrement => {
348                    self.value -= 1;
349                    Cmd::none()
350                }
351                CounterMsg::Reset => {
352                    self.value = 0;
353                    Cmd::none()
354                }
355                CounterMsg::Quit => Cmd::quit(),
356                CounterMsg::LogValue => Cmd::log(format!("value={}", self.value)),
357                CounterMsg::BatchIncrement(n) => {
358                    let cmds: Vec<_> = (0..n).map(|_| Cmd::msg(CounterMsg::Increment)).collect();
359                    Cmd::batch(cmds)
360                }
361            }
362        }
363
364        fn view(&self, frame: &mut Frame) {
365            // Render counter value as text in the first row
366            let text = format!("Count: {}", self.value);
367            for (i, c) in text.chars().enumerate() {
368                if (i as u16) < frame.width() {
369                    use ftui_render::cell::Cell;
370                    frame.buffer.set_raw(i as u16, 0, Cell::from_char(c));
371                }
372            }
373        }
374    }
375
376    fn key_event(c: char) -> Event {
377        Event::Key(KeyEvent {
378            code: KeyCode::Char(c),
379            modifiers: Modifiers::empty(),
380            kind: KeyEventKind::Press,
381        })
382    }
383
384    fn resize_event(width: u16, height: u16) -> Event {
385        Event::Resize { width, height }
386    }
387
388    #[derive(Default)]
389    struct ResizeTracker {
390        last: Option<(u16, u16)>,
391        history: Vec<(u16, u16)>,
392    }
393
394    #[derive(Debug, Clone, Copy)]
395    enum ResizeMsg {
396        Resize(u16, u16),
397        Quit,
398        Noop,
399    }
400
401    impl From<Event> for ResizeMsg {
402        fn from(event: Event) -> Self {
403            match event {
404                Event::Resize { width, height } => Self::Resize(width, height),
405                Event::Key(k) if k.code == KeyCode::Char('q') => Self::Quit,
406                _ => Self::Noop,
407            }
408        }
409    }
410
411    impl Model for ResizeTracker {
412        type Message = ResizeMsg;
413
414        fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
415            match msg {
416                ResizeMsg::Resize(width, height) => {
417                    self.last = Some((width, height));
418                    self.history.push((width, height));
419                    Cmd::none()
420                }
421                ResizeMsg::Quit => Cmd::quit(),
422                ResizeMsg::Noop => Cmd::none(),
423            }
424        }
425
426        fn view(&self, _frame: &mut Frame) {}
427    }
428
429    #[derive(Default)]
430    struct PersistModel;
431
432    #[derive(Debug, Clone, Copy)]
433    enum PersistMsg {
434        Save,
435        Restore,
436        Noop,
437    }
438
439    impl From<Event> for PersistMsg {
440        fn from(event: Event) -> Self {
441            match event {
442                Event::Key(k) if k.code == KeyCode::Char('s') => Self::Save,
443                Event::Key(k) if k.code == KeyCode::Char('r') => Self::Restore,
444                _ => Self::Noop,
445            }
446        }
447    }
448
449    impl Model for PersistModel {
450        type Message = PersistMsg;
451
452        fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
453            match msg {
454                PersistMsg::Save => Cmd::save_state(),
455                PersistMsg::Restore => Cmd::restore_state(),
456                PersistMsg::Noop => Cmd::none(),
457            }
458        }
459
460        fn view(&self, _frame: &mut Frame) {}
461    }
462
463    // ---------- Tests ----------
464
465    #[test]
466    fn new_simulator() {
467        let sim = ProgramSimulator::new(Counter {
468            value: 0,
469            initialized: false,
470        });
471        assert!(sim.is_running());
472        assert_eq!(sim.model().value, 0);
473        assert!(!sim.model().initialized);
474        assert_eq!(sim.frame_count(), 0);
475        assert!(sim.logs().is_empty());
476    }
477
478    #[test]
479    fn init_calls_model_init() {
480        let mut sim = ProgramSimulator::new(Counter {
481            value: 0,
482            initialized: false,
483        });
484        sim.init();
485        assert!(sim.model().initialized);
486    }
487
488    #[test]
489    fn inject_events_processes_all() {
490        let mut sim = ProgramSimulator::new(Counter {
491            value: 0,
492            initialized: false,
493        });
494        sim.init();
495
496        let events = vec![key_event('+'), key_event('+'), key_event('+')];
497        sim.inject_events(&events);
498
499        assert_eq!(sim.model().value, 3);
500    }
501
502    #[test]
503    fn inject_events_stops_on_quit() {
504        let mut sim = ProgramSimulator::new(Counter {
505            value: 0,
506            initialized: false,
507        });
508        sim.init();
509
510        // Quit in the middle - subsequent events should be ignored
511        let events = vec![key_event('+'), key_event('q'), key_event('+')];
512        sim.inject_events(&events);
513
514        assert_eq!(sim.model().value, 1);
515        assert!(!sim.is_running());
516    }
517
518    #[test]
519    fn save_state_flushes_registry() {
520        use crate::state_persistence::StateRegistry;
521
522        let registry = Arc::new(StateRegistry::in_memory());
523        registry.set("viewer", 1, vec![1, 2, 3]);
524        assert!(registry.is_dirty());
525
526        let mut sim = ProgramSimulator::with_registry(PersistModel, Arc::clone(&registry));
527        sim.send(PersistMsg::Save);
528
529        assert!(!registry.is_dirty());
530        let stored = registry.get("viewer").expect("entry present");
531        assert_eq!(stored.version, 1);
532        assert_eq!(stored.data, vec![1, 2, 3]);
533    }
534
535    #[test]
536    fn restore_state_round_trips_cache() {
537        use crate::state_persistence::StateRegistry;
538
539        let registry = Arc::new(StateRegistry::in_memory());
540        registry.set("viewer", 7, vec![9, 8, 7]);
541
542        let mut sim = ProgramSimulator::with_registry(PersistModel, Arc::clone(&registry));
543        sim.send(PersistMsg::Save);
544
545        let removed = registry.remove("viewer");
546        assert!(removed.is_some());
547        assert!(registry.get("viewer").is_none());
548
549        sim.send(PersistMsg::Restore);
550        let restored = registry.get("viewer").expect("restored entry");
551        assert_eq!(restored.version, 7);
552        assert_eq!(restored.data, vec![9, 8, 7]);
553    }
554
555    #[test]
556    fn resize_events_apply_in_order() {
557        let mut sim = ProgramSimulator::new(ResizeTracker::default());
558        sim.init();
559
560        let events = vec![
561            resize_event(80, 24),
562            resize_event(100, 40),
563            resize_event(120, 50),
564        ];
565        sim.inject_events(&events);
566
567        assert_eq!(sim.model().history, vec![(80, 24), (100, 40), (120, 50)]);
568        assert_eq!(sim.model().last, Some((120, 50)));
569    }
570
571    #[test]
572    fn resize_events_after_quit_are_ignored() {
573        let mut sim = ProgramSimulator::new(ResizeTracker::default());
574        sim.init();
575
576        let events = vec![resize_event(80, 24), key_event('q'), resize_event(120, 50)];
577        sim.inject_events(&events);
578
579        assert!(!sim.is_running());
580        assert_eq!(sim.model().history, vec![(80, 24)]);
581        assert_eq!(sim.model().last, Some((80, 24)));
582    }
583
584    #[test]
585    fn send_message_directly() {
586        let mut sim = ProgramSimulator::new(Counter {
587            value: 0,
588            initialized: false,
589        });
590        sim.init();
591
592        sim.send(CounterMsg::Increment);
593        sim.send(CounterMsg::Increment);
594        sim.send(CounterMsg::Decrement);
595
596        assert_eq!(sim.model().value, 1);
597    }
598
599    #[test]
600    fn capture_frame_renders_correctly() {
601        let mut sim = ProgramSimulator::new(Counter {
602            value: 42,
603            initialized: false,
604        });
605        sim.init();
606
607        let buf = sim.capture_frame(80, 24);
608
609        // "Count: 42" should be rendered
610        assert_eq!(buf.get(0, 0).unwrap().content.as_char(), Some('C'));
611        assert_eq!(buf.get(1, 0).unwrap().content.as_char(), Some('o'));
612        assert_eq!(buf.get(7, 0).unwrap().content.as_char(), Some('4'));
613        assert_eq!(buf.get(8, 0).unwrap().content.as_char(), Some('2'));
614    }
615
616    #[test]
617    fn multiple_frame_captures() {
618        let mut sim = ProgramSimulator::new(Counter {
619            value: 0,
620            initialized: false,
621        });
622        sim.init();
623
624        sim.capture_frame(80, 24);
625        sim.send(CounterMsg::Increment);
626        sim.capture_frame(80, 24);
627
628        assert_eq!(sim.frame_count(), 2);
629
630        // First frame: "Count: 0"
631        assert_eq!(
632            sim.frames()[0].get(7, 0).unwrap().content.as_char(),
633            Some('0')
634        );
635        // Second frame: "Count: 1"
636        assert_eq!(
637            sim.frames()[1].get(7, 0).unwrap().content.as_char(),
638            Some('1')
639        );
640    }
641
642    #[test]
643    fn quit_command_stops_running() {
644        let mut sim = ProgramSimulator::new(Counter {
645            value: 0,
646            initialized: false,
647        });
648        sim.init();
649
650        assert!(sim.is_running());
651        sim.send(CounterMsg::Quit);
652        assert!(!sim.is_running());
653    }
654
655    #[test]
656    fn log_command_records_text() {
657        let mut sim = ProgramSimulator::new(Counter {
658            value: 5,
659            initialized: false,
660        });
661        sim.init();
662
663        sim.send(CounterMsg::LogValue);
664
665        assert_eq!(sim.logs(), &["value=5"]);
666    }
667
668    #[test]
669    fn batch_command_executes_all() {
670        let mut sim = ProgramSimulator::new(Counter {
671            value: 0,
672            initialized: false,
673        });
674        sim.init();
675
676        sim.send(CounterMsg::BatchIncrement(5));
677
678        assert_eq!(sim.model().value, 5);
679    }
680
681    #[test]
682    fn tick_command_sets_rate() {
683        let mut sim = ProgramSimulator::new(Counter {
684            value: 0,
685            initialized: false,
686        });
687
688        assert!(sim.tick_rate().is_none());
689
690        // Manually execute a tick command through the model
691        // We'll test by checking the internal tick_rate after setting it
692        // via the execute_cmd path. Since Counter doesn't emit ticks,
693        // we'll test via the command log.
694        sim.execute_cmd(Cmd::tick(Duration::from_millis(100)));
695
696        assert_eq!(sim.tick_rate(), Some(Duration::from_millis(100)));
697    }
698
699    #[test]
700    fn command_log_records_all() {
701        let mut sim = ProgramSimulator::new(Counter {
702            value: 0,
703            initialized: false,
704        });
705        sim.init();
706
707        sim.send(CounterMsg::Increment);
708        sim.send(CounterMsg::Quit);
709
710        // init returns Cmd::None, then Increment returns Cmd::None, then Quit returns Cmd::Quit
711        assert!(sim.command_log().len() >= 3);
712        assert!(matches!(sim.command_log().last(), Some(CmdRecord::Quit)));
713    }
714
715    #[test]
716    fn clear_frames() {
717        let mut sim = ProgramSimulator::new(Counter {
718            value: 0,
719            initialized: false,
720        });
721        sim.capture_frame(10, 10);
722        sim.capture_frame(10, 10);
723        assert_eq!(sim.frame_count(), 2);
724
725        sim.clear_frames();
726        assert_eq!(sim.frame_count(), 0);
727    }
728
729    #[test]
730    fn clear_logs() {
731        let mut sim = ProgramSimulator::new(Counter {
732            value: 0,
733            initialized: false,
734        });
735        sim.init();
736        sim.send(CounterMsg::LogValue);
737        assert_eq!(sim.logs().len(), 1);
738
739        sim.clear_logs();
740        assert!(sim.logs().is_empty());
741    }
742
743    #[test]
744    fn model_mut_access() {
745        let mut sim = ProgramSimulator::new(Counter {
746            value: 0,
747            initialized: false,
748        });
749
750        sim.model_mut().value = 100;
751        assert_eq!(sim.model().value, 100);
752    }
753
754    #[test]
755    fn last_frame() {
756        let mut sim = ProgramSimulator::new(Counter {
757            value: 0,
758            initialized: false,
759        });
760
761        assert!(sim.last_frame().is_none());
762
763        sim.capture_frame(10, 10);
764        assert!(sim.last_frame().is_some());
765    }
766
767    #[test]
768    fn send_after_quit_is_ignored() {
769        let mut sim = ProgramSimulator::new(Counter {
770            value: 0,
771            initialized: false,
772        });
773        sim.init();
774
775        sim.send(CounterMsg::Quit);
776        assert!(!sim.is_running());
777
778        sim.send(CounterMsg::Increment);
779        // Value should not change since we quit
780        assert_eq!(sim.model().value, 0);
781    }
782
783    // =========================================================================
784    // DETERMINISM TESTS - ProgramSimulator determinism (bd-2nu8.10.3)
785    // =========================================================================
786
787    #[test]
788    fn identical_inputs_yield_identical_outputs() {
789        fn run_scenario() -> (i32, Vec<u8>) {
790            let mut sim = ProgramSimulator::new(Counter {
791                value: 0,
792                initialized: false,
793            });
794            sim.init();
795
796            sim.send(CounterMsg::Increment);
797            sim.send(CounterMsg::Increment);
798            sim.send(CounterMsg::Decrement);
799            sim.send(CounterMsg::BatchIncrement(3));
800
801            let buf = sim.capture_frame(20, 10);
802            let mut frame_bytes = Vec::new();
803            for y in 0..10 {
804                for x in 0..20 {
805                    if let Some(cell) = buf.get(x, y)
806                        && let Some(c) = cell.content.as_char()
807                    {
808                        frame_bytes.push(c as u8);
809                    }
810                }
811            }
812            (sim.model().value, frame_bytes)
813        }
814
815        let (value1, frame1) = run_scenario();
816        let (value2, frame2) = run_scenario();
817        let (value3, frame3) = run_scenario();
818
819        assert_eq!(value1, value2);
820        assert_eq!(value2, value3);
821        assert_eq!(value1, 4); // 0 + 1 + 1 - 1 + 3 = 4
822
823        assert_eq!(frame1, frame2);
824        assert_eq!(frame2, frame3);
825    }
826
827    #[test]
828    fn command_log_records_in_order() {
829        let mut sim = ProgramSimulator::new(Counter {
830            value: 0,
831            initialized: false,
832        });
833        sim.init();
834
835        sim.send(CounterMsg::Increment);
836        sim.send(CounterMsg::LogValue);
837        sim.send(CounterMsg::Increment);
838        sim.send(CounterMsg::LogValue);
839
840        let log = sim.command_log();
841
842        // Find Log entries and verify they're in order
843        let log_entries: Vec<_> = log
844            .iter()
845            .filter_map(|r| {
846                if let CmdRecord::Log(s) = r {
847                    Some(s.as_str())
848                } else {
849                    None
850                }
851            })
852            .collect();
853
854        assert_eq!(log_entries, vec!["value=1", "value=2"]);
855    }
856
857    #[test]
858    fn sequence_command_records_correctly() {
859        // Model that emits a sequence command
860        struct SeqModel {
861            steps: Vec<i32>,
862        }
863
864        #[derive(Debug)]
865        enum SeqMsg {
866            Step(i32),
867            TriggerSeq,
868        }
869
870        impl From<Event> for SeqMsg {
871            fn from(_: Event) -> Self {
872                SeqMsg::Step(0)
873            }
874        }
875
876        impl Model for SeqModel {
877            type Message = SeqMsg;
878
879            fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
880                match msg {
881                    SeqMsg::Step(n) => {
882                        self.steps.push(n);
883                        Cmd::none()
884                    }
885                    SeqMsg::TriggerSeq => Cmd::sequence(vec![
886                        Cmd::msg(SeqMsg::Step(1)),
887                        Cmd::msg(SeqMsg::Step(2)),
888                        Cmd::msg(SeqMsg::Step(3)),
889                    ]),
890                }
891            }
892
893            fn view(&self, _frame: &mut Frame) {}
894        }
895
896        let mut sim = ProgramSimulator::new(SeqModel { steps: vec![] });
897        sim.init();
898        sim.send(SeqMsg::TriggerSeq);
899
900        // Verify sequence is recorded
901        let has_sequence = sim
902            .command_log()
903            .iter()
904            .any(|r| matches!(r, CmdRecord::Sequence(3)));
905        assert!(has_sequence, "Should record Sequence(3)");
906
907        // Verify steps executed in order
908        assert_eq!(sim.model().steps, vec![1, 2, 3]);
909    }
910
911    #[test]
912    fn batch_command_records_correctly() {
913        let mut sim = ProgramSimulator::new(Counter {
914            value: 0,
915            initialized: false,
916        });
917        sim.init();
918
919        sim.send(CounterMsg::BatchIncrement(5));
920
921        // Should have Batch(5) in the log
922        let has_batch = sim
923            .command_log()
924            .iter()
925            .any(|r| matches!(r, CmdRecord::Batch(5)));
926        assert!(has_batch, "Should record Batch(5)");
927
928        assert_eq!(sim.model().value, 5);
929    }
930
931    struct OrderingModel {
932        trace: RefCell<Vec<&'static str>>,
933    }
934
935    impl OrderingModel {
936        fn new() -> Self {
937            Self {
938                trace: RefCell::new(Vec::new()),
939            }
940        }
941
942        fn trace(&self) -> Vec<&'static str> {
943            self.trace.borrow().clone()
944        }
945    }
946
947    #[derive(Debug)]
948    enum OrderingMsg {
949        Step(&'static str),
950        StartSequence,
951        StartBatch,
952    }
953
954    impl From<Event> for OrderingMsg {
955        fn from(_: Event) -> Self {
956            OrderingMsg::StartSequence
957        }
958    }
959
960    impl Model for OrderingModel {
961        type Message = OrderingMsg;
962
963        fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
964            match msg {
965                OrderingMsg::Step(tag) => {
966                    self.trace.borrow_mut().push(tag);
967                    Cmd::none()
968                }
969                OrderingMsg::StartSequence => Cmd::sequence(vec![
970                    Cmd::msg(OrderingMsg::Step("seq-1")),
971                    Cmd::msg(OrderingMsg::Step("seq-2")),
972                    Cmd::msg(OrderingMsg::Step("seq-3")),
973                ]),
974                OrderingMsg::StartBatch => Cmd::batch(vec![
975                    Cmd::msg(OrderingMsg::Step("batch-1")),
976                    Cmd::msg(OrderingMsg::Step("batch-2")),
977                    Cmd::msg(OrderingMsg::Step("batch-3")),
978                ]),
979            }
980        }
981
982        fn view(&self, _frame: &mut Frame) {
983            self.trace.borrow_mut().push("view");
984        }
985    }
986
987    #[test]
988    fn sequence_preserves_update_order_before_view() {
989        let mut sim = ProgramSimulator::new(OrderingModel::new());
990        sim.init();
991
992        sim.send(OrderingMsg::StartSequence);
993        sim.capture_frame(1, 1);
994
995        assert_eq!(sim.model().trace(), vec!["seq-1", "seq-2", "seq-3", "view"]);
996    }
997
998    #[test]
999    fn batch_preserves_update_order_before_view() {
1000        let mut sim = ProgramSimulator::new(OrderingModel::new());
1001        sim.init();
1002
1003        sim.send(OrderingMsg::StartBatch);
1004        sim.capture_frame(1, 1);
1005
1006        assert_eq!(
1007            sim.model().trace(),
1008            vec!["batch-1", "batch-2", "batch-3", "view"]
1009        );
1010    }
1011
1012    #[test]
1013    fn frame_dimensions_match_request() {
1014        let mut sim = ProgramSimulator::new(Counter {
1015            value: 42,
1016            initialized: false,
1017        });
1018        sim.init();
1019
1020        let buf = sim.capture_frame(100, 50);
1021        assert_eq!(buf.width(), 100);
1022        assert_eq!(buf.height(), 50);
1023    }
1024
1025    #[test]
1026    fn multiple_frame_captures_are_independent() {
1027        let mut sim = ProgramSimulator::new(Counter {
1028            value: 0,
1029            initialized: false,
1030        });
1031        sim.init();
1032
1033        // Capture at value 0
1034        sim.capture_frame(20, 10);
1035
1036        // Change value
1037        sim.send(CounterMsg::Increment);
1038        sim.send(CounterMsg::Increment);
1039
1040        // Capture at value 2
1041        sim.capture_frame(20, 10);
1042
1043        let frames = sim.frames();
1044        assert_eq!(frames.len(), 2);
1045
1046        // First frame should show "Count: 0"
1047        assert_eq!(frames[0].get(7, 0).unwrap().content.as_char(), Some('0'));
1048
1049        // Second frame should show "Count: 2"
1050        assert_eq!(frames[1].get(7, 0).unwrap().content.as_char(), Some('2'));
1051    }
1052
1053    #[test]
1054    fn inject_events_processes_in_order() {
1055        let mut sim = ProgramSimulator::new(Counter {
1056            value: 0,
1057            initialized: false,
1058        });
1059        sim.init();
1060
1061        // '+' increments, '-' decrements
1062        let events = vec![
1063            key_event('+'),
1064            key_event('+'),
1065            key_event('+'),
1066            key_event('-'),
1067            key_event('+'),
1068        ];
1069
1070        sim.inject_events(&events);
1071
1072        // 0 + 1 + 1 + 1 - 1 + 1 = 3
1073        assert_eq!(sim.model().value, 3);
1074    }
1075
1076    #[test]
1077    fn task_command_records_task() {
1078        struct TaskModel {
1079            result: Option<i32>,
1080        }
1081
1082        #[derive(Debug)]
1083        enum TaskMsg {
1084            SetResult(i32),
1085            SpawnTask,
1086        }
1087
1088        impl From<Event> for TaskMsg {
1089            fn from(_: Event) -> Self {
1090                TaskMsg::SetResult(0)
1091            }
1092        }
1093
1094        impl Model for TaskModel {
1095            type Message = TaskMsg;
1096
1097            fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1098                match msg {
1099                    TaskMsg::SetResult(v) => {
1100                        self.result = Some(v);
1101                        Cmd::none()
1102                    }
1103                    TaskMsg::SpawnTask => Cmd::task(|| {
1104                        // Simulate computation
1105                        TaskMsg::SetResult(42)
1106                    }),
1107                }
1108            }
1109
1110            fn view(&self, _frame: &mut Frame) {}
1111        }
1112
1113        let mut sim = ProgramSimulator::new(TaskModel { result: None });
1114        sim.init();
1115        sim.send(TaskMsg::SpawnTask);
1116
1117        // Task should execute synchronously in simulator
1118        assert_eq!(sim.model().result, Some(42));
1119
1120        // Should have Task record in command log
1121        let has_task = sim
1122            .command_log()
1123            .iter()
1124            .any(|r| matches!(r, CmdRecord::Task));
1125        assert!(has_task);
1126    }
1127
1128    #[test]
1129    fn tick_rate_is_set() {
1130        let mut sim = ProgramSimulator::new(Counter {
1131            value: 0,
1132            initialized: false,
1133        });
1134
1135        assert!(sim.tick_rate().is_none());
1136
1137        sim.execute_cmd(Cmd::tick(std::time::Duration::from_millis(100)));
1138
1139        assert_eq!(sim.tick_rate(), Some(std::time::Duration::from_millis(100)));
1140    }
1141
1142    #[test]
1143    fn logs_accumulate_across_messages() {
1144        let mut sim = ProgramSimulator::new(Counter {
1145            value: 0,
1146            initialized: false,
1147        });
1148        sim.init();
1149
1150        sim.send(CounterMsg::LogValue);
1151        sim.send(CounterMsg::Increment);
1152        sim.send(CounterMsg::LogValue);
1153        sim.send(CounterMsg::Increment);
1154        sim.send(CounterMsg::LogValue);
1155
1156        assert_eq!(sim.logs().len(), 3);
1157        assert_eq!(sim.logs()[0], "value=0");
1158        assert_eq!(sim.logs()[1], "value=1");
1159        assert_eq!(sim.logs()[2], "value=2");
1160    }
1161
1162    #[test]
1163    fn deterministic_frame_content_across_runs() {
1164        fn capture_frame_content(value: i32) -> Vec<Option<char>> {
1165            let mut sim = ProgramSimulator::new(Counter {
1166                value,
1167                initialized: false,
1168            });
1169            sim.init();
1170
1171            let buf = sim.capture_frame(15, 1);
1172            (0..15)
1173                .map(|x| buf.get(x, 0).and_then(|c| c.content.as_char()))
1174                .collect()
1175        }
1176
1177        let content1 = capture_frame_content(123);
1178        let content2 = capture_frame_content(123);
1179        let content3 = capture_frame_content(123);
1180
1181        assert_eq!(content1, content2);
1182        assert_eq!(content2, content3);
1183
1184        // Should be "Count: 123" followed by None (unwritten cells)
1185        let expected: Vec<Option<char>> = "Count: 123"
1186            .chars()
1187            .map(Some)
1188            .chain(std::iter::repeat_n(None, 5))
1189            .collect();
1190        assert_eq!(content1, expected);
1191    }
1192
1193    #[test]
1194    fn complex_scenario_is_deterministic() {
1195        fn run_complex_scenario() -> (i32, usize, Vec<String>) {
1196            let mut sim = ProgramSimulator::new(Counter {
1197                value: 0,
1198                initialized: false,
1199            });
1200            sim.init();
1201
1202            // Complex sequence of operations
1203            for _ in 0..10 {
1204                sim.send(CounterMsg::Increment);
1205            }
1206            sim.send(CounterMsg::LogValue);
1207
1208            sim.send(CounterMsg::BatchIncrement(5));
1209            sim.send(CounterMsg::LogValue);
1210
1211            for _ in 0..3 {
1212                sim.send(CounterMsg::Decrement);
1213            }
1214            sim.send(CounterMsg::LogValue);
1215
1216            sim.send(CounterMsg::Reset);
1217            sim.send(CounterMsg::LogValue);
1218
1219            sim.capture_frame(20, 10);
1220
1221            (
1222                sim.model().value,
1223                sim.command_log().len(),
1224                sim.logs().to_vec(),
1225            )
1226        }
1227
1228        let result1 = run_complex_scenario();
1229        let result2 = run_complex_scenario();
1230
1231        assert_eq!(result1.0, result2.0);
1232        assert_eq!(result1.1, result2.1);
1233        assert_eq!(result1.2, result2.2);
1234    }
1235
1236    #[test]
1237    fn model_unchanged_when_not_running() {
1238        let mut sim = ProgramSimulator::new(Counter {
1239            value: 5,
1240            initialized: false,
1241        });
1242        sim.init();
1243
1244        sim.send(CounterMsg::Quit);
1245
1246        let value_before = sim.model().value;
1247        sim.send(CounterMsg::Increment);
1248        sim.send(CounterMsg::BatchIncrement(10));
1249        let value_after = sim.model().value;
1250
1251        assert_eq!(value_before, value_after);
1252    }
1253
1254    #[test]
1255    fn init_produces_consistent_command_log() {
1256        // Model with init that returns a command
1257        struct InitModel {
1258            init_ran: bool,
1259        }
1260
1261        #[derive(Debug)]
1262        enum InitMsg {
1263            MarkInit,
1264        }
1265
1266        impl From<Event> for InitMsg {
1267            fn from(_: Event) -> Self {
1268                InitMsg::MarkInit
1269            }
1270        }
1271
1272        impl Model for InitModel {
1273            type Message = InitMsg;
1274
1275            fn init(&mut self) -> Cmd<Self::Message> {
1276                Cmd::msg(InitMsg::MarkInit)
1277            }
1278
1279            fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
1280                match msg {
1281                    InitMsg::MarkInit => {
1282                        self.init_ran = true;
1283                        Cmd::none()
1284                    }
1285                }
1286            }
1287
1288            fn view(&self, _frame: &mut Frame) {}
1289        }
1290
1291        let mut sim1 = ProgramSimulator::new(InitModel { init_ran: false });
1292        let mut sim2 = ProgramSimulator::new(InitModel { init_ran: false });
1293
1294        sim1.init();
1295        sim2.init();
1296
1297        assert_eq!(sim1.model().init_ran, sim2.model().init_ran);
1298        assert_eq!(sim1.command_log().len(), sim2.command_log().len());
1299    }
1300
1301    #[test]
1302    fn execute_cmd_directly() {
1303        let mut sim = ProgramSimulator::new(Counter {
1304            value: 0,
1305            initialized: false,
1306        });
1307
1308        // Execute commands directly without going through update
1309        sim.execute_cmd(Cmd::log("direct log"));
1310        sim.execute_cmd(Cmd::tick(std::time::Duration::from_secs(1)));
1311
1312        assert_eq!(sim.logs(), &["direct log"]);
1313        assert_eq!(sim.tick_rate(), Some(std::time::Duration::from_secs(1)));
1314    }
1315
1316    #[test]
1317    fn save_restore_are_noops_in_simulator() {
1318        let mut sim = ProgramSimulator::new(Counter {
1319            value: 7,
1320            initialized: false,
1321        });
1322        sim.init();
1323
1324        let log_len = sim.command_log().len();
1325        let tick_rate = sim.tick_rate();
1326        let value_before = sim.model().value;
1327
1328        sim.execute_cmd(Cmd::save_state());
1329        sim.execute_cmd(Cmd::restore_state());
1330
1331        assert_eq!(sim.command_log().len(), log_len);
1332        assert_eq!(sim.tick_rate(), tick_rate);
1333        assert_eq!(sim.model().value, value_before);
1334        assert!(sim.is_running());
1335    }
1336
1337    #[test]
1338    fn grapheme_pool_is_reused() {
1339        let mut sim = ProgramSimulator::new(Counter {
1340            value: 0,
1341            initialized: false,
1342        });
1343        sim.init();
1344
1345        // Capture multiple frames - pool should be reused
1346        for i in 0..10 {
1347            sim.model_mut().value = i;
1348            sim.capture_frame(80, 24);
1349        }
1350
1351        assert_eq!(sim.frame_count(), 10);
1352    }
1353}