Skip to main content

bubbletea/
simulator.rs

1//! Program simulator for testing lifecycle without a real terminal.
2//!
3//! This module provides a way to test Model implementations without
4//! requiring a real terminal, enabling unit tests for the Elm Architecture.
5
6use std::collections::VecDeque;
7use std::sync::Arc;
8use std::sync::atomic::{AtomicUsize, Ordering};
9
10use crate::Model;
11use crate::command::Cmd;
12use crate::message::{BatchMsg, Message, QuitMsg, SequenceMsg};
13
14/// Statistics tracked during simulation.
15#[derive(Debug, Clone, Default)]
16pub struct SimulationStats {
17    /// Number of times init() was called.
18    pub init_calls: usize,
19    /// Number of times update() was called.
20    pub update_calls: usize,
21    /// Number of times view() was called.
22    pub view_calls: usize,
23    /// Commands that were returned from init/update.
24    pub commands_returned: usize,
25    /// Whether quit was requested.
26    pub quit_requested: bool,
27}
28
29/// A simulator for testing Model implementations without a terminal.
30///
31/// # Example
32///
33/// ```rust
34/// use bubbletea::{Model, Message, Cmd, simulator::ProgramSimulator};
35///
36/// struct Counter { count: i32 }
37///
38/// impl Model for Counter {
39///     fn init(&self) -> Option<Cmd> { None }
40///     fn update(&mut self, msg: Message) -> Option<Cmd> {
41///         if let Some(n) = msg.downcast::<i32>() {
42///             self.count += n;
43///         }
44///         None
45///     }
46///     fn view(&self) -> String {
47///         format!("Count: {}", self.count)
48///     }
49/// }
50///
51/// let mut sim = ProgramSimulator::new(Counter { count: 0 });
52/// sim.send(Message::new(5));
53/// sim.send(Message::new(3));
54/// sim.step();
55/// sim.step();
56///
57/// assert_eq!(sim.model().count, 8);
58/// ```
59pub struct ProgramSimulator<M: Model> {
60    model: M,
61    input_queue: VecDeque<Message>,
62    output_views: Vec<String>,
63    stats: SimulationStats,
64    initialized: bool,
65}
66
67impl<M: Model> ProgramSimulator<M> {
68    /// Create a new simulator with the given model.
69    pub fn new(model: M) -> Self {
70        Self {
71            model,
72            input_queue: VecDeque::new(),
73            output_views: Vec::new(),
74            stats: SimulationStats::default(),
75            initialized: false,
76        }
77    }
78
79    /// Initialize the model, calling init() and capturing any returned command.
80    pub fn init(&mut self) -> Option<Cmd> {
81        if self.initialized {
82            return None;
83        }
84        self.initialized = true;
85        self.stats.init_calls += 1;
86
87        // Call init
88        let cmd = self.model.init();
89        if cmd.is_some() {
90            self.stats.commands_returned += 1;
91        }
92
93        // Call initial view
94        self.stats.view_calls += 1;
95        self.output_views.push(self.model.view());
96
97        cmd
98    }
99
100    /// Queue a message for processing.
101    pub fn send(&mut self, msg: Message) {
102        self.input_queue.push_back(msg);
103    }
104
105    /// Process one message from the queue, calling update and view.
106    ///
107    /// Returns the command returned by update, if any.
108    pub fn step(&mut self) -> Option<Cmd> {
109        // Ensure initialized
110        if !self.initialized {
111            self.init();
112        }
113
114        if let Some(msg) = self.input_queue.pop_front() {
115            // Check for quit
116            if msg.is::<QuitMsg>() {
117                self.stats.quit_requested = true;
118                return Some(crate::quit());
119            }
120
121            // Handle batch messages specially - extract and execute the commands
122            if msg.is::<BatchMsg>() {
123                if let Some(batch) = msg.downcast::<BatchMsg>() {
124                    for cmd in batch.0 {
125                        if let Some(result_msg) = cmd.execute() {
126                            self.input_queue.push_back(result_msg);
127                        }
128                    }
129                }
130                // View after batch
131                self.stats.view_calls += 1;
132                self.output_views.push(self.model.view());
133                return None;
134            }
135
136            // Handle sequence messages specially - execute commands in order
137            if msg.is::<SequenceMsg>() {
138                if let Some(seq) = msg.downcast::<SequenceMsg>() {
139                    for cmd in seq.0 {
140                        if let Some(result_msg) = cmd.execute() {
141                            self.input_queue.push_back(result_msg);
142                        }
143                    }
144                }
145                // View after sequence
146                self.stats.view_calls += 1;
147                self.output_views.push(self.model.view());
148                return None;
149            }
150
151            // Update
152            self.stats.update_calls += 1;
153            let cmd = self.model.update(msg);
154            if cmd.is_some() {
155                self.stats.commands_returned += 1;
156            }
157
158            // View
159            self.stats.view_calls += 1;
160            self.output_views.push(self.model.view());
161
162            return cmd;
163        }
164
165        None
166    }
167
168    /// Process all pending messages until the queue is empty or quit is requested.
169    ///
170    /// Returns the number of messages processed.
171    /// Has a built-in safety limit of 1000 iterations to prevent infinite loops.
172    pub fn run_until_empty(&mut self) -> usize {
173        const MAX_ITERATIONS: usize = 1000;
174        let mut processed = 0;
175        while !self.input_queue.is_empty()
176            && !self.stats.quit_requested
177            && processed < MAX_ITERATIONS
178        {
179            if let Some(cmd) = self.step() {
180                // Execute command and queue resulting message
181                if let Some(msg) = cmd.execute() {
182                    self.input_queue.push_back(msg);
183                }
184            }
185            processed += 1;
186        }
187        processed
188    }
189
190    /// Run until quit is received or max_steps is reached.
191    ///
192    /// Returns the number of steps processed.
193    pub fn run_until_quit(&mut self, max_steps: usize) -> usize {
194        let mut steps = 0;
195        while steps < max_steps && !self.stats.quit_requested {
196            if self.input_queue.is_empty() {
197                break;
198            }
199            if let Some(cmd) = self.step() {
200                // Execute command and queue resulting message
201                if let Some(msg) = cmd.execute() {
202                    self.input_queue.push_back(msg);
203                }
204            }
205            steps += 1;
206        }
207        steps
208    }
209
210    /// Get a reference to the current model state.
211    pub fn model(&self) -> &M {
212        &self.model
213    }
214
215    /// Get a mutable reference to the current model state.
216    pub fn model_mut(&mut self) -> &mut M {
217        &mut self.model
218    }
219
220    /// Consume the simulator and return the final model.
221    pub fn into_model(self) -> M {
222        self.model
223    }
224
225    /// Get the simulation statistics.
226    pub fn stats(&self) -> &SimulationStats {
227        &self.stats
228    }
229
230    /// Get all captured view outputs.
231    pub fn views(&self) -> &[String] {
232        &self.output_views
233    }
234
235    /// Get the most recent view output.
236    pub fn last_view(&self) -> Option<&str> {
237        self.output_views.last().map(String::as_str)
238    }
239
240    /// Check if quit has been requested.
241    pub fn is_quit(&self) -> bool {
242        self.stats.quit_requested
243    }
244
245    /// Check if the model has been initialized.
246    pub fn is_initialized(&self) -> bool {
247        self.initialized
248    }
249
250    /// Get the number of pending messages.
251    pub fn pending_count(&self) -> usize {
252        self.input_queue.len()
253    }
254
255    // ========================================================================
256    // Event Simulation Helpers
257    // ========================================================================
258
259    /// Simulate a key press (single character).
260    ///
261    /// # Example
262    ///
263    /// ```rust
264    /// use bubbletea::simulator::ProgramSimulator;
265    /// # use bubbletea::{Model, Message, Cmd};
266    /// # struct MyModel;
267    /// # impl Model for MyModel {
268    /// #     fn init(&self) -> Option<Cmd> { None }
269    /// #     fn update(&mut self, _: Message) -> Option<Cmd> { None }
270    /// #     fn view(&self) -> String { String::new() }
271    /// # }
272    ///
273    /// let mut sim = ProgramSimulator::new(MyModel);
274    /// sim.init();
275    /// sim.sim_key('a');  // Queue 'a' key press
276    /// sim.step();
277    /// ```
278    pub fn sim_key(&mut self, c: char) {
279        use crate::key::KeyMsg;
280        self.send(Message::new(KeyMsg::from_char(c)));
281    }
282
283    /// Simulate a special key press (Enter, Escape, Arrow keys, etc.).
284    pub fn sim_key_type(&mut self, key_type: crate::key::KeyType) {
285        use crate::key::KeyMsg;
286        self.send(Message::new(KeyMsg::from_type(key_type)));
287    }
288
289    /// Simulate a mouse event.
290    ///
291    /// # Arguments
292    ///
293    /// * `x` - Column position (0-indexed)
294    /// * `y` - Row position (0-indexed)
295    /// * `button` - Which button (Left, Right, Middle, etc.)
296    /// * `action` - What happened (Press, Release, Motion)
297    pub fn sim_mouse(
298        &mut self,
299        x: u16,
300        y: u16,
301        button: crate::mouse::MouseButton,
302        action: crate::mouse::MouseAction,
303    ) {
304        use crate::mouse::MouseMsg;
305        self.send(Message::new(MouseMsg {
306            x,
307            y,
308            button,
309            action,
310            shift: false,
311            alt: false,
312            ctrl: false,
313        }));
314    }
315
316    /// Simulate a window resize event.
317    pub fn sim_resize(&mut self, width: u16, height: u16) {
318        use crate::message::WindowSizeMsg;
319        self.send(Message::new(WindowSizeMsg { width, height }));
320    }
321
322    /// Simulate a paste operation (bracketed paste).
323    pub fn sim_paste(&mut self, text: &str) {
324        use crate::key::KeyMsg;
325        let runes: Vec<char> = text.chars().collect();
326        self.send(Message::new(KeyMsg::from_runes(runes).with_paste()));
327    }
328}
329
330/// A test model that tracks lifecycle calls with atomic counters.
331///
332/// Useful for verifying that init/update/view are called the expected
333/// number of times.
334pub struct TrackingModel {
335    /// Counter for init calls.
336    pub init_count: Arc<AtomicUsize>,
337    /// Counter for update calls.
338    pub update_count: Arc<AtomicUsize>,
339    /// Counter for view calls.
340    pub view_count: Arc<AtomicUsize>,
341    /// Internal state for testing.
342    pub value: i32,
343}
344
345impl TrackingModel {
346    /// Create a new tracking model with fresh counters.
347    pub fn new() -> Self {
348        Self {
349            init_count: Arc::new(AtomicUsize::new(0)),
350            update_count: Arc::new(AtomicUsize::new(0)),
351            view_count: Arc::new(AtomicUsize::new(0)),
352            value: 0,
353        }
354    }
355
356    /// Create a new tracking model with shared counters.
357    pub fn with_counters(
358        init_count: Arc<AtomicUsize>,
359        update_count: Arc<AtomicUsize>,
360        view_count: Arc<AtomicUsize>,
361    ) -> Self {
362        Self {
363            init_count,
364            update_count,
365            view_count,
366            value: 0,
367        }
368    }
369
370    /// Get the current init count.
371    pub fn init_calls(&self) -> usize {
372        self.init_count.load(Ordering::SeqCst)
373    }
374
375    /// Get the current update count.
376    pub fn update_calls(&self) -> usize {
377        self.update_count.load(Ordering::SeqCst)
378    }
379
380    /// Get the current view count.
381    pub fn view_calls(&self) -> usize {
382        self.view_count.load(Ordering::SeqCst)
383    }
384}
385
386impl Default for TrackingModel {
387    fn default() -> Self {
388        Self::new()
389    }
390}
391
392impl Model for TrackingModel {
393    fn init(&self) -> Option<Cmd> {
394        self.init_count.fetch_add(1, Ordering::SeqCst);
395        None
396    }
397
398    fn update(&mut self, msg: Message) -> Option<Cmd> {
399        self.update_count.fetch_add(1, Ordering::SeqCst);
400
401        // Handle increment/decrement messages
402        if let Some(n) = msg.downcast::<i32>() {
403            self.value += n;
404        }
405
406        None
407    }
408
409    fn view(&self) -> String {
410        self.view_count.fetch_add(1, Ordering::SeqCst);
411        format!("Value: {}", self.value)
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn test_simulator_init_called_once() {
421        let model = TrackingModel::new();
422        let init_count = model.init_count.clone();
423
424        let mut sim = ProgramSimulator::new(model);
425
426        // Before init
427        assert_eq!(init_count.load(Ordering::SeqCst), 0);
428
429        // Explicit init
430        sim.init();
431        assert_eq!(init_count.load(Ordering::SeqCst), 1);
432
433        // Second init should not increment
434        sim.init();
435        assert_eq!(init_count.load(Ordering::SeqCst), 1);
436    }
437
438    #[test]
439    fn test_simulator_view_called_after_init() {
440        let model = TrackingModel::new();
441        let view_count = model.view_count.clone();
442
443        let mut sim = ProgramSimulator::new(model);
444        sim.init();
445
446        // View called once after init
447        assert_eq!(view_count.load(Ordering::SeqCst), 1);
448        assert_eq!(sim.views().len(), 1);
449        assert_eq!(sim.last_view(), Some("Value: 0"));
450    }
451
452    #[test]
453    fn test_simulator_update_increments_value() {
454        let model = TrackingModel::new();
455        let mut sim = ProgramSimulator::new(model);
456
457        sim.init();
458        sim.send(Message::new(5));
459        sim.send(Message::new(3));
460        sim.step();
461        sim.step();
462
463        assert_eq!(sim.model().value, 8);
464        assert_eq!(sim.stats().update_calls, 2);
465    }
466
467    #[test]
468    fn test_simulator_view_called_after_each_update() {
469        let model = TrackingModel::new();
470        let view_count = model.view_count.clone();
471
472        let mut sim = ProgramSimulator::new(model);
473        sim.init();
474
475        // 1 view from init
476        assert_eq!(view_count.load(Ordering::SeqCst), 1);
477
478        sim.send(Message::new(1));
479        sim.step();
480        // 1 from init + 1 from update
481        assert_eq!(view_count.load(Ordering::SeqCst), 2);
482
483        sim.send(Message::new(2));
484        sim.step();
485        // 1 from init + 2 from updates
486        assert_eq!(view_count.load(Ordering::SeqCst), 3);
487    }
488
489    #[test]
490    fn test_simulator_quit_stops_processing() {
491        let model = TrackingModel::new();
492        let mut sim = ProgramSimulator::new(model);
493
494        sim.init();
495        sim.send(Message::new(1));
496        sim.send(Message::new(QuitMsg));
497        sim.send(Message::new(2)); // Should not be processed
498
499        sim.run_until_quit(10);
500
501        assert!(sim.is_quit());
502        assert_eq!(sim.model().value, 1); // Only first increment processed
503    }
504
505    #[test]
506    fn test_simulator_run_until_empty() {
507        let model = TrackingModel::new();
508        let mut sim = ProgramSimulator::new(model);
509
510        sim.init();
511        sim.send(Message::new(1));
512        sim.send(Message::new(2));
513        sim.send(Message::new(3));
514
515        let processed = sim.run_until_empty();
516
517        assert_eq!(processed, 3);
518        assert_eq!(sim.model().value, 6);
519    }
520
521    #[test]
522    fn test_simulator_stats() {
523        let model = TrackingModel::new();
524        let mut sim = ProgramSimulator::new(model);
525
526        sim.init();
527        sim.send(Message::new(1));
528        sim.send(Message::new(2));
529        sim.step();
530        sim.step();
531
532        let stats = sim.stats();
533        assert_eq!(stats.init_calls, 1);
534        assert_eq!(stats.update_calls, 2);
535        assert_eq!(stats.view_calls, 3); // 1 init + 2 updates
536        assert!(!stats.quit_requested);
537    }
538
539    #[test]
540    fn test_simulator_into_model() {
541        let model = TrackingModel::new();
542        let mut sim = ProgramSimulator::new(model);
543
544        sim.init();
545        sim.send(Message::new(42));
546        sim.step();
547
548        let final_model = sim.into_model();
549        assert_eq!(final_model.value, 42);
550    }
551
552    #[test]
553    fn test_simulator_implicit_init() {
554        let model = TrackingModel::new();
555        let init_count = model.init_count.clone();
556
557        let mut sim = ProgramSimulator::new(model);
558
559        // step() should implicitly init
560        sim.send(Message::new(1));
561        sim.step();
562
563        assert_eq!(init_count.load(Ordering::SeqCst), 1);
564        assert!(sim.is_initialized());
565    }
566
567    #[test]
568    fn test_simulator_batch_command() {
569        use crate::batch;
570
571        // Model that triggers a batch command on a specific message
572        struct BatchTrigger;
573        #[derive(Clone, Copy)]
574        struct SetValue(i32);
575        #[derive(Clone, Copy)]
576        struct AddValue(i32);
577
578        struct BatchModel {
579            value: i32,
580        }
581
582        impl Model for BatchModel {
583            fn init(&self) -> Option<crate::Cmd> {
584                None
585            }
586
587            fn update(&mut self, msg: Message) -> Option<crate::Cmd> {
588                if msg.is::<BatchTrigger>() {
589                    // Return a batch of two commands
590                    return batch(vec![
591                        Some(crate::Cmd::new(|| Message::new(SetValue(10)))),
592                        Some(crate::Cmd::new(|| Message::new(AddValue(5)))),
593                    ]);
594                }
595                if let Some(SetValue(v)) = msg.downcast_ref::<SetValue>() {
596                    self.value = *v;
597                } else if let Some(AddValue(v)) = msg.downcast_ref::<AddValue>() {
598                    self.value += *v;
599                }
600                None
601            }
602
603            fn view(&self) -> String {
604                format!("Value: {}", self.value)
605            }
606        }
607
608        let mut sim = ProgramSimulator::new(BatchModel { value: 0 });
609        sim.init();
610
611        // Send the batch trigger message
612        sim.send(Message::new(BatchTrigger));
613
614        // Step once to get the batch command
615        let cmd = sim.step();
616        assert!(cmd.is_some(), "Should return batch command");
617
618        // Execute the batch command, which returns BatchMsg
619        let batch_msg = cmd.unwrap().execute();
620        assert!(batch_msg.is_some(), "Batch command should return BatchMsg");
621
622        // Send the BatchMsg to the simulator
623        sim.send(batch_msg.unwrap());
624
625        // Process all messages
626        sim.run_until_empty();
627
628        // Value should be 10 + 5 = 15
629        assert_eq!(
630            sim.model().value,
631            15,
632            "Batch commands should set 10 then add 5"
633        );
634    }
635
636    // ========================================================================
637    // Event Simulation Tests (bd-ikfq)
638    // ========================================================================
639
640    #[test]
641    fn test_sim_key_sends_char() {
642        use crate::key::{KeyMsg, KeyType};
643
644        struct KeyModel {
645            keys: Vec<char>,
646        }
647
648        impl Model for KeyModel {
649            fn init(&self) -> Option<crate::Cmd> {
650                None
651            }
652            fn update(&mut self, msg: Message) -> Option<crate::Cmd> {
653                if let Some(key) = msg.downcast_ref::<KeyMsg>()
654                    && key.key_type == KeyType::Runes
655                {
656                    self.keys.extend(&key.runes);
657                }
658                None
659            }
660            fn view(&self) -> String {
661                format!("Keys: {:?}", self.keys)
662            }
663        }
664
665        let mut sim = ProgramSimulator::new(KeyModel { keys: Vec::new() });
666        sim.init();
667        sim.sim_key('a');
668        sim.sim_key('b');
669        sim.sim_key('c');
670        sim.run_until_empty();
671
672        assert_eq!(sim.model().keys, vec!['a', 'b', 'c']);
673    }
674
675    #[test]
676    fn test_sim_key_type_sends_special_keys() {
677        use crate::key::{KeyMsg, KeyType};
678
679        struct KeyModel {
680            special_keys: Vec<KeyType>,
681        }
682
683        impl Model for KeyModel {
684            fn init(&self) -> Option<crate::Cmd> {
685                None
686            }
687            fn update(&mut self, msg: Message) -> Option<crate::Cmd> {
688                if let Some(key) = msg.downcast_ref::<KeyMsg>()
689                    && key.key_type != KeyType::Runes
690                {
691                    self.special_keys.push(key.key_type);
692                }
693                None
694            }
695            fn view(&self) -> String {
696                format!("Keys: {:?}", self.special_keys)
697            }
698        }
699
700        let mut sim = ProgramSimulator::new(KeyModel {
701            special_keys: Vec::new(),
702        });
703        sim.init();
704        sim.sim_key_type(KeyType::Enter);
705        sim.sim_key_type(KeyType::Esc);
706        sim.sim_key_type(KeyType::Tab);
707        sim.sim_key_type(KeyType::Up);
708        sim.sim_key_type(KeyType::Down);
709        sim.run_until_empty();
710
711        assert_eq!(
712            sim.model().special_keys,
713            vec![
714                KeyType::Enter,
715                KeyType::Esc,
716                KeyType::Tab,
717                KeyType::Up,
718                KeyType::Down,
719            ]
720        );
721    }
722
723    #[test]
724    fn test_sim_mouse_sends_clicks() {
725        use crate::mouse::{MouseAction, MouseButton, MouseMsg};
726
727        struct MouseModel {
728            clicks: Vec<(u16, u16)>,
729        }
730
731        impl Model for MouseModel {
732            fn init(&self) -> Option<crate::Cmd> {
733                None
734            }
735            fn update(&mut self, msg: Message) -> Option<crate::Cmd> {
736                if let Some(mouse) = msg.downcast_ref::<MouseMsg>()
737                    && mouse.button == MouseButton::Left
738                    && mouse.action == MouseAction::Press
739                {
740                    self.clicks.push((mouse.x, mouse.y));
741                }
742                None
743            }
744            fn view(&self) -> String {
745                format!("Clicks: {:?}", self.clicks)
746            }
747        }
748
749        let mut sim = ProgramSimulator::new(MouseModel { clicks: Vec::new() });
750        sim.init();
751        sim.sim_mouse(10, 5, MouseButton::Left, MouseAction::Press);
752        sim.sim_mouse(20, 15, MouseButton::Left, MouseAction::Press);
753        sim.run_until_empty();
754
755        assert_eq!(sim.model().clicks, vec![(10, 5), (20, 15)]);
756    }
757
758    #[test]
759    fn test_sim_resize_sends_dimensions() {
760        use crate::message::WindowSizeMsg;
761
762        struct SizeModel {
763            width: u16,
764            height: u16,
765        }
766
767        impl Model for SizeModel {
768            fn init(&self) -> Option<crate::Cmd> {
769                None
770            }
771            fn update(&mut self, msg: Message) -> Option<crate::Cmd> {
772                if let Some(size) = msg.downcast_ref::<WindowSizeMsg>() {
773                    self.width = size.width;
774                    self.height = size.height;
775                }
776                None
777            }
778            fn view(&self) -> String {
779                format!("{}x{}", self.width, self.height)
780            }
781        }
782
783        let mut sim = ProgramSimulator::new(SizeModel {
784            width: 0,
785            height: 0,
786        });
787        sim.init();
788        sim.sim_resize(120, 40);
789        sim.run_until_empty();
790
791        assert_eq!(sim.model().width, 120);
792        assert_eq!(sim.model().height, 40);
793    }
794
795    #[test]
796    fn test_sim_paste_sends_text() {
797        use crate::key::KeyMsg;
798
799        struct PasteModel {
800            pasted: String,
801        }
802
803        impl Model for PasteModel {
804            fn init(&self) -> Option<crate::Cmd> {
805                None
806            }
807            fn update(&mut self, msg: Message) -> Option<crate::Cmd> {
808                if let Some(key) = msg.downcast_ref::<KeyMsg>()
809                    && key.paste
810                {
811                    self.pasted = key.runes.iter().collect();
812                }
813                None
814            }
815            fn view(&self) -> String {
816                format!("Pasted: {}", self.pasted)
817            }
818        }
819
820        let mut sim = ProgramSimulator::new(PasteModel {
821            pasted: String::new(),
822        });
823        sim.init();
824        sim.sim_paste("Hello, World!");
825        sim.run_until_empty();
826
827        assert_eq!(sim.model().pasted, "Hello, World!");
828    }
829
830    // ========================================================================
831    // Sequence Command Tests (bd-ikfq)
832    // ========================================================================
833
834    #[test]
835    fn test_simulator_sequence_command() {
836        use crate::sequence;
837
838        struct SequenceTrigger;
839        #[derive(Clone, Copy)]
840        struct Append(char);
841
842        struct SequenceModel {
843            chars: String,
844        }
845
846        impl Model for SequenceModel {
847            fn init(&self) -> Option<crate::Cmd> {
848                None
849            }
850            fn update(&mut self, msg: Message) -> Option<crate::Cmd> {
851                if msg.is::<SequenceTrigger>() {
852                    return sequence(vec![
853                        Some(crate::Cmd::new(|| Message::new(Append('A')))),
854                        Some(crate::Cmd::new(|| Message::new(Append('B')))),
855                        Some(crate::Cmd::new(|| Message::new(Append('C')))),
856                    ]);
857                }
858                if let Some(Append(c)) = msg.downcast_ref::<Append>() {
859                    self.chars.push(*c);
860                }
861                None
862            }
863            fn view(&self) -> String {
864                self.chars.clone()
865            }
866        }
867
868        let mut sim = ProgramSimulator::new(SequenceModel {
869            chars: String::new(),
870        });
871        sim.init();
872        sim.send(Message::new(SequenceTrigger));
873
874        // Step to get the sequence command
875        let cmd = sim.step();
876        assert!(cmd.is_some());
877
878        // Execute and send the result
879        if let Some(msg) = cmd.unwrap().execute() {
880            sim.send(msg);
881        }
882
883        sim.run_until_empty();
884
885        // Sequence should execute in order: A, B, C
886        assert_eq!(sim.model().chars, "ABC");
887    }
888
889    // ========================================================================
890    // Edge Case Tests (bd-ikfq)
891    // ========================================================================
892
893    #[test]
894    fn test_empty_batch_does_not_panic() {
895        use crate::batch;
896
897        struct EmptyBatchModel;
898
899        impl Model for EmptyBatchModel {
900            fn init(&self) -> Option<crate::Cmd> {
901                // Return an empty batch
902                batch(vec![])
903            }
904            fn update(&mut self, _: Message) -> Option<crate::Cmd> {
905                None
906            }
907            fn view(&self) -> String {
908                "ok".to_string()
909            }
910        }
911
912        let mut sim = ProgramSimulator::new(EmptyBatchModel);
913        let cmd = sim.init();
914
915        // Should handle empty batch gracefully
916        if let Some(c) = cmd {
917            let msg = c.execute();
918            if let Some(m) = msg {
919                sim.send(m);
920                sim.run_until_empty();
921            }
922        }
923
924        assert_eq!(sim.last_view(), Some("ok"));
925    }
926
927    #[test]
928    fn test_recursive_updates_bounded() {
929        // Model that spawns new messages from update
930        struct RecursiveModel {
931            count: usize,
932        }
933
934        impl Model for RecursiveModel {
935            fn init(&self) -> Option<crate::Cmd> {
936                None
937            }
938            fn update(&mut self, msg: Message) -> Option<crate::Cmd> {
939                if let Some(&n) = msg.downcast_ref::<usize>() {
940                    self.count += 1;
941                    if n > 0 {
942                        // Spawn more messages
943                        return Some(crate::Cmd::new(move || Message::new(n - 1)));
944                    }
945                }
946                None
947            }
948            fn view(&self) -> String {
949                format!("Count: {}", self.count)
950            }
951        }
952
953        let mut sim = ProgramSimulator::new(RecursiveModel { count: 0 });
954        sim.init();
955        sim.send(Message::new(100usize)); // Will spawn 100 recursive messages
956
957        let processed = sim.run_until_empty();
958
959        // Should process all but stay bounded by MAX_ITERATIONS
960        assert!(processed <= 1000);
961        assert_eq!(sim.model().count, 101); // Initial + 100 recursive
962    }
963
964    #[test]
965    fn test_large_message_queue() {
966        let model = TrackingModel::new();
967        let mut sim = ProgramSimulator::new(model);
968        sim.init();
969
970        // Queue many messages
971        for i in 0_i32..500 {
972            sim.send(Message::new(i));
973        }
974
975        let processed = sim.run_until_empty();
976
977        assert_eq!(processed, 500);
978        // Sum of 0..500 = 499*500/2 = 124750
979        assert_eq!(sim.model().value, 124750);
980    }
981
982    #[test]
983    fn test_model_mut_allows_direct_modification() {
984        let model = TrackingModel::new();
985        let mut sim = ProgramSimulator::new(model);
986        sim.init();
987
988        // Directly modify the model
989        sim.model_mut().value = 999;
990
991        assert_eq!(sim.model().value, 999);
992    }
993
994    #[test]
995    fn test_step_without_messages_returns_none() {
996        let model = TrackingModel::new();
997        let mut sim = ProgramSimulator::new(model);
998        sim.init();
999
1000        // No messages in queue
1001        let cmd = sim.step();
1002        assert!(cmd.is_none());
1003    }
1004
1005    #[test]
1006    fn test_views_accumulate() {
1007        let model = TrackingModel::new();
1008        let mut sim = ProgramSimulator::new(model);
1009        sim.init();
1010
1011        assert_eq!(sim.views().len(), 1);
1012
1013        sim.send(Message::new(1));
1014        sim.step();
1015        assert_eq!(sim.views().len(), 2);
1016
1017        sim.send(Message::new(2));
1018        sim.step();
1019        assert_eq!(sim.views().len(), 3);
1020
1021        // Views should show progression
1022        assert_eq!(sim.views()[0], "Value: 0");
1023        assert_eq!(sim.views()[1], "Value: 1");
1024        assert_eq!(sim.views()[2], "Value: 3");
1025    }
1026}