Skip to main content

behaviorsim_rs/entity/
entity_builder.rs

1//! Entity builder for fluent construction.
2//!
3//! The EntityBuilder provides a fluent API for constructing entities
4//! with proper validation. Species is required; other fields have defaults.
5
6use crate::context::EcologicalContext;
7use crate::enums::{LifeStage, PersonalityProfile, Species};
8use crate::state::{
9    Disposition, Hexaco, IndividualState, MentalHealth, Mood, Needs, PersonCharacteristics,
10    SocialCognition,
11};
12// Note: Mood::from_personality is used below to derive baseline affect from HEXACO
13use crate::types::{Duration, EntityId, Timestamp};
14
15use super::Entity;
16
17/// Error type for entity build failures.
18///
19/// This error is returned when `EntityBuilder::build()` fails validation.
20///
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub enum EntityBuildError {
23    /// Species is required but was not set.
24    MissingSpecies,
25
26    /// Age is required but was not set.
27    MissingAge,
28
29    /// The entity ID is invalid (empty string).
30    InvalidId(String),
31}
32
33impl std::fmt::Display for EntityBuildError {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        match self {
36            EntityBuildError::MissingSpecies => write!(f, "Species is required but was not set"),
37            EntityBuildError::MissingAge => write!(f, "Age is required but was not set"),
38            EntityBuildError::InvalidId(reason) => write!(f, "Invalid entity ID: {}", reason),
39        }
40    }
41}
42
43impl std::error::Error for EntityBuildError {}
44
45/// Builder for constructing Entity instances.
46///
47/// The builder provides a fluent API for setting entity properties.
48/// Species and age are required; all other properties have sensible defaults.
49///
50/// # Required Fields
51///
52/// - `species` - Must be set before calling `build()`
53/// - `age` - Must be set before calling `build()`
54///
55/// # Optional Fields with Defaults
56///
57/// - `id` - Auto-generated UUID if not set
58/// - `life_stage` - Derived from age and species if not set
59/// - `personality` - Neutral HEXACO (Balanced profile) if not set
60/// - `person_characteristics` - Neutral (0.5, 0.5, 0.5) if not set
61///
62#[derive(Debug, Clone, Default)]
63pub struct EntityBuilder {
64    id: Option<String>,
65    species: Option<Species>,
66    age: Option<Duration>,
67    birth_date: Option<Timestamp>,
68    life_stage: Option<LifeStage>,
69    personality: Option<PersonalityProfile>,
70    hexaco: Option<Hexaco>,
71    person_characteristics: Option<PersonCharacteristics>,
72    mood: Option<Mood>,
73    needs: Option<Needs>,
74    mental_health: Option<MentalHealth>,
75    social_cognition: Option<SocialCognition>,
76    disposition: Option<Disposition>,
77    context: Option<EcologicalContext>,
78}
79
80impl EntityBuilder {
81    /// Creates a new entity builder with no fields set.
82    ///
83    #[must_use]
84    pub fn new() -> Self {
85        EntityBuilder::default()
86    }
87
88    /// Sets the entity ID.
89    ///
90    /// If not set, a UUID will be generated.
91    ///
92    #[must_use]
93    pub fn id(mut self, id: impl Into<String>) -> Self {
94        self.id = Some(id.into());
95        self
96    }
97
98    /// Sets the species (required).
99    ///
100    /// Species determines time scaling, lifespan, and which subsystems
101    /// are active.
102    ///
103    #[must_use]
104    pub fn species(mut self, species: Species) -> Self {
105        self.species = Some(species);
106        self
107    }
108
109    /// Sets the entity's age.
110    ///
111    /// Age affects life stage determination if life_stage is not
112    /// explicitly set.
113    ///
114    #[must_use]
115    pub fn age(mut self, age: Duration) -> Self {
116        self.age = Some(age);
117        self
118    }
119
120    /// Sets the entity's birth date.
121    ///
122    /// When set, age at any timestamp can be computed as:
123    /// `query_timestamp - birth_date`
124    ///
125    /// Note: If both `age()` and `birth_date()` are set, the explicit `age()`
126    /// is used as the anchor age. The birth_date is stored separately for
127    /// timestamp-based age computation.
128    ///
129    #[must_use]
130    pub fn birth_date(mut self, birth_date: Timestamp) -> Self {
131        self.birth_date = Some(birth_date);
132        self
133    }
134
135    /// Sets the life stage explicitly.
136    ///
137    /// If not set, the life stage is derived from age and species.
138    ///
139    #[must_use]
140    pub fn life_stage(mut self, stage: LifeStage) -> Self {
141        self.life_stage = Some(stage);
142        self
143    }
144
145    /// Sets personality using a preset profile.
146    ///
147    /// This sets HEXACO values based on the profile. If both
148    /// `personality()` and `hexaco()` are called, the later call wins.
149    ///
150    #[must_use]
151    pub fn personality(mut self, profile: PersonalityProfile) -> Self {
152        self.personality = Some(profile);
153        self.hexaco = None; // Profile overrides raw HEXACO
154        self
155    }
156
157    /// Sets HEXACO personality values directly.
158    ///
159    /// This allows fine-grained control over personality dimensions.
160    /// If both `personality()` and `hexaco()` are called, the later call wins.
161    ///
162    #[must_use]
163    pub fn hexaco(mut self, hexaco: Hexaco) -> Self {
164        self.hexaco = Some(hexaco);
165        self.personality = None; // Raw HEXACO overrides profile
166        self
167    }
168
169    /// Sets person characteristics (PPCT model).
170    ///
171    /// Person characteristics include demand, resource, and force factors
172    /// that influence proximal processes.
173    ///
174    #[must_use]
175    pub fn person_characteristics(mut self, pc: PersonCharacteristics) -> Self {
176        self.person_characteristics = Some(pc);
177        self
178    }
179
180    /// Sets the initial mood state.
181    ///
182    /// Mood contains the PAD (Pleasure-Arousal-Dominance) dimensions.
183    ///
184    #[must_use]
185    pub fn mood(mut self, mood: Mood) -> Self {
186        self.mood = Some(mood);
187        self
188    }
189
190    /// Sets the initial needs state.
191    ///
192    /// Needs includes fatigue, stress, purpose, and other physiological/psychological needs.
193    ///
194    #[must_use]
195    pub fn needs(mut self, needs: Needs) -> Self {
196        self.needs = Some(needs);
197        self
198    }
199
200    /// Sets the initial mental health state.
201    ///
202    /// Mental health includes ITS factors (depression, hopelessness,
203    /// acquired capability, etc.).
204    ///
205    #[must_use]
206    pub fn mental_health(mut self, mental_health: MentalHealth) -> Self {
207        self.mental_health = Some(mental_health);
208        self
209    }
210
211    /// Sets the initial social cognition state.
212    ///
213    /// Social cognition includes loneliness, perceived caring/liability,
214    /// self-hate, and other interpersonal beliefs.
215    ///
216    #[must_use]
217    pub fn social_cognition(mut self, social_cognition: SocialCognition) -> Self {
218        self.social_cognition = Some(social_cognition);
219        self
220    }
221
222    /// Sets the initial disposition state.
223    ///
224    /// Disposition includes behavioral tendencies like empathy, aggression,
225    /// grievance, and prosocial behavior.
226    ///
227    #[must_use]
228    pub fn disposition(mut self, disposition: Disposition) -> Self {
229        self.disposition = Some(disposition);
230        self
231    }
232
233    /// Sets the initial ecological context.
234    ///
235    /// Allows pre-populating the entity's ecological context with
236    /// microsystems, exosystem, macrosystem, and chronosystem values.
237    ///
238    #[must_use]
239    pub fn with_context(mut self, context: EcologicalContext) -> Self {
240        self.context = Some(context);
241        self
242    }
243
244    /// Builds the entity.
245    ///
246    /// # Errors
247    ///
248    /// Returns `EntityBuildError::MissingSpecies` if species was not set.
249    /// Returns `EntityBuildError::MissingAge` if age was not set.
250    /// Returns `EntityBuildError::InvalidId` if the ID is empty.
251    ///
252    pub fn build(self) -> Result<Entity, EntityBuildError> {
253        // Validate required fields
254        let species = self.species.ok_or(EntityBuildError::MissingSpecies)?;
255        let age = self.age.ok_or(EntityBuildError::MissingAge)?;
256
257        // Generate or validate ID
258        let id_string = self.id.unwrap_or_else(generate_uuid);
259        let id = match EntityId::new(id_string) {
260            Ok(id) => id,
261            Err(err) => return Err(EntityBuildError::InvalidId(err.reason)),
262        };
263
264        // Get birth_date (optional)
265        let birth_date = self.birth_date;
266
267        // Determine life stage: explicit > derived from age
268        let life_stage = self
269            .life_stage
270            .unwrap_or_else(|| LifeStage::from_age_years_for_species(&species, age.as_years_f64()));
271
272        // Build HEXACO: explicit hexaco > profile > default
273        let hexaco = if let Some(h) = self.hexaco {
274            h
275        } else if let Some(profile) = self.personality {
276            Hexaco::from_profile(profile)
277        } else {
278            Hexaco::from_profile(PersonalityProfile::Balanced)
279        };
280
281        // Build person characteristics
282        let person_characteristics = self.person_characteristics.unwrap_or_default();
283
284        // Build individual state with required components
285        let mut individual_state = IndividualState::new()
286            .with_hexaco(hexaco.clone())
287            .with_person_characteristics(person_characteristics);
288
289        // Apply mood: explicit mood overrides, otherwise derive from personality
290        if let Some(mood) = self.mood {
291            individual_state = individual_state.with_mood(mood);
292        } else {
293            // Derive baseline affect from personality traits
294            individual_state = individual_state.with_mood(Mood::from_personality(&hexaco));
295        }
296        if let Some(needs) = self.needs {
297            individual_state = individual_state.with_needs(needs);
298        }
299        if let Some(mental_health) = self.mental_health {
300            individual_state = individual_state.with_mental_health(mental_health);
301        }
302        if let Some(social_cognition) = self.social_cognition {
303            individual_state = individual_state.with_social_cognition(social_cognition);
304        }
305        if let Some(disposition) = self.disposition {
306            individual_state = individual_state.with_disposition(disposition);
307        }
308
309        // Build entity with or without custom context
310        if let Some(context) = self.context {
311            Ok(Entity::new_with_context(
312                id,
313                species,
314                age,
315                birth_date,
316                life_stage,
317                individual_state,
318                context,
319            ))
320        } else {
321            Ok(Entity::new(
322                id,
323                species,
324                age,
325                birth_date,
326                life_stage,
327                individual_state,
328            ))
329        }
330    }
331}
332
333/// Generates a UUID-like unique identifier.
334///
335/// This uses a simple counter-based approach for determinism in tests.
336/// In production, this could be replaced with actual UUID generation.
337fn generate_uuid() -> String {
338    use std::sync::atomic::{AtomicU64, Ordering};
339    static COUNTER: AtomicU64 = AtomicU64::new(1);
340    let count = COUNTER.fetch_add(1, Ordering::Relaxed);
341    format!("entity_{:016x}", count)
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn builder_sets_species() {
350        let entity = EntityBuilder::new()
351            .species(Species::Human)
352            .age(crate::types::Duration::years(30))
353            .build()
354            .unwrap();
355
356        assert_eq!(entity.species(), &Species::Human);
357    }
358
359    #[test]
360    fn builder_sets_life_stage() {
361        let entity = EntityBuilder::new()
362            .species(Species::Human)
363            .age(crate::types::Duration::years(30))
364            .life_stage(LifeStage::Adult)
365            .build()
366            .unwrap();
367
368        assert_eq!(entity.life_stage(), LifeStage::Adult);
369    }
370
371    #[test]
372    fn builder_sets_personality_profile() {
373        let entity = EntityBuilder::new()
374            .species(Species::Human)
375            .age(crate::types::Duration::years(30))
376            .personality(PersonalityProfile::Leader)
377            .build()
378            .unwrap();
379
380        // Leader has high extraversion
381        assert!(entity.individual_state().hexaco().extraversion() > 0.5);
382    }
383
384    #[test]
385    fn builder_sets_person_characteristics() {
386        let pc = PersonCharacteristics::new().with_cognitive_ability_base(0.9);
387
388        let entity = EntityBuilder::new()
389            .species(Species::Human)
390            .age(crate::types::Duration::years(30))
391            .person_characteristics(pc)
392            .build()
393            .unwrap();
394
395        assert!(
396            (entity
397                .individual_state()
398                .person_characteristics()
399                .cognitive_ability()
400                .base()
401                - 0.9)
402                .abs()
403                < f32::EPSILON
404        );
405    }
406
407    #[test]
408    fn builder_produces_entity() {
409        let result = EntityBuilder::new().species(Species::Human).age(crate::types::Duration::years(30)).build();
410
411        assert!(result.is_ok());
412        let entity = result.unwrap();
413        assert_eq!(entity.species(), &Species::Human);
414    }
415
416    #[test]
417    fn builder_requires_species() {
418        let result = EntityBuilder::new().build();
419
420        assert_eq!(result.unwrap_err(), EntityBuildError::MissingSpecies);
421    }
422
423    #[test]
424    fn builder_defaults_life_stage() {
425        // Age 30 -> YoungAdult for human (18-30 range)
426        let entity = EntityBuilder::new()
427            .species(Species::Human)
428            .age(Duration::years(30))
429            .build()
430            .unwrap();
431
432        assert_eq!(entity.life_stage(), LifeStage::YoungAdult);
433
434        // Age 40 -> Adult for human (31-55 range)
435        let adult = EntityBuilder::new()
436            .species(Species::Human)
437            .age(Duration::years(40))
438            .build()
439            .unwrap();
440
441        assert_eq!(adult.life_stage(), LifeStage::Adult);
442
443        // Age 8 -> Child for human
444        let child = EntityBuilder::new()
445            .species(Species::Human)
446            .age(Duration::years(8))
447            .build()
448            .unwrap();
449
450        assert_eq!(child.life_stage(), LifeStage::Child);
451    }
452
453    #[test]
454    fn builder_sets_id() {
455        let entity = EntityBuilder::new()
456            .id("test_entity")
457            .species(Species::Human)
458            .age(crate::types::Duration::years(30))
459            .build()
460            .unwrap();
461
462        assert_eq!(entity.id().as_str(), "test_entity");
463    }
464
465    #[test]
466    fn builder_generates_id_if_not_set() {
467        let entity = EntityBuilder::new()
468            .species(Species::Human)
469            .age(crate::types::Duration::years(30))
470            .build()
471            .unwrap();
472
473        assert!(!entity.id().as_str().is_empty());
474        assert!(entity.id().as_str().starts_with("entity_"));
475    }
476
477    #[test]
478    fn builder_sets_age() {
479        let entity = EntityBuilder::new()
480            .species(Species::Human)
481            .age(Duration::years(25))
482            .build()
483            .unwrap();
484
485        assert_eq!(entity.age().as_years(), 25);
486    }
487
488    #[test]
489    fn builder_requires_age() {
490        let result = EntityBuilder::new().species(Species::Human).build();
491
492        assert_eq!(result.unwrap_err(), EntityBuildError::MissingAge);
493    }
494
495    #[test]
496    fn builder_sets_hexaco_directly() {
497        let hexaco = Hexaco::new().with_openness(0.9).with_neuroticism(-0.8);
498
499        let entity = EntityBuilder::new()
500            .species(Species::Human)
501            .age(crate::types::Duration::years(30))
502            .hexaco(hexaco)
503            .build()
504            .unwrap();
505
506        assert!((entity.individual_state().hexaco().openness() - 0.9).abs() < f32::EPSILON);
507        assert!((entity.individual_state().hexaco().neuroticism() - (-0.8)).abs() < f32::EPSILON);
508    }
509
510    #[test]
511    fn hexaco_overrides_personality() {
512        let hexaco = Hexaco::uniform(0.3);
513
514        let entity = EntityBuilder::new()
515            .species(Species::Human)
516            .age(crate::types::Duration::years(30))
517            .personality(PersonalityProfile::Leader) // Would set high extraversion
518            .hexaco(hexaco) // Overrides to 0.3
519            .build()
520            .unwrap();
521
522        assert!((entity.individual_state().hexaco().extraversion() - 0.3).abs() < f32::EPSILON);
523    }
524
525    #[test]
526    fn personality_overrides_hexaco() {
527        let hexaco = Hexaco::uniform(0.3);
528
529        let entity = EntityBuilder::new()
530            .species(Species::Human)
531            .age(crate::types::Duration::years(30))
532            .hexaco(hexaco)
533            .personality(PersonalityProfile::Leader) // Overrides
534            .build()
535            .unwrap();
536
537        // Leader has high extraversion (0.8 -> 0.6 in -1 to 1 range)
538        assert!(entity.individual_state().hexaco().extraversion() > 0.5);
539    }
540
541    #[test]
542    fn builder_clone() {
543        let builder = EntityBuilder::new()
544            .species(Species::Human)
545            .age(Duration::years(30));
546
547        let cloned = builder.clone();
548
549        let entity1 = builder.build().unwrap();
550        let entity2 = cloned.build().unwrap();
551
552        assert_eq!(entity1.species(), entity2.species());
553        assert_eq!(entity1.age(), entity2.age());
554    }
555
556    #[test]
557    fn empty_id_returns_error() {
558        let result = EntityBuilder::new().id("").species(Species::Human).age(crate::types::Duration::years(30)).build();
559
560        assert_eq!(
561            result.unwrap_err(),
562            EntityBuildError::InvalidId("ID cannot be empty".to_string())
563        );
564    }
565
566    #[test]
567    fn builder_debug() {
568        let builder = EntityBuilder::new().species(Species::Human);
569        let debug = format!("{:?}", builder);
570        assert!(debug.contains("EntityBuilder"));
571    }
572
573    #[test]
574    fn error_display() {
575        let err = EntityBuildError::MissingSpecies;
576        let display = format!("{}", err);
577        assert!(display.contains("Species"));
578
579        let err_age = EntityBuildError::MissingAge;
580        let display_age = format!("{}", err_age);
581        assert!(display_age.contains("Age"));
582
583        let err2 = EntityBuildError::InvalidId("test reason".to_string());
584        let display2 = format!("{}", err2);
585        assert!(display2.contains("test reason"));
586    }
587
588    #[test]
589    fn error_debug() {
590        let err = EntityBuildError::MissingSpecies;
591        let debug = format!("{:?}", err);
592        assert!(debug.contains("MissingSpecies"));
593
594        let err_age = EntityBuildError::MissingAge;
595        let debug_age = format!("{:?}", err_age);
596        assert!(debug_age.contains("MissingAge"));
597    }
598
599    #[test]
600    fn error_is_std_error() {
601        use std::error::Error;
602
603        let err: &dyn Error = &EntityBuildError::MissingSpecies;
604        // Verify it's a valid std::error::Error
605        assert!(err.source().is_none());
606
607        let err_age: &dyn Error = &EntityBuildError::MissingAge;
608        assert!(err_age.source().is_none());
609
610        let err2: &dyn Error = &EntityBuildError::InvalidId("test".to_string());
611        assert!(err2.source().is_none());
612    }
613
614    #[test]
615    fn builder_new() {
616        let builder = EntityBuilder::new();
617        let debug = format!("{:?}", builder);
618        assert!(debug.contains("EntityBuilder"));
619    }
620
621    #[test]
622    fn dog_age_derives_life_stage() {
623        // 2-year-old dog should be YoungAdult
624        let dog = EntityBuilder::new()
625            .species(Species::Dog)
626            .age(Duration::years(2))
627            .build()
628            .unwrap();
629
630        assert_eq!(dog.life_stage(), LifeStage::YoungAdult);
631    }
632
633    #[test]
634    fn explicit_life_stage_overrides_age_derived() {
635        let entity = EntityBuilder::new()
636            .species(Species::Human)
637            .age(Duration::years(30)) // Would be Adult
638            .life_stage(LifeStage::Elder) // Override to Elder
639            .build()
640            .unwrap();
641
642        assert_eq!(entity.life_stage(), LifeStage::Elder);
643    }
644
645    #[test]
646    fn default_personality_is_balanced() {
647        let entity = EntityBuilder::new()
648            .species(Species::Human)
649            .age(crate::types::Duration::years(30))
650            .build()
651            .unwrap();
652
653        // Balanced profile has 0.5 for all, which maps to 0.0 in -1 to 1 range
654        assert!((entity.individual_state().hexaco().openness() - 0.0).abs() < 0.01);
655    }
656
657    #[test]
658    fn default_person_characteristics_are_neutral() {
659        let entity = EntityBuilder::new()
660            .species(Species::Human)
661            .age(crate::types::Duration::years(30))
662            .build()
663            .unwrap();
664
665        // Default PC has neutral values (around 0.5)
666        let pc = entity.individual_state().person_characteristics();
667        assert!(pc.resource() >= 0.3 && pc.resource() <= 0.7);
668        assert!(pc.force() >= 0.3 && pc.force() <= 0.7);
669    }
670
671    #[test]
672    fn builder_with_context() {
673        use crate::context::{Microsystem, WorkContext};
674        use crate::types::MicrosystemId;
675
676        let mut context = EcologicalContext::default();
677        let work_id = MicrosystemId::new("work_primary").unwrap();
678        context.add_microsystem(work_id, Microsystem::new_work(WorkContext::default()));
679
680        let entity = EntityBuilder::new()
681            .species(Species::Human)
682            .age(crate::types::Duration::years(30))
683            .with_context(context)
684            .build()
685            .unwrap();
686
687        assert_eq!(entity.context().microsystem_count(), 1);
688    }
689
690    #[test]
691    fn builder_without_context_uses_default() {
692        let entity = EntityBuilder::new()
693            .species(Species::Human)
694            .age(crate::types::Duration::years(30))
695            .build()
696            .unwrap();
697
698        // Default context has no microsystems
699        assert_eq!(entity.context().microsystem_count(), 0);
700    }
701
702    #[test]
703    fn builder_sets_birth_date() {
704        let birth = Timestamp::from_ymd_hms(1990, 6, 15, 0, 0, 0);
705        let entity = EntityBuilder::new()
706            .id("person_001")
707            .species(Species::Human)
708            .age(crate::types::Duration::years(30))
709            .birth_date(birth)
710            .build()
711            .unwrap();
712
713        assert_eq!(entity.birth_date(), Some(birth));
714    }
715
716    #[test]
717    fn builder_without_birth_date_returns_none() {
718        let entity = EntityBuilder::new()
719            .species(Species::Human)
720            .age(crate::types::Duration::years(30))
721            .build()
722            .unwrap();
723
724        assert!(entity.birth_date().is_none());
725    }
726
727    #[test]
728    fn builder_birth_date_can_be_used_with_age() {
729        let birth = Timestamp::from_ymd_hms(1990, 6, 15, 0, 0, 0);
730        let entity = EntityBuilder::new()
731            .species(Species::Human)
732            .birth_date(birth)
733            .age(Duration::years(30))
734            .build()
735            .unwrap();
736
737        // Both should be set independently
738        assert_eq!(entity.birth_date(), Some(birth));
739        assert_eq!(entity.age().as_years(), 30);
740    }
741
742    #[test]
743    fn builder_sets_mood() {
744        use crate::state::Mood;
745
746        let mood = Mood::new().with_valence_base(0.6).with_arousal_base(-0.3);
747
748        let entity = EntityBuilder::new()
749            .species(Species::Human)
750            .age(crate::types::Duration::years(30))
751            .mood(mood.clone())
752            .build()
753            .unwrap();
754
755        assert_eq!(entity.individual_state().mood(), &mood);
756    }
757
758    #[test]
759    fn builder_sets_needs() {
760        use crate::state::Needs;
761
762        let needs = Needs::new().with_fatigue_base(0.4).with_stress_base(0.2);
763
764        let entity = EntityBuilder::new()
765            .species(Species::Human)
766            .age(crate::types::Duration::years(30))
767            .needs(needs.clone())
768            .build()
769            .unwrap();
770
771        assert_eq!(entity.individual_state().needs(), &needs);
772    }
773
774    #[test]
775    fn builder_sets_mental_health() {
776        use crate::state::MentalHealth;
777
778        let mh = MentalHealth::new()
779            .with_depression_base(0.15)
780            .with_hopelessness_base(0.1);
781
782        let entity = EntityBuilder::new()
783            .species(Species::Human)
784            .age(crate::types::Duration::years(30))
785            .mental_health(mh.clone())
786            .build()
787            .unwrap();
788
789        assert_eq!(entity.individual_state().mental_health(), &mh);
790    }
791
792    #[test]
793    fn builder_sets_social_cognition() {
794        use crate::state::SocialCognition;
795
796        let sc = SocialCognition::new()
797            .with_loneliness_base(0.25)
798            .with_perceived_reciprocal_caring_base(0.7);
799
800        let entity = EntityBuilder::new()
801            .species(Species::Human)
802            .age(crate::types::Duration::years(30))
803            .social_cognition(sc.clone())
804            .build()
805            .unwrap();
806
807        assert_eq!(entity.individual_state().social_cognition(), &sc);
808    }
809
810    #[test]
811    fn builder_sets_disposition() {
812        use crate::state::Disposition;
813
814        let disp = Disposition::new()
815            .with_empathy_base(0.8)
816            .with_impulse_control_base(0.7);
817
818        let entity = EntityBuilder::new()
819            .species(Species::Human)
820            .age(crate::types::Duration::years(30))
821            .disposition(disp.clone())
822            .build()
823            .unwrap();
824
825        assert_eq!(entity.individual_state().disposition(), &disp);
826    }
827
828    #[test]
829    fn builder_without_optional_state_uses_defaults() {
830        use crate::state::{Disposition, MentalHealth, Needs, SocialCognition};
831
832        let entity = EntityBuilder::new()
833            .species(Species::Human)
834            .age(crate::types::Duration::years(30))
835            .build()
836            .unwrap();
837
838        // Mood is derived from personality (not default) - see builder_derives_mood_from_personality test
839        // Other components should be default
840        assert_eq!(entity.individual_state().needs(), &Needs::default());
841        assert_eq!(
842            entity.individual_state().mental_health(),
843            &MentalHealth::default()
844        );
845        assert_eq!(
846            entity.individual_state().social_cognition(),
847            &SocialCognition::default()
848        );
849        assert_eq!(
850            entity.individual_state().disposition(),
851            &Disposition::default()
852        );
853    }
854
855    #[test]
856    fn builder_derives_mood_from_personality() {
857        // Extraverted personality should produce positive baseline valence
858        let entity = EntityBuilder::new()
859            .species(Species::Human)
860            .age(crate::types::Duration::years(30))
861            .personality(PersonalityProfile::Leader) // Leaders are extraverted
862            .build()
863            .unwrap();
864
865        // Leader profile has high extraversion, so baseline valence should be positive
866        let valence = entity.individual_state().mood().valence_base();
867        assert!(valence > 0.0);
868    }
869
870    #[test]
871    fn explicit_mood_overrides_personality_derived() {
872        use crate::state::Mood;
873
874        // Set explicit mood that differs from what personality would derive
875        let explicit_mood = Mood::new()
876            .with_valence_base(-0.5) // Negative valence despite personality
877            .with_arousal_base(0.3);
878
879        let entity = EntityBuilder::new()
880            .species(Species::Human)
881            .age(crate::types::Duration::years(30))
882            .personality(PersonalityProfile::Leader) // Would derive positive valence
883            .mood(explicit_mood.clone()) // Override with explicit negative
884            .build()
885            .unwrap();
886
887        // Explicit mood should override personality-derived mood
888        assert_eq!(entity.individual_state().mood(), &explicit_mood);
889        assert!((entity.individual_state().mood().valence_base() - (-0.5)).abs() < f32::EPSILON);
890    }
891
892    #[test]
893    fn builder_sets_all_state_components_together() {
894        use crate::state::{Disposition, MentalHealth, Mood, Needs, SocialCognition};
895
896        let mood = Mood::new().with_valence_base(0.5);
897        let needs = Needs::new().with_purpose_base(0.8);
898        let mh = MentalHealth::new().with_depression_base(0.1);
899        let sc = SocialCognition::new().with_loneliness_base(0.2);
900        let disp = Disposition::new().with_empathy_base(0.9);
901
902        let entity = EntityBuilder::new()
903            .species(Species::Human)
904            .age(crate::types::Duration::years(30))
905            .mood(mood.clone())
906            .needs(needs.clone())
907            .mental_health(mh.clone())
908            .social_cognition(sc.clone())
909            .disposition(disp.clone())
910            .build()
911            .unwrap();
912
913        assert_eq!(entity.individual_state().mood(), &mood);
914        assert_eq!(entity.individual_state().needs(), &needs);
915        assert_eq!(entity.individual_state().mental_health(), &mh);
916        assert_eq!(entity.individual_state().social_cognition(), &sc);
917        assert_eq!(entity.individual_state().disposition(), &disp);
918    }
919}