Skip to main content

ixa/
context.rs

1//! A manager for the state of a discrete-event simulation
2//!
3//! Defines a [`Context`] that is intended to provide the foundational mechanism
4//! for storing and manipulating the state of a given simulation.
5use std::any::{Any, TypeId};
6use std::cell::OnceCell;
7use std::collections::VecDeque;
8use std::fmt::{Display, Formatter};
9use std::marker::PhantomData;
10use std::rc::Rc;
11
12use crate::data_plugin::DataPlugin;
13use crate::entity::entity_store::EntityStore;
14use crate::entity::multi_property::emit_pre_main_diagnostics;
15use crate::entity::property::Property;
16use crate::entity::property_value_store_core::PropertyValueStoreCore;
17use crate::entity::Entity;
18use crate::execution_stats::{
19    log_execution_statistics, print_execution_statistics, ExecutionProfilingCollector,
20    ExecutionStatistics,
21};
22use crate::global_properties::get_global_property_count;
23use crate::plan_queue::{PlanId, PlanQueue};
24use crate::{get_data_plugin_count, trace, warn, HashMap, HashMapExt};
25
26/// The common callback used by multiple [`Context`] methods for future events
27type Callback = dyn FnOnce(&mut Context);
28
29/// A handler for an event type `E`
30type EventHandler<E> = dyn Fn(&mut Context, E);
31
32/// An opaque token for a registered event listener.
33///
34/// Pass this token to [`Context::unsubscribe_from_event`] to stop the listener
35/// from receiving future emissions of the same event type.
36#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
37pub struct EventListenerId<E: IxaEvent> {
38    id: usize,
39    event_type: PhantomData<fn() -> E>,
40}
41
42impl<E: IxaEvent> EventListenerId<E> {
43    fn new(id: usize) -> Self {
44        Self {
45            id,
46            event_type: PhantomData,
47        }
48    }
49}
50
51struct EventHandlerRegistration<E: IxaEvent> {
52    listener_id: EventListenerId<E>,
53    handler: Rc<EventHandler<E>>,
54}
55
56pub trait IxaEvent: Copy + 'static {
57    /// Called after [`Context::subscribe_to_event`] registers a listener for
58    /// this event type.
59    fn on_subscribe(_context: &mut Context) {}
60
61    /// Called after [`Context::unsubscribe_from_event`] successfully removes a
62    /// listener for this event type.
63    fn on_unsubscribe(_context: &mut Context) {}
64}
65
66/// An enum to indicate the phase for plans at a given time.
67///
68/// Most plans will occur as `Normal`. Plans with phase `First` are
69/// handled before all `Normal` plans, and those with phase `Last` are
70/// handled after all `Normal` plans. In all cases ties between plans at the
71/// same time and with the same phase are handled in the order of scheduling.
72///
73#[derive(PartialEq, Eq, Ord, Clone, Copy, PartialOrd, Hash, Debug)]
74pub enum ExecutionPhase {
75    First,
76    Normal,
77    Last,
78}
79
80impl Display for ExecutionPhase {
81    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
82        write!(f, "{self:?}")
83    }
84}
85
86/// Tracks event-loop shutdown state and the current shutdown lifecycle phase.
87///
88/// This is private implementation state, not public API. `Context::shutdown`
89/// requests normal shutdown and `Context::abort` requests an immediate stop of
90/// the current `execute` loop. The stopped status is deliberately cleared when a
91/// later `execute` call begins.
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93enum ShutdownStatus {
94    /// Normal execution; no shutdown has been requested.
95    None,
96    /// Normal shutdown requested or in progress.
97    ///
98    /// In this state, callbacks still run first, but regular plans are executed
99    /// only if they are scheduled at `Context::current_time`. Simulation time is
100    /// not advanced.
101    Normal,
102    /// Drain the distinguished shutdown-time plan queue.
103    ///
104    /// Once this state is reached, regular plans are not inspected again during
105    /// the same execution pass, even if shutdown-time work schedules a regular
106    /// plan at the current simulation time. Callbacks are still executed.
107    ShutdownTimePlans,
108    /// Stop the current `execute` event loop.
109    ///
110    /// This is set by `Context::abort` and when the shutdown-time queue is
111    /// exhausted. Manual `execute_single_step` calls clear this state when there
112    /// is no callback to run.
113    Stopped,
114}
115
116/// A manager for the state of a discrete-event simulation
117///
118/// Provides core simulation services including
119/// * Maintaining a notion of time
120/// * Scheduling events to occur at some point in the future and executing them
121///   at that time
122/// * Holding data that can be accessed by simulation modules
123///
124/// Simulations are constructed out of a series of interacting modules that
125/// take turns manipulating the [`Context`] through a mutable reference. Modules
126/// store data in the simulation using the [`DataPlugin`] trait that allows them
127/// to retrieve data by type.
128///
129/// The future event list of the simulation is a queue of `Callback` objects -
130/// called `plans` - that will assume control of the [`Context`] at a future point
131/// in time and execute the logic in the associated `FnOnce(&mut Context)`
132/// closure. Modules can add plans to this queue through the [`Context`].
133///
134/// The simulation also has a separate callback mechanism. Callbacks
135/// fire before the next timed event (even if it is scheduled for the
136/// current time). This allows modules to schedule actions for immediate
137/// execution but outside of the current iteration of the event loop.
138///
139/// Modules can also emit 'events' that other modules can subscribe to handle by
140/// event type. This allows modules to broadcast that specific things have
141/// occurred and have other modules take turns reacting to these occurrences.
142///
143pub struct Context {
144    plan_queue: PlanQueue,
145    callback_queue: VecDeque<Box<Callback>>,
146    event_handlers: HashMap<TypeId, Box<dyn Any>>,
147    next_event_listener_id: usize,
148    pub(crate) entity_store: EntityStore,
149    data_plugins: Vec<OnceCell<Box<dyn Any>>>,
150    pub(crate) global_properties: Vec<OnceCell<Box<dyn Any>>>,
151    current_time: Option<f64>,
152    start_time: Option<f64>,
153    shutdown_status: ShutdownStatus,
154    execution_profiler: ExecutionProfilingCollector,
155    pub(crate) print_execution_statistics: bool,
156}
157
158impl Context {
159    /// Create a new empty `Context`
160    #[must_use]
161    pub fn new() -> Context {
162        emit_pre_main_diagnostics();
163
164        // Create a vector to accommodate all registered data plugins
165        let data_plugins = std::iter::repeat_with(OnceCell::new)
166            .take(get_data_plugin_count())
167            .collect();
168        let global_properties = std::iter::repeat_with(OnceCell::new)
169            .take(get_global_property_count())
170            .collect();
171
172        Context {
173            plan_queue: PlanQueue::new(),
174            callback_queue: VecDeque::new(),
175            event_handlers: HashMap::new(),
176            next_event_listener_id: 0,
177            entity_store: EntityStore::new(),
178            data_plugins,
179            global_properties,
180            current_time: None,
181            start_time: None,
182            shutdown_status: ShutdownStatus::None,
183            execution_profiler: ExecutionProfilingCollector::new(),
184            print_execution_statistics: false,
185        }
186    }
187
188    pub(crate) fn get_property_value_store<E: Entity, P: Property<E>>(
189        &self,
190    ) -> &PropertyValueStoreCore<E, P> {
191        self.entity_store.get_property_store::<E>().get::<P>()
192    }
193    pub(crate) fn get_property_value_store_mut<E: Entity, P: Property<E>>(
194        &mut self,
195    ) -> &mut PropertyValueStoreCore<E, P> {
196        self.entity_store
197            .get_property_store_mut::<E>()
198            .get_mut::<P>()
199    }
200
201    /// Register to handle emission of events of type E
202    ///
203    /// Handlers will be called upon event emission in order of subscription as
204    /// queued `Callback`s with the appropriate event.
205    pub fn subscribe_to_event<E: IxaEvent>(
206        &mut self,
207        handler: impl Fn(&mut Context, E) + 'static,
208    ) -> EventListenerId<E> {
209        let listener_id = EventListenerId::new(self.next_event_listener_id);
210        self.next_event_listener_id = self
211            .next_event_listener_id
212            .checked_add(1)
213            .unwrap_or_else(|| panic!("event listener id overflow"));
214
215        let handler_vec = self
216            .event_handlers
217            .entry(TypeId::of::<E>())
218            .or_insert_with(|| Box::<Vec<EventHandlerRegistration<E>>>::default());
219        let handler_vec: &mut Vec<EventHandlerRegistration<E>> =
220            handler_vec.downcast_mut().unwrap();
221        handler_vec.push(EventHandlerRegistration {
222            listener_id,
223            handler: Rc::new(handler),
224        });
225        E::on_subscribe(self);
226        listener_id
227    }
228
229    /// Unsubscribe a previously registered event listener.
230    ///
231    /// Returns `true` if a listener was unsubscribed and `false` if the token is
232    /// unknown, already unsubscribed, or otherwise absent.
233    #[allow(clippy::missing_panics_doc)]
234    pub fn unsubscribe_from_event<E: IxaEvent>(
235        &mut self,
236        listener_id: &EventListenerId<E>,
237    ) -> bool {
238        {
239            let Some(handler_vec) = self.event_handlers.get_mut(&TypeId::of::<E>()) else {
240                return false;
241            };
242            let handler_vec: &mut Vec<EventHandlerRegistration<E>> =
243                handler_vec.downcast_mut().unwrap();
244            let Some(index) = handler_vec
245                .iter()
246                .position(|entry| entry.listener_id.id == listener_id.id)
247            else {
248                return false;
249            };
250
251            handler_vec.swap_remove(index);
252        }
253
254        E::on_unsubscribe(self);
255        true
256    }
257
258    pub(crate) fn has_event_handlers<E: IxaEvent>(&self) -> bool {
259        self.event_handlers
260            .get(&TypeId::of::<E>())
261            .is_some_and(|handler_vec| {
262                let handler_vec: &Vec<EventHandlerRegistration<E>> =
263                    handler_vec.downcast_ref().unwrap();
264                !handler_vec.is_empty()
265            })
266    }
267
268    /// Emit an event of type E to be handled by registered receivers
269    ///
270    /// Receivers will handle events in the order that they have subscribed and
271    /// are queued as callbacks
272    pub fn emit_event<E: IxaEvent>(&mut self, event: E) {
273        // Destructure to obtain event handlers and plan queue
274        let Context {
275            event_handlers,
276            callback_queue,
277            ..
278        } = self;
279        if let Some(handler_vec) = event_handlers.get(&TypeId::of::<E>()) {
280            let handler_vec: &Vec<EventHandlerRegistration<E>> =
281                handler_vec.downcast_ref().unwrap();
282            for registration in handler_vec {
283                let handler_clone = Rc::clone(&registration.handler);
284                callback_queue.push_back(Box::new(move |context| handler_clone(context, event)));
285            }
286        }
287    }
288
289    /// Add a plan to the future event list at the specified time in the normal
290    /// phase
291    ///
292    /// The supplied time is converted to `f64` before validation.
293    ///
294    /// ```
295    /// use ixa::Context;
296    ///
297    /// struct ModelTime(f64);
298    ///
299    /// impl From<ModelTime> for f64 {
300    ///     fn from(time: ModelTime) -> Self {
301    ///         time.0
302    ///     }
303    /// }
304    ///
305    /// let mut context = Context::new();
306    /// context.add_plan(ModelTime(1.0), |_| {});
307    /// ```
308    ///
309    /// Returns a [`PlanId`] for the newly-added plan that can be used to cancel it
310    /// if needed.
311    /// # Panics
312    ///
313    /// Panics if time is in the past, infinite, or NaN.
314    pub fn add_plan(
315        &mut self,
316        time: impl Into<f64>,
317        callback: impl FnOnce(&mut Context) + 'static,
318    ) -> PlanId {
319        self.add_plan_with_phase(time, callback, ExecutionPhase::Normal)
320    }
321
322    /// Add a plan to the future event list at the specified time and with the
323    /// specified phase (first, normal, or last among plans at the
324    /// specified time)
325    ///
326    /// The supplied time is converted to `f64` before validation.
327    ///
328    /// Returns a [`PlanId`] for the newly-added plan that can be used to cancel it
329    /// if needed.
330    /// # Panics
331    ///
332    /// Panics if time is in the past, infinite, or NaN.
333    pub fn add_plan_with_phase(
334        &mut self,
335        time: impl Into<f64>,
336        callback: impl FnOnce(&mut Context) + 'static,
337        phase: ExecutionPhase,
338    ) -> PlanId {
339        self.add_plan_with_phase_and_passivity(time.into(), callback, phase, false)
340    }
341
342    /// Add a passive plan to the future event list at the specified time in the
343    /// normal phase.
344    ///
345    /// Passive plans execute like regular plans but do not keep the simulation
346    /// timeline alive.
347    ///
348    /// The supplied time is converted to `f64` before validation.
349    ///
350    /// Returns a [`PlanId`] for the newly-added plan that can be used to cancel it
351    /// if needed.
352    /// # Panics
353    ///
354    /// Panics if time is in the past, infinite, or NaN.
355    pub fn add_passive_plan(
356        &mut self,
357        time: impl Into<f64>,
358        callback: impl FnOnce(&mut Context) + 'static,
359    ) -> PlanId {
360        self.add_passive_plan_with_phase(time, callback, ExecutionPhase::Normal)
361    }
362
363    /// Schedule [`Context::shutdown`] at the specified maximum simulation time.
364    ///
365    /// The shutdown request is scheduled through [`Context::add_passive_plan`],
366    /// so it does not keep the simulation timeline alive when non-passive work is
367    /// exhausted before `time`. If execution reaches `time`, normal shutdown
368    /// finishes queued callbacks and regular plans at that time before running
369    /// shutdown-time plans. If execution ends earlier, the future passive
370    /// shutdown plan remains queued like any other future passive plan.
371    ///
372    /// This schedules the shutdown request itself. Use [`Context::add_shutdown_plan`]
373    /// to schedule work that should run during normal shutdown.
374    ///
375    /// Returns a [`PlanId`] that can be passed to [`Context::cancel_plan`].
376    ///
377    /// # Examples
378    ///
379    /// ```
380    /// use ixa::Context;
381    ///
382    /// let mut context = Context::new();
383    /// context.schedule_shutdown(10.0);
384    /// context.add_plan(20.0, |_| {});
385    ///
386    /// context.execute();
387    /// assert_eq!(context.get_current_time(), 10.0);
388    /// ```
389    ///
390    /// # Panics
391    ///
392    /// Panics if `time` is in the past, infinite, or NaN.
393    pub fn schedule_shutdown(&mut self, time: f64) -> PlanId {
394        self.add_passive_plan(time, Context::shutdown)
395    }
396
397    /// Add a passive plan to the future event list at the specified time and
398    /// with the specified phase.
399    ///
400    /// Passive plans execute like regular plans but do not keep the simulation
401    /// timeline alive.
402    ///
403    /// The supplied time is converted to `f64` before validation.
404    ///
405    /// Returns a [`PlanId`] for the newly-added plan that can be used to cancel it
406    /// if needed.
407    /// # Panics
408    ///
409    /// Panics if time is in the past, infinite, or NaN.
410    pub fn add_passive_plan_with_phase(
411        &mut self,
412        time: impl Into<f64>,
413        callback: impl FnOnce(&mut Context) + 'static,
414        phase: ExecutionPhase,
415    ) -> PlanId {
416        self.add_plan_with_phase_and_passivity(time.into(), callback, phase, true)
417    }
418
419    fn add_plan_with_phase_and_passivity(
420        &mut self,
421        time: f64,
422        callback: impl FnOnce(&mut Context) + 'static,
423        phase: ExecutionPhase,
424        is_passive: bool,
425    ) -> PlanId {
426        let current = self.get_current_time();
427        assert!(!time.is_nan(), "Time {time} is invalid: cannot be NaN");
428        assert!(
429            !time.is_infinite(),
430            "Time {time} is invalid: cannot be infinite"
431        );
432        assert!(
433            time >= current,
434            "Time {time} is invalid: cannot be less than the current time ({}). Consider calling set_start_time() before scheduling plans.",
435            current
436        );
437        self.plan_queue
438            .add_plan(time, Box::new(callback), phase, is_passive)
439    }
440
441    /// Add a plan to execute during shutdown-time in the normal phase.
442    ///
443    /// Shutdown-time plans execute after regular plans at the current simulation
444    /// time are exhausted during normal shutdown, and after natural exhaustion of
445    /// the regular plan queue.
446    ///
447    /// Returns a [`PlanId`] for the newly-added plan that can be used to cancel it
448    /// if needed.
449    pub fn add_shutdown_plan(&mut self, callback: impl FnOnce(&mut Context) + 'static) -> PlanId {
450        self.add_shutdown_plan_with_phase(callback, ExecutionPhase::Normal)
451    }
452
453    /// Add a plan to execute during shutdown-time with the specified phase.
454    ///
455    /// Shutdown-time plans have no simulation time. They are ordered by phase and
456    /// insertion order.
457    ///
458    /// Returns a [`PlanId`] for the newly-added plan that can be used to cancel it
459    /// if needed.
460    pub fn add_shutdown_plan_with_phase(
461        &mut self,
462        callback: impl FnOnce(&mut Context) + 'static,
463        phase: ExecutionPhase,
464    ) -> PlanId {
465        self.plan_queue.add_shutdown_plan(Box::new(callback), phase)
466    }
467
468    pub(crate) fn evaluate_periodic_and_schedule_next(
469        &mut self,
470        period: f64,
471        callback: impl Fn(&mut Context) + 'static,
472        phase: ExecutionPhase,
473    ) {
474        trace!(
475            "evaluate periodic at {} (period={})",
476            self.get_current_time(),
477            period
478        );
479        callback(self);
480        let next_time = self.get_current_time() + period;
481        self.add_passive_plan_with_phase(
482            next_time,
483            move |context| context.evaluate_periodic_and_schedule_next(period, callback, phase),
484            phase,
485        );
486    }
487
488    /// Add a passive periodic plan with specified priority to the future event
489    /// list.
490    ///
491    /// Periodic plans reschedule themselves after every run. They do not keep
492    /// the simulation timeline alive: when no non-passive plans remain, normal
493    /// shutdown begins, and only passive plans at the final current time can
494    /// still run during that execution pass. Future passive periodic plans
495    /// remain queued and may run if later non-passive work is scheduled.
496    ///
497    /// The supplied period is converted to `f64` before validation.
498    ///
499    /// Notes:
500    /// * The first periodic plan is scheduled at time `0.0`. If `set_start_time` was
501    ///   set to a positive value, this will currently panic because the first plan
502    ///   occurs before the start time (see issue #634 for future behavior).
503    ///
504    /// # Panics
505    ///
506    /// Panics if plan period is negative, infinite, or NaN.
507    pub fn add_periodic_plan_with_phase(
508        &mut self,
509        period: impl Into<f64>,
510        callback: impl Fn(&mut Context) + 'static,
511        phase: ExecutionPhase,
512    ) {
513        let period = period.into();
514        assert!(
515            period > 0.0 && !period.is_nan() && !period.is_infinite(),
516            "Period must be greater than 0"
517        );
518
519        self.add_passive_plan_with_phase(
520            0.0,
521            move |context| context.evaluate_periodic_and_schedule_next(period, callback, phase),
522            phase,
523        );
524    }
525
526    /// Cancel a plan that has been added to the queue
527    ///
528    /// # Panics
529    ///
530    /// This function panics if you cancel a plan which has already been
531    /// cancelled or executed.
532    pub fn cancel_plan(&mut self, plan_id: &PlanId) {
533        trace!("canceling plan {plan_id:?}");
534        let result = self.plan_queue.cancel_plan(plan_id);
535        if result.is_none() {
536            warn!("Tried to cancel nonexistent plan with ID = {plan_id:?}");
537        }
538    }
539
540    /// Add a `Callback` to the queue to be executed before the next plan
541    pub fn queue_callback(&mut self, callback: impl FnOnce(&mut Context) + 'static) {
542        trace!("queuing callback");
543        self.callback_queue.push_back(Box::new(callback));
544    }
545
546    /// Retrieve a mutable reference to the data container associated with a
547    /// [`DataPlugin`]
548    ///
549    /// If the data container has not been already added to the [`Context`] then
550    /// this function will use the [`DataPlugin::init`] method
551    /// to construct a new data container and store it in the [`Context`].
552    ///
553    /// Returns a mutable reference to the data container
554    #[must_use]
555    pub fn get_data_mut<T: DataPlugin>(&mut self, _data_plugin: T) -> &mut T::DataContainer {
556        let index = T::index_within_context();
557
558        // If the data plugin is already initialized, return a mutable reference.
559        if self.data_plugins[index].get().is_some() {
560            return self.data_plugins[index]
561                .get_mut()
562                .unwrap()
563                .downcast_mut::<T::DataContainer>()
564                .expect("TypeID does not match data plugin type");
565        }
566
567        // Initialize the data plugin if not already initialized.
568        let data = T::init(self);
569        let cell = self
570            .data_plugins
571            .get_mut(index)
572            .unwrap_or_else(|| panic!("No data plugin found with index = {index:?}. You must use the `define_data_plugin!` macro to create a data plugin."));
573        let _ = cell.set(Box::new(data));
574        cell.get_mut()
575            .unwrap()
576            .downcast_mut::<T::DataContainer>()
577            .expect("TypeID does not match data plugin type. You must use the `define_data_plugin!` macro to create a data plugin.")
578    }
579
580    /// Retrieve a reference to the data container associated with a
581    /// [`DataPlugin`]
582    ///
583    /// Returns a reference to the data container if it exists or else `None`
584    #[must_use]
585    pub fn get_data<T: DataPlugin>(&self, _data_plugin: T) -> &T::DataContainer {
586        let index = T::index_within_context();
587        self.data_plugins
588            .get(index)
589            .unwrap_or_else(|| panic!("No data plugin found with index = {index:?}. You must use the `define_data_plugin!` macro to create a data plugin."))
590            .get_or_init(|| Box::new(T::init(self)))
591            .downcast_ref::<T::DataContainer>()
592            .expect("TypeID does not match data plugin type. You must use the `define_data_plugin!` macro to create a data plugin.")
593    }
594
595    /// Request normal shutdown.
596    ///
597    /// Normal shutdown stops simulation time from advancing. Execution continues
598    /// through queued callbacks, regular plans at the current time, and then
599    /// shutdown-time plans. Calling `shutdown` during shutdown-time execution
600    /// does not return execution to regular current-time plans.
601    pub fn shutdown(&mut self) {
602        trace!("shutdown context");
603        if self.shutdown_status == ShutdownStatus::None {
604            self.shutdown_status = ShutdownStatus::Normal;
605        }
606    }
607
608    /// Stop the current event loop immediately.
609    ///
610    /// Abort only stops the current `execute` loop. The stopped status is cleared
611    /// when `execute` is called again.
612    pub fn abort(&mut self) {
613        trace!("abort context");
614        self.shutdown_status = ShutdownStatus::Stopped;
615    }
616
617    /// Get the current simulation time
618    ///
619    /// Returns the current time in the simulation. The behavior depends on execution state:
620    /// * During execution: returns the time of the currently executing plan or callback
621    /// * Before execution: returns the start time (if set via [`Context::set_start_time`]), or `0.0`
622    ///
623    /// The time can be negative if a negative start time was set before execution.
624    #[must_use]
625    pub fn get_current_time(&self) -> f64 {
626        self.current_time.or(self.start_time).unwrap_or(0.0)
627    }
628
629    /// Set the start time for the simulation. Must be finite.
630    ///
631    /// The supplied start time is converted to `f64` before validation.
632    ///
633    /// * Call before `Context.execute()`.
634    /// * `start_time` must be finite (not NaN or infinite).
635    /// * May be called only once.
636    /// * If plans are already scheduled, `start_time` must be earlier than or equal to
637    ///   the earliest scheduled plan time.
638    ///
639    /// # Panics
640    ///
641    /// Panics if:
642    /// * `start_time` is NaN or infinite.
643    /// * the start time was already set.
644    /// * `Context::execute()` has been called.
645    /// * `start_time` is later than the earliest scheduled plan time.
646    pub fn set_start_time(&mut self, start_time: impl Into<f64>) {
647        let start_time = start_time.into();
648        assert!(
649            !start_time.is_nan() && !start_time.is_infinite(),
650            "Start time {start_time} must be finite"
651        );
652        assert!(
653            self.start_time.is_none(),
654            "Start time has already been set. It can only be set once."
655        );
656        assert!(
657            self.current_time.is_none(),
658            "Start time cannot be set after execution has begun."
659        );
660        if let Some(next_time) = self.plan_queue.next_time() {
661            assert!(
662                start_time <= next_time,
663                "Start time {} is later than the earliest scheduled plan time {}. Remove or reschedule existing plans first.",
664                start_time,
665                next_time
666            );
667        }
668        self.start_time = Some(start_time);
669    }
670
671    /// Get the start time that was set via `set_start_time`, or `None` if not set.
672    #[must_use]
673    pub fn get_start_time(&self) -> Option<f64> {
674        self.start_time
675    }
676
677    /// Execute the simulation until callbacks and plans are exhausted and shutdown
678    /// work is complete.
679    pub fn execute(&mut self) {
680        trace!("entering event loop");
681
682        if self.shutdown_status == ShutdownStatus::Stopped {
683            self.shutdown_status = ShutdownStatus::None;
684        }
685
686        if self.current_time.is_none() {
687            self.current_time = Some(self.start_time.unwrap_or(0.0));
688        }
689
690        // Start plan loop
691        loop {
692            if self.shutdown_status == ShutdownStatus::Stopped {
693                self.shutdown_status = ShutdownStatus::None;
694                break;
695            }
696
697            self.execute_single_step();
698            self.execution_profiler.refresh();
699        }
700
701        let stats = self.get_execution_statistics();
702        if self.print_execution_statistics {
703            print_execution_statistics(&stats);
704            #[cfg(feature = "profiling")]
705            crate::profiling::print_profiling_data();
706        } else {
707            log_execution_statistics(&stats);
708        }
709    }
710
711    /// Executes a single callback, plan, or shutdown status transition.
712    pub fn execute_single_step(&mut self) {
713        // Callbacks always have priority over plan selection. This remains true
714        // even in `Stopped` during manual stepping; `Stopped` only stops the
715        // `execute` loop, not the ability to explicitly step callbacks manually.
716        if let Some(callback) = self.callback_queue.pop_front() {
717            trace!("calling callback");
718            callback(self);
719            return;
720        }
721
722        // No callback is available, so the shutdown status determines which
723        // plan queue, if any, can provide the next unit of work.
724        match self.shutdown_status {
725            ShutdownStatus::None => {
726                // Normal execution may advance simulation time to the next
727                // regular plan only while non-passive regular work remains. Once no
728                // non-passive regular plans remain, enter normal shutdown to drain
729                // current-time regular work without advancing time.
730                if let Some(plan) = self.plan_queue.pop_next_if_active() {
731                    trace!("calling plan at {:.6}", plan.time);
732                    self.current_time = Some(plan.time);
733                    (plan.data)(self);
734                } else {
735                    self.shutdown_status = ShutdownStatus::Normal;
736                }
737            }
738            ShutdownStatus::Normal => {
739                // Normal shutdown drains only regular plans scheduled at the
740                // current simulation time. Future regular plans must remain in
741                // the queue so a later `execute` call can run them.
742                if let Some(plan) = self.plan_queue.pop_next_at(self.get_current_time()) {
743                    trace!("calling plan at {:.6}", plan.time);
744                    (plan.data)(self);
745                } else {
746                    self.shutdown_status = ShutdownStatus::ShutdownTimePlans;
747                }
748            }
749            ShutdownStatus::ShutdownTimePlans => {
750                // Once shutdown-time draining begins, do not return to the
751                // regular plan queue during this execution pass.
752                if let Some(plan) = self.plan_queue.pop_next_shutdown() {
753                    trace!("calling shutdown-time plan");
754                    (plan.data)(self);
755                } else {
756                    self.shutdown_status = ShutdownStatus::Stopped;
757                }
758            }
759            ShutdownStatus::Stopped => {
760                // `execute` exits before calling `execute_single_step` in this
761                // state. This arm supports manual single-step use after a prior
762                // stop by consuming the stopped status when no callback exists.
763                self.shutdown_status = ShutdownStatus::None;
764            }
765        }
766    }
767
768    #[must_use]
769    pub fn get_execution_statistics(&mut self) -> ExecutionStatistics {
770        #[allow(unused_mut)]
771        let mut stats = self.execution_profiler.compute_final_statistics();
772        #[cfg(feature = "profiling")]
773        {
774            stats.max_plans_in_flight = self.plan_queue.max_plans_in_flight;
775            stats.max_plan_queue_memory_in_use = self.plan_queue.max_memory_in_use;
776        }
777        stats
778    }
779}
780
781pub trait ContextBase: Sized {
782    fn subscribe_to_event<E: IxaEvent>(
783        &mut self,
784        handler: impl Fn(&mut Context, E) + 'static,
785    ) -> EventListenerId<E>;
786    fn unsubscribe_from_event<E: IxaEvent>(&mut self, listener_id: &EventListenerId<E>) -> bool;
787    fn emit_event<E: IxaEvent>(&mut self, event: E);
788    fn add_plan(
789        &mut self,
790        time: impl Into<f64>,
791        callback: impl FnOnce(&mut Context) + 'static,
792    ) -> PlanId;
793    fn add_plan_with_phase(
794        &mut self,
795        time: impl Into<f64>,
796        callback: impl FnOnce(&mut Context) + 'static,
797        phase: ExecutionPhase,
798    ) -> PlanId;
799    fn add_passive_plan(
800        &mut self,
801        time: impl Into<f64>,
802        callback: impl FnOnce(&mut Context) + 'static,
803    ) -> PlanId;
804    fn add_passive_plan_with_phase(
805        &mut self,
806        time: impl Into<f64>,
807        callback: impl FnOnce(&mut Context) + 'static,
808        phase: ExecutionPhase,
809    ) -> PlanId;
810    /// Schedule normal shutdown as a passive plan at the specified maximum time.
811    ///
812    /// See [`Context::schedule_shutdown`] for full behavior and panic semantics.
813    fn schedule_shutdown(&mut self, time: f64) -> PlanId {
814        self.add_passive_plan(time, Context::shutdown)
815    }
816    fn add_shutdown_plan(&mut self, callback: impl FnOnce(&mut Context) + 'static) -> PlanId;
817    fn add_shutdown_plan_with_phase(
818        &mut self,
819        callback: impl FnOnce(&mut Context) + 'static,
820        phase: ExecutionPhase,
821    ) -> PlanId;
822    fn add_periodic_plan_with_phase(
823        &mut self,
824        period: impl Into<f64>,
825        callback: impl Fn(&mut Context) + 'static,
826        phase: ExecutionPhase,
827    );
828    fn cancel_plan(&mut self, plan_id: &PlanId);
829    fn queue_callback(&mut self, callback: impl FnOnce(&mut Context) + 'static);
830    #[must_use]
831    fn get_data_mut<T: DataPlugin>(&mut self, plugin: T) -> &mut T::DataContainer;
832    #[must_use]
833    fn get_data<T: DataPlugin>(&self, plugin: T) -> &T::DataContainer;
834    #[must_use]
835    fn get_current_time(&self) -> f64;
836    #[must_use]
837    fn get_execution_statistics(&mut self) -> ExecutionStatistics;
838    fn abort(&mut self);
839}
840impl ContextBase for Context {
841    delegate::delegate! {
842        to self {
843            fn subscribe_to_event<E: IxaEvent>(&mut self, handler: impl Fn(&mut Context, E) + 'static) -> EventListenerId<E>;
844            fn unsubscribe_from_event<E: IxaEvent>(&mut self, listener_id: &EventListenerId<E>) -> bool;
845            fn emit_event<E: IxaEvent>(&mut self, event: E);
846            fn add_plan(&mut self, time: impl Into<f64>, callback: impl FnOnce(&mut Context) + 'static) -> PlanId;
847            fn add_plan_with_phase(&mut self, time: impl Into<f64>, callback: impl FnOnce(&mut Context) + 'static, phase: ExecutionPhase) -> PlanId;
848            fn add_passive_plan(&mut self, time: impl Into<f64>, callback: impl FnOnce(&mut Context) + 'static) -> PlanId;
849            fn add_passive_plan_with_phase(&mut self, time: impl Into<f64>, callback: impl FnOnce(&mut Context) + 'static, phase: ExecutionPhase) -> PlanId;
850            fn add_shutdown_plan(&mut self, callback: impl FnOnce(&mut Context) + 'static) -> PlanId;
851            fn add_shutdown_plan_with_phase(&mut self, callback: impl FnOnce(&mut Context) + 'static, phase: ExecutionPhase) -> PlanId;
852            fn add_periodic_plan_with_phase(&mut self, period: impl Into<f64>, callback: impl Fn(&mut Context) + 'static, phase: ExecutionPhase);
853            fn cancel_plan(&mut self, plan_id: &PlanId);
854            fn queue_callback(&mut self, callback: impl FnOnce(&mut Context) + 'static);
855            fn get_data_mut<T: DataPlugin>(&mut self, plugin: T) -> &mut T::DataContainer;
856            fn get_data<T: DataPlugin>(&self, plugin: T) -> &T::DataContainer;
857            fn get_current_time(&self) -> f64;
858            fn get_execution_statistics(&mut self) -> ExecutionStatistics;
859            fn abort(&mut self);
860        }
861    }
862}
863
864impl Default for Context {
865    fn default() -> Self {
866        Self::new()
867    }
868}
869
870#[cfg(test)]
871mod tests {
872    // We allow defining items that are never used to test macros.
873    #![allow(dead_code)]
874    use std::cell::RefCell;
875    use std::marker::PhantomData;
876
877    use super::*;
878    use crate::{
879        define_data_plugin, define_entity, define_property, with, ContextEntitiesExt, IxaEvent,
880    };
881
882    #[derive(Clone, Copy)]
883    struct WrappedF64(f64);
884
885    impl From<WrappedF64> for f64 {
886        fn from(value: WrappedF64) -> Self {
887            value.0
888        }
889    }
890
891    define_data_plugin!(ComponentA, Vec<u32>, vec![]);
892    define_data_plugin!(UnsubscribeHookObservations, Vec<bool>, vec![]);
893
894    define_entity!(Person);
895
896    define_property!(struct Age(u8), Person);
897
898    define_property!(
899        enum InfectionStatus {
900            Susceptible,
901            Infected,
902            Recovered,
903        },
904        Person,
905        default_const = InfectionStatus::Susceptible
906    );
907
908    define_property!(
909        struct Vaccinated(bool),
910        Person,
911        default_const = Vaccinated(false)
912    );
913
914    #[test]
915    fn empty_context() {
916        let mut context = Context::new();
917        context.execute();
918        assert_eq!(context.get_current_time(), 0.0);
919    }
920
921    #[test]
922    fn get_data() {
923        let mut context = Context::new();
924        context.get_data_mut(ComponentA).push(1);
925        assert_eq!(*context.get_data(ComponentA), vec![1],);
926    }
927
928    fn add_plan(context: &mut Context, time: f64, value: u32) -> PlanId {
929        context.add_plan(time, move |context| {
930            context.get_data_mut(ComponentA).push(value);
931        })
932    }
933
934    fn add_plan_with_phase(
935        context: &mut Context,
936        time: f64,
937        value: u32,
938        phase: ExecutionPhase,
939    ) -> PlanId {
940        context.add_plan_with_phase(
941            time,
942            move |context| {
943                context.get_data_mut(ComponentA).push(value);
944            },
945            phase,
946        )
947    }
948
949    fn add_passive_plan(context: &mut Context, time: f64, value: u32) -> PlanId {
950        context.add_passive_plan(time, move |context| {
951            context.get_data_mut(ComponentA).push(value);
952        })
953    }
954
955    fn add_passive_plan_with_phase(
956        context: &mut Context,
957        time: f64,
958        value: u32,
959        phase: ExecutionPhase,
960    ) -> PlanId {
961        context.add_passive_plan_with_phase(
962            time,
963            move |context| {
964                context.get_data_mut(ComponentA).push(value);
965            },
966            phase,
967        )
968    }
969
970    fn add_wrapped_plan_through_context_base(context: &mut impl ContextBase, value: u32) {
971        context.add_plan(WrappedF64(0.0), move |context| {
972            context.get_data_mut(ComponentA).push(value);
973        });
974    }
975
976    #[test]
977    fn time_boundaries_accept_into_f64() {
978        let mut context = Context::new();
979        context.set_start_time(WrappedF64(0.0));
980
981        add_wrapped_plan_through_context_base(&mut context, 1);
982        context.add_plan_with_phase(
983            WrappedF64(0.0),
984            |context| context.get_data_mut(ComponentA).push(2),
985            ExecutionPhase::First,
986        );
987        context.add_passive_plan(WrappedF64(0.0), |context| {
988            context.get_data_mut(ComponentA).push(3);
989        });
990        context.add_passive_plan_with_phase(
991            WrappedF64(0.0),
992            |context| context.get_data_mut(ComponentA).push(4),
993            ExecutionPhase::Last,
994        );
995        context.add_periodic_plan_with_phase(
996            WrappedF64(1.0),
997            |context| context.get_data_mut(ComponentA).push(5),
998            ExecutionPhase::Normal,
999        );
1000
1001        context.execute();
1002
1003        let mut observed = context.get_data(ComponentA).clone();
1004        observed.sort_unstable();
1005        assert_eq!(observed, vec![1, 2, 3, 4, 5]);
1006        assert_eq!(context.get_start_time(), Some(0.0));
1007    }
1008
1009    #[test]
1010    #[should_panic(expected = "Time inf is invalid")]
1011    fn infinite_plan_time() {
1012        let mut context = Context::new();
1013        add_plan(&mut context, f64::INFINITY, 0);
1014    }
1015
1016    #[test]
1017    #[should_panic(expected = "Time NaN is invalid")]
1018    fn nan_plan_time() {
1019        let mut context = Context::new();
1020        add_plan(&mut context, f64::NAN, 0);
1021    }
1022
1023    #[test]
1024    #[should_panic(expected = "Time NaN is invalid")]
1025    fn wrapped_nan_plan_time_panics() {
1026        let mut context = Context::new();
1027        context.add_plan(WrappedF64(f64::NAN), |_| {});
1028    }
1029
1030    #[test]
1031    fn timed_plan_only() {
1032        let mut context = Context::new();
1033        add_plan(&mut context, 1.0, 1);
1034        context.execute();
1035        assert_eq!(context.get_current_time(), 1.0);
1036        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1037    }
1038
1039    #[test]
1040    fn callback_only() {
1041        let mut context = Context::new();
1042        context.queue_callback(|context| {
1043            context.get_data_mut(ComponentA).push(1);
1044        });
1045        context.execute();
1046        assert_eq!(context.get_current_time(), 0.0);
1047        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1048    }
1049
1050    #[test]
1051    fn callback_before_timed_plan() {
1052        let mut context = Context::new();
1053        context.queue_callback(|context| {
1054            context.get_data_mut(ComponentA).push(1);
1055        });
1056        add_plan(&mut context, 1.0, 2);
1057        context.execute();
1058        assert_eq!(context.get_current_time(), 1.0);
1059        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2]);
1060    }
1061
1062    #[test]
1063    fn callback_adds_timed_plan() {
1064        let mut context = Context::new();
1065        context.queue_callback(|context| {
1066            context.get_data_mut(ComponentA).push(1);
1067            add_plan(context, 1.0, 2);
1068            context.get_data_mut(ComponentA).push(3);
1069        });
1070        context.execute();
1071        assert_eq!(context.get_current_time(), 1.0);
1072        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 3, 2]);
1073    }
1074
1075    #[test]
1076    fn callback_adds_callback_and_timed_plan() {
1077        let mut context = Context::new();
1078        context.queue_callback(|context| {
1079            context.get_data_mut(ComponentA).push(1);
1080            add_plan(context, 1.0, 2);
1081            context.queue_callback(|context| {
1082                context.get_data_mut(ComponentA).push(4);
1083            });
1084            context.get_data_mut(ComponentA).push(3);
1085        });
1086        context.execute();
1087        assert_eq!(context.get_current_time(), 1.0);
1088        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 3, 4, 2]);
1089    }
1090
1091    #[test]
1092    fn timed_plan_adds_callback_and_timed_plan() {
1093        let mut context = Context::new();
1094        context.add_plan(1.0, |context| {
1095            context.get_data_mut(ComponentA).push(1);
1096            // We add the plan first, but the callback will fire first.
1097            add_plan(context, 2.0, 3);
1098            context.queue_callback(|context| {
1099                context.get_data_mut(ComponentA).push(2);
1100            });
1101        });
1102        context.execute();
1103        assert_eq!(context.get_current_time(), 2.0);
1104        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
1105    }
1106
1107    #[test]
1108    fn cancel_plan() {
1109        let mut context = Context::new();
1110        let to_cancel = add_plan(&mut context, 2.0, 1);
1111        context.add_plan(1.0, move |context| {
1112            context.cancel_plan(&to_cancel);
1113        });
1114        context.execute();
1115        assert_eq!(context.get_current_time(), 1.0);
1116        let test_vec: Vec<u32> = vec![];
1117        assert_eq!(*context.get_data_mut(ComponentA), test_vec);
1118    }
1119
1120    #[test]
1121    fn add_plan_with_current_time() {
1122        let mut context = Context::new();
1123        context.add_plan(1.0, move |context| {
1124            context.get_data_mut(ComponentA).push(1);
1125            add_plan(context, 1.0, 2);
1126            context.queue_callback(|context| {
1127                context.get_data_mut(ComponentA).push(3);
1128            });
1129        });
1130        context.execute();
1131        assert_eq!(context.get_current_time(), 1.0);
1132        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 3, 2]);
1133    }
1134
1135    #[test]
1136    fn plans_at_same_time_fire_in_order() {
1137        let mut context = Context::new();
1138        add_plan(&mut context, 1.0, 1);
1139        add_plan(&mut context, 1.0, 2);
1140        context.execute();
1141        assert_eq!(context.get_current_time(), 1.0);
1142        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2]);
1143    }
1144
1145    #[test]
1146    fn check_plan_phase_ordering() {
1147        assert!(ExecutionPhase::First < ExecutionPhase::Normal);
1148        assert!(ExecutionPhase::Normal < ExecutionPhase::Last);
1149    }
1150
1151    #[test]
1152    fn plans_at_same_time_follow_phase() {
1153        let mut context = Context::new();
1154        add_plan(&mut context, 1.0, 1);
1155        add_plan_with_phase(&mut context, 1.0, 5, ExecutionPhase::Last);
1156        add_plan_with_phase(&mut context, 1.0, 3, ExecutionPhase::First);
1157        add_plan(&mut context, 1.0, 2);
1158        add_plan_with_phase(&mut context, 1.0, 6, ExecutionPhase::Last);
1159        add_plan_with_phase(&mut context, 1.0, 4, ExecutionPhase::First);
1160        context.execute();
1161        assert_eq!(context.get_current_time(), 1.0);
1162        assert_eq!(*context.get_data_mut(ComponentA), vec![3, 4, 1, 2, 5, 6]);
1163    }
1164
1165    #[derive(IxaEvent)]
1166    struct Event1 {
1167        pub data: usize,
1168    }
1169
1170    #[derive(IxaEvent)]
1171    struct Event2 {
1172        pub data: usize,
1173    }
1174
1175    #[derive(Clone, Copy)]
1176    struct EventWithOnUnsubscribe;
1177
1178    impl IxaEvent for EventWithOnUnsubscribe {
1179        fn on_unsubscribe(context: &mut Context) {
1180            let has_event_handlers = context.has_event_handlers::<Self>();
1181            context
1182                .get_data_mut(UnsubscribeHookObservations)
1183                .push(has_event_handlers);
1184        }
1185    }
1186
1187    struct NotCopy;
1188
1189    #[derive(IxaEvent)]
1190    struct GenericEvent<T> {
1191        pub data: usize,
1192        _marker: PhantomData<T>,
1193    }
1194
1195    #[test]
1196    fn simple_event() {
1197        let mut context = Context::new();
1198        let obs_data = Rc::new(RefCell::new(0));
1199        let obs_data_clone = Rc::clone(&obs_data);
1200
1201        context.subscribe_to_event::<Event1>(move |_, event| {
1202            *obs_data_clone.borrow_mut() = event.data;
1203        });
1204
1205        context.emit_event(Event1 { data: 1 });
1206        context.execute();
1207        assert_eq!(*obs_data.borrow(), 1);
1208    }
1209
1210    #[test]
1211    fn derive_ixa_event_implements_copy_for_generic_events() {
1212        fn assert_clone<T: Clone>() {}
1213        fn assert_copy<T: Copy>() {}
1214        assert_clone::<GenericEvent<NotCopy>>();
1215        assert_copy::<GenericEvent<NotCopy>>();
1216
1217        let mut context = Context::new();
1218        let obs_data = Rc::new(RefCell::new(0));
1219        let obs_data_clone = Rc::clone(&obs_data);
1220
1221        context.subscribe_to_event::<GenericEvent<NotCopy>>(move |_, event| {
1222            *obs_data_clone.borrow_mut() = event.data;
1223        });
1224
1225        let event = GenericEvent::<NotCopy> {
1226            data: 5,
1227            _marker: PhantomData,
1228        };
1229        let copied_event = event;
1230
1231        assert_eq!(copied_event.data, 5);
1232        context.emit_event(copied_event);
1233        context.execute();
1234        assert_eq!(*obs_data.borrow(), 5);
1235    }
1236
1237    #[test]
1238    fn multiple_events() {
1239        let mut context = Context::new();
1240        let obs_data = Rc::new(RefCell::new(0));
1241        let obs_data_clone = Rc::clone(&obs_data);
1242
1243        context.subscribe_to_event::<Event1>(move |_, event| {
1244            *obs_data_clone.borrow_mut() += event.data;
1245        });
1246
1247        context.emit_event(Event1 { data: 1 });
1248        context.emit_event(Event1 { data: 2 });
1249        context.execute();
1250
1251        // Both of these should have been received.
1252        assert_eq!(*obs_data.borrow(), 3);
1253    }
1254
1255    #[test]
1256    fn multiple_event_handlers() {
1257        let mut context = Context::new();
1258        let obs_data1 = Rc::new(RefCell::new(0));
1259        let obs_data1_clone = Rc::clone(&obs_data1);
1260        let obs_data2 = Rc::new(RefCell::new(0));
1261        let obs_data2_clone = Rc::clone(&obs_data2);
1262
1263        context.subscribe_to_event::<Event1>(move |_, event| {
1264            *obs_data1_clone.borrow_mut() = event.data;
1265        });
1266        context.subscribe_to_event::<Event1>(move |_, event| {
1267            *obs_data2_clone.borrow_mut() = event.data;
1268        });
1269        context.emit_event(Event1 { data: 1 });
1270        context.execute();
1271        assert_eq!(*obs_data1.borrow(), 1);
1272        assert_eq!(*obs_data2.borrow(), 1);
1273    }
1274
1275    #[test]
1276    fn multiple_event_types() {
1277        let mut context = Context::new();
1278        let obs_data1 = Rc::new(RefCell::new(0));
1279        let obs_data1_clone = Rc::clone(&obs_data1);
1280        let obs_data2 = Rc::new(RefCell::new(0));
1281        let obs_data2_clone = Rc::clone(&obs_data2);
1282
1283        context.subscribe_to_event::<Event1>(move |_, event| {
1284            *obs_data1_clone.borrow_mut() = event.data;
1285        });
1286        context.subscribe_to_event::<Event2>(move |_, event| {
1287            *obs_data2_clone.borrow_mut() = event.data;
1288        });
1289        context.emit_event(Event1 { data: 1 });
1290        context.emit_event(Event2 { data: 2 });
1291        context.execute();
1292        assert_eq!(*obs_data1.borrow(), 1);
1293        assert_eq!(*obs_data2.borrow(), 2);
1294    }
1295
1296    #[test]
1297    fn unsubscribe_from_event_before_emit() {
1298        let mut context = Context::new();
1299        let obs_data = Rc::new(RefCell::new(0));
1300        let obs_data_clone = Rc::clone(&obs_data);
1301
1302        let listener_id = context.subscribe_to_event::<Event1>(move |_, event| {
1303            *obs_data_clone.borrow_mut() = event.data;
1304        });
1305
1306        assert!(context.has_event_handlers::<Event1>());
1307        assert!(context.unsubscribe_from_event(&listener_id));
1308        assert!(!context.has_event_handlers::<Event1>());
1309
1310        context.emit_event(Event1 { data: 1 });
1311        context.execute();
1312        assert_eq!(*obs_data.borrow(), 0);
1313    }
1314
1315    #[test]
1316    fn unsubscribe_from_event_calls_hook_after_removal() {
1317        let mut context = Context::new();
1318        let listener_id = context.subscribe_to_event::<EventWithOnUnsubscribe>(|_, _| {});
1319
1320        assert!(context.unsubscribe_from_event(&listener_id));
1321
1322        assert_eq!(context.get_data(UnsubscribeHookObservations), &vec![false]);
1323    }
1324
1325    #[test]
1326    fn unsubscribe_from_event_calls_hook_only_for_successful_removals() {
1327        let mut context = Context::new();
1328        let first_listener = context.subscribe_to_event::<EventWithOnUnsubscribe>(|_, _| {});
1329        let second_listener = context.subscribe_to_event::<EventWithOnUnsubscribe>(|_, _| {});
1330        let unknown_listener = EventListenerId::<EventWithOnUnsubscribe>::new(usize::MAX);
1331
1332        assert!(context.unsubscribe_from_event(&first_listener));
1333        assert!(!context.unsubscribe_from_event(&first_listener));
1334        assert!(!context.unsubscribe_from_event(&unknown_listener));
1335        assert!(context.unsubscribe_from_event(&second_listener));
1336
1337        assert_eq!(
1338            context.get_data(UnsubscribeHookObservations),
1339            &vec![true, false]
1340        );
1341    }
1342
1343    #[test]
1344    fn unsubscribe_from_event_preserves_other_listeners_in_order() {
1345        let mut context = Context::new();
1346        let observed = Rc::new(RefCell::new(Vec::new()));
1347
1348        let observed_clone = Rc::clone(&observed);
1349        context.subscribe_to_event::<Event1>(move |_, event| {
1350            observed_clone.borrow_mut().push(event.data);
1351        });
1352        let observed_clone = Rc::clone(&observed);
1353        let listener_to_unsubscribe = context.subscribe_to_event::<Event1>(move |_, event| {
1354            observed_clone.borrow_mut().push(event.data + 10);
1355        });
1356        let observed_clone = Rc::clone(&observed);
1357        context.subscribe_to_event::<Event1>(move |_, event| {
1358            observed_clone.borrow_mut().push(event.data + 20);
1359        });
1360
1361        assert!(context.unsubscribe_from_event(&listener_to_unsubscribe));
1362
1363        context.emit_event(Event1 { data: 1 });
1364        context.execute();
1365        assert_eq!(*observed.borrow(), vec![1, 21]);
1366    }
1367
1368    #[test]
1369    fn unsubscribe_from_event_does_not_affect_other_event_types() {
1370        let mut context = Context::new();
1371        let obs_data1 = Rc::new(RefCell::new(0));
1372        let obs_data1_clone = Rc::clone(&obs_data1);
1373        let obs_data2 = Rc::new(RefCell::new(0));
1374        let obs_data2_clone = Rc::clone(&obs_data2);
1375
1376        let listener_id = context.subscribe_to_event::<Event1>(move |_, event| {
1377            *obs_data1_clone.borrow_mut() = event.data;
1378        });
1379        context.subscribe_to_event::<Event2>(move |_, event| {
1380            *obs_data2_clone.borrow_mut() = event.data;
1381        });
1382
1383        assert!(context.unsubscribe_from_event(&listener_id));
1384
1385        context.emit_event(Event1 { data: 1 });
1386        context.emit_event(Event2 { data: 2 });
1387        context.execute();
1388        assert_eq!(*obs_data1.borrow(), 0);
1389        assert_eq!(*obs_data2.borrow(), 2);
1390    }
1391
1392    #[test]
1393    fn unsubscribe_from_event_does_not_cancel_already_queued_callback() {
1394        let mut context = Context::new();
1395        let obs_data = Rc::new(RefCell::new(0));
1396        let obs_data_clone = Rc::clone(&obs_data);
1397
1398        let listener_id = context.subscribe_to_event::<Event1>(move |_, event| {
1399            *obs_data_clone.borrow_mut() = event.data;
1400        });
1401
1402        context.emit_event(Event1 { data: 1 });
1403        assert!(context.unsubscribe_from_event(&listener_id));
1404        context.execute();
1405        assert_eq!(*obs_data.borrow(), 1);
1406    }
1407
1408    #[test]
1409    fn unsubscribe_from_event_returns_false_when_already_unsubscribed() {
1410        let mut context = Context::new();
1411        let listener_id = context.subscribe_to_event::<Event1>(move |_, _| {});
1412
1413        assert!(context.unsubscribe_from_event(&listener_id));
1414        assert!(!context.unsubscribe_from_event(&listener_id));
1415    }
1416
1417    #[test]
1418    fn unsubscribe_from_event_returns_false_for_unknown_listener_id() {
1419        let mut context1 = Context::new();
1420        let mut context2 = Context::new();
1421        let obs_data = Rc::new(RefCell::new(0));
1422        let obs_data_clone = Rc::clone(&obs_data);
1423
1424        context1.subscribe_to_event::<Event1>(move |_, _| {});
1425        let unknown_listener_id = context1.subscribe_to_event::<Event1>(move |_, _| {});
1426        context2.subscribe_to_event::<Event1>(move |_, event| {
1427            *obs_data_clone.borrow_mut() = event.data;
1428        });
1429
1430        assert!(!context2.unsubscribe_from_event(&unknown_listener_id));
1431
1432        context2.emit_event(Event1 { data: 1 });
1433        context2.execute();
1434        assert_eq!(*obs_data.borrow(), 1);
1435    }
1436
1437    #[test]
1438    fn subscribe_after_event() {
1439        let mut context = Context::new();
1440        let obs_data = Rc::new(RefCell::new(0));
1441        let obs_data_clone = Rc::clone(&obs_data);
1442
1443        context.emit_event(Event1 { data: 1 });
1444        context.subscribe_to_event::<Event1>(move |_, event| {
1445            *obs_data_clone.borrow_mut() = event.data;
1446        });
1447
1448        context.execute();
1449        assert_eq!(*obs_data.borrow(), 0);
1450    }
1451
1452    #[test]
1453    fn shutdown_runs_current_time_plans_and_preserves_future_plan() {
1454        let mut context = Context::new();
1455        add_plan(&mut context, 1.0, 1);
1456        context.add_plan(1.5, |context| {
1457            context.get_data_mut(ComponentA).push(2);
1458            context.shutdown();
1459        });
1460        add_plan(&mut context, 1.5, 3);
1461        add_plan(&mut context, 2.0, 2);
1462        context.execute();
1463        assert_eq!(context.get_current_time(), 1.5);
1464        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
1465
1466        context.execute();
1467        assert_eq!(context.get_current_time(), 2.0);
1468        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3, 2]);
1469    }
1470
1471    #[test]
1472    fn shutdown_runs_queued_callbacks() {
1473        let mut context = Context::new();
1474        add_plan(&mut context, 1.0, 1);
1475        context.add_plan(1.5, |context| {
1476            context.queue_callback(|context| {
1477                context.get_data_mut(ComponentA).push(3);
1478            });
1479            context.shutdown();
1480        });
1481        context.execute();
1482        assert_eq!(context.get_current_time(), 1.5);
1483        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 3]);
1484    }
1485
1486    #[test]
1487    fn shutdown_runs_queued_events() {
1488        let mut context = Context::new();
1489        let obs_data = Rc::new(RefCell::new(0));
1490        let obs_data_clone = Rc::clone(&obs_data);
1491        context.subscribe_to_event::<Event1>(move |_, event| {
1492            *obs_data_clone.borrow_mut() = event.data;
1493        });
1494        context.emit_event(Event1 { data: 1 });
1495        context.shutdown();
1496        context.execute();
1497        assert_eq!(*obs_data.borrow(), 1);
1498    }
1499
1500    #[test]
1501    fn shutdown_runs_regular_plans_at_current_time_all_phases() {
1502        let mut context = Context::new();
1503        context.add_plan_with_phase(
1504            1.0,
1505            |context| {
1506                context.get_data_mut(ComponentA).push(1);
1507            },
1508            ExecutionPhase::First,
1509        );
1510        context.add_plan(1.0, |context| {
1511            context.get_data_mut(ComponentA).push(2);
1512            context.shutdown();
1513        });
1514        add_plan(&mut context, 1.0, 3);
1515        add_plan_with_phase(&mut context, 1.0, 4, ExecutionPhase::Last);
1516        add_plan(&mut context, 2.0, 5);
1517
1518        context.execute();
1519
1520        assert_eq!(context.get_current_time(), 1.0);
1521        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3, 4]);
1522    }
1523
1524    #[test]
1525    fn shutdown_time_plans_run_after_current_time_plans() {
1526        let mut context = Context::new();
1527        context.add_shutdown_plan(|context| {
1528            context.get_data_mut(ComponentA).push(3);
1529        });
1530        context.add_plan(1.0, |context| {
1531            context.get_data_mut(ComponentA).push(1);
1532            context.shutdown();
1533        });
1534        add_plan(&mut context, 1.0, 2);
1535
1536        context.execute();
1537
1538        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
1539    }
1540
1541    #[test]
1542    fn shutdown_plan_phase_order_is_respected() {
1543        let mut context = Context::new();
1544        context.add_shutdown_plan_with_phase(
1545            |context| {
1546                context.get_data_mut(ComponentA).push(3);
1547            },
1548            ExecutionPhase::Last,
1549        );
1550        context.add_shutdown_plan_with_phase(
1551            |context| {
1552                context.get_data_mut(ComponentA).push(1);
1553            },
1554            ExecutionPhase::First,
1555        );
1556        context.add_shutdown_plan(|context| {
1557            context.get_data_mut(ComponentA).push(2);
1558        });
1559        context.execute();
1560
1561        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
1562    }
1563
1564    #[test]
1565    fn shutdown_plan_callbacks_are_drained() {
1566        let mut context = Context::new();
1567        context.add_shutdown_plan(|context| {
1568            context.get_data_mut(ComponentA).push(1);
1569            context.queue_callback(|context| {
1570                context.get_data_mut(ComponentA).push(2);
1571            });
1572        });
1573        context.add_shutdown_plan(|context| {
1574            context.get_data_mut(ComponentA).push(3);
1575        });
1576
1577        context.execute();
1578
1579        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
1580    }
1581
1582    #[test]
1583    fn shutdown_time_does_not_return_to_regular_queue() {
1584        let mut context = Context::new();
1585        context.add_shutdown_plan(|context| {
1586            context.get_data_mut(ComponentA).push(1);
1587            context.add_plan(context.get_current_time(), |context| {
1588                context.get_data_mut(ComponentA).push(3);
1589            });
1590        });
1591        context.add_shutdown_plan(|context| {
1592            context.get_data_mut(ComponentA).push(2);
1593        });
1594
1595        context.execute();
1596        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2]);
1597
1598        context.execute();
1599        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
1600    }
1601
1602    #[test]
1603    fn passive_only_initial_time_runs_during_normal_shutdown() {
1604        let mut context = Context::new();
1605        add_passive_plan(&mut context, 0.0, 1);
1606        add_passive_plan(&mut context, 1.0, 2);
1607
1608        context.execute();
1609
1610        assert_eq!(context.get_current_time(), 0.0);
1611        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1612    }
1613
1614    #[test]
1615    fn passive_plans_at_final_non_passive_time_run_across_phases() {
1616        let mut context = Context::new();
1617        add_passive_plan_with_phase(&mut context, 1.0, 1, ExecutionPhase::First);
1618        add_plan(&mut context, 1.0, 2);
1619        add_passive_plan_with_phase(&mut context, 1.0, 3, ExecutionPhase::Last);
1620
1621        context.execute();
1622
1623        assert_eq!(context.get_current_time(), 1.0);
1624        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
1625    }
1626
1627    #[test]
1628    fn passive_future_plan_survives_until_later_non_passive_work() {
1629        let mut context = Context::new();
1630        add_plan(&mut context, 1.0, 1);
1631        add_passive_plan(&mut context, 2.0, 2);
1632
1633        context.execute();
1634
1635        assert_eq!(context.get_current_time(), 1.0);
1636        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1637
1638        add_plan(&mut context, 2.0, 3);
1639        context.execute();
1640
1641        assert_eq!(context.get_current_time(), 2.0);
1642        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
1643    }
1644
1645    #[test]
1646    fn scheduled_shutdown_stops_at_requested_time_and_drains_current_time_work() {
1647        let mut context = Context::new();
1648        add_plan(&mut context, 1.0, 1);
1649        context.schedule_shutdown(2.0);
1650        add_plan(&mut context, 2.0, 2);
1651        add_plan(&mut context, 3.0, 3);
1652
1653        context.execute();
1654
1655        assert_eq!(context.get_current_time(), 2.0);
1656        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2]);
1657
1658        context.execute();
1659
1660        assert_eq!(context.get_current_time(), 3.0);
1661        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
1662    }
1663
1664    #[test]
1665    fn scheduled_shutdown_does_not_keep_the_timeline_alive() {
1666        let mut context = Context::new();
1667        context.schedule_shutdown(5.0);
1668
1669        context.execute();
1670
1671        assert_eq!(context.get_current_time(), 0.0);
1672
1673        add_plan(&mut context, 10.0, 1);
1674        context.execute();
1675
1676        assert_eq!(context.get_current_time(), 5.0);
1677        assert_eq!(*context.get_data_mut(ComponentA), Vec::<u32>::new());
1678    }
1679
1680    #[test]
1681    fn scheduled_shutdown_can_be_cancelled() {
1682        let mut context = Context::new();
1683        let shutdown_plan = context.schedule_shutdown(2.0);
1684        add_plan(&mut context, 3.0, 1);
1685        context.cancel_plan(&shutdown_plan);
1686
1687        context.execute();
1688
1689        assert_eq!(context.get_current_time(), 3.0);
1690        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1691    }
1692
1693    #[test]
1694    fn cancel_plan_can_cancel_shutdown_plan() {
1695        let mut context = Context::new();
1696        let to_cancel = context.add_shutdown_plan(|context| {
1697            context.get_data_mut(ComponentA).push(1);
1698        });
1699        context.cancel_plan(&to_cancel);
1700
1701        context.execute();
1702
1703        assert_eq!(*context.get_data_mut(ComponentA), Vec::<u32>::new());
1704    }
1705
1706    #[test]
1707    fn abort_inside_plan_stops_execute_loop() {
1708        let mut context = Context::new();
1709        context.add_plan(1.0, |context| {
1710            context.get_data_mut(ComponentA).push(1);
1711            context.queue_callback(|context| {
1712                context.get_data_mut(ComponentA).push(2);
1713            });
1714            context.abort();
1715        });
1716        add_plan(&mut context, 2.0, 3);
1717
1718        context.execute();
1719        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1720
1721        context.execute();
1722        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
1723    }
1724
1725    #[test]
1726    fn abort_before_execute_does_not_poison_later_execute() {
1727        let mut context = Context::new();
1728        context.abort();
1729        add_plan(&mut context, 1.0, 1);
1730
1731        context.execute();
1732
1733        assert_eq!(context.get_current_time(), 1.0);
1734        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1735    }
1736
1737    #[test]
1738    fn abort_during_normal_shutdown_exits_immediately() {
1739        let mut context = Context::new();
1740        context.add_plan(1.0, |context| {
1741            context.get_data_mut(ComponentA).push(1);
1742            context.shutdown();
1743        });
1744        context.add_plan(1.0, |context| {
1745            context.get_data_mut(ComponentA).push(2);
1746            context.abort();
1747        });
1748        add_plan(&mut context, 1.0, 3);
1749
1750        context.execute();
1751        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2]);
1752
1753        context.execute();
1754        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
1755    }
1756
1757    #[test]
1758    fn shutdown_does_not_restart_stopped_status() {
1759        let mut context = Context::new();
1760        context.abort();
1761        context.shutdown();
1762        add_plan(&mut context, 1.0, 1);
1763
1764        context.execute();
1765
1766        assert_eq!(context.get_current_time(), 1.0);
1767        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1768    }
1769
1770    #[test]
1771    fn execute_single_step_runs_one_status_transition() {
1772        let mut context = Context::new();
1773        context.add_shutdown_plan(|context| {
1774            context.get_data_mut(ComponentA).push(1);
1775        });
1776
1777        context.execute_single_step();
1778        assert_eq!(*context.get_data_mut(ComponentA), Vec::<u32>::new());
1779
1780        context.execute_single_step();
1781        assert_eq!(*context.get_data_mut(ComponentA), Vec::<u32>::new());
1782
1783        context.execute_single_step();
1784        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1785    }
1786
1787    #[test]
1788    fn execute_single_step_stopped_runs_callback_then_resets() {
1789        let mut context = Context::new();
1790        context.abort();
1791        context.queue_callback(|context| {
1792            context.get_data_mut(ComponentA).push(1);
1793        });
1794        add_plan(&mut context, 0.0, 2);
1795
1796        context.execute_single_step();
1797        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1798
1799        context.execute_single_step();
1800        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1801
1802        context.execute_single_step();
1803        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2]);
1804    }
1805
1806    #[test]
1807    fn periodic_plan_self_schedules() {
1808        // checks whether the periodic plan schedules itself passively without
1809        // keeping execution alive after non-passive plans are exhausted.
1810        let mut context = Context::new();
1811        context.add_periodic_plan_with_phase(
1812            1.0,
1813            |context| {
1814                let time = context.get_current_time();
1815                context.get_data_mut(ComponentA).push(time as u32);
1816            },
1817            ExecutionPhase::Last,
1818        );
1819        context.add_plan(1.0, move |_context| {});
1820        context.add_plan(1.5, move |_context| {});
1821        context.execute();
1822        assert_eq!(context.get_current_time(), 1.5);
1823
1824        assert_eq!(*context.get_data(ComponentA), vec![0, 1]); // time 0.0 and 1.0
1825    }
1826
1827    // Tests for negative time handling
1828
1829    #[test]
1830    fn negative_plan_time_allowed() {
1831        let mut context = Context::new();
1832        context.set_start_time(-1.0);
1833        add_plan(&mut context, -1.0, 1);
1834        context.execute();
1835        assert_eq!(context.get_current_time(), -1.0);
1836        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1837    }
1838
1839    #[test]
1840    fn add_plan_get_current_time() {
1841        let mut context = Context::new();
1842        let current_time = context.get_current_time();
1843        add_plan(&mut context, current_time, 1);
1844        context.execute();
1845        assert_eq!(context.get_current_time(), 0.0);
1846        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1847    }
1848
1849    #[test]
1850    fn multiple_negative_plans() {
1851        let mut context = Context::new();
1852        context.set_start_time(-3.0);
1853        add_plan(&mut context, -3.0, 1);
1854        add_plan(&mut context, -1.0, 3);
1855        add_plan(&mut context, -2.0, 2);
1856        context.execute();
1857        assert_eq!(context.get_current_time(), -1.0);
1858        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
1859    }
1860
1861    #[test]
1862    fn negative_and_positive_plans() {
1863        let mut context = Context::new();
1864        context.set_start_time(-1.0);
1865        add_plan(&mut context, -1.0, 1);
1866        add_plan(&mut context, 1.0, 3);
1867        add_plan(&mut context, 0.0, 2);
1868        context.execute();
1869        assert_eq!(context.get_current_time(), 1.0);
1870        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
1871    }
1872
1873    #[test]
1874    fn get_current_time_before_execute_defaults() {
1875        let mut context = Context::new();
1876        assert_eq!(context.get_current_time(), 0.0);
1877
1878        context.set_start_time(-2.0);
1879        assert_eq!(context.get_current_time(), -2.0);
1880    }
1881
1882    #[test]
1883    fn get_current_time_initializes_to_zero_when_all_positive() {
1884        let mut context = Context::new();
1885        let seen_time = Rc::new(RefCell::new(f64::NAN));
1886        let seen_time_clone = Rc::clone(&seen_time);
1887        context.queue_callback(move |ctx| {
1888            *seen_time_clone.borrow_mut() = ctx.get_current_time();
1889        });
1890        context.execute();
1891        assert_eq!(*seen_time.borrow(), 0.0);
1892    }
1893
1894    #[test]
1895    fn get_current_time_initializes_to_zero_when_empty() {
1896        let mut context = Context::new();
1897        context.execute();
1898        assert_eq!(context.get_current_time(), 0.0);
1899    }
1900
1901    #[test]
1902    fn get_current_time_initializes_to_zero_with_plan() {
1903        let mut context = Context::new();
1904        add_plan(&mut context, 0.0, 1);
1905        context.execute();
1906        assert_eq!(context.get_current_time(), 0.0);
1907        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1908    }
1909
1910    #[test]
1911    #[should_panic(expected = "Time -1 is invalid")]
1912    fn negative_time_from_callback_panics() {
1913        let mut context = Context::new();
1914        context.queue_callback(|context| {
1915            context.get_data_mut(ComponentA).push(1);
1916            add_plan(context, -1.0, 2);
1917        });
1918        add_plan(&mut context, 1.0, 3);
1919        context.execute();
1920    }
1921
1922    #[test]
1923    fn large_negative_time() {
1924        let mut context = Context::new();
1925        context.set_start_time(-1_000_000.0);
1926        add_plan(&mut context, -1_000_000.0, 1);
1927        context.execute();
1928        assert_eq!(context.get_current_time(), -1_000_000.0);
1929        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1930    }
1931
1932    #[test]
1933    fn very_small_negative_time() {
1934        let mut context = Context::new();
1935        context.set_start_time(-1e-10);
1936        add_plan(&mut context, -1e-10, 1);
1937        context.execute();
1938        assert_eq!(context.get_current_time(), -1e-10);
1939        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
1940    }
1941
1942    #[test]
1943    fn negative_time_ordering_with_phases() {
1944        let mut context = Context::new();
1945        context.set_start_time(-1.0);
1946        add_plan_with_phase(&mut context, -1.0, 1, ExecutionPhase::Normal);
1947        add_plan_with_phase(&mut context, -1.0, 3, ExecutionPhase::Last);
1948        add_plan_with_phase(&mut context, -1.0, 2, ExecutionPhase::First);
1949        context.execute();
1950        assert_eq!(context.get_current_time(), -1.0);
1951        assert_eq!(*context.get_data_mut(ComponentA), vec![2, 1, 3]);
1952    }
1953
1954    #[test]
1955    #[should_panic(expected = "Time 4 is invalid")]
1956    fn cannot_schedule_plan_before_current_time() {
1957        let mut context = Context::new();
1958        add_plan(&mut context, 5.0, 1);
1959        context.add_plan(5.0, |context| {
1960            // At time 5.0, we cannot schedule a plan at time 4.0
1961            add_plan(context, 4.0, 2);
1962        });
1963        context.execute();
1964    }
1965
1966    #[test]
1967    fn get_current_time_multiple_calls_before_execute() {
1968        let mut context = Context::new();
1969        context.set_start_time(-2.0);
1970        add_plan(&mut context, -2.0, 1);
1971        context.execute();
1972        assert_eq!(context.get_current_time(), -2.0);
1973    }
1974
1975    #[test]
1976    fn negative_plan_can_add_positive_plan() {
1977        let mut context = Context::new();
1978        context.set_start_time(-1.0);
1979        add_plan(&mut context, -1.0, 1);
1980        context.add_plan(-1.0, |context| {
1981            add_plan(context, 2.0, 2);
1982        });
1983        context.execute();
1984        assert_eq!(context.get_current_time(), 2.0);
1985        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2]);
1986    }
1987
1988    #[test]
1989    fn negative_plan_can_schedule_negative_plan() {
1990        let mut context = Context::new();
1991        context.set_start_time(-2.0);
1992        add_plan(&mut context, -2.0, 1);
1993        context.add_plan(-2.0, |context| {
1994            add_plan(context, -1.0, 2);
1995        });
1996        context.execute();
1997        assert_eq!(context.get_current_time(), -1.0);
1998        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2]);
1999    }
2000
2001    #[test]
2002    #[should_panic(expected = "Start time has already been set. It can only be set once.")]
2003    fn set_start_time_only_once() {
2004        let mut context = Context::new();
2005        context.set_start_time(1.0);
2006        context.set_start_time(2.0);
2007    }
2008
2009    // Additional coverage around time and plans
2010
2011    #[test]
2012    #[should_panic(expected = "Start time NaN must be finite")]
2013    fn set_start_time_nan_panics() {
2014        let mut context = Context::new();
2015        context.set_start_time(f64::NAN);
2016    }
2017
2018    #[test]
2019    #[should_panic(expected = "Start time inf must be finite")]
2020    fn set_start_time_inf_panics() {
2021        let mut context = Context::new();
2022        context.set_start_time(f64::INFINITY);
2023    }
2024
2025    #[test]
2026    fn set_start_time_equal_to_earliest_plan_allowed() {
2027        let mut context = Context::new();
2028        context.set_start_time(-2.0);
2029        add_plan(&mut context, -2.0, 1);
2030        context.execute();
2031        assert_eq!(context.get_current_time(), -2.0);
2032        assert_eq!(*context.get_data_mut(ComponentA), vec![1]);
2033    }
2034
2035    // Note: adding a plan earlier than current_time after setting start time
2036    // is already covered by `add_plan_less_than_current_time_panics`.
2037
2038    #[test]
2039    fn set_start_time_with_only_callbacks_keeps_time() {
2040        let mut context = Context::new();
2041        context.set_start_time(5.0);
2042        context.queue_callback(|ctx| {
2043            ctx.get_data_mut(ComponentA).push(42);
2044        });
2045        context.execute();
2046        assert_eq!(context.get_current_time(), 5.0);
2047        assert_eq!(*context.get_data_mut(ComponentA), vec![42]);
2048    }
2049
2050    #[test]
2051    fn multiple_plans_final_time_is_last() {
2052        let mut context = Context::new();
2053        add_plan(&mut context, 1.0, 1);
2054        add_plan(&mut context, 3.0, 3);
2055        add_plan(&mut context, 2.0, 2);
2056        context.execute();
2057        assert_eq!(context.get_current_time(), 3.0);
2058        assert_eq!(*context.get_data_mut(ComponentA), vec![1, 2, 3]);
2059    }
2060
2061    #[test]
2062    fn add_plan_same_time_fifo_and_phases() {
2063        let mut context = Context::new();
2064        add_plan_with_phase(&mut context, 1.0, 3, ExecutionPhase::Last);
2065        add_plan(&mut context, 1.0, 1);
2066        add_plan_with_phase(&mut context, 1.0, 2, ExecutionPhase::First);
2067        add_plan(&mut context, 1.0, 4);
2068        context.execute();
2069        assert_eq!(context.get_current_time(), 1.0);
2070        assert_eq!(*context.get_data_mut(ComponentA), vec![2, 1, 4, 3]);
2071    }
2072
2073    #[test]
2074    #[should_panic(expected = "Time -2 is invalid")]
2075    fn add_plan_less_than_current_time_panics() {
2076        let mut context = Context::new();
2077        context.set_start_time(-1.0);
2078        add_plan(&mut context, -1.0, 1);
2079        // Attempt to schedule before current time
2080        add_plan(&mut context, -2.0, 2);
2081    }
2082
2083    #[test]
2084    #[should_panic(expected = "Period must be greater than 0")]
2085    fn add_periodic_plan_zero_period_panics() {
2086        let mut context = Context::new();
2087        context.add_periodic_plan_with_phase(0.0, |_ctx| {}, ExecutionPhase::Normal);
2088    }
2089
2090    #[test]
2091    #[should_panic(expected = "Period must be greater than 0")]
2092    fn add_periodic_plan_nan_panics() {
2093        let mut context = Context::new();
2094        context.add_periodic_plan_with_phase(f64::NAN, |_ctx| {}, ExecutionPhase::Normal);
2095    }
2096
2097    #[test]
2098    #[should_panic(expected = "Period must be greater than 0")]
2099    fn add_periodic_plan_inf_panics() {
2100        let mut context = Context::new();
2101        context.add_periodic_plan_with_phase(f64::INFINITY, |_ctx| {}, ExecutionPhase::Normal);
2102    }
2103
2104    #[test]
2105    fn shutdown_status_reset() {
2106        // This test verifies that shutdown_status is properly reset after
2107        // being acted upon. This allows the context to be reused after shutdown.
2108        let mut context = Context::new();
2109        let _: PersonId = context.add_entity(with!(Person, Age(50))).unwrap();
2110
2111        // Schedule a plan at time 0.0 that calls shutdown
2112        context.add_plan(0.0, |ctx| {
2113            ctx.shutdown();
2114        });
2115
2116        // First execute - should run until shutdown
2117        context.execute();
2118        assert_eq!(context.get_current_time(), 0.0);
2119        assert_eq!(context.get_entity_count::<Person>(), 1);
2120
2121        // Add a new plan at time 2.0
2122        context.add_plan(2.0, |ctx| {
2123            let _: PersonId = ctx.add_entity(with!(Person, Age(50))).unwrap();
2124        });
2125
2126        // Second execute - should execute the new plan
2127        // If shutdown_status wasn't reset, this would immediately break
2128        // without executing the plan, leaving population at 1.
2129        context.execute();
2130        assert_eq!(context.get_current_time(), 2.0);
2131        assert_eq!(
2132            context.get_entity_count::<Person>(),
2133            2,
2134            "If this fails, shutdown_status was not properly reset"
2135        );
2136    }
2137}