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