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