Skip to main content

behaviorsim_rs/simulation/
state_query.rs

1//! State query API for timestamp-based state access.
2//!
3//! This module provides `EntityQueryHandle` for querying entity state at
4//! any timestamp, and `ComputedState` as the result type.
5
6use crate::context::apply_context_effects;
7use crate::entity::Entity;
8use crate::enums::{AlertTrigger, Direction, Emotion, HexacoPath, LifeDomain, LifeStage, StatePath};
9use crate::memory::{
10    apply_memory_consolidation, create_memory_from_event, MemoryEntry, MemoryLayer, MemoryLayers,
11};
12use crate::processor::{
13    advance_state, apply_developmental_effects, apply_interpreted_event_to_state,
14    compute_trust_modulation_factor, interpret_event, regress_state,
15    reverse_interpreted_event_from_state, InterpretedEvent,
16};
17use crate::relationship::{TrustContext, TrustDecision};
18use crate::simulation::{RegressionQuality, Simulation, TimestampedEvent};
19use crate::state::{
20    apply_formative_modifiers, effective_base_at, BaseShiftRecord, IndividualState,
21    StateInterpreter,
22};
23use crate::types::{Alert, Duration, EntityId, Timestamp};
24use std::collections::HashMap;
25
26/// A handle for querying entity state at different timestamps.
27///
28/// This handle provides the `state_at()` method, which is the primary
29/// consumer API for getting entity state at any point in time.
30///
31pub struct EntityQueryHandle<'a> {
32    simulation: &'a Simulation,
33    entity_id: EntityId,
34}
35
36impl<'a> EntityQueryHandle<'a> {
37    /// Creates a new query handle.
38    pub(crate) fn new(simulation: &'a Simulation, entity_id: EntityId) -> Self {
39        EntityQueryHandle {
40            simulation,
41            entity_id,
42        }
43    }
44
45    /// Returns the entity ID.
46    #[must_use]
47    pub fn entity_id(&self) -> &EntityId {
48        &self.entity_id
49    }
50
51    /// Returns the anchor timestamp for this entity.
52    #[must_use]
53    pub fn anchor_timestamp(&self) -> Option<Timestamp> {
54        self.simulation
55            .get_anchored_entity(&self.entity_id)
56            .map(|a| a.anchor_timestamp())
57    }
58
59    /// Computes the entity's state at the given timestamp.
60    ///
61    /// This is the primary consumer API. It computes state by:
62    /// 1. Starting from the anchor state
63    /// 2. Applying decay forward or reversing backward
64    /// 3. Applying events in the time range
65    ///
66    /// # Arguments
67    ///
68    /// * `timestamp` - The time at which to compute state
69    ///
70    /// # Returns
71    ///
72    /// The computed state, or `None` if the entity doesn't exist.
73    ///
74    #[must_use]
75    pub fn state_at(&self, timestamp: Timestamp) -> ComputedState {
76        let anchored = self
77            .simulation
78            .get_anchored_entity(&self.entity_id)
79            .expect("EntityQueryHandle created for non-existent entity - use Simulation::entity() to check existence");
80        let anchor_timestamp = anchored.anchor_timestamp();
81        let entity = anchored.entity();
82
83        // Clone the individual state as our starting point
84        let mut state = entity.individual_state().clone();
85        let species = entity.species().clone();
86
87        // Short-circuit: if querying at anchor timestamp, return anchor state
88        if timestamp == anchor_timestamp {
89            let age_at_timestamp = self.compute_age_at_timestamp(entity, timestamp);
90            let life_stage =
91                LifeStage::from_age_years_for_species(&species, age_at_timestamp.as_years() as f64);
92
93            let pad_bounds = entity.context().compute_pad_bounds();
94            state
95                .mood_mut()
96                .valence_mut()
97                .set_bounds(pad_bounds.valence_min, pad_bounds.valence_max);
98            state
99                .mood_mut()
100                .arousal_mut()
101                .set_bounds(pad_bounds.arousal_min, pad_bounds.arousal_max);
102            state
103                .mood_mut()
104                .dominance_mut()
105                .set_bounds(pad_bounds.dominance_min, pad_bounds.dominance_max);
106
107            let interpreter = StateInterpreter::from_state(&state);
108            return ComputedState {
109                individual_state: state,
110                age_at_timestamp,
111                life_stage,
112                regression_quality: RegressionQuality::Exact,
113                alerts: std::cell::OnceCell::new(),
114                interpretations: interpreter.interpretations().clone(),
115                summary: interpreter.summary().to_string(),
116                delta_summary: None,
117            };
118        }
119
120        // Determine direction: forward or backward
121        let is_forward = timestamp > anchor_timestamp;
122
123        // Get events targeting this entity in the relevant time range
124        // Forward: (anchor, target] - exclude anchor, include target
125        // Backward: [target, anchor) - include target, exclude anchor
126        let events = self.get_sorted_events_for_range(anchor_timestamp, timestamp, is_forward);
127
128        // Compute regression quality based on events
129        let regression_quality = if is_forward {
130            RegressionQuality::Exact
131        } else {
132            self.determine_regression_quality(&events)
133        };
134
135        // Interpret events once using the anchor entity's personality
136        // Personality (HEXACO) is stable, so using anchor state is appropriate
137        let interpreted_events: Vec<InterpretedEvent> = events
138            .iter()
139            .map(|te| interpret_event(te.event(), entity))
140            .collect();
141
142        // Collect base shift records from events that have formative shifts
143        // These represent permanent personality changes from significant life events
144        let base_shift_records: Vec<BaseShiftRecord> =
145            collect_base_shift_records(&events, entity, timestamp, is_forward);
146
147        // Helper to compute age at a given timestamp for developmental effects
148        let compute_age_at = |ts: Timestamp| -> Duration {
149            if let Some(birth_date) = entity.birth_date() {
150                if ts >= birth_date {
151                    ts - birth_date
152                } else {
153                    Duration::zero()
154                }
155            } else {
156                // Without birth_date, age is constant at anchor age
157                entity.age()
158            }
159        };
160
161        // Computed memories from events (only for forward projection)
162        let mut computed_memories = MemoryLayers::new();
163
164        if is_forward {
165            // Forward: use cursor pattern to track current time position
166            // This avoids compounding decay by advancing in deltas between events
167            let mut cursor = anchor_timestamp;
168
169            for (te, interpreted) in events.iter().zip(interpreted_events.iter()) {
170                // Advance from cursor to this event's timestamp
171                let delta = te.timestamp() - cursor;
172                state = advance_state(state, delta);
173
174                // Apply developmental effects to scale event impact
175                // Compute entity's age at the time of this event
176                let age_at_event = compute_age_at(te.timestamp());
177                let age_days = age_at_event.as_days();
178                let dev_factor =
179                    apply_developmental_effects(entity, te.event(), 1.0, age_days, te.timestamp());
180
181                // Apply trust modulation for interpersonal events
182                // If the event has a source with whom we have a relationship, prior trust
183                // amplifies the impact (betrayal from trusted person hurts more)
184                let trustor_propensity = state.disposition().trustor_propensity_effective();
185                let trust_factor = self.compute_trust_factor_for_event(
186                    entity,
187                    te.event(),
188                    te.timestamp(),
189                    trustor_propensity,
190                );
191
192                // Combine developmental and trust modulation factors
193                let combined_factor = dev_factor * trust_factor;
194
195                // Scale the interpreted event by the combined factor
196                let scaled_interpreted = interpreted.scaled_by(combined_factor);
197
198                // Apply the scaled interpreted event deltas
199                state = apply_interpreted_event_to_state(state, &scaled_interpreted);
200
201                // Create memory from this event (AFTER state is updated to capture
202                // the emotional context at encoding time)
203                let memory = create_memory_from_event(
204                    &scaled_interpreted,
205                    &state,
206                    age_at_event,
207                    Some(&self.entity_id),
208                );
209                computed_memories.add(MemoryLayer::Immediate, memory);
210
211                // Move cursor forward
212                cursor = te.timestamp();
213            }
214
215            // Advance remaining time from cursor to target timestamp
216            let remaining = timestamp - cursor;
217            state = advance_state(state, remaining);
218        } else {
219            // Backward: use cursor pattern in reverse
220            // Start at anchor and work backward through events in reverse order
221            let mut cursor = anchor_timestamp;
222
223            // Events are sorted chronologically, so iterate in reverse
224            // Use indices to access both events and interpreted events in sync
225            for i in (0..events.len()).rev() {
226                let te = &events[i];
227                let interpreted = &interpreted_events[i];
228
229                // Regress from cursor to this event's timestamp
230                let delta = cursor - te.timestamp();
231                state = regress_state(state, delta);
232
233                // Apply developmental effects to scale event impact for reversal
234                // Compute entity's age at the time of this event
235                let age_at_event = compute_age_at(te.timestamp());
236                let age_days = age_at_event.as_days();
237                let dev_factor =
238                    apply_developmental_effects(entity, te.event(), 1.0, age_days, te.timestamp());
239
240                // Apply trust modulation for interpersonal events (same as forward)
241                let trustor_propensity = state.disposition().trustor_propensity_effective();
242                let trust_factor = self.compute_trust_factor_for_event(
243                    entity,
244                    te.event(),
245                    te.timestamp(),
246                    trustor_propensity,
247                );
248
249                // Combine developmental and trust modulation factors
250                let combined_factor = dev_factor * trust_factor;
251
252                // Scale the interpreted event by the combined factor
253                let scaled_interpreted = interpreted.scaled_by(combined_factor);
254
255                // Reverse the scaled interpreted event using its actual deltas
256                state = reverse_interpreted_event_from_state(state, &scaled_interpreted);
257                // Move cursor backward
258                cursor = te.timestamp();
259            }
260
261            // Regress remaining time from cursor to target timestamp
262            let remaining = cursor - timestamp;
263            state = regress_state(state, remaining);
264        }
265
266        // Apply hook points AFTER decay and events, in order:
267        // 1. Context effects (ecological systems)
268        // 2. Memory consolidation (salience decay, layer transfer)
269        //
270        // Developmental effects (plasticity, sensitive periods) are applied above
271        // during event processing via apply_developmental_effects().
272        let total_duration = if is_forward {
273            timestamp - anchor_timestamp
274        } else {
275            anchor_timestamp - timestamp
276        };
277        let relationship_quality = estimate_relationship_quality(entity);
278        let age_at_timestamp = self.compute_age_at_timestamp(entity, timestamp);
279        let life_stage =
280            LifeStage::from_age_years_for_species(&species, age_at_timestamp.as_years() as f64);
281        state = apply_context_effects(
282            state,
283            entity.context(),
284            relationship_quality,
285            total_duration,
286            life_stage,
287            age_at_timestamp.as_years() as f64,
288            timestamp,
289        );
290
291        // Merge anchor memories with computed memories for consolidation
292        // For forward projection, we combine entity's anchor memories with
293        // memories computed from events processed in this query.
294        // For backward regression, we only use anchor memories (events being
295        // reversed haven't happened yet at the query timestamp).
296        let merged_memories = if is_forward {
297            merge_memory_layers(entity.memories(), &computed_memories)
298        } else {
299            entity.memories().clone()
300        };
301        state = apply_memory_consolidation(state, &merged_memories, total_duration);
302
303        // Apply formative base shifts to HEXACO personality traits
304        // This computes effective base values for each trait based on accumulated shifts
305        state = apply_base_shifts_to_state(state, &base_shift_records, timestamp);
306
307        // Apply context-dependent PAD bounds for emotional expression
308        let pad_bounds = entity.context().compute_pad_bounds();
309        state
310            .mood_mut()
311            .valence_mut()
312            .set_bounds(pad_bounds.valence_min, pad_bounds.valence_max);
313        state
314            .mood_mut()
315            .arousal_mut()
316            .set_bounds(pad_bounds.arousal_min, pad_bounds.arousal_max);
317        state
318            .mood_mut()
319            .dominance_mut()
320            .set_bounds(pad_bounds.dominance_min, pad_bounds.dominance_max);
321
322        let baseline_state = entity.individual_state();
323        let interpreter = StateInterpreter::from_state_with_baseline(&state, baseline_state);
324        ComputedState {
325            individual_state: state,
326            age_at_timestamp,
327            life_stage,
328            regression_quality,
329            alerts: std::cell::OnceCell::new(),
330            interpretations: interpreter.interpretations().clone(),
331            summary: interpreter.summary().to_string(),
332            delta_summary: interpreter.delta_summary().map(|s| s.to_string()),
333        }
334    }
335
336    /// Gets events in the time range, sorted chronologically.
337    ///
338    /// # Boundary Rules
339    ///
340    /// - Forward projection: (anchor, target] - excludes anchor, includes target
341    /// - Backward regression: (target, anchor] - excludes target, includes anchor
342    ///
343    /// The anchor state already reflects events that occurred at anchor time,
344    /// so for forward projection we exclude anchor. For backward regression,
345    /// we include anchor events (which need to be reversed) but exclude target
346    /// events (which should not exist in the pre-event state).
347    fn get_sorted_events_for_range(
348        &self,
349        anchor: Timestamp,
350        target: Timestamp,
351        is_forward: bool,
352    ) -> Vec<&'a TimestampedEvent> {
353        let mut events: Vec<_> = self
354            .simulation
355            .events_for(&self.entity_id)
356            .into_iter()
357            .filter(|te| {
358                let ts = te.timestamp();
359                if is_forward {
360                    // Forward: (anchor, target] - after anchor, up to and including target
361                    ts > anchor && ts <= target
362                } else {
363                    // Backward: (target, anchor] - after target, up to and including anchor
364                    ts > target && ts <= anchor
365                }
366            })
367            .collect();
368
369        events.sort_by_key(|te| te.timestamp());
370        events
371    }
372
373    /// Determines regression quality based on events.
374    ///
375    /// Regression is approximate when:
376    /// - Trauma events are present (AC increases are not reversible)
377    fn determine_regression_quality(&self, events: &[&TimestampedEvent]) -> RegressionQuality {
378        for te in events {
379            let event = te.event();
380            let spec = event.spec();
381
382            // Trauma events (AC > 0) have non-reversible Acquired Capability increases
383            if spec.impact.acquired_capability > 0.0 {
384                return RegressionQuality::Approximate;
385            }
386
387        }
388
389        RegressionQuality::Exact
390    }
391
392    /// Computes the entity's age at a given timestamp.
393    ///
394    /// If the entity has a birth_date set, computes age as:
395    /// `timestamp - birth_date`
396    ///
397    /// If no birth_date is set, returns the anchor age (constant) since we cannot
398    /// compute age progression without knowing when the entity was born.
399    fn compute_age_at_timestamp(
400        &self,
401        entity: &crate::entity::Entity,
402        timestamp: Timestamp,
403    ) -> Duration {
404        // If entity has a birth date, compute age from that directly
405        if let Some(birth_date) = entity.birth_date() {
406            if timestamp >= birth_date {
407                return timestamp - birth_date;
408            } else {
409                // Before birth - return zero
410                return Duration::zero();
411            }
412        }
413
414        // Fallback: without birth_date, age remains constant at anchor age.
415        // We cannot compute age progression without knowing when the entity was born.
416        entity.age()
417    }
418
419    /// Computes the trust modulation factor for an interpersonal event.
420    ///
421    /// If the event has a source with whom the target entity has a relationship,
422    /// the prior trust level modulates the event's psychological impact.
423    ///
424    /// Per Mayer's trust model:
425    /// - High trust + betrayal = amplified harm (vulnerability was exploited)
426    /// - Low trust + betrayal = expected/buffered (wasn't vulnerable to them)
427    ///
428    /// # Arguments
429    ///
430    /// * `event` - The event to check for trust modulation
431    /// * `event_timestamp` - When the event occurred (for relationship lookup)
432    ///
433    /// # Returns
434    ///
435    /// A scaling factor for the event's impact (1.0 = no modulation)
436    fn compute_trust_factor_for_event(
437        &self,
438        entity: &Entity,
439        event: &crate::event::Event,
440        event_timestamp: Timestamp,
441        trustor_propensity: f32,
442    ) -> f64 {
443        // No source = no interpersonal trust modulation
444        let Some(source_id) = event.source() else {
445            return 1.0;
446        };
447
448        // Find relationship between target (self.entity_id) and source
449        let relationships = self.simulation.relationships_for(&self.entity_id);
450        let relevant_rel = relationships.iter().find(|tr| {
451            // Relationship must exist before the event occurred
452            tr.formed_timestamp() <= event_timestamp && tr.involves(source_id)
453        });
454
455        let Some(timestamped_rel) = relevant_rel else {
456            // No relationship with source = stranger, no modulation
457            return 1.0;
458        };
459
460        // Determine direction: target is trustor, source is trustee
461        let direction = if timestamped_rel.entity_a() == &self.entity_id {
462            Direction::AToB
463        } else {
464            Direction::BToA
465        };
466
467        let context = self.build_trust_context(entity, event);
468        let life_domain = event
469            .payload()
470            .life_domain()
471            .unwrap_or(LifeDomain::Relationship);
472        compute_trust_modulation_factor(
473            event,
474            Some(timestamped_rel.relationship()),
475            direction,
476            trustor_propensity,
477            &context,
478            life_domain,
479        )
480    }
481
482    fn build_trust_context(&self, entity: &Entity, event: &crate::event::Event) -> TrustContext {
483        let ecology = entity.context();
484        let macrosystem = ecology.macrosystem();
485        let exosystem = ecology.exosystem();
486        let historical = ecology.chronosystem().historical_period();
487
488        let mut social_norms = 1.0 - macrosystem.cultural_stress;
489        let institutional_safeguards = macrosystem.institutional_structure.rule_of_law
490            * (1.0 - macrosystem.institutional_structure.corruption_level);
491        let institutional_support = exosystem.institutional_support;
492        let cultural_expectations = (historical.institutional_trust * 0.6
493            + (1.0 - macrosystem.cultural_stress) * 0.4)
494            .clamp(0.0, 1.0);
495        let mut time_pressure = 0.5;
496
497        if let Some(context_id) = event.microsystem_context() {
498            if let Some(micro) = ecology.get_microsystem(context_id) {
499                let warmth = micro.warmth();
500                let hostility = micro.hostility();
501                social_norms += (warmth - hostility) * 0.2;
502                time_pressure = ((micro.interaction_frequency() + micro.interaction_complexity())
503                    / 2.0)
504                    .clamp(0.0, 1.0);
505            }
506        }
507
508        TrustContext::new()
509            .with_social_norms(social_norms.clamp(0.0, 1.0) as f32)
510            .with_institutional_safeguards(institutional_safeguards.clamp(0.0, 1.0) as f32)
511            .with_time_pressure(time_pressure as f32)
512            .with_institutional_support(institutional_support.clamp(0.0, 1.0) as f32)
513            .with_cultural_expectations(cultural_expectations as f32)
514    }
515
516    /// Computes the trust decision for this entity (trustor) toward a trustee at a timestamp.
517    ///
518    /// This wires together the trustor's dispositional propensity at the time,
519    /// the current relationship state, the provided situational context,
520    /// and the relevant life domain for competence.
521    /// `stakes` optionally scales perceived risk based on consequence severity.
522    #[must_use]
523    pub fn trust_decision_for(
524        &self,
525        trustee: &EntityId,
526        timestamp: Timestamp,
527        context: &TrustContext,
528        life_domain: LifeDomain,
529        stakes: Option<crate::enums::ActionStakes>,
530    ) -> Option<TrustDecision> {
531        let relationships = self.simulation.relationships_for(&self.entity_id);
532        let relevant = relationships
533            .iter()
534            .find(|tr| tr.formed_timestamp() <= timestamp && tr.involves(trustee))?;
535
536        let direction = if relevant.entity_a() == &self.entity_id {
537            Direction::AToB
538        } else {
539            Direction::BToA
540        };
541
542        let trustor_propensity = self
543            .state_at(timestamp)
544            .individual_state()
545            .disposition()
546            .trustor_propensity_effective();
547
548        Some(
549            relevant
550                .relationship()
551                .compute_trust_decision(
552                    direction,
553                    trustor_propensity,
554                    context,
555                    life_domain,
556                    stakes,
557                ),
558        )
559    }
560
561    /// Returns memories that exist at the given timestamp.
562    ///
563    /// A memory "exists" at a timestamp if it was formed before or at that time.
564    /// This method returns all memories from the entity's memory layers that
565    /// were formed at or before the specified timestamp.
566    ///
567    /// # Arguments
568    ///
569    /// * `timestamp` - The time at which to query existing memories
570    ///
571    /// # Returns
572    ///
573    /// A vector of cloned memory entries that exist at the timestamp.
574    /// Returns an empty vector if the entity doesn't exist.
575    ///
576    #[must_use]
577    pub fn memories_at(&self, timestamp: Timestamp) -> Vec<MemoryEntry> {
578        let Some(anchored) = self.simulation.get_anchored_entity(&self.entity_id) else {
579            return Vec::new();
580        };
581
582        let entity = anchored.entity();
583        let anchor_timestamp = anchored.anchor_timestamp();
584        let anchor_age = entity.age();
585
586        // Compute the entity's age at the query timestamp
587        let age_at_timestamp = if timestamp >= anchor_timestamp {
588            let elapsed = timestamp - anchor_timestamp;
589            anchor_age + elapsed
590        } else {
591            let elapsed = anchor_timestamp - timestamp;
592            if elapsed < anchor_age {
593                anchor_age - elapsed
594            } else {
595                Duration::zero()
596            }
597        };
598
599        // Get all memories and filter by those formed at or before the computed age
600        // MemoryEntry.timestamp() returns the entity's age when the memory was formed
601        entity
602            .memories()
603            .all_memories()
604            .filter(|memory: &&MemoryEntry| memory.timestamp() <= age_at_timestamp)
605            .cloned()
606            .collect()
607    }
608}
609
610fn estimate_relationship_quality(entity: &Entity) -> f64 {
611    let attached_count = entity
612        .relationship_slots()
613        .iter()
614        .filter(|slot| slot.is_attached())
615        .count();
616
617    if attached_count > 0 {
618        0.5 + 0.1 * (attached_count as f64).min(5.0)
619    } else {
620        0.3
621    }
622}
623
624/// Merges two memory layer structures into a new combined structure.
625///
626/// This is used to combine anchor memories (from entity creation) with
627/// computed memories (from events processed during state_at). All memories
628/// from both sources are copied into the new structure, preserving their
629/// layer assignments.
630fn merge_memory_layers(anchor: &MemoryLayers, computed: &MemoryLayers) -> MemoryLayers {
631    let mut merged = MemoryLayers::new();
632
633    // Copy all anchor memories
634    for memory in anchor.immediate() {
635        merged.add(MemoryLayer::Immediate, memory.clone());
636    }
637    for memory in anchor.short_term() {
638        merged.add(MemoryLayer::ShortTerm, memory.clone());
639    }
640    for memory in anchor.long_term() {
641        merged.add(MemoryLayer::LongTerm, memory.clone());
642    }
643    for memory in anchor.legacy() {
644        merged.add(MemoryLayer::Legacy, memory.clone());
645    }
646
647    // Copy all computed memories
648    for memory in computed.immediate() {
649        merged.add(MemoryLayer::Immediate, memory.clone());
650    }
651    for memory in computed.short_term() {
652        merged.add(MemoryLayer::ShortTerm, memory.clone());
653    }
654    for memory in computed.long_term() {
655        merged.add(MemoryLayer::LongTerm, memory.clone());
656    }
657    for memory in computed.legacy() {
658        merged.add(MemoryLayer::Legacy, memory.clone());
659    }
660
661    merged
662}
663
664/// The computed state of an entity at a specific timestamp.
665///
666/// This is the result of calling `state_at()` on an `EntityQueryHandle`.
667/// It contains the computed individual state, age, life stage, and
668/// regression quality indicator.
669///
670#[derive(Debug)]
671pub struct ComputedState {
672    /// The computed individual state.
673    pub individual_state: IndividualState,
674    /// The entity's age at the queried timestamp.
675    pub age_at_timestamp: Duration,
676    /// The entity's life stage at the queried timestamp.
677    pub life_stage: LifeStage,
678    /// Quality indicator for backward regression.
679    regression_quality: RegressionQuality,
680    /// Cached alerts (lazy computed with interior mutability).
681    alerts: std::cell::OnceCell<Vec<Alert>>,
682    /// Human-readable interpretations of psychological dimensions.
683    pub interpretations: HashMap<String, String>,
684    /// Condensed plain-English summary paragraph.
685    pub summary: String,
686    /// Delta emphasis summary showing changes from baseline.
687    pub delta_summary: Option<String>,
688}
689
690impl ComputedState {
691    /// Returns a reference to the individual state.
692    #[must_use]
693    pub fn individual_state(&self) -> &IndividualState {
694        &self.individual_state
695    }
696
697    /// Returns graded emotion membership based on the current PAD state.
698    #[must_use]
699    pub fn emotion_membership(&self) -> HashMap<Emotion, f64> {
700        self.individual_state.mood().emotion_membership()
701    }
702
703    /// Returns the age at the queried timestamp.
704    #[must_use]
705    pub fn age_at_timestamp(&self) -> Duration {
706        self.age_at_timestamp
707    }
708
709    /// Returns the life stage at the queried timestamp.
710    #[must_use]
711    pub fn life_stage(&self) -> LifeStage {
712        self.life_stage
713    }
714
715    /// Returns the regression quality indicator.
716    ///
717    /// This indicates whether the state was computed exactly or approximately.
718    /// Forward projections are always Exact. Backward regressions may be
719    /// Approximate if the time range contains spiral-triggering events.
720    #[must_use]
721    pub fn regression_quality(&self) -> RegressionQuality {
722        self.regression_quality
723    }
724
725    /// Returns alerts generated during state computation.
726    ///
727    /// This is lazily computed on first access. Alerts include threshold
728    /// violations and feedback loop detections.
729    ///
730    /// Returns a cloned vector of alerts per the spec API.
731    #[must_use]
732    pub fn alerts(&self) -> Vec<Alert> {
733        self.alerts.get_or_init(|| self.compute_alerts()).clone()
734    }
735
736    /// Computes alerts for this state.
737    fn compute_alerts(&self) -> Vec<Alert> {
738        let state = &self.individual_state;
739        let social = state.social_cognition();
740        let mental = state.mental_health();
741        let timestamp = self.age_at_timestamp;
742        let mut alerts = Vec::new();
743
744        let desire = mental.compute_suicidal_desire(social);
745        if desire >= 0.8 {
746            alerts.push(Alert::critical(
747                AlertTrigger::threshold(
748                    StatePath::MentalHealth(crate::enums::MentalHealthPath::SuicidalDesire),
749                    desire as f64,
750                ),
751                timestamp,
752                "High suicidal desire detected",
753            ));
754        } else if desire >= 0.5 {
755            alerts.push(Alert::warning(
756                AlertTrigger::threshold(
757                    StatePath::MentalHealth(crate::enums::MentalHealthPath::SuicidalDesire),
758                    desire as f64,
759                ),
760                timestamp,
761                "Elevated suicidal desire detected",
762            ));
763        }
764
765        let attempt_risk = mental.compute_attempt_risk(social);
766        if attempt_risk >= 0.7 {
767            alerts.push(Alert::critical(
768                AlertTrigger::threshold(
769                    StatePath::MentalHealth(crate::enums::MentalHealthPath::AttemptRisk),
770                    attempt_risk as f64,
771                ),
772                timestamp,
773                "High attempt risk detected",
774            ));
775        } else if attempt_risk >= 0.4 {
776            alerts.push(Alert::warning(
777                AlertTrigger::threshold(
778                    StatePath::MentalHealth(crate::enums::MentalHealthPath::AttemptRisk),
779                    attempt_risk as f64,
780                ),
781                timestamp,
782                "Elevated attempt risk detected",
783            ));
784        }
785
786        let stress = state.needs().stress_effective();
787        let fatigue = state.needs().fatigue_effective();
788        let impulse_control = state.disposition().impulse_control_effective();
789        if stress > 0.8 && fatigue > 0.7 && impulse_control < 0.3 {
790            alerts.push(Alert::warning(
791                AlertTrigger::spiral(crate::enums::SpiralType::Stress),
792                timestamp,
793                "Stress spiral indicators detected",
794            ));
795        }
796
797        let depression = mental.depression_effective();
798        let loneliness = social.loneliness_effective();
799        if depression > 0.7 && loneliness > 0.7 {
800            alerts.push(Alert::warning(
801                AlertTrigger::spiral(crate::enums::SpiralType::Depression),
802                timestamp,
803                "Depression-loneliness spiral indicators detected",
804            ));
805        }
806
807        alerts
808    }
809
810    /// Gets the effective value for a state path.
811    ///
812    /// This is a convenience method that delegates to the individual state.
813    ///
814    /// # Arguments
815    ///
816    /// * `path` - The state path to query
817    ///
818    /// # Returns
819    ///
820    /// The effective value (base + delta) for stored paths,
821    /// or the computed value for derived paths.
822    ///
823    #[must_use]
824    pub fn get_effective(&self, path: StatePath) -> f64 {
825        use crate::enums::{
826            DispositionPath, HexacoPath, MentalHealthPath, MoodPath, NeedsPath,
827            PersonCharacteristicsPath, SocialCognitionPath,
828        };
829
830        let state = &self.individual_state;
831
832        let value: f32 = match path {
833            StatePath::Hexaco(p) => match p {
834                HexacoPath::HonestyHumility => state.hexaco().honesty_humility(),
835                HexacoPath::Neuroticism => state.hexaco().neuroticism(),
836                HexacoPath::Extraversion => state.hexaco().extraversion(),
837                HexacoPath::Agreeableness => state.hexaco().agreeableness(),
838                HexacoPath::Conscientiousness => state.hexaco().conscientiousness(),
839                HexacoPath::Openness => state.hexaco().openness(),
840            },
841            StatePath::Mood(p) => match p {
842                MoodPath::Valence => state.mood().valence_effective(),
843                MoodPath::Arousal => state.mood().arousal_effective(),
844                MoodPath::Dominance => state.mood().dominance_effective(),
845            },
846            StatePath::Needs(p) => match p {
847                NeedsPath::Stress => state.needs().stress_effective(),
848                NeedsPath::Fatigue => state.needs().fatigue_effective(),
849                NeedsPath::Purpose => state.needs().purpose_effective(),
850            },
851            StatePath::SocialCognition(p) => match p {
852                SocialCognitionPath::Loneliness => state.social_cognition().loneliness_effective(),
853                SocialCognitionPath::PerceivedReciprocalCaring => state
854                    .social_cognition()
855                    .perceived_reciprocal_caring_effective(),
856                SocialCognitionPath::PerceivedLiability => {
857                    state.social_cognition().perceived_liability_effective()
858                }
859                SocialCognitionPath::SelfHate => state.social_cognition().self_hate_effective(),
860                SocialCognitionPath::PerceivedCompetence => {
861                    state.social_cognition().perceived_competence_effective()
862                }
863            },
864            StatePath::MentalHealth(p) => match p {
865                MentalHealthPath::Depression => state.mental_health().depression_effective(),
866                MentalHealthPath::AcquiredCapability => {
867                    state.mental_health().acquired_capability_effective()
868                }
869                MentalHealthPath::InterpersonalHopelessness => {
870                    state.mental_health().interpersonal_hopelessness_effective()
871                }
872                MentalHealthPath::ThwartedBelongingness => state.compute_thwarted_belongingness(),
873                MentalHealthPath::PerceivedBurdensomeness => {
874                    state.compute_perceived_burdensomeness()
875                }
876                MentalHealthPath::SuicidalDesire => state.compute_suicidal_desire(),
877                MentalHealthPath::AttemptRisk => state.compute_attempt_risk(),
878                MentalHealthPath::SelfWorth => state.mental_health().self_worth_effective(),
879                MentalHealthPath::Hopelessness => state.mental_health().hopelessness_effective(),
880            },
881            StatePath::Disposition(p) => match p {
882                DispositionPath::Empathy => state.disposition().empathy_effective(),
883                DispositionPath::Aggression => state.disposition().aggression_effective(),
884                DispositionPath::Grievance => state.disposition().grievance_effective(),
885                DispositionPath::ImpulseControl => state.disposition().impulse_control_effective(),
886                DispositionPath::Reactance => state.disposition().reactance_effective(),
887                DispositionPath::TrustorPropensity => {
888                    state.disposition().trustor_propensity_effective()
889                }
890            },
891            StatePath::PersonCharacteristics(p) => match p {
892                PersonCharacteristicsPath::SocialCapital => {
893                    state.person_characteristics().social_capital_effective()
894                }
895                PersonCharacteristicsPath::CognitiveAbility => {
896                    state.person_characteristics().cognitive_ability_effective()
897                }
898                PersonCharacteristicsPath::EmotionalRegulationAssets => state
899                    .person_characteristics()
900                    .emotional_regulation_assets_effective(),
901                PersonCharacteristicsPath::MaterialSecurity => {
902                    state.person_characteristics().material_security_effective()
903                }
904                PersonCharacteristicsPath::ExperienceDiversity => state
905                    .person_characteristics()
906                    .experience_diversity_effective(),
907                PersonCharacteristicsPath::BaselineMotivation => state
908                    .person_characteristics()
909                    .baseline_motivation_effective(),
910                PersonCharacteristicsPath::PersistenceTendency => state
911                    .person_characteristics()
912                    .persistence_tendency_effective(),
913                PersonCharacteristicsPath::CuriosityTendency => state
914                    .person_characteristics()
915                    .curiosity_tendency_effective(),
916                // Composite values
917                PersonCharacteristicsPath::Resource => state.person_characteristics().resource(),
918                PersonCharacteristicsPath::Force => state.person_characteristics().force(),
919            },
920        };
921
922        f64::from(value)
923    }
924}
925
926impl Clone for ComputedState {
927    fn clone(&self) -> Self {
928        ComputedState {
929            individual_state: self.individual_state.clone(),
930            age_at_timestamp: self.age_at_timestamp,
931            life_stage: self.life_stage,
932            regression_quality: self.regression_quality,
933            alerts: match self.alerts.get() {
934                Some(v) => {
935                    let cell = std::cell::OnceCell::new();
936                    let _ = cell.set(v.clone());
937                    cell
938                }
939                None => std::cell::OnceCell::new(),
940            },
941            interpretations: self.interpretations.clone(),
942            summary: self.summary.clone(),
943            delta_summary: self.delta_summary.clone(),
944        }
945    }
946}
947
948/// Collects base shift records from events that have formative personality shifts.
949///
950/// For forward queries, collects shifts from events before the query timestamp.
951/// For backward queries, we don't collect shifts (they don't exist yet in the past).
952fn collect_base_shift_records(
953    events: &[&TimestampedEvent],
954    entity: &Entity,
955    query_timestamp: Timestamp,
956    is_forward: bool,
957) -> Vec<BaseShiftRecord> {
958    // Backward queries don't include formative events (they haven't happened yet)
959    if !is_forward {
960        return Vec::new();
961    }
962
963    let reference_timestamp = entity
964        .birth_date()
965        .unwrap_or_else(|| Timestamp::from_ymd_hms(1970, 1, 1, 0, 0, 0));
966
967    let mut records = Vec::new();
968    let mut cumulative_positive: HashMap<HexacoPath, f32> = HashMap::new();
969    let mut cumulative_negative: HashMap<HexacoPath, f32> = HashMap::new();
970
971    for te in events {
972        let event = te.event();
973
974        // Skip events without base shifts
975        if !event.has_base_shifts() {
976            continue;
977        }
978
979        // Skip events after query timestamp
980        if te.timestamp() > query_timestamp {
981            continue;
982        }
983
984        // Compute entity's age at event time for plasticity modifiers
985        let age_at_event = if let Some(birth_date) = entity.birth_date() {
986            if te.timestamp() >= birth_date {
987                (te.timestamp() - birth_date).as_years() as u16
988            } else {
989                0
990            }
991        } else {
992            entity.age().as_years() as u16
993        };
994
995        // Convert event timestamp to Duration from reference
996        let event_duration = if te.timestamp() >= reference_timestamp {
997            te.timestamp() - reference_timestamp
998        } else {
999            Duration::zero()
1000        };
1001
1002        // Process each base shift in the event
1003        for (trait_path, raw_amount) in event.base_shifts() {
1004            // Get existing cumulative in this direction
1005            let existing = if *raw_amount > 0.0 {
1006                *cumulative_positive.get(trait_path).unwrap_or(&0.0)
1007            } else {
1008                *cumulative_negative.get(trait_path).unwrap_or(&0.0)
1009            };
1010
1011            // Apply all modifiers: plasticity, trait stability, saturation, caps
1012            let modified = apply_formative_modifiers(
1013                *raw_amount,
1014                *trait_path,
1015                age_at_event,
1016                existing,
1017                entity.species(),
1018            );
1019
1020            // Skip zero shifts
1021            if modified.abs() < f32::EPSILON {
1022                continue;
1023            }
1024
1025            // Create the base shift record
1026            let record = BaseShiftRecord::new(event_duration, *trait_path, modified);
1027
1028            // Update cumulative tracking
1029            if modified > 0.0 {
1030                *cumulative_positive.entry(*trait_path).or_insert(0.0) += modified.abs();
1031            } else {
1032                *cumulative_negative.entry(*trait_path).or_insert(0.0) += modified.abs();
1033            }
1034
1035            records.push(record);
1036        }
1037    }
1038
1039    records
1040}
1041
1042/// Applies accumulated base shifts to HEXACO personality traits in the state.
1043///
1044/// For each HEXACO trait, computes the effective base value using all
1045/// applicable base shift records, then updates the state's HEXACO values.
1046fn apply_base_shifts_to_state(
1047    mut state: IndividualState,
1048    shift_records: &[BaseShiftRecord],
1049    query_timestamp: Timestamp,
1050) -> IndividualState {
1051    // If no shifts, return state unchanged
1052    if shift_records.is_empty() {
1053        return state;
1054    }
1055
1056    // Convert query timestamp to Duration for effective_base_at computation
1057    // Using a fixed reference of 1970 to be consistent with collect_base_shift_records
1058    let reference = Timestamp::from_ymd_hms(1970, 1, 1, 0, 0, 0);
1059    let query_duration = if query_timestamp >= reference {
1060        query_timestamp - reference
1061    } else {
1062        Duration::zero()
1063    };
1064
1065    // Process each HEXACO trait
1066    for trait_path in HexacoPath::all() {
1067        // Filter records for this trait
1068        let trait_records: Vec<_> = shift_records
1069            .iter()
1070            .filter(|r| r.trait_path() == trait_path)
1071            .cloned()
1072            .collect();
1073
1074        // Skip if no records for this trait
1075        if trait_records.is_empty() {
1076            continue;
1077        }
1078
1079        // Get current base value
1080        let current_base = match trait_path {
1081            HexacoPath::Openness => state.hexaco().openness(),
1082            HexacoPath::Conscientiousness => state.hexaco().conscientiousness(),
1083            HexacoPath::Extraversion => state.hexaco().extraversion(),
1084            HexacoPath::Agreeableness => state.hexaco().agreeableness(),
1085            HexacoPath::Neuroticism => state.hexaco().neuroticism(),
1086            HexacoPath::HonestyHumility => state.hexaco().honesty_humility(),
1087        };
1088
1089        // Compute effective base with accumulated shifts
1090        let effective = effective_base_at(current_base, &trait_records, query_duration);
1091
1092        // Update the trait value in state
1093        match trait_path {
1094            HexacoPath::Openness => state.hexaco_mut().set_openness(effective),
1095            HexacoPath::Conscientiousness => state.hexaco_mut().set_conscientiousness(effective),
1096            HexacoPath::Extraversion => state.hexaco_mut().set_extraversion(effective),
1097            HexacoPath::Agreeableness => state.hexaco_mut().set_agreeableness(effective),
1098            HexacoPath::Neuroticism => state.hexaco_mut().set_neuroticism(effective),
1099            HexacoPath::HonestyHumility => state.hexaco_mut().set_honesty_humility(effective),
1100        }
1101    }
1102
1103    state
1104}
1105
1106#[cfg(test)]
1107mod tests {
1108    use super::*;
1109    use crate::entity::EntityBuilder;
1110    use crate::enums::{EventType, RelationshipSchema, Species};
1111    use crate::event::EventBuilder;
1112    use crate::memory::{MemoryEntry, MemoryLayer, MemoryLayers};
1113
1114    fn create_simulation() -> Simulation {
1115        let reference = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
1116        Simulation::new(reference)
1117    }
1118
1119    fn create_human(id: &str) -> crate::entity::Entity {
1120        EntityBuilder::new()
1121            .id(id)
1122            .species(Species::Human)
1123            .age(crate::types::Duration::years(30))
1124            .build()
1125            .unwrap()
1126    }
1127
1128    #[test]
1129    fn entity_query_handle_entity_id() {
1130        let sim = create_simulation();
1131        let handle = EntityQueryHandle::new(&sim, EntityId::new("test").unwrap());
1132        assert_eq!(handle.entity_id().as_str(), "test");
1133    }
1134
1135    #[test]
1136    fn entity_query_handle_anchor_timestamp() {
1137        let mut sim = create_simulation();
1138        let entity = create_human("person_001");
1139        let anchor = Timestamp::from_ymd_hms(2024, 1, 15, 12, 0, 0);
1140        sim.add_entity(entity, anchor);
1141
1142        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1143        assert_eq!(handle.anchor_timestamp(), Some(anchor));
1144    }
1145
1146    #[test]
1147    fn state_at_anchor_returns_original_state() {
1148        let mut sim = create_simulation();
1149        let entity = create_human("person_001");
1150        let anchor = sim.reference_date();
1151        sim.add_entity(entity, anchor);
1152
1153        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1154        let _state = handle.state_at(anchor);
1155        // state_at now returns ComputedState directly, no need to check is_some
1156    }
1157
1158    #[test]
1159    fn state_at_forward_applies_decay() {
1160        let mut sim = create_simulation();
1161        let mut entity = create_human("person_001");
1162
1163        // Set a non-zero delta that will decay
1164        entity
1165            .individual_state_mut()
1166            .mood_mut()
1167            .add_valence_delta(0.5);
1168
1169        let anchor = sim.reference_date();
1170        sim.add_entity(entity, anchor);
1171
1172        // Query 1 week later
1173        let future = anchor + Duration::weeks(1);
1174        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1175        let computed = handle.state_at(future);
1176
1177        // Valence should have decayed (6-hour half-life, so 1 week = many half-lives)
1178        let valence = computed.get_effective(StatePath::Mood(crate::enums::MoodPath::Valence));
1179        assert!(valence < 0.1); // Nearly fully decayed
1180    }
1181
1182    #[test]
1183    fn trust_decision_helpers_wire_propensity_and_relationship() {
1184        let mut sim = create_simulation();
1185        let reference = sim.reference_date();
1186
1187        let alice = create_human("alice");
1188        let bob = create_human("bob");
1189        sim.add_entity(alice, reference);
1190        sim.add_entity(bob, reference);
1191
1192        let alice_id = EntityId::new("alice").unwrap();
1193        let bob_id = EntityId::new("bob").unwrap();
1194
1195        let formed = reference;
1196        let rel_id = sim.add_relationship(
1197            alice_id.clone(),
1198            bob_id.clone(),
1199            RelationshipSchema::Peer,
1200            formed,
1201        );
1202
1203        let support_time = reference + Duration::days(3);
1204        let support = EventBuilder::new(EventType::ReceiveSupportEmotional)
1205            .source(bob_id.clone())
1206            .target(alice_id.clone())
1207            .severity(0.7)
1208            .build()
1209            .unwrap();
1210        sim.add_event(support, support_time);
1211
1212        let context = TrustContext::new();
1213        let life_domain = LifeDomain::Relationship;
1214        let handle = sim.entity(&alice_id).unwrap();
1215        let decision_from_handle = handle
1216            .trust_decision_for(&bob_id, support_time, &context, life_domain, None)
1217            .unwrap();
1218        let decision_from_sim = sim
1219            .trust_decision_at(&alice_id, &bob_id, support_time, &context, life_domain, None)
1220            .unwrap();
1221
1222        assert_eq!(decision_from_handle, decision_from_sim);
1223        let support = decision_from_handle.support_willingness();
1224        assert!(support.is_finite());
1225        assert!((0.0..=1.0).contains(&support));
1226
1227        let rel = sim.get_relationship(&rel_id).unwrap().relationship();
1228        let direct = rel.compute_trust_decision(
1229            Direction::AToB,
1230            handle
1231                .state_at(support_time)
1232                .individual_state()
1233                .disposition()
1234                .trustor_propensity_effective(),
1235            &context,
1236            life_domain,
1237            None,
1238        );
1239        assert_eq!(decision_from_handle, direct);
1240    }
1241
1242    #[test]
1243    fn trust_decision_for_returns_none_without_relationship() {
1244        let mut sim = create_simulation();
1245        let reference = sim.reference_date();
1246
1247        let alice = create_human("alice");
1248        let bob = create_human("bob");
1249        sim.add_entity(alice, reference);
1250        sim.add_entity(bob, reference);
1251
1252        let alice_id = EntityId::new("alice").unwrap();
1253        let bob_id = EntityId::new("bob").unwrap();
1254
1255        let handle = sim.entity(&alice_id).unwrap();
1256        let decision = handle.trust_decision_for(
1257            &bob_id,
1258            reference,
1259            &TrustContext::new(),
1260            LifeDomain::Relationship,
1261            None,
1262        );
1263
1264        assert!(decision.is_none());
1265    }
1266
1267    #[test]
1268    fn trust_decision_for_uses_b_to_a_direction() {
1269        let mut sim = create_simulation();
1270        let reference = sim.reference_date();
1271
1272        let alice = create_human("alice");
1273        let bob = create_human("bob");
1274        sim.add_entity(alice, reference);
1275        sim.add_entity(bob, reference);
1276
1277        let alice_id = EntityId::new("alice").unwrap();
1278        let bob_id = EntityId::new("bob").unwrap();
1279        sim.add_relationship(
1280            alice_id.clone(),
1281            bob_id.clone(),
1282            RelationshipSchema::Peer,
1283            reference,
1284        );
1285
1286        let handle = sim.entity(&bob_id).unwrap();
1287        let decision = handle.trust_decision_for(
1288            &alice_id,
1289            reference,
1290            &TrustContext::new(),
1291            LifeDomain::Relationship,
1292            None,
1293        );
1294
1295        assert!(decision.is_some());
1296    }
1297
1298    #[test]
1299    fn merge_memory_layers_copies_anchor_and_computed_memories() {
1300        let mut anchor = MemoryLayers::new();
1301        let mut computed = MemoryLayers::new();
1302
1303        anchor.add(
1304            MemoryLayer::Immediate,
1305            MemoryEntry::new(Duration::days(1), "anchor_immediate"),
1306        );
1307        anchor.add(
1308            MemoryLayer::ShortTerm,
1309            MemoryEntry::new(Duration::days(2), "anchor_short_term"),
1310        );
1311        anchor.add(
1312            MemoryLayer::LongTerm,
1313            MemoryEntry::new(Duration::days(3), "anchor_long_term"),
1314        );
1315        anchor.add(
1316            MemoryLayer::Legacy,
1317            MemoryEntry::new(Duration::days(4), "anchor_legacy"),
1318        );
1319
1320        computed.add(
1321            MemoryLayer::Immediate,
1322            MemoryEntry::new(Duration::days(5), "computed_immediate"),
1323        );
1324        computed.add(
1325            MemoryLayer::ShortTerm,
1326            MemoryEntry::new(Duration::days(6), "computed_short_term"),
1327        );
1328        computed.add(
1329            MemoryLayer::LongTerm,
1330            MemoryEntry::new(Duration::days(7), "computed_long_term"),
1331        );
1332        computed.add(
1333            MemoryLayer::Legacy,
1334            MemoryEntry::new(Duration::days(8), "computed_legacy"),
1335        );
1336
1337        let merged = merge_memory_layers(&anchor, &computed);
1338
1339        assert_eq!(merged.immediate_count(), 2);
1340        assert_eq!(merged.short_term_count(), 2);
1341        assert_eq!(merged.long_term_count(), 2);
1342        assert_eq!(merged.legacy_count(), 2);
1343        assert_eq!(merged.total_count(), 8);
1344        assert!(merged
1345            .immediate()
1346            .iter()
1347            .any(|memory| memory.summary() == "anchor_immediate"));
1348        assert!(merged
1349            .immediate()
1350            .iter()
1351            .any(|memory| memory.summary() == "computed_immediate"));
1352    }
1353
1354    #[test]
1355    fn state_at_backward_regresses_state() {
1356        let mut sim = create_simulation();
1357        let entity = create_human("person_001");
1358        let anchor = Timestamp::from_ymd_hms(2024, 6, 1, 0, 0, 0);
1359        sim.add_entity(entity, anchor);
1360
1361        // Query 1 month earlier
1362        let past = Timestamp::from_ymd_hms(2024, 5, 1, 0, 0, 0);
1363        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1364        let _state = handle.state_at(past);
1365        // state_at now returns ComputedState directly
1366    }
1367
1368    #[test]
1369    fn computed_state_accessors() {
1370        let mut sim = create_simulation();
1371        let entity = create_human("person_001");
1372        let anchor = sim.reference_date();
1373        sim.add_entity(entity, anchor);
1374
1375        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1376        let state = handle.state_at(anchor);
1377
1378        // Check all accessors
1379        let _ = state.individual_state();
1380        let _ = state.age_at_timestamp();
1381        let _ = state.life_stage();
1382        let _ = state.regression_quality();
1383    }
1384
1385    #[test]
1386    fn estimate_relationship_quality_increases_with_attachments() {
1387        use crate::types::RelationshipId;
1388
1389        let mut entity = create_human("person_001");
1390        let relationship_id = RelationshipId::new("rel_001_002").unwrap();
1391        entity.relationship_slots_mut()[0].attach_for_test(relationship_id);
1392
1393        let quality = estimate_relationship_quality(&entity);
1394        assert!(quality > 0.3);
1395    }
1396
1397    #[test]
1398    fn computed_state_alerts_lazy() {
1399        let mut sim = create_simulation();
1400        let entity = create_human("person_001");
1401        let anchor = sim.reference_date();
1402        sim.add_entity(entity, anchor);
1403
1404        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1405        let state = handle.state_at(anchor);
1406
1407        // First access computes alerts (immutable access via interior mutability)
1408        let alerts = state.alerts();
1409        assert!(alerts.is_empty()); // No alerts by default
1410
1411        // Second access should use cached value
1412        let alerts2 = state.alerts();
1413        assert!(alerts2.is_empty());
1414    }
1415
1416    fn computed_state_with(individual_state: IndividualState) -> ComputedState {
1417        ComputedState {
1418            individual_state,
1419            age_at_timestamp: Duration::days(1),
1420            life_stage: LifeStage::Adult,
1421            regression_quality: RegressionQuality::Exact,
1422            alerts: std::cell::OnceCell::new(),
1423            interpretations: HashMap::new(),
1424            summary: String::new(),
1425            delta_summary: None,
1426        }
1427    }
1428
1429    #[test]
1430    fn computed_state_alerts_high_risk_paths() {
1431        let social = crate::state::SocialCognition::new()
1432            .with_loneliness_base(1.0)
1433            .with_perceived_reciprocal_caring_base(0.0)
1434            .with_perceived_liability_base(1.0)
1435            .with_self_hate_base(1.0);
1436        let mental = crate::state::MentalHealth::new()
1437            .with_interpersonal_hopelessness_base(1.0)
1438            .with_acquired_capability_base(1.0)
1439            .with_depression_base(0.2);
1440        let individual_state = IndividualState::new()
1441            .with_social_cognition(social)
1442            .with_mental_health(mental);
1443
1444        let computed = computed_state_with(individual_state);
1445        let alerts = computed.alerts();
1446
1447        assert!(alerts.iter().any(|a| matches!(
1448            a.trigger(),
1449            AlertTrigger::ThresholdExceeded(StatePath::MentalHealth(crate::enums::MentalHealthPath::SuicidalDesire), _)
1450        )));
1451        assert!(alerts.iter().any(|a| matches!(
1452            a.trigger(),
1453            AlertTrigger::ThresholdExceeded(StatePath::MentalHealth(crate::enums::MentalHealthPath::AttemptRisk), _)
1454        )));
1455    }
1456
1457    #[test]
1458    fn computed_state_alerts_warning_paths() {
1459        let social = crate::state::SocialCognition::new()
1460            .with_loneliness_base(0.9)
1461            .with_perceived_reciprocal_caring_base(0.1)
1462            .with_perceived_liability_base(0.9)
1463            .with_self_hate_base(0.9);
1464        let mental = crate::state::MentalHealth::new()
1465            .with_interpersonal_hopelessness_base(0.8)
1466            .with_acquired_capability_base(0.8)
1467            .with_depression_base(0.2);
1468        let individual_state = IndividualState::new()
1469            .with_social_cognition(social)
1470            .with_mental_health(mental);
1471
1472        let computed = computed_state_with(individual_state);
1473        let alerts = computed.alerts();
1474
1475        assert!(alerts.iter().any(|a| matches!(
1476            a.trigger(),
1477            AlertTrigger::ThresholdExceeded(StatePath::MentalHealth(crate::enums::MentalHealthPath::SuicidalDesire), _)
1478        )));
1479        assert!(alerts.iter().any(|a| matches!(
1480            a.trigger(),
1481            AlertTrigger::ThresholdExceeded(StatePath::MentalHealth(crate::enums::MentalHealthPath::AttemptRisk), _)
1482        )));
1483    }
1484
1485    #[test]
1486    fn computed_state_alerts_detect_spirals() {
1487        let needs = crate::state::Needs::new()
1488            .with_stress_base(0.9)
1489            .with_fatigue_base(0.8);
1490        let disposition = crate::state::Disposition::new().with_impulse_control_base(0.2);
1491        let social = crate::state::SocialCognition::new().with_loneliness_base(0.8);
1492        let mental = crate::state::MentalHealth::new().with_depression_base(0.8);
1493        let individual_state = IndividualState::new()
1494            .with_needs(needs)
1495            .with_disposition(disposition)
1496            .with_social_cognition(social)
1497            .with_mental_health(mental);
1498
1499        let computed = computed_state_with(individual_state);
1500        let alerts = computed.alerts();
1501
1502        assert!(alerts.iter().any(|a| matches!(
1503            a.trigger(),
1504            AlertTrigger::SpiralDetected(crate::enums::SpiralType::Stress)
1505        )));
1506        assert!(alerts.iter().any(|a| matches!(
1507            a.trigger(),
1508            AlertTrigger::SpiralDetected(crate::enums::SpiralType::Depression)
1509        )));
1510    }
1511
1512    #[test]
1513    fn build_trust_context_uses_event_microsystem() {
1514        use crate::event::EventBuilder;
1515        use crate::enums::EventType;
1516
1517        let mut sim = create_simulation();
1518        let mut entity = create_human("person_001");
1519        let anchor = sim.reference_date();
1520
1521        let micro_id = crate::types::MicrosystemId::new("work_ctx").unwrap();
1522        let mut work = crate::context::WorkContext::default();
1523        work.warmth = 0.8;
1524        work.hostility = 0.1;
1525        work.interaction_profile.interaction_frequency = 0.9;
1526        work.interaction_profile.interaction_complexity = 0.9;
1527        entity
1528            .context_mut()
1529            .add_microsystem(micro_id.clone(), crate::context::Microsystem::new_work(work));
1530
1531        sim.add_entity(entity.clone(), anchor);
1532        let handle = sim.entity(&entity.id()).unwrap();
1533
1534        let event = EventBuilder::new(EventType::ReceiveSupportEmotional)
1535            .context(micro_id)
1536            .build()
1537            .unwrap();
1538
1539        let context = handle.build_trust_context(&entity, &event);
1540        assert!(context.time_pressure() > 0.5);
1541        assert!(context.social_norms() > 0.5);
1542    }
1543
1544    #[test]
1545    fn build_trust_context_missing_microsystem_keeps_defaults() {
1546        use crate::event::EventBuilder;
1547        use crate::enums::EventType;
1548
1549        let mut sim = create_simulation();
1550        let entity = create_human("person_001");
1551        let anchor = sim.reference_date();
1552        sim.add_entity(entity.clone(), anchor);
1553        let handle = sim.entity(&entity.id()).unwrap();
1554
1555        let micro_id = crate::types::MicrosystemId::new("unknown_ctx").unwrap();
1556        let event = EventBuilder::new(EventType::ReceiveSupportEmotional)
1557            .context(micro_id)
1558            .build()
1559            .unwrap();
1560
1561        let context = handle.build_trust_context(&entity, &event);
1562        assert!((context.time_pressure() - 0.5).abs() < f32::EPSILON);
1563    }
1564
1565    #[test]
1566    fn computed_state_get_effective_mood() {
1567        let mut sim = create_simulation();
1568        let entity = create_human("person_001");
1569        let anchor = sim.reference_date();
1570        sim.add_entity(entity, anchor);
1571
1572        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1573        let state = handle.state_at(anchor);
1574
1575        let valence = state.get_effective(StatePath::Mood(crate::enums::MoodPath::Valence));
1576        assert!(valence >= -1.0 && valence <= 1.0);
1577
1578        let arousal = state.get_effective(StatePath::Mood(crate::enums::MoodPath::Arousal));
1579        assert!(arousal >= -1.0 && arousal <= 1.0);
1580
1581        let dominance = state.get_effective(StatePath::Mood(crate::enums::MoodPath::Dominance));
1582        assert!(dominance >= -1.0 && dominance <= 1.0);
1583    }
1584
1585    #[test]
1586    fn computed_state_emotion_membership_sums_to_one() {
1587        let mut sim = create_simulation();
1588        let entity = create_human("person_001");
1589        let anchor = sim.reference_date();
1590        sim.add_entity(entity, anchor);
1591
1592        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1593        let state = handle.state_at(anchor);
1594
1595        let membership = state.emotion_membership();
1596        let total: f64 = membership.values().copied().sum();
1597        assert!((total - 1.0).abs() < 1e-6);
1598    }
1599
1600    #[test]
1601    fn computed_state_get_effective_needs() {
1602        let mut sim = create_simulation();
1603        let entity = create_human("person_001");
1604        let anchor = sim.reference_date();
1605        sim.add_entity(entity, anchor);
1606
1607        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1608        let state = handle.state_at(anchor);
1609
1610        use crate::enums::{NeedsPath, SocialCognitionPath};
1611
1612        // get_effective now returns f64 directly - verify values are in valid range
1613        let loneliness =
1614            state.get_effective(StatePath::SocialCognition(SocialCognitionPath::Loneliness));
1615        assert!(loneliness >= 0.0 && loneliness <= 1.0);
1616
1617        let prc = state.get_effective(StatePath::SocialCognition(
1618            SocialCognitionPath::PerceivedReciprocalCaring,
1619        ));
1620        assert!(prc >= 0.0 && prc <= 1.0);
1621
1622        let liability = state.get_effective(StatePath::SocialCognition(
1623            SocialCognitionPath::PerceivedLiability,
1624        ));
1625        assert!(liability >= 0.0 && liability <= 1.0);
1626
1627        let self_hate =
1628            state.get_effective(StatePath::SocialCognition(SocialCognitionPath::SelfHate));
1629        assert!(self_hate >= 0.0 && self_hate <= 1.0);
1630
1631        let perceived_competence = state.get_effective(StatePath::SocialCognition(
1632            SocialCognitionPath::PerceivedCompetence,
1633        ));
1634        assert!(perceived_competence >= 0.0 && perceived_competence <= 1.0);
1635
1636        let stress = state.get_effective(StatePath::Needs(NeedsPath::Stress));
1637        assert!(stress >= 0.0 && stress <= 1.0);
1638
1639        let fatigue = state.get_effective(StatePath::Needs(NeedsPath::Fatigue));
1640        assert!(fatigue >= 0.0 && fatigue <= 1.0);
1641
1642        let purpose = state.get_effective(StatePath::Needs(NeedsPath::Purpose));
1643        assert!(purpose >= 0.0 && purpose <= 1.0);
1644    }
1645
1646    #[test]
1647    fn computed_state_get_effective_mental_health() {
1648        let mut sim = create_simulation();
1649        let entity = create_human("person_001");
1650        let anchor = sim.reference_date();
1651        sim.add_entity(entity, anchor);
1652
1653        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1654        let state = handle.state_at(anchor);
1655
1656        use crate::enums::MentalHealthPath;
1657
1658        // get_effective now returns f64 directly - verify values are in valid range
1659        let depression = state.get_effective(StatePath::MentalHealth(MentalHealthPath::Depression));
1660        assert!(depression >= 0.0 && depression <= 1.0);
1661
1662        let ac = state.get_effective(StatePath::MentalHealth(
1663            MentalHealthPath::AcquiredCapability,
1664        ));
1665        assert!(ac >= 0.0 && ac <= 1.0);
1666
1667        let ih = state.get_effective(StatePath::MentalHealth(
1668            MentalHealthPath::InterpersonalHopelessness,
1669        ));
1670        assert!(ih >= 0.0 && ih <= 1.0);
1671
1672        let tb = state.get_effective(StatePath::MentalHealth(
1673            MentalHealthPath::ThwartedBelongingness,
1674        ));
1675        assert!(tb >= 0.0 && tb <= 1.0);
1676
1677        let pb = state.get_effective(StatePath::MentalHealth(
1678            MentalHealthPath::PerceivedBurdensomeness,
1679        ));
1680        assert!(pb >= 0.0 && pb <= 1.0);
1681
1682        let desire = state.get_effective(StatePath::MentalHealth(MentalHealthPath::SuicidalDesire));
1683        assert!(desire >= 0.0 && desire <= 1.0);
1684
1685        let risk = state.get_effective(StatePath::MentalHealth(MentalHealthPath::AttemptRisk));
1686        assert!(risk >= 0.0 && risk <= 1.0);
1687    }
1688
1689    #[test]
1690    fn computed_state_get_effective_hexaco() {
1691        let mut sim = create_simulation();
1692        let entity = create_human("person_001");
1693        let anchor = sim.reference_date();
1694        sim.add_entity(entity, anchor);
1695
1696        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1697        let state = handle.state_at(anchor);
1698
1699        use crate::enums::HexacoPath;
1700
1701        // get_effective now returns f64 directly - HEXACO values are 0.0-1.0
1702        let hh = state.get_effective(StatePath::Hexaco(HexacoPath::HonestyHumility));
1703        assert!(hh >= 0.0 && hh <= 1.0);
1704
1705        let n = state.get_effective(StatePath::Hexaco(HexacoPath::Neuroticism));
1706        assert!(n >= 0.0 && n <= 1.0);
1707
1708        let e = state.get_effective(StatePath::Hexaco(HexacoPath::Extraversion));
1709        assert!(e >= 0.0 && e <= 1.0);
1710
1711        let a = state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
1712        assert!(a >= 0.0 && a <= 1.0);
1713
1714        let c = state.get_effective(StatePath::Hexaco(HexacoPath::Conscientiousness));
1715        assert!(c >= 0.0 && c <= 1.0);
1716
1717        let o = state.get_effective(StatePath::Hexaco(HexacoPath::Openness));
1718        assert!(o >= 0.0 && o <= 1.0);
1719    }
1720
1721    #[test]
1722    fn computed_state_get_effective_disposition() {
1723        let mut sim = create_simulation();
1724        let entity = create_human("person_001");
1725        let anchor = sim.reference_date();
1726        sim.add_entity(entity, anchor);
1727
1728        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1729        let state = handle.state_at(anchor);
1730
1731        use crate::enums::DispositionPath;
1732
1733        // get_effective now returns f64 directly
1734        let empathy = state.get_effective(StatePath::Disposition(DispositionPath::Empathy));
1735        assert!(empathy >= 0.0 && empathy <= 1.0);
1736
1737        let aggression = state.get_effective(StatePath::Disposition(DispositionPath::Aggression));
1738        assert!(aggression >= 0.0 && aggression <= 1.0);
1739
1740        let grievance = state.get_effective(StatePath::Disposition(DispositionPath::Grievance));
1741        assert!(grievance >= 0.0 && grievance <= 1.0);
1742    }
1743
1744    #[test]
1745    fn computed_state_get_effective_person_characteristics() {
1746        let mut sim = create_simulation();
1747        let entity = create_human("person_001");
1748        let anchor = sim.reference_date();
1749        sim.add_entity(entity, anchor);
1750
1751        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1752        let state = handle.state_at(anchor);
1753
1754        use crate::enums::PersonCharacteristicsPath;
1755
1756        // get_effective now returns f64 directly
1757        let sc = state.get_effective(StatePath::PersonCharacteristics(
1758            PersonCharacteristicsPath::SocialCapital,
1759        ));
1760        assert!(sc >= 0.0 && sc <= 1.0);
1761
1762        let ca = state.get_effective(StatePath::PersonCharacteristics(
1763            PersonCharacteristicsPath::CognitiveAbility,
1764        ));
1765        assert!(ca >= 0.0 && ca <= 1.0);
1766
1767        let ms = state.get_effective(StatePath::PersonCharacteristics(
1768            PersonCharacteristicsPath::MaterialSecurity,
1769        ));
1770        assert!(ms >= 0.0 && ms <= 1.0);
1771    }
1772
1773    #[test]
1774    fn state_at_with_event_applies_event() {
1775        let mut sim = create_simulation();
1776        let entity = create_human("person_001");
1777        let entity_id = EntityId::new("person_001").unwrap();
1778        let anchor = sim.reference_date();
1779        sim.add_entity(entity, anchor);
1780
1781        // Add an event 1 day after anchor
1782        let event = EventBuilder::new(EventType::EndRelationshipRomantic)
1783            .target(entity_id.clone())
1784            .severity(0.7)
1785            .build()
1786            .unwrap();
1787        let event_time = anchor + Duration::days(1);
1788        sim.add_event(event, event_time);
1789
1790        // Query 2 days after anchor (after the event)
1791        let query_time = anchor + Duration::days(2);
1792        let handle = sim.entity(&entity_id).unwrap();
1793        let _state = handle.state_at(query_time);
1794        // Event would have affected valence (social exclusion is negative)
1795    }
1796
1797    #[test]
1798    fn age_at_timestamp_forward_with_birth_date() {
1799        let mut sim = create_simulation();
1800        let anchor = sim.reference_date();
1801        // Entity born 25 years before anchor
1802        let birth_date = anchor - Duration::years(25);
1803        let entity = EntityBuilder::new()
1804            .id("person_001")
1805            .species(Species::Human)
1806            .birth_date(birth_date)
1807            .age(Duration::years(25))
1808            .build()
1809            .unwrap();
1810        sim.add_entity(entity, anchor);
1811
1812        // Query 10 years later - age should be 35
1813        let future = anchor + Duration::years(10);
1814        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1815        let state = handle.state_at(future);
1816
1817        assert_eq!(state.age_at_timestamp().as_years(), 35);
1818    }
1819
1820    #[test]
1821    fn age_at_timestamp_forward_without_birth_date_is_constant() {
1822        let mut sim = create_simulation();
1823        let entity = EntityBuilder::new()
1824            .id("person_001")
1825            .species(Species::Human)
1826            .age(Duration::years(25))
1827            .build()
1828            .unwrap();
1829        let anchor = sim.reference_date();
1830        sim.add_entity(entity, anchor);
1831
1832        // Query 10 years later - age should still be 25 (constant without birth_date)
1833        let future = anchor + Duration::years(10);
1834        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1835        let state = handle.state_at(future);
1836
1837        assert_eq!(state.age_at_timestamp().as_years(), 25);
1838    }
1839
1840    #[test]
1841    fn age_at_timestamp_backward_with_birth_date() {
1842        let mut sim = create_simulation();
1843        let anchor = sim.reference_date();
1844        // Entity born 25 years before anchor
1845        let birth_date = anchor - Duration::years(25);
1846        let entity = EntityBuilder::new()
1847            .id("person_001")
1848            .species(Species::Human)
1849            .birth_date(birth_date)
1850            .age(Duration::years(25))
1851            .build()
1852            .unwrap();
1853        sim.add_entity(entity, anchor);
1854
1855        // Query 10 years earlier - age should be 15
1856        let past = anchor - Duration::years(10);
1857        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1858        let state = handle.state_at(past);
1859
1860        assert_eq!(state.age_at_timestamp().as_years(), 15);
1861    }
1862
1863    #[test]
1864    fn age_at_timestamp_backward_without_birth_date_is_constant() {
1865        let mut sim = create_simulation();
1866        let entity = EntityBuilder::new()
1867            .id("person_001")
1868            .species(Species::Human)
1869            .age(Duration::years(25))
1870            .build()
1871            .unwrap();
1872        let anchor = sim.reference_date();
1873        sim.add_entity(entity, anchor);
1874
1875        // Query 10 years earlier - age should still be 25 (constant without birth_date)
1876        let past = anchor - Duration::years(10);
1877        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1878        let state = handle.state_at(past);
1879
1880        assert_eq!(state.age_at_timestamp().as_years(), 25);
1881    }
1882
1883    #[test]
1884    fn life_stage_at_timestamp_with_birth_date() {
1885        let mut sim = create_simulation();
1886        let anchor = sim.reference_date();
1887        // Entity born 10 years before anchor
1888        let birth_date = anchor - Duration::years(10);
1889        let entity = EntityBuilder::new()
1890            .id("person_001")
1891            .species(Species::Human)
1892            .birth_date(birth_date)
1893            .age(Duration::years(10))
1894            .build()
1895            .unwrap();
1896        sim.add_entity(entity, anchor);
1897
1898        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1899
1900        // At anchor (age 10): Child
1901        let state = handle.state_at(anchor);
1902        assert_eq!(state.life_stage(), LifeStage::Child);
1903
1904        // 10 years later (age 20): YoungAdult
1905        let future = anchor + Duration::years(10);
1906        let state2 = handle.state_at(future);
1907        assert_eq!(state2.life_stage(), LifeStage::YoungAdult);
1908    }
1909
1910    #[test]
1911    fn life_stage_at_timestamp_without_birth_date_is_constant() {
1912        let mut sim = create_simulation();
1913        let entity = EntityBuilder::new()
1914            .id("person_001")
1915            .species(Species::Human)
1916            .age(Duration::years(10))
1917            .build()
1918            .unwrap();
1919        let anchor = sim.reference_date();
1920        sim.add_entity(entity, anchor);
1921
1922        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1923
1924        // At anchor (age 10): Child
1925        let state = handle.state_at(anchor);
1926        assert_eq!(state.life_stage(), LifeStage::Child);
1927
1928        // 10 years later - without birth_date, age stays 10, still Child
1929        let future = anchor + Duration::years(10);
1930        let state2 = handle.state_at(future);
1931        assert_eq!(state2.life_stage(), LifeStage::Child);
1932    }
1933
1934    #[test]
1935    fn regression_quality_forward_is_exact() {
1936        let mut sim = create_simulation();
1937        let entity = create_human("person_001");
1938        let anchor = sim.reference_date();
1939        sim.add_entity(entity, anchor);
1940
1941        let future = anchor + Duration::days(30);
1942        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1943        let state = handle.state_at(future);
1944
1945        assert!(state.regression_quality().is_exact());
1946    }
1947
1948    #[test]
1949    fn computed_state_debug() {
1950        let mut sim = create_simulation();
1951        let entity = create_human("person_001");
1952        let anchor = sim.reference_date();
1953        sim.add_entity(entity, anchor);
1954
1955        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1956        let state = handle.state_at(anchor);
1957
1958        let debug = format!("{:?}", state);
1959        assert!(debug.contains("ComputedState"));
1960    }
1961
1962    #[test]
1963    fn computed_state_clone() {
1964        let mut sim = create_simulation();
1965        let entity = create_human("person_001");
1966        let anchor = sim.reference_date();
1967        sim.add_entity(entity, anchor);
1968
1969        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
1970        let state = handle.state_at(anchor);
1971        let cloned = state.clone();
1972
1973        assert_eq!(state.age_at_timestamp(), cloned.age_at_timestamp());
1974    }
1975
1976    #[test]
1977    #[should_panic(expected = "EntityQueryHandle created for non-existent entity")]
1978    fn state_at_nonexistent_entity_panics() {
1979        let sim = create_simulation();
1980        let unknown = EntityId::new("unknown").unwrap();
1981        let handle = EntityQueryHandle::new(&sim, unknown);
1982
1983        // This should panic because the entity doesn't exist
1984        let _state = handle.state_at(sim.reference_date());
1985    }
1986
1987    #[test]
1988    fn anchor_timestamp_nonexistent_entity() {
1989        let sim = create_simulation();
1990        let unknown = EntityId::new("unknown").unwrap();
1991        let handle = EntityQueryHandle::new(&sim, unknown);
1992
1993        assert!(handle.anchor_timestamp().is_none());
1994    }
1995
1996    #[test]
1997    fn state_at_backward_with_events() {
1998        let mut sim = create_simulation();
1999        let entity = create_human("person_001");
2000        let entity_id = EntityId::new("person_001").unwrap();
2001
2002        // Set anchor to a later date
2003        let anchor = Timestamp::from_ymd_hms(2024, 6, 1, 0, 0, 0);
2004        sim.add_entity(entity, anchor);
2005
2006        // Add an event before the anchor (in the past relative to anchor)
2007        let event = EventBuilder::new(EventType::EndRelationshipRomantic)
2008            .target(entity_id.clone())
2009            .severity(0.7)
2010            .build()
2011            .unwrap();
2012        let event_time = Timestamp::from_ymd_hms(2024, 3, 1, 0, 0, 0);
2013        sim.add_event(event, event_time);
2014
2015        // Query state before the anchor (backward regression through events)
2016        let past = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
2017        let handle = sim.entity(&entity_id).unwrap();
2018        let computed = handle.state_at(past);
2019
2020        // Backward regression through events should still work
2021        assert!(computed.regression_quality().is_exact());
2022    }
2023
2024    #[test]
2025    fn computed_state_get_effective_mental_health_all_paths() {
2026        let mut sim = create_simulation();
2027        let entity = create_human("person_001");
2028        let anchor = sim.reference_date();
2029        sim.add_entity(entity, anchor);
2030
2031        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
2032        let state = handle.state_at(anchor);
2033
2034        use crate::enums::MentalHealthPath;
2035
2036        // Test all mental health paths including SelfWorth and Hopelessness
2037        // get_effective now returns f64 directly
2038        let self_worth = state.get_effective(StatePath::MentalHealth(MentalHealthPath::SelfWorth));
2039        assert!(self_worth >= 0.0 && self_worth <= 1.0);
2040
2041        let hopelessness =
2042            state.get_effective(StatePath::MentalHealth(MentalHealthPath::Hopelessness));
2043        assert!(hopelessness >= 0.0 && hopelessness <= 1.0);
2044    }
2045
2046    #[test]
2047    fn computed_state_get_effective_disposition_all_paths() {
2048        let mut sim = create_simulation();
2049        let entity = create_human("person_001");
2050        let anchor = sim.reference_date();
2051        sim.add_entity(entity, anchor);
2052
2053        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
2054        let state = handle.state_at(anchor);
2055
2056        use crate::enums::DispositionPath;
2057
2058        // Test all disposition paths - get_effective now returns f64 directly
2059        let impulse = state.get_effective(StatePath::Disposition(DispositionPath::ImpulseControl));
2060        assert!(impulse >= 0.0 && impulse <= 1.0);
2061
2062        let reactance = state.get_effective(StatePath::Disposition(DispositionPath::Reactance));
2063        assert!(reactance >= 0.0 && reactance <= 1.0);
2064
2065        let trust = state.get_effective(StatePath::Disposition(DispositionPath::TrustorPropensity));
2066        assert!(trust >= 0.0 && trust <= 1.0);
2067    }
2068
2069    #[test]
2070    fn computed_state_get_effective_person_characteristics_all_paths() {
2071        let mut sim = create_simulation();
2072        let entity = create_human("person_001");
2073        let anchor = sim.reference_date();
2074        sim.add_entity(entity, anchor);
2075
2076        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
2077        let state = handle.state_at(anchor);
2078
2079        use crate::enums::PersonCharacteristicsPath;
2080
2081        // Test all person characteristics paths - get_effective now returns f64 directly
2082        let era = state.get_effective(StatePath::PersonCharacteristics(
2083            PersonCharacteristicsPath::EmotionalRegulationAssets,
2084        ));
2085        assert!(era >= 0.0 && era <= 1.0);
2086
2087        let ed = state.get_effective(StatePath::PersonCharacteristics(
2088            PersonCharacteristicsPath::ExperienceDiversity,
2089        ));
2090        assert!(ed >= 0.0 && ed <= 1.0);
2091
2092        let bm = state.get_effective(StatePath::PersonCharacteristics(
2093            PersonCharacteristicsPath::BaselineMotivation,
2094        ));
2095        assert!(bm >= 0.0 && bm <= 1.0);
2096
2097        let pt = state.get_effective(StatePath::PersonCharacteristics(
2098            PersonCharacteristicsPath::PersistenceTendency,
2099        ));
2100        assert!(pt >= 0.0 && pt <= 1.0);
2101
2102        let ct = state.get_effective(StatePath::PersonCharacteristics(
2103            PersonCharacteristicsPath::CuriosityTendency,
2104        ));
2105        assert!(ct >= 0.0 && ct <= 1.0);
2106
2107        // Composite values can be any f64
2108        let _resource = state.get_effective(StatePath::PersonCharacteristics(
2109            PersonCharacteristicsPath::Resource,
2110        ));
2111
2112        let _force = state.get_effective(StatePath::PersonCharacteristics(
2113            PersonCharacteristicsPath::Force,
2114        ));
2115    }
2116
2117    #[test]
2118    fn memories_at_returns_empty_for_new_entity() {
2119        let mut sim = create_simulation();
2120        let entity = create_human("person_001");
2121        let anchor = sim.reference_date();
2122        sim.add_entity(entity, anchor);
2123
2124        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
2125        let memories = handle.memories_at(anchor);
2126
2127        assert!(memories.is_empty());
2128    }
2129
2130    #[test]
2131    fn memories_at_returns_empty_for_nonexistent_entity() {
2132        let sim = create_simulation();
2133        let unknown = EntityId::new("unknown").unwrap();
2134        let handle = EntityQueryHandle::new(&sim, unknown);
2135
2136        let memories = handle.memories_at(sim.reference_date());
2137        assert!(memories.is_empty());
2138    }
2139
2140    #[test]
2141    fn memories_at_filters_by_timestamp() {
2142        use crate::memory::MemoryTag;
2143
2144        let mut sim = create_simulation();
2145
2146        // Create entity at age 25
2147        let mut entity = EntityBuilder::new()
2148            .id("person_001")
2149            .species(Species::Human)
2150            .age(Duration::years(25))
2151            .build()
2152            .unwrap();
2153
2154        // Create a memory at "current" age (age 25)
2155        entity.create_memory(
2156            "First memory at age 25",
2157            vec![],
2158            vec![MemoryTag::Personal],
2159            0.5,
2160            None,
2161        );
2162
2163        let anchor = sim.reference_date();
2164        sim.add_entity(entity, anchor);
2165
2166        // Query at anchor time - should see the memory
2167        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
2168        let memories_at_anchor = handle.memories_at(anchor);
2169        assert_eq!(memories_at_anchor.len(), 1);
2170
2171        // Query in the past (before entity's current age) - should not see memory
2172        // Entity is age 25 at anchor. Memory formed at age 25.
2173        // Going back 10 years makes entity age 15, so memory shouldn't exist.
2174        let past = anchor - Duration::years(10);
2175        let memories_past = handle.memories_at(past);
2176        assert!(memories_past.is_empty());
2177    }
2178
2179    #[test]
2180    fn memories_at_forward_in_time() {
2181        // Test the forward path: timestamp >= anchor_timestamp
2182        use crate::memory::MemoryTag;
2183
2184        let mut sim = create_simulation();
2185
2186        // Create entity at age 25
2187        let mut entity = EntityBuilder::new()
2188            .id("person_001")
2189            .species(Species::Human)
2190            .age(Duration::years(25))
2191            .build()
2192            .unwrap();
2193
2194        // Create a memory at "current" age (age 25)
2195        entity.create_memory(
2196            "Memory at age 25",
2197            vec![],
2198            vec![MemoryTag::Personal],
2199            0.5,
2200            None,
2201        );
2202
2203        let anchor = sim.reference_date();
2204        sim.add_entity(entity, anchor);
2205
2206        // Query 10 years in the future - entity is now 35
2207        // Memory formed at age 25, so it should still exist
2208        let future = anchor + Duration::years(10);
2209        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
2210        let memories_future = handle.memories_at(future);
2211
2212        // Memory should still exist (formed at age 25, now age 35)
2213        assert_eq!(memories_future.len(), 1);
2214    }
2215
2216    #[test]
2217    fn age_computed_from_birth_date() {
2218        let mut sim = create_simulation();
2219
2220        // Entity born on June 15, 1990
2221        let birth = Timestamp::from_ymd_hms(1990, 6, 15, 0, 0, 0);
2222        let entity = EntityBuilder::new()
2223            .id("person_001")
2224            .species(Species::Human)
2225            .age(crate::types::Duration::years(30))
2226            .birth_date(birth)
2227            .build()
2228            .unwrap();
2229
2230        let anchor = sim.reference_date(); // 2024-01-01
2231        sim.add_entity(entity, anchor);
2232
2233        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
2234
2235        // Query at anchor date (2024-01-01)
2236        let state = handle.state_at(anchor);
2237        let age_at_anchor = state.age_at_timestamp();
2238
2239        // Should be approximately 33-34 years old
2240        assert!(age_at_anchor.as_years() >= 33);
2241        assert!(age_at_anchor.as_years() <= 34);
2242
2243        // Query 10 years later
2244        let future = anchor + Duration::years(10);
2245        let future_state = handle.state_at(future);
2246        let age_at_future = future_state.age_at_timestamp();
2247
2248        // Should be approximately 43-44 years old
2249        assert!(age_at_future.as_years() >= 43);
2250        assert!(age_at_future.as_years() <= 44);
2251    }
2252
2253    #[test]
2254    fn age_before_birth_returns_zero() {
2255        // Entity born on 2000-01-01
2256        let birth = Timestamp::from_ymd_hms(2000, 1, 1, 0, 0, 0);
2257        let entity = EntityBuilder::new()
2258            .id("person_001")
2259            .species(Species::Human)
2260            .age(crate::types::Duration::years(30))
2261            .birth_date(birth)
2262            .build()
2263            .unwrap();
2264
2265        let anchor = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
2266        let mut sim = Simulation::new(Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0));
2267        sim.add_entity(entity, anchor);
2268
2269        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
2270
2271        // Query before birth
2272        let before_birth = Timestamp::from_ymd_hms(1990, 1, 1, 0, 0, 0);
2273        let state = handle.state_at(before_birth);
2274
2275        // Age should be zero
2276        assert!(state.age_at_timestamp().is_zero());
2277    }
2278
2279    #[test]
2280    fn regression_through_trauma_is_approximate() {
2281        let mut sim = create_simulation();
2282        let entity = create_human("person_001");
2283        let entity_id = EntityId::new("person_001").unwrap();
2284
2285        // Set anchor to a later date
2286        let anchor = Timestamp::from_ymd_hms(2024, 6, 1, 0, 0, 0);
2287        sim.add_entity(entity, anchor);
2288
2289        // Add a trauma event (Violence) before the anchor
2290        let trauma_event = EventBuilder::new(EventType::ExperienceCombatMilitary)
2291            .target(entity_id.clone())
2292            .severity(0.8)
2293            .build()
2294            .unwrap();
2295        let event_time = Timestamp::from_ymd_hms(2024, 3, 1, 0, 0, 0);
2296        sim.add_event(trauma_event, event_time);
2297
2298        // Query state before the trauma event (backward regression through trauma)
2299        let past = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
2300        let handle = sim.entity(&entity_id).unwrap();
2301        let state = handle.state_at(past);
2302
2303        // Regression through trauma should be approximate (AC not reversible)
2304        assert!(state.regression_quality().is_approximate());
2305    }
2306
2307    #[test]
2308    fn regression_without_trauma_is_exact() {
2309        let mut sim = create_simulation();
2310        let entity = create_human("person_001");
2311        let entity_id = EntityId::new("person_001").unwrap();
2312
2313        // Set anchor to a later date
2314        let anchor = Timestamp::from_ymd_hms(2024, 6, 1, 0, 0, 0);
2315        sim.add_entity(entity, anchor);
2316
2317        // Add a social exclusion event (not trauma) before the anchor
2318        let social_event = EventBuilder::new(EventType::EndRelationshipRomantic)
2319            .target(entity_id.clone())
2320            .severity(0.5)
2321            .build()
2322            .unwrap();
2323        let event_time = Timestamp::from_ymd_hms(2024, 3, 1, 0, 0, 0);
2324        sim.add_event(social_event, event_time);
2325
2326        // Query state before the event (backward regression)
2327        let past = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
2328        let handle = sim.entity(&entity_id).unwrap();
2329        let state = handle.state_at(past);
2330
2331        // Regression through non-trauma events should be exact
2332        assert!(state.regression_quality().is_exact());
2333    }
2334
2335    #[test]
2336    fn estimate_relationship_quality_baseline() {
2337        let entity = create_human("person_001");
2338        let baseline = estimate_relationship_quality(&entity);
2339        assert!((baseline - 0.3).abs() < f64::EPSILON);
2340    }
2341
2342    #[test]
2343    fn computed_state_clone_with_cached_alerts() {
2344        let mut sim = create_simulation();
2345        let entity = create_human("person_001");
2346        let anchor = sim.reference_date();
2347        sim.add_entity(entity, anchor);
2348
2349        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
2350        let state = handle.state_at(anchor);
2351
2352        // Compute and cache alerts first
2353        let alerts1 = state.alerts();
2354        assert!(alerts1.is_empty());
2355
2356        // Clone the state with cached alerts
2357        let cloned = state.clone();
2358
2359        // The cloned state should also have the cached alerts
2360        let alerts2 = cloned.alerts();
2361        assert!(alerts2.is_empty());
2362    }
2363
2364    #[test]
2365    fn computed_state_clone_without_cached_alerts() {
2366        // Test cloning when alerts haven't been accessed yet (None branch)
2367        let mut sim = create_simulation();
2368        let entity = create_human("person_001");
2369        let anchor = sim.reference_date();
2370        sim.add_entity(entity, anchor);
2371
2372        let handle = sim.entity(&EntityId::new("person_001").unwrap()).unwrap();
2373        let state = handle.state_at(anchor);
2374
2375        // Clone WITHOUT accessing alerts first
2376        let cloned = state.clone();
2377
2378        // Both original and cloned should work independently
2379        let alerts1 = state.alerts();
2380        let alerts2 = cloned.alerts();
2381        assert!(alerts1.is_empty());
2382        assert!(alerts2.is_empty());
2383    }
2384
2385    #[test]
2386    fn memories_at_before_birth_returns_empty() {
2387        // Test the case where query timestamp is so far in the past
2388        // that elapsed >= anchor_age (age would be negative, so returns zero)
2389        use crate::memory::MemoryTag;
2390
2391        let mut sim = create_simulation();
2392
2393        // Create entity at age 5 (young entity)
2394        let mut entity = EntityBuilder::new()
2395            .id("child_001")
2396            .species(Species::Human)
2397            .age(Duration::years(5))
2398            .build()
2399            .unwrap();
2400
2401        // Create a memory at "current" age (age 5)
2402        entity.create_memory(
2403            "Memory at age 5",
2404            vec![],
2405            vec![MemoryTag::Personal],
2406            0.5,
2407            None,
2408        );
2409
2410        let anchor = sim.reference_date(); // 2024-01-01
2411        sim.add_entity(entity, anchor);
2412
2413        // Query 10 years in the past from anchor
2414        // Entity is age 5 at anchor, so 10 years ago they weren't born yet
2415        let past = anchor - Duration::years(10);
2416        let handle = sim.entity(&EntityId::new("child_001").unwrap()).unwrap();
2417        let memories = handle.memories_at(past);
2418
2419        // Should return empty because age would be negative
2420        assert!(memories.is_empty());
2421    }
2422
2423    #[test]
2424    fn developmental_effects_entity_without_birth_date_uses_anchor_age() {
2425        // Test that developmental effects use anchor age when no birth_date is set
2426        let mut sim = create_simulation();
2427
2428        // Create entity with just age (no birth_date)
2429        let entity = EntityBuilder::new()
2430            .id("person_001")
2431            .species(Species::Human)
2432            .age(Duration::years(30))
2433            .build()
2434            .unwrap();
2435
2436        let entity_id = EntityId::new("person_001").unwrap();
2437        let anchor = sim.reference_date();
2438        sim.add_entity(entity, anchor);
2439
2440        // Add an event
2441        let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
2442            .target(entity_id.clone())
2443            .severity(0.5)
2444            .build()
2445            .unwrap();
2446        let event_time = anchor + Duration::days(10);
2447        sim.add_event(event, event_time);
2448
2449        // Query state after event - developmental effects should use anchor age
2450        let query_time = anchor + Duration::days(20);
2451        let handle = sim.entity(&entity_id).unwrap();
2452        let state = handle.state_at(query_time);
2453
2454        // State should be valid (developmental effects applied correctly)
2455        let valence = state.get_effective(StatePath::Mood(crate::enums::MoodPath::Valence));
2456        assert!(valence >= -1.0 && valence <= 1.0);
2457    }
2458
2459    #[test]
2460    fn developmental_effects_event_after_birth_date_uses_birth_age() {
2461        // Test developmental effects when event timestamp is after birth_date
2462        let birth_date = Timestamp::from_ymd_hms(2000, 1, 1, 0, 0, 0);
2463        let entity = EntityBuilder::new()
2464            .id("person_001")
2465            .species(Species::Human)
2466            .age(crate::types::Duration::years(30))
2467            .birth_date(birth_date)
2468            .build()
2469            .unwrap();
2470
2471        let entity_id = EntityId::new("person_001").unwrap();
2472        let anchor = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
2473        let mut sim = Simulation::new(anchor);
2474        sim.add_entity(entity, anchor);
2475
2476        let event = EventBuilder::new(EventType::AchieveGoalMajor)
2477            .target(entity_id.clone())
2478            .severity(0.5)
2479            .build()
2480            .unwrap();
2481        let event_time = anchor + Duration::days(10);
2482        sim.add_event(event, event_time);
2483
2484        let query_time = anchor + Duration::days(20);
2485        let handle = sim.entity(&entity_id).unwrap();
2486        assert_eq!(
2487            handle
2488                .get_sorted_events_for_range(anchor, query_time, true)
2489                .len(),
2490            1
2491        );
2492        let state = handle.state_at(query_time);
2493
2494        assert!(!state.age_at_timestamp().is_zero());
2495    }
2496
2497    #[test]
2498    fn developmental_effects_event_before_birth_date() {
2499        // Test developmental effects when event timestamp is before birth_date
2500        // This is an edge case that shouldn't happen in practice but must be handled
2501
2502        // Create entity with birth_date
2503        let birth_date = Timestamp::from_ymd_hms(2000, 1, 1, 0, 0, 0);
2504        let entity = EntityBuilder::new()
2505            .id("person_001")
2506            .species(Species::Human)
2507            .age(crate::types::Duration::years(30))
2508            .birth_date(birth_date)
2509            .build()
2510            .unwrap();
2511
2512        let entity_id = EntityId::new("person_001").unwrap();
2513        let anchor = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
2514        let mut sim = Simulation::new(anchor);
2515        sim.add_entity(entity, anchor);
2516
2517        // Add an event at a timestamp BEFORE birth_date
2518        // This is conceptually invalid but tests the edge case handling
2519        let event = EventBuilder::new(EventType::AchieveGoalMajor)
2520            .target(entity_id.clone())
2521            .severity(0.5)
2522            .build()
2523            .unwrap();
2524        let event_time = Timestamp::from_ymd_hms(1990, 1, 1, 0, 0, 0); // Before birth
2525        sim.add_event(event, event_time);
2526
2527        // Query state far enough in the past so the pre-birth event is in range
2528        let query_time = Timestamp::from_ymd_hms(1980, 1, 1, 0, 0, 0);
2529        let handle = sim.entity(&entity_id).unwrap();
2530        assert_eq!(
2531            handle
2532                .get_sorted_events_for_range(anchor, query_time, false)
2533                .len(),
2534            1
2535        );
2536        let state = handle.state_at(query_time);
2537
2538        // The age at the event should be treated as zero (floor at birth)
2539        // This tests the Duration::zero() branch in compute_age_at
2540        assert!(state.age_at_timestamp().is_zero());
2541    }
2542
2543    // Formative events tests
2544
2545    #[test]
2546    fn formative_event_shifts_personality() {
2547        let mut sim = create_simulation();
2548        let reference = sim.reference_date();
2549
2550        // Create a human with birth date 25 years before reference
2551        let entity = EntityBuilder::new()
2552            .id("person_001")
2553            .species(Species::Human)
2554            .age(crate::types::Duration::years(30))
2555            .birth_date(reference - Duration::years(25))
2556            .build()
2557            .unwrap();
2558
2559        let entity_id = entity.id().clone();
2560        let anchor = reference;
2561        sim.add_entity(entity, anchor);
2562
2563        // Get baseline agreeableness
2564        let handle = sim.entity(&entity_id).unwrap();
2565        let baseline_state = handle.state_at(anchor);
2566        let baseline_agreeableness =
2567            baseline_state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
2568
2569        // Add a formative event with a base shift
2570        let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
2571            .target(entity_id.clone())
2572            .severity(0.9)
2573            .with_base_shift(HexacoPath::Agreeableness, -0.15)
2574            .build()
2575            .unwrap();
2576        let event_time = anchor + Duration::days(1);
2577        sim.add_event(event, event_time);
2578
2579        // Query after the formative event
2580        let later = anchor + Duration::days(2);
2581        let handle = sim.entity(&entity_id).unwrap();
2582        let later_state = handle.state_at(later);
2583        let later_agreeableness =
2584            later_state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
2585
2586        // Agreeableness should have decreased due to the base shift
2587        assert!(later_agreeableness < baseline_agreeableness);
2588    }
2589
2590    #[test]
2591    fn formative_event_backward_query_no_shift() {
2592        let mut sim = create_simulation();
2593        let reference = sim.reference_date();
2594
2595        // Create a human with birth date 25 years before reference
2596        let entity = EntityBuilder::new()
2597            .id("person_001")
2598            .species(Species::Human)
2599            .age(crate::types::Duration::years(30))
2600            .birth_date(reference - Duration::years(25))
2601            .build()
2602            .unwrap();
2603
2604        let entity_id = entity.id().clone();
2605        let anchor = reference + Duration::days(10);
2606        sim.add_entity(entity, anchor);
2607
2608        // Add a formative event BEFORE the anchor
2609        let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
2610            .target(entity_id.clone())
2611            .severity(0.9)
2612            .with_base_shift(HexacoPath::Agreeableness, -0.15)
2613            .build()
2614            .unwrap();
2615        let event_time = reference + Duration::days(5);
2616        sim.add_event(event, event_time);
2617
2618        // Query BEFORE the formative event (backward from anchor)
2619        let earlier = reference + Duration::days(1);
2620        let handle = sim.entity(&entity_id).unwrap();
2621        let earlier_state = handle.state_at(earlier);
2622        let earlier_agreeableness =
2623            earlier_state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
2624
2625        // Query at anchor
2626        let anchor_state = handle.state_at(anchor);
2627        let anchor_agreeableness =
2628            anchor_state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
2629
2630        // For backward query, the shift hasn't happened yet
2631        // Both should be equal to the anchor state's agreeableness
2632        // (since backward query doesn't apply formative events)
2633        assert!((earlier_agreeableness - anchor_agreeableness).abs() < 0.1);
2634    }
2635
2636    #[test]
2637    fn formative_event_multiple_shifts_same_trait() {
2638        let mut sim = create_simulation();
2639        let reference = sim.reference_date();
2640
2641        // Create a human with birth date 25 years before reference
2642        let entity = EntityBuilder::new()
2643            .id("person_001")
2644            .species(Species::Human)
2645            .age(crate::types::Duration::years(30))
2646            .birth_date(reference - Duration::years(25))
2647            .build()
2648            .unwrap();
2649
2650        let entity_id = entity.id().clone();
2651        let anchor = reference;
2652        sim.add_entity(entity, anchor);
2653
2654        // Get baseline
2655        let handle = sim.entity(&entity_id).unwrap();
2656        let baseline_state = handle.state_at(anchor);
2657        let baseline_agreeableness =
2658            baseline_state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
2659
2660        // Add first formative event
2661        let event1 = EventBuilder::new(EventType::ExperienceCombatMilitary)
2662            .target(entity_id.clone())
2663            .severity(0.8)
2664            .with_base_shift(HexacoPath::Agreeableness, -0.10)
2665            .build()
2666            .unwrap();
2667        sim.add_event(event1, anchor + Duration::days(1));
2668
2669        // Add second formative event
2670        let event2 = EventBuilder::new(EventType::ExperienceCombatMilitary)
2671            .target(entity_id.clone())
2672            .severity(0.7)
2673            .with_base_shift(HexacoPath::Agreeableness, -0.08)
2674            .build()
2675            .unwrap();
2676        sim.add_event(event2, anchor + Duration::days(2));
2677
2678        // Query after both events
2679        let later = anchor + Duration::days(3);
2680        let handle = sim.entity(&entity_id).unwrap();
2681        let later_state = handle.state_at(later);
2682        let later_agreeableness =
2683            later_state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
2684
2685        // Agreeableness should have decreased by cumulative amount (with modifiers)
2686        assert!(later_agreeableness < baseline_agreeableness);
2687    }
2688
2689    #[test]
2690    fn formative_event_no_shift_leaves_trait_unchanged() {
2691        let mut sim = create_simulation();
2692        let reference = sim.reference_date();
2693
2694        let entity = EntityBuilder::new()
2695            .id("person_001")
2696            .species(Species::Human)
2697            .age(crate::types::Duration::years(30))
2698            .birth_date(reference - Duration::years(25))
2699            .build()
2700            .unwrap();
2701
2702        let entity_id = entity.id().clone();
2703        let anchor = reference;
2704        sim.add_entity(entity, anchor);
2705
2706        // Get baseline
2707        let handle = sim.entity(&entity_id).unwrap();
2708        let baseline_state = handle.state_at(anchor);
2709        let baseline_openness =
2710            baseline_state.get_effective(StatePath::Hexaco(HexacoPath::Openness));
2711
2712        // Add event WITHOUT base shift
2713        let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
2714            .target(entity_id.clone())
2715            .severity(0.8)
2716            .build()
2717            .unwrap();
2718        sim.add_event(event, anchor + Duration::days(1));
2719
2720        // Query after the event
2721        let later = anchor + Duration::days(2);
2722        let handle = sim.entity(&entity_id).unwrap();
2723        let later_state = handle.state_at(later);
2724        let later_openness = later_state.get_effective(StatePath::Hexaco(HexacoPath::Openness));
2725
2726        // Openness should be unchanged (events without base shifts don't affect personality base)
2727        assert!((later_openness - baseline_openness).abs() < 0.01);
2728    }
2729
2730    #[test]
2731    fn collect_base_shifts_empty_for_backward_query() {
2732        let entity = EntityBuilder::new()
2733            .id("test")
2734            .species(Species::Human)
2735            .age(crate::types::Duration::years(30))
2736            .build()
2737            .unwrap();
2738
2739        let records = collect_base_shift_records(
2740            &[],
2741            &entity,
2742            Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0),
2743            false, // backward query
2744        );
2745
2746        assert!(records.is_empty());
2747    }
2748
2749    #[test]
2750    fn apply_base_shifts_empty_records_returns_unchanged() {
2751        let state = IndividualState::new();
2752        let original_openness = state.hexaco().openness();
2753
2754        let result =
2755            apply_base_shifts_to_state(state, &[], Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0));
2756
2757        assert!((result.hexaco().openness() - original_openness).abs() < f32::EPSILON);
2758    }
2759
2760    #[test]
2761    fn formative_event_all_hexaco_traits() {
2762        // Test all six HEXACO traits get shifted
2763        let mut sim = create_simulation();
2764        let reference = sim.reference_date();
2765
2766        let entity = EntityBuilder::new()
2767            .id("person_001")
2768            .species(Species::Human)
2769            .age(crate::types::Duration::years(30))
2770            .birth_date(reference - Duration::years(25))
2771            .build()
2772            .unwrap();
2773
2774        let entity_id = entity.id().clone();
2775        let anchor = reference;
2776        sim.add_entity(entity, anchor);
2777
2778        // Add event with all 6 traits
2779        let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
2780            .target(entity_id.clone())
2781            .severity(0.9)
2782            .with_base_shift(HexacoPath::Openness, 0.10)
2783            .with_base_shift(HexacoPath::Conscientiousness, -0.08)
2784            .with_base_shift(HexacoPath::Extraversion, 0.12)
2785            .with_base_shift(HexacoPath::Agreeableness, -0.15)
2786            .with_base_shift(HexacoPath::Neuroticism, 0.20)
2787            .with_base_shift(HexacoPath::HonestyHumility, -0.05)
2788            .build()
2789            .unwrap();
2790        sim.add_event(event, anchor + Duration::days(1));
2791
2792        // Query after event
2793        let later = anchor + Duration::days(2);
2794        let handle = sim.entity(&entity_id).unwrap();
2795        let state = handle.state_at(later);
2796
2797        // Verify we can access all traits (proving they were processed)
2798        let _ = state.get_effective(StatePath::Hexaco(HexacoPath::Openness));
2799        let _ = state.get_effective(StatePath::Hexaco(HexacoPath::Conscientiousness));
2800        let _ = state.get_effective(StatePath::Hexaco(HexacoPath::Extraversion));
2801        let _ = state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
2802        let _ = state.get_effective(StatePath::Hexaco(HexacoPath::Neuroticism));
2803        let _ = state.get_effective(StatePath::Hexaco(HexacoPath::HonestyHumility));
2804    }
2805
2806    #[test]
2807    fn formative_event_positive_and_negative_shifts() {
2808        // Test both positive and negative shifts are handled
2809        let mut sim = create_simulation();
2810        let reference = sim.reference_date();
2811
2812        let entity = EntityBuilder::new()
2813            .id("person_001")
2814            .species(Species::Human)
2815            .age(crate::types::Duration::years(30))
2816            .birth_date(reference - Duration::years(25))
2817            .build()
2818            .unwrap();
2819
2820        let entity_id = entity.id().clone();
2821        let anchor = reference;
2822        sim.add_entity(entity, anchor);
2823
2824        // Get baseline
2825        let handle = sim.entity(&entity_id).unwrap();
2826        let baseline_state = handle.state_at(anchor);
2827        let baseline_extraversion =
2828            baseline_state.get_effective(StatePath::Hexaco(HexacoPath::Extraversion));
2829
2830        // Add event with positive shift
2831        let event = EventBuilder::new(EventType::AchieveGoalMajor)
2832            .target(entity_id.clone())
2833            .severity(0.7)
2834            .with_base_shift(HexacoPath::Extraversion, 0.15)
2835            .build()
2836            .unwrap();
2837        sim.add_event(event, anchor + Duration::days(1));
2838
2839        // Query after event
2840        let later = anchor + Duration::days(2);
2841        let handle = sim.entity(&entity_id).unwrap();
2842        let later_state = handle.state_at(later);
2843        let later_extraversion =
2844            later_state.get_effective(StatePath::Hexaco(HexacoPath::Extraversion));
2845
2846        // Positive shift should increase the trait
2847        assert!(later_extraversion > baseline_extraversion);
2848    }
2849
2850    #[test]
2851    fn formative_event_entity_without_birth_date() {
2852        // Test entity without birth date uses anchor age
2853        let mut sim = create_simulation();
2854        let reference = sim.reference_date();
2855
2856        // Entity without birth date (uses default age)
2857        let entity = EntityBuilder::new()
2858            .id("person_001")
2859            .species(Species::Human)
2860            .age(Duration::years(30))
2861            .build()
2862            .unwrap();
2863
2864        let entity_id = entity.id().clone();
2865        let anchor = reference;
2866        sim.add_entity(entity, anchor);
2867
2868        // Add formative event
2869        let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
2870            .target(entity_id.clone())
2871            .severity(0.8)
2872            .with_base_shift(HexacoPath::Openness, -0.10)
2873            .build()
2874            .unwrap();
2875        sim.add_event(event, anchor + Duration::days(1));
2876
2877        // Query after event (should work even without birth date)
2878        let later = anchor + Duration::days(2);
2879        let handle = sim.entity(&entity_id).unwrap();
2880        let state = handle.state_at(later);
2881
2882        // Just verify it doesn't panic and produces valid state
2883        let openness = state.get_effective(StatePath::Hexaco(HexacoPath::Openness));
2884        assert!(openness >= -1.0 && openness <= 1.0);
2885    }
2886
2887    #[test]
2888    fn formative_event_after_query_timestamp_ignored() {
2889        // Test that events after query timestamp are not included
2890        let mut sim = create_simulation();
2891        let reference = sim.reference_date();
2892
2893        let entity = EntityBuilder::new()
2894            .id("person_001")
2895            .species(Species::Human)
2896            .age(crate::types::Duration::years(30))
2897            .birth_date(reference - Duration::years(25))
2898            .build()
2899            .unwrap();
2900
2901        let entity_id = entity.id().clone();
2902        let anchor = reference;
2903        sim.add_entity(entity, anchor);
2904
2905        // Get baseline
2906        let handle = sim.entity(&entity_id).unwrap();
2907        let baseline_state = handle.state_at(anchor);
2908        let baseline_agreeableness =
2909            baseline_state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
2910
2911        // Add formative event in the future
2912        let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
2913            .target(entity_id.clone())
2914            .severity(0.9)
2915            .with_base_shift(HexacoPath::Agreeableness, -0.30)
2916            .build()
2917            .unwrap();
2918        sim.add_event(event, anchor + Duration::days(10));
2919
2920        // Query BEFORE the event
2921        let before_event = anchor + Duration::days(5);
2922        let handle = sim.entity(&entity_id).unwrap();
2923        let state = handle.state_at(before_event);
2924        let agreeableness = state.get_effective(StatePath::Hexaco(HexacoPath::Agreeableness));
2925
2926        // Should be close to baseline since event hasn't happened yet
2927        assert!((agreeableness - baseline_agreeableness).abs() < 0.01);
2928    }
2929
2930    #[test]
2931    fn collect_base_shifts_forward_query_with_events() {
2932        // Direct test of collect_base_shift_records for forward query
2933        let entity = EntityBuilder::new()
2934            .id("test")
2935            .species(Species::Human)
2936            .age(crate::types::Duration::years(30))
2937            .birth_date(Timestamp::from_ymd_hms(1999, 1, 1, 0, 0, 0))
2938            .build()
2939            .unwrap();
2940
2941        let records = collect_base_shift_records(
2942            &[],
2943            &entity,
2944            Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0),
2945            true, // forward query
2946        );
2947
2948        // Empty events list = empty records
2949        assert!(records.is_empty());
2950    }
2951
2952    #[test]
2953    fn apply_base_shifts_updates_all_traits() {
2954        use crate::state::BaseShiftRecord;
2955
2956        let state = IndividualState::new();
2957        let original_openness = state.hexaco().openness();
2958
2959        // Create a shift record directly
2960        let records = vec![BaseShiftRecord::new(
2961            Duration::days(1),
2962            HexacoPath::Openness,
2963            0.15,
2964        )];
2965
2966        let result = apply_base_shifts_to_state(
2967            state.clone(),
2968            &records,
2969            Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0),
2970        );
2971
2972        // Openness should have changed
2973        assert!((result.hexaco().openness() - original_openness).abs() > 0.01);
2974    }
2975
2976    #[test]
2977    fn apply_base_shifts_each_hexaco_trait() {
2978        use crate::state::BaseShiftRecord;
2979
2980        // Test each trait individually
2981        for trait_path in HexacoPath::all() {
2982            let state = IndividualState::new();
2983            let records = vec![BaseShiftRecord::new(Duration::days(1), trait_path, 0.10)];
2984
2985            let result = apply_base_shifts_to_state(
2986                state,
2987                &records,
2988                Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0),
2989            );
2990
2991            // Verify result is valid (no panic, valid state)
2992            let value = match trait_path {
2993                HexacoPath::Openness => result.hexaco().openness(),
2994                HexacoPath::Conscientiousness => result.hexaco().conscientiousness(),
2995                HexacoPath::Extraversion => result.hexaco().extraversion(),
2996                HexacoPath::Agreeableness => result.hexaco().agreeableness(),
2997                HexacoPath::Neuroticism => result.hexaco().neuroticism(),
2998                HexacoPath::HonestyHumility => result.hexaco().honesty_humility(),
2999            };
3000            assert!(value >= -1.0 && value <= 1.0);
3001        }
3002    }
3003
3004    #[test]
3005    fn collect_base_shifts_event_after_query_timestamp_skipped() {
3006        // Direct test: event with base shift AFTER query timestamp should be skipped (line 828)
3007        use crate::simulation::TimestampedEvent;
3008
3009        let entity = EntityBuilder::new()
3010            .id("test")
3011            .species(Species::Human)
3012            .age(crate::types::Duration::years(30))
3013            .birth_date(Timestamp::from_ymd_hms(1999, 1, 1, 0, 0, 0))
3014            .build()
3015            .unwrap();
3016
3017        // Create event with base shift at day 100
3018        let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
3019            .with_base_shift(HexacoPath::Agreeableness, -0.20)
3020            .build()
3021            .unwrap();
3022        let te = TimestampedEvent::new(event, Timestamp::from_ymd_hms(2024, 4, 10, 0, 0, 0));
3023
3024        // Query timestamp is BEFORE event timestamp
3025        let query_ts = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
3026        let records = collect_base_shift_records(&[&te], &entity, query_ts, true);
3027
3028        // Event after query should be skipped
3029        assert!(records.is_empty());
3030    }
3031
3032    #[test]
3033    fn collect_base_shifts_event_before_birth_date_age_zero() {
3034        // Direct test: event BEFORE entity birth date (line 836 - age = 0)
3035        use crate::simulation::TimestampedEvent;
3036
3037        // Entity born in 2010
3038        let entity = EntityBuilder::new()
3039            .id("test")
3040            .species(Species::Human)
3041            .age(crate::types::Duration::years(30))
3042            .birth_date(Timestamp::from_ymd_hms(2010, 1, 1, 0, 0, 0))
3043            .build()
3044            .unwrap();
3045
3046        // Event in 2005 - BEFORE entity was born
3047        let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
3048            .with_base_shift(HexacoPath::Neuroticism, 0.25)
3049            .build()
3050            .unwrap();
3051        let te = TimestampedEvent::new(event, Timestamp::from_ymd_hms(2005, 6, 1, 0, 0, 0));
3052
3053        // Query in 2020 (forward query)
3054        let query_ts = Timestamp::from_ymd_hms(2020, 1, 1, 0, 0, 0);
3055        let records = collect_base_shift_records(&[&te], &entity, query_ts, true);
3056
3057        // Event should still be processed with age 0 (high plasticity)
3058        assert_eq!(records.len(), 1);
3059    }
3060
3061    #[test]
3062    fn collect_base_shifts_entity_without_birth_date_uses_anchor_age() {
3063        // Direct test: entity without birth_date uses entity.age() (line 839)
3064        use crate::simulation::TimestampedEvent;
3065
3066        // Entity without birth_date, just has age
3067        let entity = EntityBuilder::new()
3068            .id("test")
3069            .species(Species::Human)
3070            .age(Duration::years(35))
3071            .build()
3072            .unwrap();
3073
3074        // Event with base shift
3075        let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
3076            .with_base_shift(HexacoPath::Conscientiousness, -0.15)
3077            .build()
3078            .unwrap();
3079        let te = TimestampedEvent::new(event, Timestamp::from_ymd_hms(2024, 1, 15, 0, 0, 0));
3080
3081        // Query after event
3082        let query_ts = Timestamp::from_ymd_hms(2024, 6, 1, 0, 0, 0);
3083        let records = collect_base_shift_records(&[&te], &entity, query_ts, true);
3084
3085        // Should have processed the event using entity.age() (35 years)
3086        assert_eq!(records.len(), 1);
3087    }
3088
3089    #[test]
3090    fn collect_base_shifts_event_before_1970_reference() {
3091        // Direct test: event timestamp before 1970 reference (line 846)
3092        use crate::simulation::TimestampedEvent;
3093
3094        // Entity born in 1940
3095        let entity = EntityBuilder::new()
3096            .id("test")
3097            .species(Species::Human)
3098            .age(crate::types::Duration::years(30))
3099            .birth_date(Timestamp::from_ymd_hms(1940, 1, 1, 0, 0, 0))
3100            .build()
3101            .unwrap();
3102
3103        // Event in 1960 - before 1970 reference timestamp
3104        let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
3105            .with_base_shift(HexacoPath::Extraversion, -0.20)
3106            .build()
3107            .unwrap();
3108        let te = TimestampedEvent::new(event, Timestamp::from_ymd_hms(1960, 6, 1, 0, 0, 0));
3109
3110        // Query in 2000
3111        let query_ts = Timestamp::from_ymd_hms(2000, 1, 1, 0, 0, 0);
3112        let records = collect_base_shift_records(&[&te], &entity, query_ts, true);
3113
3114        // Event before 1970 should still be processed (uses Duration::zero)
3115        assert_eq!(records.len(), 1);
3116    }
3117
3118    #[test]
3119    fn collect_base_shifts_tiny_shift_rounds_to_zero() {
3120        // Direct test: shift that becomes zero after modifiers (line 869)
3121        use crate::simulation::TimestampedEvent;
3122
3123        // Entity at age 80 (low plasticity: 0.6)
3124        // Extraversion has stability 0.85, so trait_modifier = 0.15
3125        // Very small input shift that after modifiers approaches zero
3126        let entity = EntityBuilder::new()
3127            .id("test")
3128            .species(Species::Human)
3129            .age(crate::types::Duration::years(30))
3130            .birth_date(Timestamp::from_ymd_hms(1940, 1, 1, 0, 0, 0))
3131            .build()
3132            .unwrap();
3133
3134        // Tiny shift: 1e-8 * 0.6 (age plasticity) * 0.15 (trait modifier) = ~9e-10
3135        // f32::EPSILON is ~1.19e-7, so result should be well below that
3136        let event = EventBuilder::new(EventType::ExperienceCombatMilitary)
3137            .with_base_shift(HexacoPath::Extraversion, 1e-8)
3138            .build()
3139            .unwrap();
3140        let te = TimestampedEvent::new(event, Timestamp::from_ymd_hms(2020, 6, 1, 0, 0, 0));
3141
3142        // Query after event
3143        let query_ts = Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0);
3144        let records = collect_base_shift_records(&[&te], &entity, query_ts, true);
3145
3146        // The modified shift should be near zero and skipped
3147        assert!(records.is_empty());
3148    }
3149
3150    #[test]
3151    fn collect_base_shifts_positive_cumulative_tracking() {
3152        // Direct test: positive shift updates cumulative_positive (line 877)
3153        use crate::simulation::TimestampedEvent;
3154
3155        // Young entity (high plasticity)
3156        let entity = EntityBuilder::new()
3157            .id("test")
3158            .species(Species::Human)
3159            .age(crate::types::Duration::years(30))
3160            .birth_date(Timestamp::from_ymd_hms(2000, 1, 1, 0, 0, 0))
3161            .build()
3162            .unwrap();
3163
3164        // POSITIVE shift (not negative) to hit line 877
3165        let event = EventBuilder::new(EventType::AchieveGoalMajor)
3166            .with_base_shift(HexacoPath::Agreeableness, 0.25)
3167            .build()
3168            .unwrap();
3169        let te = TimestampedEvent::new(event, Timestamp::from_ymd_hms(2024, 1, 15, 0, 0, 0));
3170
3171        let query_ts = Timestamp::from_ymd_hms(2024, 6, 1, 0, 0, 0);
3172        let records = collect_base_shift_records(&[&te], &entity, query_ts, true);
3173
3174        // Should have one positive shift record
3175        assert_eq!(records.len(), 1);
3176        assert!(records[0].immediate() > 0.0);
3177    }
3178
3179    #[test]
3180    fn collect_base_shifts_multiple_positive_shifts_diminishing() {
3181        // Test that multiple positive shifts get diminishing returns
3182        use crate::simulation::TimestampedEvent;
3183
3184        let entity = EntityBuilder::new()
3185            .id("test")
3186            .species(Species::Human)
3187            .age(crate::types::Duration::years(30))
3188            .birth_date(Timestamp::from_ymd_hms(2000, 1, 1, 0, 0, 0))
3189            .build()
3190            .unwrap();
3191
3192        // First positive shift
3193        let event1 = EventBuilder::new(EventType::AchieveGoalMajor)
3194            .with_base_shift(HexacoPath::Agreeableness, 0.30)
3195            .build()
3196            .unwrap();
3197        let te1 = TimestampedEvent::new(event1, Timestamp::from_ymd_hms(2024, 1, 15, 0, 0, 0));
3198
3199        // Second positive shift - should have diminishing returns
3200        let event2 = EventBuilder::new(EventType::AchieveGoalMajor)
3201            .with_base_shift(HexacoPath::Agreeableness, 0.30)
3202            .build()
3203            .unwrap();
3204        let te2 = TimestampedEvent::new(event2, Timestamp::from_ymd_hms(2024, 2, 15, 0, 0, 0));
3205
3206        let query_ts = Timestamp::from_ymd_hms(2024, 6, 1, 0, 0, 0);
3207        let records = collect_base_shift_records(&[&te1, &te2], &entity, query_ts, true);
3208
3209        // Both shifts should be recorded
3210        assert_eq!(records.len(), 2);
3211
3212        // Second shift should be smaller due to diminishing returns
3213        assert!(records[1].immediate() < records[0].immediate());
3214    }
3215
3216    #[test]
3217    fn apply_base_shifts_empty_returns_unchanged() {
3218        // Direct test for empty shift records (line 899-901)
3219        let state = IndividualState::new();
3220        let original_openness = state.hexaco().openness();
3221
3222        let result = apply_base_shifts_to_state(
3223            state,
3224            &[], // empty records
3225            Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0),
3226        );
3227
3228        assert!((result.hexaco().openness() - original_openness).abs() < f32::EPSILON);
3229    }
3230
3231    #[test]
3232    fn apply_base_shifts_query_before_1970() {
3233        // Direct test: query timestamp before 1970 (line 909)
3234        use crate::state::BaseShiftRecord;
3235
3236        let state = IndividualState::new();
3237
3238        let records = vec![BaseShiftRecord::new(
3239            Duration::days(1),
3240            HexacoPath::Openness,
3241            0.15,
3242        )];
3243
3244        // Query timestamp before 1970
3245        let result = apply_base_shifts_to_state(
3246            state,
3247            &records,
3248            Timestamp::from_ymd_hms(1950, 1, 1, 0, 0, 0),
3249        );
3250
3251        // Should still work (uses Duration::zero for query)
3252        assert!(result.hexaco().openness() >= -1.0 && result.hexaco().openness() <= 1.0);
3253    }
3254}