Skip to main content

behaviorsim_rs/context/
mesosystem.rs

1//! Mesosystem implementation for computed cross-context linkages.
2//!
3//! The mesosystem represents the interactions and connections between
4//! microsystems. Per Bronfenbrenner, mesosystem values are *linkages*
5//! between microsystems, not stored scalars.
6//!
7//! # Computed Values
8//!
9//! Mesosystem values are always computed from microsystem data:
10//! - Spillover effects (stress from one domain affecting another)
11//! - Role conflicts (competing demands from multiple roles)
12//!
13//! A cache is maintained for performance, invalidated per-simulation-step.
14//!
15//! # Proximal Process Gates
16//!
17//! Effects only apply when interaction frequency and complexity thresholds
18//! are met, per Bronfenbrenner's PPCT model.
19
20use crate::context::microsystem::Microsystem;
21use crate::types::MicrosystemId;
22use std::cell::{Cell, RefCell};
23use std::collections::HashMap;
24
25/// Default interaction frequency threshold for proximal processes.
26///
27/// Effects are blocked when frequency is below this threshold.
28pub const INTERACTION_FREQUENCY_THRESHOLD: f64 = 0.3;
29
30/// Default interaction complexity threshold for proximal processes.
31///
32/// Effects are blocked when complexity is below this threshold.
33pub const INTERACTION_COMPLEXITY_THRESHOLD: f64 = 0.3;
34
35/// Default reciprocity threshold for proximal processes.
36///
37/// Effects are blocked when reciprocity is below this threshold.
38pub const INTERACTION_RECIPROCITY_THRESHOLD: f64 = 0.6;
39
40/// Error returned when proximal process gate blocks an effect.
41#[derive(Debug, Clone, PartialEq)]
42pub enum ProximalProcessGateError {
43    /// Interaction frequency was below threshold.
44    FrequencyBelowThreshold {
45        /// Actual frequency value.
46        actual: f64,
47        /// Required threshold.
48        threshold: f64,
49    },
50    /// Interaction complexity was below threshold.
51    ComplexityBelowThreshold {
52        /// Actual complexity value.
53        actual: f64,
54        /// Required threshold.
55        threshold: f64,
56    },
57    /// Both frequency and complexity were below thresholds.
58    BothBelowThreshold {
59        /// Actual frequency value.
60        actual_frequency: f64,
61        /// Frequency threshold.
62        frequency_threshold: f64,
63        /// Actual complexity value.
64        actual_complexity: f64,
65        /// Complexity threshold.
66        complexity_threshold: f64,
67    },
68    /// Reciprocity was below threshold.
69    ReciprocityBelowThreshold {
70        /// Actual reciprocity value.
71        actual: f64,
72        /// Required threshold.
73        threshold: f64,
74    },
75}
76
77impl std::fmt::Display for ProximalProcessGateError {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        match self {
80            ProximalProcessGateError::FrequencyBelowThreshold { actual, threshold } => {
81                write!(
82                    f,
83                    "Interaction frequency {} is below threshold {}",
84                    actual, threshold
85                )
86            }
87            ProximalProcessGateError::ComplexityBelowThreshold { actual, threshold } => {
88                write!(
89                    f,
90                    "Interaction complexity {} is below threshold {}",
91                    actual, threshold
92                )
93            }
94            ProximalProcessGateError::BothBelowThreshold {
95                actual_frequency,
96                frequency_threshold,
97                actual_complexity,
98                complexity_threshold,
99            } => {
100                write!(
101                    f,
102                    "Interaction frequency {} is below threshold {} and complexity {} is below threshold {}",
103                    actual_frequency, frequency_threshold, actual_complexity, complexity_threshold
104                )
105            }
106            ProximalProcessGateError::ReciprocityBelowThreshold { actual, threshold } => {
107                write!(
108                    f,
109                    "Interaction reciprocity {} is below threshold {}",
110                    actual, threshold
111                )
112            }
113        }
114    }
115}
116
117impl std::error::Error for ProximalProcessGateError {}
118
119/// Checks whether proximal process thresholds are met.
120///
121/// # Arguments
122///
123/// * `frequency` - Interaction frequency (0-1)
124/// * `complexity` - Interaction complexity (0-1)
125/// * `frequency_threshold` - Minimum frequency required
126/// * `complexity_threshold` - Minimum complexity required
127///
128/// # Returns
129///
130/// `Ok(())` if both thresholds are met, `Err(ProximalProcessGateError)` otherwise.
131pub fn passes_proximal_process_gate(
132    frequency: f64,
133    complexity: f64,
134    frequency_threshold: f64,
135    complexity_threshold: f64,
136) -> Result<(), ProximalProcessGateError> {
137    let freq_ok = frequency >= frequency_threshold;
138    let complex_ok = complexity >= complexity_threshold;
139
140    if !freq_ok && !complex_ok {
141        Err(ProximalProcessGateError::BothBelowThreshold {
142            actual_frequency: frequency,
143            frequency_threshold,
144            actual_complexity: complexity,
145            complexity_threshold,
146        })
147    } else if !freq_ok {
148        Err(ProximalProcessGateError::FrequencyBelowThreshold {
149            actual: frequency,
150            threshold: frequency_threshold,
151        })
152    } else if !complex_ok {
153        Err(ProximalProcessGateError::ComplexityBelowThreshold {
154            actual: complexity,
155            threshold: complexity_threshold,
156        })
157    } else {
158        Ok(())
159    }
160}
161
162/// Checks whether proximal process thresholds are met, including reciprocity.
163///
164/// # Arguments
165///
166/// * `frequency` - Interaction frequency (0-1)
167/// * `complexity` - Interaction complexity (0-1)
168/// * `reciprocity` - Reciprocity balance (0-1)
169/// * `frequency_threshold` - Minimum frequency required
170/// * `complexity_threshold` - Minimum complexity required
171/// * `reciprocity_threshold` - Minimum reciprocity required
172///
173/// # Returns
174///
175/// `Ok(())` if all thresholds are met, `Err(ProximalProcessGateError)` otherwise.
176pub fn passes_proximal_process_gate_with_reciprocity(
177    frequency: f64,
178    complexity: f64,
179    reciprocity: f64,
180    frequency_threshold: f64,
181    complexity_threshold: f64,
182    reciprocity_threshold: f64,
183) -> Result<(), ProximalProcessGateError> {
184    passes_proximal_process_gate(frequency, complexity, frequency_threshold, complexity_threshold)?;
185
186    if reciprocity < reciprocity_threshold {
187        return Err(ProximalProcessGateError::ReciprocityBelowThreshold {
188            actual: reciprocity,
189            threshold: reciprocity_threshold,
190        });
191    }
192
193    Ok(())
194}
195
196/// Represents a linkage between two microsystems.
197#[derive(Debug, Clone, PartialEq)]
198pub struct MesosystemLinkage {
199    /// Source microsystem ID.
200    pub from: MicrosystemId,
201
202    /// Target microsystem ID.
203    pub to: MicrosystemId,
204
205    /// Spillover coefficient (0-1).
206    pub spillover: f64,
207
208    /// Role conflict score (0-1).
209    pub role_conflict: f64,
210}
211
212/// Persisted mesosystem state computed from microsystems.
213#[derive(Debug, Clone, PartialEq)]
214pub struct MesosystemState {
215    /// Competing demands between work and family contexts.
216    pub work_family_conflict: f64,
217    /// How family supports social engagement.
218    pub family_social_support: f64,
219    /// Identity mismatch between work and social groups.
220    pub work_social_conflict: f64,
221    /// Alignment of role expectations across contexts.
222    pub value_alignment_consistency: f64,
223    /// Alignment of autonomy norms across contexts.
224    pub autonomy_norm_consistency: f64,
225    /// Aggregate mesosystem consistency score.
226    pub mesosystem_consistency: f64,
227    /// Overlap of actors across microsystems.
228    pub shared_membership_strength: f64,
229    /// Whether microsystem developmental effects should apply.
230    pub microsystem_influence_weight: f64,
231}
232
233impl Default for MesosystemState {
234    fn default() -> Self {
235        MesosystemState {
236            work_family_conflict: 0.0,
237            family_social_support: 0.0,
238            work_social_conflict: 0.0,
239            value_alignment_consistency: 1.0,
240            autonomy_norm_consistency: 1.0,
241            mesosystem_consistency: 1.0,
242            shared_membership_strength: 0.0,
243            microsystem_influence_weight: 0.0,
244        }
245    }
246}
247
248impl MesosystemState {
249    /// Computes mesosystem state from the current microsystems.
250    #[must_use]
251    pub fn compute(microsystems: &HashMap<MicrosystemId, Microsystem>) -> Self {
252        let mut work_ids = Vec::new();
253        let mut family_ids = Vec::new();
254
255        let mut work_role_clarity = Vec::new();
256        let mut family_role_clarity = Vec::new();
257        let mut social_predictability = Vec::new();
258
259        let mut work_predictability = Vec::new();
260        let mut family_predictability = Vec::new();
261
262        let mut family_support = Vec::new();
263        let mut work_low_predictability = false;
264        let mut social_high_warmth = false;
265        let mut total_frequency = 0.0;
266        let mut total_complexity = 0.0;
267        let mut count = 0.0;
268
269        for (id, micro) in microsystems {
270            total_frequency += micro.interaction_frequency();
271            total_complexity += micro.interaction_complexity();
272            count += 1.0;
273            if let Some(work) = micro.work() {
274                work_ids.push(id.clone());
275                work_role_clarity.push(work.role_clarity);
276                work_predictability.push(work.predictability);
277                if work.predictability < 0.4 {
278                    work_low_predictability = true;
279                }
280            }
281
282            if let Some(family) = micro.family() {
283                family_ids.push(id.clone());
284                family_role_clarity.push(family.role_clarity);
285                family_predictability.push(family.predictability);
286                family_support.push((family.warmth - family.hostility).max(0.0));
287            }
288
289            if let Some(social) = micro.social() {
290                social_predictability.push(social.predictability);
291                if social.warmth > 0.7 {
292                    social_high_warmth = true;
293                }
294            }
295        }
296
297        let work_family_conflict = if work_ids.is_empty() || family_ids.is_empty() {
298            0.0
299        } else {
300            let cache = MesosystemCache::new();
301            let mut total = 0.0;
302            let count = work_ids.len() * family_ids.len();
303
304            for work_id in &work_ids {
305                for family_id in &family_ids {
306                    total += cache.get_role_conflict(work_id, family_id, microsystems);
307                }
308            }
309
310            total / count as f64
311        };
312
313        let family_social_support = average(&family_support).unwrap_or(0.0);
314        let work_social_conflict = if work_low_predictability && social_high_warmth {
315            0.3
316        } else {
317            0.0
318        };
319
320        let value_alignment_consistency = consistency_from_values(&[
321            average(&work_role_clarity),
322            average(&family_role_clarity),
323            average(&social_predictability),
324        ]);
325
326        let autonomy_norm_consistency = consistency_from_values(&[
327            average(&work_predictability),
328            average(&family_predictability),
329            average(&social_predictability),
330        ]);
331
332        let mesosystem_consistency = value_alignment_consistency;
333        let shared_membership_strength =
334            MesosystemCache::compute_shared_membership_strength(microsystems);
335        let (avg_frequency, avg_complexity) = if count > 0.0 {
336            (total_frequency / count, total_complexity / count)
337        } else {
338            (0.0, 0.0)
339        };
340        let microsystem_influence_weight = match passes_proximal_process_gate(
341            avg_frequency,
342            avg_complexity,
343            INTERACTION_FREQUENCY_THRESHOLD,
344            INTERACTION_COMPLEXITY_THRESHOLD,
345        ) {
346            Ok(()) => 1.0,
347            Err(_) => 0.0,
348        };
349
350        MesosystemState {
351            work_family_conflict,
352            family_social_support,
353            work_social_conflict,
354            value_alignment_consistency,
355            autonomy_norm_consistency,
356            mesosystem_consistency,
357            shared_membership_strength,
358            microsystem_influence_weight,
359        }
360    }
361}
362
363fn average(values: &[f64]) -> Option<f64> {
364    if values.is_empty() {
365        None
366    } else {
367        Some(values.iter().sum::<f64>() / values.len() as f64)
368    }
369}
370
371fn consistency_from_values(values: &[Option<f64>]) -> f64 {
372    let filtered: Vec<f64> = values.iter().copied().flatten().collect();
373    if filtered.len() < 2 {
374        return 1.0;
375    }
376
377    let mean = filtered.iter().sum::<f64>() / filtered.len() as f64;
378    let variance = filtered
379        .iter()
380        .map(|&value| (value - mean).powi(2))
381        .sum::<f64>()
382        / filtered.len() as f64;
383
384    1.0 - (variance * 2.0).clamp(0.0, 1.0)
385}
386
387/// Cache for computed mesosystem linkages.
388///
389/// Mesosystem values are computed from microsystem data but cached
390/// for performance. The cache is invalidated at the end of each
391/// simulation step.
392#[derive(Debug, Clone, PartialEq)]
393pub struct MesosystemCache {
394    /// Cached spillover values keyed by (from, to) pair.
395    spillover_cache: RefCell<HashMap<(MicrosystemId, MicrosystemId), f64>>,
396
397    /// Cached role conflict values keyed by unordered pair (smaller, larger).
398    role_conflict_cache: RefCell<HashMap<(MicrosystemId, MicrosystemId), f64>>,
399
400    /// Whether the cache is valid.
401    valid: Cell<bool>,
402}
403
404impl MesosystemCache {
405    /// Creates a new empty cache.
406    #[must_use]
407    pub fn new() -> Self {
408        MesosystemCache {
409            spillover_cache: RefCell::new(HashMap::new()),
410            role_conflict_cache: RefCell::new(HashMap::new()),
411            valid: Cell::new(false),
412        }
413    }
414
415    /// Invalidates the cache, forcing recomputation on next access.
416    pub fn invalidate(&mut self) {
417        self.spillover_cache.get_mut().clear();
418        self.role_conflict_cache.get_mut().clear();
419        self.valid.set(false);
420    }
421
422    /// Returns whether the cache has been invalidated.
423    #[must_use]
424    pub fn is_valid(&self) -> bool {
425        self.valid.get()
426    }
427
428    /// Gets or computes spillover from one microsystem to another.
429    ///
430    /// Spillover represents stress transfer coefficient from source to target.
431    /// Returns a value in range [0.0, 1.0]:
432    /// - 0.0 means no spillover
433    /// - 1.0 means complete stress transfer
434    ///
435    /// Formula: spillover = if source_stress > 0.5 { (source_stress - 0.5) * 0.3 } else { 0.0 }
436    ///
437    /// # Arguments
438    ///
439    /// * `from` - Source microsystem ID
440    /// * `to` - Target microsystem ID
441    /// * `microsystems` - Map of all microsystems
442    ///
443    /// # Returns
444    ///
445    /// Spillover coefficient, or 0.0 if either microsystem doesn't exist.
446    pub fn get_spillover(
447        &self,
448        from: &MicrosystemId,
449        to: &MicrosystemId,
450        microsystems: &HashMap<MicrosystemId, Microsystem>,
451    ) -> f64 {
452        // Check cache first
453        let key = (from.clone(), to.clone());
454        if let Some(&cached) = self.spillover_cache.borrow().get(&key) {
455            return cached;
456        }
457
458        // Compute spillover
459        let source = match microsystems.get(from) {
460            Some(m) => m,
461            None => return 0.0,
462        };
463
464        if microsystems.get(to).is_none() {
465            return 0.0;
466        }
467
468        // Threshold-based spillover: only stress above 0.5 leaks across contexts.
469        let source_stress = source.stress_level();
470        let spillover = if source_stress > 0.5 {
471            (source_stress - 0.5) * 0.3
472        } else {
473            0.0
474        };
475        let spillover = spillover.clamp(0.0, 1.0);
476        self.spillover_cache
477            .borrow_mut()
478            .insert((from.clone(), to.clone()), spillover);
479        self.valid.set(true);
480        spillover
481    }
482
483    /// Gets or computes role conflict between two microsystems.
484    ///
485    /// Role conflict is symmetric - the order of arguments doesn't matter.
486    /// Based on competing time demands, role expectations, and value conflicts.
487    ///
488    /// # Arguments
489    ///
490    /// * `context_a` - First microsystem ID
491    /// * `context_b` - Second microsystem ID
492    /// * `microsystems` - Map of all microsystems
493    ///
494    /// # Returns
495    ///
496    /// Role conflict score in [0.0, 1.0], or 0.0 if either doesn't exist.
497    pub fn get_role_conflict(
498        &self,
499        context_a: &MicrosystemId,
500        context_b: &MicrosystemId,
501        microsystems: &HashMap<MicrosystemId, Microsystem>,
502    ) -> f64 {
503        // Make key order-independent for symmetric lookup
504        let key = if context_a.as_str() < context_b.as_str() {
505            (context_a.clone(), context_b.clone())
506        } else {
507            (context_b.clone(), context_a.clone())
508        };
509
510        if let Some(&cached) = self.role_conflict_cache.borrow().get(&key) {
511            return cached;
512        }
513
514        // Compute role conflict
515        let micro_a = match microsystems.get(context_a) {
516            Some(m) => m,
517            None => return 0.0,
518        };
519
520        let micro_b = match microsystems.get(context_b) {
521            Some(m) => m,
522            None => return 0.0,
523        };
524
525        // Role conflict based on:
526        // 1. Both contexts having high demands (stress)
527        // 2. Different warmth levels (inconsistent treatment)
528        // 3. High interaction frequency in both (time conflict)
529
530        let stress_a = micro_a.stress_level();
531        let stress_b = micro_b.stress_level();
532
533        // Time conflict: both demand high frequency
534        let freq_a = micro_a.interaction_frequency();
535        let freq_b = micro_b.interaction_frequency();
536        let time_conflict = if freq_a > 0.5 && freq_b > 0.5 {
537            (freq_a + freq_b - 1.0) * 0.5
538        } else {
539            0.0
540        };
541
542        // Stress conflict: both have high stress
543        let stress_conflict = if stress_a > 0.5 && stress_b > 0.5 {
544            (stress_a + stress_b - 1.0) * 0.4
545        } else {
546            0.0
547        };
548
549        // Treatment inconsistency: different warmth/hostility levels
550        let warmth_diff = (micro_a.warmth() - micro_b.warmth()).abs();
551        let hostility_diff = (micro_a.hostility() - micro_b.hostility()).abs();
552        let treatment_conflict = (warmth_diff + hostility_diff) * 0.3;
553
554        let total = time_conflict + stress_conflict + treatment_conflict;
555        let total = total.clamp(0.0, 1.0);
556        self.role_conflict_cache.borrow_mut().insert(key, total);
557        self.valid.set(true);
558        total
559    }
560
561    /// Lists all active linkages between microsystems.
562    ///
563    /// Returns pairs of microsystem IDs that have meaningful connections.
564    /// A linkage is considered active if both microsystems exist and
565    /// have non-trivial interaction frequency.
566    pub fn list_linkages(
567        &self,
568        microsystems: &HashMap<MicrosystemId, Microsystem>,
569    ) -> Vec<(MicrosystemId, MicrosystemId)> {
570        let ids: Vec<_> = microsystems.keys().cloned().collect();
571        let mut linkages = Vec::new();
572
573        for i in 0..ids.len() {
574            for j in (i + 1)..ids.len() {
575                let a = &ids[i];
576                let b = &ids[j];
577
578                // Only include if both have meaningful interaction frequency
579                let micro_a = &microsystems[a];
580                let micro_b = &microsystems[b];
581
582                if micro_a.interaction_frequency() > 0.1 && micro_b.interaction_frequency() > 0.1 {
583                    linkages.push((a.clone(), b.clone()));
584                }
585            }
586        }
587
588        linkages
589    }
590
591    /// Computes mesosystem consistency across all microsystems.
592    ///
593    /// Per spec: measures value alignment variance across contexts.
594    /// Low consistency increases stress.
595    ///
596    /// # Arguments
597    ///
598    /// * `microsystems` - Map of all microsystems
599    ///
600    /// # Returns
601    ///
602    /// Consistency score in [0.0, 1.0] where 1.0 is perfectly consistent.
603    #[must_use]
604    pub fn compute_consistency(microsystems: &HashMap<MicrosystemId, Microsystem>) -> f64 {
605        if microsystems.len() < 2 {
606            return 1.0; // Single or no microsystems = perfect consistency
607        }
608
609        let mut work_role_clarity = Vec::new();
610        let mut family_role_clarity = Vec::new();
611        let mut social_predictability = Vec::new();
612
613        for micro in microsystems.values() {
614            match micro {
615                Microsystem::Work(work) => work_role_clarity.push(work.role_clarity),
616                Microsystem::Family(family) => family_role_clarity.push(family.role_clarity),
617                Microsystem::Social(social) => social_predictability.push(social.predictability),
618                _ => {}
619            }
620        }
621
622        consistency_from_values(&[
623            average(&work_role_clarity),
624            average(&family_role_clarity),
625            average(&social_predictability),
626        ])
627    }
628
629    /// Computes shared membership strength across microsystems.
630    ///
631    /// # Arguments
632    ///
633    /// * `microsystems` - Map of all microsystems
634    ///
635    /// # Returns
636    ///
637    /// Shared membership score in [0.0, 1.0].
638    #[must_use]
639    pub fn compute_shared_membership_strength(
640        microsystems: &HashMap<MicrosystemId, Microsystem>,
641    ) -> f64 {
642        use std::collections::HashSet;
643
644        // Collect all entity IDs from all microsystems
645        let mut all_ids: HashSet<String> = HashSet::new();
646        let mut id_to_context_count: HashMap<String, usize> = HashMap::new();
647
648        for micro in microsystems.values() {
649            let member_ids: Vec<&crate::types::EntityId> = match micro {
650                Microsystem::Work(w) => {
651                    let mut ids: Vec<_> = w.peer_ids.iter().collect();
652                    if let Some(ref sup) = w.supervisor_id {
653                        ids.push(sup);
654                    }
655                    ids
656                }
657                Microsystem::Family(f) => f.family_unit.iter().collect(),
658                Microsystem::Social(s) => s.close_friends.iter().collect(),
659                Microsystem::Education(e) => {
660                    let mut ids: Vec<_> = e.peer_ids.iter().collect();
661                    ids.extend(e.instructors.iter());
662                    ids
663                }
664                Microsystem::Healthcare(h) => {
665                    if let Some(ref provider) = h.primary_provider_id {
666                        vec![provider]
667                    } else {
668                        vec![]
669                    }
670                }
671                Microsystem::Religious(r) => {
672                    if let Some(ref leader) = r.leader_id {
673                        vec![leader]
674                    } else {
675                        vec![]
676                    }
677                }
678                Microsystem::Neighborhood(n) => n.proximity_network.iter().collect(),
679            };
680
681            for id in member_ids {
682                let id_str = id.as_str().to_string();
683                all_ids.insert(id_str.clone());
684                *id_to_context_count.entry(id_str).or_insert(0) += 1;
685            }
686        }
687
688        if all_ids.is_empty() {
689            return 0.0;
690        }
691
692        // Count how many appear in 2+ contexts
693        let overlap_count = id_to_context_count
694            .values()
695            .filter(|&&count| count >= 2)
696            .count();
697
698        (overlap_count as f64 / all_ids.len() as f64).clamp(0.0, 1.0)
699    }
700}
701
702impl Default for MesosystemCache {
703    fn default() -> Self {
704        MesosystemCache::new()
705    }
706}
707
708#[cfg(test)]
709mod tests {
710    use super::*;
711    use crate::context::microsystem::{
712        EducationContext, FamilyContext, SocialContext, WorkContext,
713    };
714
715    // --- ProximalProcessGateError tests ---
716
717    #[test]
718    fn proximal_process_gate_allows_sufficient_thresholds() {
719        let result = passes_proximal_process_gate(0.5, 0.5, 0.3, 0.3);
720        assert!(result.is_ok());
721    }
722
723    #[test]
724    fn proximal_process_gate_with_reciprocity_blocks_low_reciprocity() {
725        let result = passes_proximal_process_gate_with_reciprocity(0.6, 0.6, 0.4, 0.3, 0.3, 0.6);
726        assert!(result.is_err());
727        let err = result.unwrap_err();
728        assert!(matches!(
729            err,
730            ProximalProcessGateError::ReciprocityBelowThreshold { actual, threshold }
731                if (actual - 0.4).abs() < f64::EPSILON
732                    && (threshold - 0.6).abs() < f64::EPSILON
733        ));
734    }
735
736    #[test]
737    fn proximal_process_frequency_gate_blocks_low_frequency() {
738        let result = passes_proximal_process_gate(0.2, 0.5, 0.3, 0.3);
739        assert!(result.is_err());
740        let err = result.unwrap_err();
741        assert!(matches!(
742            err,
743            ProximalProcessGateError::FrequencyBelowThreshold { actual, threshold }
744                if (actual - 0.2).abs() < f64::EPSILON
745                    && (threshold - 0.3).abs() < f64::EPSILON
746        ));
747    }
748
749    #[test]
750    fn proximal_process_complexity_gate_blocks_low_complexity() {
751        let result = passes_proximal_process_gate(0.5, 0.2, 0.3, 0.3);
752        assert!(result.is_err());
753        let err = result.unwrap_err();
754        assert!(matches!(
755            err,
756            ProximalProcessGateError::ComplexityBelowThreshold { actual, threshold }
757                if (actual - 0.2).abs() < f64::EPSILON
758                    && (threshold - 0.3).abs() < f64::EPSILON
759        ));
760    }
761
762    #[test]
763    fn proximal_process_gate_returns_error_with_reason() {
764        let result = passes_proximal_process_gate(0.1, 0.1, 0.3, 0.3);
765        assert!(result.is_err());
766        let err = result.unwrap_err();
767        assert!(matches!(
768            err,
769            ProximalProcessGateError::BothBelowThreshold {
770                actual_frequency,
771                frequency_threshold,
772                actual_complexity,
773                complexity_threshold,
774            } if (actual_frequency - 0.1).abs() < f64::EPSILON
775                && (frequency_threshold - 0.3).abs() < f64::EPSILON
776                && (actual_complexity - 0.1).abs() < f64::EPSILON
777                && (complexity_threshold - 0.3).abs() < f64::EPSILON
778        ));
779    }
780
781    #[test]
782    fn proximal_process_gate_error_display() {
783        let err = ProximalProcessGateError::FrequencyBelowThreshold {
784            actual: 0.2,
785            threshold: 0.3,
786        };
787        let display = format!("{}", err);
788        assert!(display.contains("0.2"));
789        assert!(display.contains("0.3"));
790
791        let err = ProximalProcessGateError::ComplexityBelowThreshold {
792            actual: 0.2,
793            threshold: 0.3,
794        };
795        let display = format!("{}", err);
796        assert!(display.contains("complexity"));
797
798        let err = ProximalProcessGateError::BothBelowThreshold {
799            actual_frequency: 0.1,
800            frequency_threshold: 0.3,
801            actual_complexity: 0.2,
802            complexity_threshold: 0.4,
803        };
804        let display = format!("{}", err);
805        assert!(display.contains("frequency"));
806        assert!(display.contains("complexity"));
807
808        let err = ProximalProcessGateError::ReciprocityBelowThreshold {
809            actual: 0.2,
810            threshold: 0.6,
811        };
812        let display = format!("{}", err);
813        assert!(display.contains("reciprocity"));
814    }
815
816    // --- MesosystemCache tests ---
817
818    #[test]
819    fn mesosystem_cache_new() {
820        let cache = MesosystemCache::new();
821        assert!(!cache.is_valid());
822    }
823
824    #[test]
825    fn mesosystem_cache_invalidate() {
826        let mut cache = MesosystemCache::new();
827        cache.valid.set(true);
828        cache.invalidate();
829        assert!(!cache.is_valid());
830    }
831
832    #[test]
833    fn mesosystem_cache_marks_valid_after_compute() {
834        let cache = MesosystemCache::new();
835        let mut microsystems = HashMap::new();
836
837        let mut work = WorkContext::default();
838        work.workload_stress = 0.8;
839        let work_id = MicrosystemId::new("work").unwrap();
840        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
841
842        let family_id = MicrosystemId::new("family").unwrap();
843        microsystems.insert(
844            family_id.clone(),
845            Microsystem::new_family(FamilyContext::default()),
846        );
847
848        let _ = cache.get_spillover(&work_id, &family_id, &microsystems);
849        assert!(cache.is_valid());
850    }
851
852    #[test]
853    fn mesosystem_work_stress_spills_to_home() {
854        let cache = MesosystemCache::new();
855        let mut microsystems = HashMap::new();
856
857        // Create high-stress work
858        let mut work = WorkContext::default();
859        work.workload_stress = 0.8;
860        work.interaction_profile.interaction_frequency = 0.7;
861        let work_id = MicrosystemId::new("work").unwrap();
862        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
863
864        // Create family with low boundary (low predictability/stability)
865        let mut family = FamilyContext::default();
866        family.predictability = 0.3;
867        family.stability = 0.3;
868        let family_id = MicrosystemId::new("family").unwrap();
869        microsystems.insert(family_id.clone(), Microsystem::new_family(family));
870
871        let spillover = cache.get_spillover(&work_id, &family_id, &microsystems);
872
873        // Spillover follows threshold-based formula
874        let expected = (0.8 - 0.5) * 0.3;
875        assert!((spillover - expected).abs() < 1e-6);
876    }
877
878    #[test]
879    fn mesosystem_role_conflict_computation() {
880        let cache = MesosystemCache::new();
881        let mut microsystems = HashMap::new();
882
883        // Create high-demand work
884        let mut work = WorkContext::default();
885        work.workload_stress = 0.8;
886        work.interaction_profile.interaction_frequency = 0.8;
887        let work_id = MicrosystemId::new("work").unwrap();
888        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
889
890        // Create high-demand family
891        let mut family = FamilyContext::default();
892        family.caregiving_burden = 0.8;
893        family.interaction_profile.interaction_frequency = 0.8;
894        let family_id = MicrosystemId::new("family").unwrap();
895        microsystems.insert(family_id.clone(), Microsystem::new_family(family));
896
897        let conflict = cache.get_role_conflict(&work_id, &family_id, &microsystems);
898
899        // Should have significant conflict
900        assert!(conflict > 0.2);
901        assert!(conflict <= 1.0);
902    }
903
904    #[test]
905    fn mesosystem_role_conflict_symmetric() {
906        let cache = MesosystemCache::new();
907        let mut microsystems = HashMap::new();
908
909        let work_id = MicrosystemId::new("work").unwrap();
910        let family_id = MicrosystemId::new("family").unwrap();
911
912        microsystems.insert(
913            work_id.clone(),
914            Microsystem::new_work(WorkContext::default()),
915        );
916        microsystems.insert(
917            family_id.clone(),
918            Microsystem::new_family(FamilyContext::default()),
919        );
920
921        let conflict_ab = cache.get_role_conflict(&work_id, &family_id, &microsystems);
922        let conflict_ba = cache.get_role_conflict(&family_id, &work_id, &microsystems);
923
924        assert!((conflict_ab - conflict_ba).abs() < f64::EPSILON);
925    }
926
927    #[test]
928    fn mesosystem_spillover_nonexistent_source() {
929        let cache = MesosystemCache::new();
930        let microsystems = HashMap::new();
931
932        let from = MicrosystemId::new("nonexistent").unwrap();
933        let to = MicrosystemId::new("also_nonexistent").unwrap();
934
935        let spillover = cache.get_spillover(&from, &to, &microsystems);
936        assert!((spillover - 0.0).abs() < f64::EPSILON);
937    }
938
939    #[test]
940    fn mesosystem_role_conflict_nonexistent() {
941        let cache = MesosystemCache::new();
942        let microsystems = HashMap::new();
943
944        let a = MicrosystemId::new("nonexistent").unwrap();
945        let b = MicrosystemId::new("also_nonexistent").unwrap();
946
947        let conflict = cache.get_role_conflict(&a, &b, &microsystems);
948        assert!((conflict - 0.0).abs() < f64::EPSILON);
949    }
950
951    #[test]
952    fn mesosystem_list_linkages() {
953        let cache = MesosystemCache::new();
954        let mut microsystems = HashMap::new();
955
956        let work_id = MicrosystemId::new("work").unwrap();
957        let family_id = MicrosystemId::new("family").unwrap();
958        let social_id = MicrosystemId::new("social").unwrap();
959
960        microsystems.insert(
961            work_id.clone(),
962            Microsystem::new_work(WorkContext::default()),
963        );
964        microsystems.insert(
965            family_id.clone(),
966            Microsystem::new_family(FamilyContext::default()),
967        );
968        microsystems.insert(
969            social_id.clone(),
970            Microsystem::new_social(SocialContext::default()),
971        );
972
973        let linkages = cache.list_linkages(&microsystems);
974
975        // Should have 3 linkages (work-family, work-social, family-social)
976        assert_eq!(linkages.len(), 3);
977
978        let work = microsystems
979            .get_mut(&work_id)
980            .and_then(Microsystem::work_mut)
981            .expect("work microsystem missing");
982        work.interaction_profile.interaction_frequency = 0.0;
983
984        let linkages = cache.list_linkages(&microsystems);
985        assert_eq!(linkages.len(), 1);
986    }
987
988    #[test]
989    fn mesosystem_cache_retains_values_until_invalidated() {
990        let cache = MesosystemCache::new();
991        let mut microsystems = HashMap::new();
992
993        let work_id = MicrosystemId::new("work").unwrap();
994        let family_id = MicrosystemId::new("family").unwrap();
995
996        let mut work = WorkContext::default();
997        work.workload_stress = 0.5;
998        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
999        microsystems.insert(
1000            family_id.clone(),
1001            Microsystem::new_family(FamilyContext::default()),
1002        );
1003
1004        // Get spillover - computes and caches from current microsystem state
1005        let spillover1 = cache.get_spillover(&work_id, &family_id, &microsystems);
1006
1007        // Modify work stress
1008        let work = microsystems
1009            .get_mut(&work_id)
1010            .and_then(Microsystem::work_mut)
1011            .expect("work microsystem missing");
1012        work.workload_stress = 0.9;
1013
1014        // Get spillover again - should return cached value without invalidation
1015        let spillover2 = cache.get_spillover(&work_id, &family_id, &microsystems);
1016
1017        assert!((spillover1 - spillover2).abs() < f64::EPSILON);
1018    }
1019
1020    #[test]
1021    fn mesosystem_recomputes_on_microsystem_change() {
1022        let mut cache = MesosystemCache::new();
1023        let mut microsystems = HashMap::new();
1024
1025        let work_id = MicrosystemId::new("work").unwrap();
1026        let family_id = MicrosystemId::new("family").unwrap();
1027
1028        let mut work = WorkContext::default();
1029        work.workload_stress = 0.3;
1030        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1031
1032        let mut family = FamilyContext::default();
1033        family.predictability = 0.5;
1034        family.stability = 0.5;
1035        microsystems.insert(family_id.clone(), Microsystem::new_family(family));
1036
1037        let spillover1 = cache.get_spillover(&work_id, &family_id, &microsystems);
1038
1039        // Invalidate cache and change stress
1040        cache.invalidate();
1041        let work = microsystems
1042            .get_mut(&work_id)
1043            .and_then(Microsystem::work_mut)
1044            .expect("work microsystem missing");
1045        work.workload_stress = 0.9;
1046
1047        let spillover2 = cache.get_spillover(&work_id, &family_id, &microsystems);
1048
1049        // Higher stress should lead to higher spillover
1050        assert!(spillover2 > spillover1);
1051    }
1052
1053    #[test]
1054    fn mesosystem_consistency() {
1055        let mut microsystems = HashMap::new();
1056
1057        // Create microsystems with similar value alignment
1058        let mut work = WorkContext::default();
1059        work.role_clarity = 0.7;
1060        microsystems.insert(
1061            MicrosystemId::new("work").unwrap(),
1062            Microsystem::new_work(work),
1063        );
1064
1065        let mut family = FamilyContext::default();
1066        family.role_clarity = 0.7;
1067        microsystems.insert(
1068            MicrosystemId::new("family").unwrap(),
1069            Microsystem::new_family(family),
1070        );
1071
1072        let consistency = MesosystemCache::compute_consistency(&microsystems);
1073
1074        // Similar value alignment = high consistency
1075        assert!(consistency > 0.8);
1076    }
1077
1078    #[test]
1079    fn mesosystem_consistency_low_variance() {
1080        let mut microsystems = HashMap::new();
1081
1082        // Create microsystems with maximally different value alignment
1083        let mut work = WorkContext::default();
1084        work.role_clarity = 1.0;
1085        microsystems.insert(
1086            MicrosystemId::new("work").unwrap(),
1087            Microsystem::new_work(work),
1088        );
1089
1090        let mut family = FamilyContext::default();
1091        family.role_clarity = 0.0;
1092        microsystems.insert(
1093            MicrosystemId::new("family").unwrap(),
1094            Microsystem::new_family(family),
1095        );
1096
1097        let consistency = MesosystemCache::compute_consistency(&microsystems);
1098
1099        // Max variance in [0, 1] yields 0.5 consistency.
1100        assert!((consistency - 0.5).abs() < f64::EPSILON);
1101    }
1102
1103    #[test]
1104    fn mesosystem_consistency_single_microsystem() {
1105        let mut microsystems = HashMap::new();
1106        microsystems.insert(
1107            MicrosystemId::new("work").unwrap(),
1108            Microsystem::new_work(WorkContext::default()),
1109        );
1110
1111        let consistency = MesosystemCache::compute_consistency(&microsystems);
1112        assert!((consistency - 1.0).abs() < f64::EPSILON);
1113    }
1114
1115    #[test]
1116    fn mesosystem_consistency_empty() {
1117        let microsystems = HashMap::new();
1118        let consistency = MesosystemCache::compute_consistency(&microsystems);
1119        assert!((consistency - 1.0).abs() < f64::EPSILON);
1120    }
1121
1122    #[test]
1123    fn mesosystem_consistency_includes_social_and_ignores_other_types() {
1124        let mut microsystems = HashMap::new();
1125
1126        let mut work = WorkContext::default();
1127        work.role_clarity = 0.6;
1128        microsystems.insert(
1129            MicrosystemId::new("work").unwrap(),
1130            Microsystem::new_work(work),
1131        );
1132
1133        let mut family = FamilyContext::default();
1134        family.role_clarity = 0.4;
1135        microsystems.insert(
1136            MicrosystemId::new("family").unwrap(),
1137            Microsystem::new_family(family),
1138        );
1139
1140        let mut social = SocialContext::default();
1141        social.predictability = 0.7;
1142        microsystems.insert(
1143            MicrosystemId::new("social").unwrap(),
1144            Microsystem::new_social(social),
1145        );
1146
1147        microsystems.insert(
1148            MicrosystemId::new("education").unwrap(),
1149            Microsystem::new_education(EducationContext::default()),
1150        );
1151
1152        let consistency = MesosystemCache::compute_consistency(&microsystems);
1153        assert!((0.0..=1.0).contains(&consistency));
1154    }
1155
1156    #[test]
1157    fn mesosystem_shared_membership_empty() {
1158        let microsystems = HashMap::new();
1159        let strength = MesosystemCache::compute_shared_membership_strength(&microsystems);
1160        assert!((strength - 0.0).abs() < f64::EPSILON);
1161    }
1162
1163    #[test]
1164    fn mesosystem_shared_membership_no_overlap() {
1165        let mut microsystems = HashMap::new();
1166
1167        let mut work = WorkContext::default();
1168        work.peer_ids = vec![
1169            crate::types::EntityId::new("alice").unwrap(),
1170            crate::types::EntityId::new("bob").unwrap(),
1171        ];
1172        microsystems.insert(
1173            MicrosystemId::new("work").unwrap(),
1174            Microsystem::new_work(work),
1175        );
1176
1177        let mut family = FamilyContext::default();
1178        family.family_unit = vec![
1179            crate::types::EntityId::new("charlie").unwrap(),
1180            crate::types::EntityId::new("diana").unwrap(),
1181        ];
1182        microsystems.insert(
1183            MicrosystemId::new("family").unwrap(),
1184            Microsystem::new_family(family),
1185        );
1186
1187        let strength = MesosystemCache::compute_shared_membership_strength(&microsystems);
1188        assert!((strength - 0.0).abs() < f64::EPSILON);
1189    }
1190
1191    #[test]
1192    fn mesosystem_shared_membership_with_overlap() {
1193        let mut microsystems = HashMap::new();
1194
1195        let shared_id = crate::types::EntityId::new("shared_person").unwrap();
1196
1197        let mut work = WorkContext::default();
1198        work.peer_ids = vec![
1199            shared_id.clone(),
1200            crate::types::EntityId::new("bob").unwrap(),
1201        ];
1202        microsystems.insert(
1203            MicrosystemId::new("work").unwrap(),
1204            Microsystem::new_work(work),
1205        );
1206
1207        let mut social = SocialContext::default();
1208        social.close_friends = vec![shared_id, crate::types::EntityId::new("charlie").unwrap()];
1209        microsystems.insert(
1210            MicrosystemId::new("social").unwrap(),
1211            Microsystem::new_social(social),
1212        );
1213
1214        let strength = MesosystemCache::compute_shared_membership_strength(&microsystems);
1215
1216        // 1 overlap out of 3 unique people = 0.333...
1217        assert!(strength > 0.3 && strength < 0.4);
1218    }
1219
1220    // --- MesosystemState tests ---
1221
1222    #[test]
1223    fn mesosystem_state_defaults_when_empty() {
1224        let microsystems = HashMap::new();
1225        let state = MesosystemState::compute(&microsystems);
1226        assert_eq!(state, MesosystemState::default());
1227    }
1228
1229    #[test]
1230    fn mesosystem_state_computes_cross_context_metrics() {
1231        use crate::types::EntityId;
1232
1233        let mut microsystems = HashMap::new();
1234        let shared_id = EntityId::new("shared_person").unwrap();
1235
1236        let mut work = WorkContext::default();
1237        work.role_clarity = 0.2;
1238        work.predictability = 0.3;
1239        work.warmth = 0.2;
1240        work.hostility = 0.1;
1241        work.workload_stress = 0.8;
1242        work.peer_ids = vec![shared_id.clone()];
1243        work.interaction_profile.interaction_frequency = 0.8;
1244        microsystems.insert(
1245            MicrosystemId::new("work").unwrap(),
1246            Microsystem::new_work(work),
1247        );
1248
1249        let mut family = FamilyContext::default();
1250        family.role_clarity = 0.8;
1251        family.predictability = 0.9;
1252        family.warmth = 0.7;
1253        family.hostility = 0.1;
1254        family.caregiving_burden = 0.8;
1255        family.interaction_profile.interaction_frequency = 0.8;
1256        microsystems.insert(
1257            MicrosystemId::new("family").unwrap(),
1258            Microsystem::new_family(family),
1259        );
1260
1261        let mut social = SocialContext::default();
1262        social.predictability = 0.9;
1263        social.warmth = 0.8;
1264        social.hostility = 0.1;
1265        social.close_friends = vec![shared_id];
1266        social.interaction_profile.interaction_frequency = 0.6;
1267        microsystems.insert(
1268            MicrosystemId::new("social").unwrap(),
1269            Microsystem::new_social(social),
1270        );
1271
1272        let state = MesosystemState::compute(&microsystems);
1273
1274        assert!(state.work_family_conflict > 0.0);
1275        assert!((state.family_social_support - 0.6).abs() < 1e-6);
1276        assert!((state.work_social_conflict - 0.3).abs() < f64::EPSILON);
1277        assert!((state.shared_membership_strength - 1.0).abs() < f64::EPSILON);
1278        assert!(state.microsystem_influence_weight > 0.9);
1279
1280        let consistency = |values: &[f64]| {
1281            let mean = values.iter().sum::<f64>() / values.len() as f64;
1282            let variance = values
1283                .iter()
1284                .map(|&value| (value - mean).powi(2))
1285                .sum::<f64>()
1286                / values.len() as f64;
1287            1.0 - (variance * 2.0).clamp(0.0, 1.0)
1288        };
1289
1290        let expected_value_alignment = consistency(&[0.2, 0.8, 0.9]);
1291        let expected_autonomy = consistency(&[0.3, 0.9, 0.9]);
1292        let expected_meso = expected_value_alignment;
1293
1294        assert!((state.value_alignment_consistency - expected_value_alignment).abs() < 1e-6);
1295        assert!((state.autonomy_norm_consistency - expected_autonomy).abs() < 1e-6);
1296        assert!((state.mesosystem_consistency - expected_meso).abs() < 1e-6);
1297    }
1298
1299    // --- MesosystemLinkage tests ---
1300
1301    #[test]
1302    fn mesosystem_linkage_creation() {
1303        let linkage = MesosystemLinkage {
1304            from: MicrosystemId::new("work").unwrap(),
1305            to: MicrosystemId::new("family").unwrap(),
1306            spillover: 0.3,
1307            role_conflict: 0.2,
1308        };
1309
1310        assert_eq!(linkage.from.as_str(), "work");
1311        assert_eq!(linkage.to.as_str(), "family");
1312    }
1313
1314    #[test]
1315    fn mesosystem_linkage_clone_eq() {
1316        let linkage1 = MesosystemLinkage {
1317            from: MicrosystemId::new("work").unwrap(),
1318            to: MicrosystemId::new("family").unwrap(),
1319            spillover: 0.3,
1320            role_conflict: 0.2,
1321        };
1322        let linkage2 = linkage1.clone();
1323        assert_eq!(linkage1, linkage2);
1324    }
1325
1326    #[test]
1327    fn mesosystem_cache_clone_eq() {
1328        let cache1 = MesosystemCache::new();
1329        let cache2 = cache1.clone();
1330        assert_eq!(cache1, cache2);
1331    }
1332
1333    #[test]
1334    fn mesosystem_cache_default() {
1335        let cache = MesosystemCache::default();
1336        assert!(!cache.is_valid());
1337    }
1338
1339    #[test]
1340    fn proximal_process_gate_error_clone_eq() {
1341        let err1 = ProximalProcessGateError::FrequencyBelowThreshold {
1342            actual: 0.2,
1343            threshold: 0.3,
1344        };
1345        let err2 = err1.clone();
1346        assert_eq!(err1, err2);
1347    }
1348
1349    // --- Additional coverage tests for mesosystem ---
1350
1351    #[test]
1352    fn mesosystem_spillover_to_social_target() {
1353        use crate::context::microsystem::SocialContext;
1354
1355        let cache = MesosystemCache::new();
1356        let mut microsystems = HashMap::new();
1357
1358        let mut work = WorkContext::default();
1359        work.workload_stress = 0.8;
1360        work.interaction_profile.interaction_frequency = 0.7;
1361        let work_id = MicrosystemId::new("work").unwrap();
1362        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1363
1364        let social = SocialContext::default();
1365        let social_id = MicrosystemId::new("social").unwrap();
1366        microsystems.insert(social_id.clone(), Microsystem::new_social(social));
1367
1368        let spillover = cache.get_spillover(&work_id, &social_id, &microsystems);
1369        assert!(spillover >= 0.0 && spillover <= 1.0);
1370    }
1371
1372    #[test]
1373    fn mesosystem_spillover_to_education_target() {
1374        use crate::context::microsystem::EducationContext;
1375
1376        let cache = MesosystemCache::new();
1377        let mut microsystems = HashMap::new();
1378
1379        let mut work = WorkContext::default();
1380        work.workload_stress = 0.8;
1381        work.interaction_profile.interaction_frequency = 0.7;
1382        let work_id = MicrosystemId::new("work").unwrap();
1383        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1384
1385        let edu = EducationContext::default();
1386        let edu_id = MicrosystemId::new("education").unwrap();
1387        microsystems.insert(edu_id.clone(), Microsystem::new_education(edu));
1388
1389        let spillover = cache.get_spillover(&work_id, &edu_id, &microsystems);
1390        assert!(spillover >= 0.0 && spillover <= 1.0);
1391    }
1392
1393    #[test]
1394    fn mesosystem_spillover_to_healthcare_target() {
1395        use crate::context::microsystem::HealthcareContext;
1396
1397        let cache = MesosystemCache::new();
1398        let mut microsystems = HashMap::new();
1399
1400        let mut work = WorkContext::default();
1401        work.workload_stress = 0.8;
1402        work.interaction_profile.interaction_frequency = 0.7;
1403        let work_id = MicrosystemId::new("work").unwrap();
1404        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1405
1406        let hc = HealthcareContext::default();
1407        let hc_id = MicrosystemId::new("healthcare").unwrap();
1408        microsystems.insert(hc_id.clone(), Microsystem::new_healthcare(hc));
1409
1410        let spillover = cache.get_spillover(&work_id, &hc_id, &microsystems);
1411        assert!(spillover >= 0.0 && spillover <= 1.0);
1412    }
1413
1414    #[test]
1415    fn mesosystem_spillover_to_religious_target() {
1416        use crate::context::microsystem::ReligiousContext;
1417
1418        let cache = MesosystemCache::new();
1419        let mut microsystems = HashMap::new();
1420
1421        let mut work = WorkContext::default();
1422        work.workload_stress = 0.8;
1423        work.interaction_profile.interaction_frequency = 0.7;
1424        let work_id = MicrosystemId::new("work").unwrap();
1425        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1426
1427        let rel = ReligiousContext::default();
1428        let rel_id = MicrosystemId::new("religious").unwrap();
1429        microsystems.insert(rel_id.clone(), Microsystem::new_religious(rel));
1430
1431        let spillover = cache.get_spillover(&work_id, &rel_id, &microsystems);
1432        assert!(spillover >= 0.0 && spillover <= 1.0);
1433    }
1434
1435    #[test]
1436    fn mesosystem_spillover_to_neighborhood_target() {
1437        use crate::context::microsystem::NeighborhoodContext;
1438
1439        let cache = MesosystemCache::new();
1440        let mut microsystems = HashMap::new();
1441
1442        let mut work = WorkContext::default();
1443        work.workload_stress = 0.8;
1444        work.interaction_profile.interaction_frequency = 0.7;
1445        let work_id = MicrosystemId::new("work").unwrap();
1446        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1447
1448        let nb = NeighborhoodContext::default();
1449        let nb_id = MicrosystemId::new("neighborhood").unwrap();
1450        microsystems.insert(nb_id.clone(), Microsystem::new_neighborhood(nb));
1451
1452        let spillover = cache.get_spillover(&work_id, &nb_id, &microsystems);
1453        assert!(spillover >= 0.0 && spillover <= 1.0);
1454    }
1455
1456    #[test]
1457    fn mesosystem_shared_membership_with_education() {
1458        use crate::context::microsystem::EducationContext;
1459        use crate::types::EntityId;
1460
1461        let mut microsystems = HashMap::new();
1462
1463        let shared_id = EntityId::new("shared_person").unwrap();
1464
1465        let mut edu = EducationContext::default();
1466        edu.peer_ids = vec![shared_id.clone()];
1467        edu.instructors = vec![EntityId::new("instructor").unwrap()];
1468        microsystems.insert(
1469            MicrosystemId::new("education").unwrap(),
1470            Microsystem::new_education(edu),
1471        );
1472
1473        let mut social = SocialContext::default();
1474        social.close_friends = vec![shared_id.clone()];
1475        microsystems.insert(
1476            MicrosystemId::new("social").unwrap(),
1477            Microsystem::new_social(social),
1478        );
1479
1480        let strength = MesosystemCache::compute_shared_membership_strength(&microsystems);
1481        assert!(strength > 0.0);
1482    }
1483
1484    #[test]
1485    fn mesosystem_shared_membership_with_healthcare() {
1486        use crate::context::microsystem::HealthcareContext;
1487        use crate::types::EntityId;
1488
1489        let mut microsystems = HashMap::new();
1490
1491        let shared_id = EntityId::new("shared_person").unwrap();
1492
1493        let mut hc = HealthcareContext::default();
1494        hc.primary_provider_id = Some(shared_id.clone());
1495        microsystems.insert(
1496            MicrosystemId::new("healthcare").unwrap(),
1497            Microsystem::new_healthcare(hc),
1498        );
1499
1500        let mut social = SocialContext::default();
1501        social.close_friends = vec![shared_id.clone()];
1502        microsystems.insert(
1503            MicrosystemId::new("social").unwrap(),
1504            Microsystem::new_social(social),
1505        );
1506
1507        let strength = MesosystemCache::compute_shared_membership_strength(&microsystems);
1508        assert!(strength > 0.0);
1509    }
1510
1511    #[test]
1512    fn mesosystem_shared_membership_with_religious() {
1513        use crate::context::microsystem::ReligiousContext;
1514        use crate::types::EntityId;
1515
1516        let mut microsystems = HashMap::new();
1517
1518        let shared_id = EntityId::new("shared_person").unwrap();
1519
1520        let mut rel = ReligiousContext::default();
1521        rel.leader_id = Some(shared_id.clone());
1522        microsystems.insert(
1523            MicrosystemId::new("religious").unwrap(),
1524            Microsystem::new_religious(rel),
1525        );
1526
1527        let mut social = SocialContext::default();
1528        social.close_friends = vec![shared_id.clone()];
1529        microsystems.insert(
1530            MicrosystemId::new("social").unwrap(),
1531            Microsystem::new_social(social),
1532        );
1533
1534        let strength = MesosystemCache::compute_shared_membership_strength(&microsystems);
1535        assert!(strength > 0.0);
1536    }
1537
1538    #[test]
1539    fn mesosystem_shared_membership_with_neighborhood() {
1540        use crate::context::microsystem::NeighborhoodContext;
1541        use crate::types::EntityId;
1542
1543        let mut microsystems = HashMap::new();
1544
1545        let shared_id = EntityId::new("shared_person").unwrap();
1546
1547        let mut nb = NeighborhoodContext::default();
1548        nb.proximity_network = vec![shared_id.clone()];
1549        microsystems.insert(
1550            MicrosystemId::new("neighborhood").unwrap(),
1551            Microsystem::new_neighborhood(nb),
1552        );
1553
1554        let mut social = SocialContext::default();
1555        social.close_friends = vec![shared_id.clone()];
1556        microsystems.insert(
1557            MicrosystemId::new("social").unwrap(),
1558            Microsystem::new_social(social),
1559        );
1560
1561        let strength = MesosystemCache::compute_shared_membership_strength(&microsystems);
1562        assert!(strength > 0.0);
1563    }
1564
1565    #[test]
1566    fn mesosystem_shared_membership_healthcare_no_provider() {
1567        use crate::context::microsystem::HealthcareContext;
1568
1569        let mut microsystems = HashMap::new();
1570
1571        let hc = HealthcareContext::default(); // No primary_provider_id set
1572        microsystems.insert(
1573            MicrosystemId::new("healthcare").unwrap(),
1574            Microsystem::new_healthcare(hc),
1575        );
1576
1577        // Should not panic, just return 0.0 or low value
1578        let strength = MesosystemCache::compute_shared_membership_strength(&microsystems);
1579        assert!((strength - 0.0).abs() < f64::EPSILON);
1580    }
1581
1582    #[test]
1583    fn mesosystem_shared_membership_religious_no_leader() {
1584        use crate::context::microsystem::ReligiousContext;
1585
1586        let mut microsystems = HashMap::new();
1587
1588        let rel = ReligiousContext::default(); // No leader_id set
1589        microsystems.insert(
1590            MicrosystemId::new("religious").unwrap(),
1591            Microsystem::new_religious(rel),
1592        );
1593
1594        // Should not panic, just return 0.0 or low value
1595        let strength = MesosystemCache::compute_shared_membership_strength(&microsystems);
1596        assert!((strength - 0.0).abs() < f64::EPSILON);
1597    }
1598
1599    #[test]
1600    fn mesosystem_shared_membership_with_supervisor() {
1601        use crate::types::EntityId;
1602
1603        let mut microsystems = HashMap::new();
1604
1605        let shared_id = EntityId::new("supervisor_friend").unwrap();
1606
1607        let mut work = WorkContext::default();
1608        work.peer_ids = vec![EntityId::new("coworker").unwrap()];
1609        work.supervisor_id = Some(shared_id.clone());
1610        microsystems.insert(
1611            MicrosystemId::new("work").unwrap(),
1612            Microsystem::new_work(work),
1613        );
1614
1615        let mut social = SocialContext::default();
1616        social.close_friends = vec![shared_id.clone()];
1617        microsystems.insert(
1618            MicrosystemId::new("social").unwrap(),
1619            Microsystem::new_social(social),
1620        );
1621
1622        let strength = MesosystemCache::compute_shared_membership_strength(&microsystems);
1623        // supervisor_friend appears in both contexts
1624        assert!(strength > 0.0);
1625    }
1626
1627    #[test]
1628    fn mesosystem_spillover_target_missing() {
1629        let cache = MesosystemCache::new();
1630        let mut microsystems = HashMap::new();
1631
1632        let mut work = WorkContext::default();
1633        work.workload_stress = 0.8;
1634        let work_id = MicrosystemId::new("work").unwrap();
1635        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1636
1637        let family_id = MicrosystemId::new("family").unwrap();
1638        // family_id not in microsystems
1639
1640        let spillover = cache.get_spillover(&work_id, &family_id, &microsystems);
1641        assert!((spillover - 0.0).abs() < f64::EPSILON);
1642    }
1643
1644    #[test]
1645    fn mesosystem_role_conflict_uses_cache() {
1646        let cache = MesosystemCache::new();
1647        let mut microsystems = HashMap::new();
1648
1649        let mut work = WorkContext::default();
1650        work.workload_stress = 0.8;
1651        work.interaction_profile.interaction_frequency = 0.8;
1652        let work_id = MicrosystemId::new("work").unwrap();
1653        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1654
1655        let mut family = FamilyContext::default();
1656        family.caregiving_burden = 0.8;
1657        family.interaction_profile.interaction_frequency = 0.8;
1658        let family_id = MicrosystemId::new("family").unwrap();
1659        microsystems.insert(family_id.clone(), Microsystem::new_family(family));
1660
1661        // First call computes and potentially caches
1662        let conflict1 = cache.get_role_conflict(&work_id, &family_id, &microsystems);
1663
1664        // Second call should return same value (though cache isn't being stored in current impl)
1665        let conflict2 = cache.get_role_conflict(&work_id, &family_id, &microsystems);
1666
1667        assert!((conflict1 - conflict2).abs() < f64::EPSILON);
1668    }
1669
1670    #[test]
1671    fn mesosystem_spillover_to_work_target() {
1672        // Test spillover TO work (not from work) to cover the Work branch in role_boundary match
1673        let cache = MesosystemCache::new();
1674        let mut microsystems = HashMap::new();
1675
1676        let mut family = FamilyContext::default();
1677        family.caregiving_burden = 0.8;
1678        family.hostility = 0.3;
1679        family.interaction_profile.interaction_frequency = 0.7;
1680        let family_id = MicrosystemId::new("family").unwrap();
1681        microsystems.insert(family_id.clone(), Microsystem::new_family(family));
1682
1683        let work = WorkContext::default();
1684        let work_id = MicrosystemId::new("work").unwrap();
1685        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1686
1687        // Spillover from family TO work
1688        let spillover = cache.get_spillover(&family_id, &work_id, &microsystems);
1689        assert!(spillover >= 0.0 && spillover <= 1.0);
1690    }
1691
1692    #[test]
1693    fn mesosystem_role_conflict_missing_context_b() {
1694        let cache = MesosystemCache::new();
1695        let mut microsystems = HashMap::new();
1696
1697        let work = WorkContext::default();
1698        let work_id = MicrosystemId::new("work").unwrap();
1699        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1700
1701        let family_id = MicrosystemId::new("family").unwrap();
1702        // Note: family_id NOT inserted into microsystems
1703
1704        let conflict = cache.get_role_conflict(&work_id, &family_id, &microsystems);
1705        assert!((conflict - 0.0).abs() < f64::EPSILON);
1706    }
1707
1708    #[test]
1709    fn mesosystem_spillover_uses_cache() {
1710        let cache = MesosystemCache::new();
1711        let mut microsystems = HashMap::new();
1712
1713        let mut work = WorkContext::default();
1714        work.workload_stress = 0.8;
1715        work.interaction_profile.interaction_frequency = 0.7;
1716        let work_id = MicrosystemId::new("work").unwrap();
1717        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1718
1719        let family = FamilyContext::default();
1720        let family_id = MicrosystemId::new("family").unwrap();
1721        microsystems.insert(family_id.clone(), Microsystem::new_family(family));
1722
1723        // First call should compute and store
1724        let spillover1 = cache.get_spillover(&work_id, &family_id, &microsystems);
1725
1726        // Second call should return cached value
1727        let spillover2 = cache.get_spillover(&work_id, &family_id, &microsystems);
1728        assert!((spillover1 - spillover2).abs() < f64::EPSILON);
1729        assert!(cache.spillover_cache.borrow().contains_key(&(work_id, family_id)));
1730    }
1731
1732    #[test]
1733    fn mesosystem_role_conflict_uses_cache_properly() {
1734        let cache = MesosystemCache::new();
1735        let mut microsystems = HashMap::new();
1736
1737        let mut work = WorkContext::default();
1738        work.workload_stress = 0.8;
1739        work.interaction_profile.interaction_frequency = 0.8;
1740        let work_id = MicrosystemId::new("work").unwrap();
1741        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1742
1743        let mut family = FamilyContext::default();
1744        family.caregiving_burden = 0.8;
1745        family.interaction_profile.interaction_frequency = 0.8;
1746        let family_id = MicrosystemId::new("family").unwrap();
1747        microsystems.insert(family_id.clone(), Microsystem::new_family(family));
1748
1749        // First call computes
1750        let conflict1 = cache.get_role_conflict(&work_id, &family_id, &microsystems);
1751
1752        // Second call should return cached value
1753        let conflict2 = cache.get_role_conflict(&work_id, &family_id, &microsystems);
1754        assert!((conflict1 - conflict2).abs() < f64::EPSILON);
1755        assert!(cache.role_conflict_cache.borrow().len() >= 1);
1756    }
1757
1758
1759    #[test]
1760    fn mesosystem_shared_member_linkage() {
1761        // Shared participants create linkage between microsystems
1762        use crate::types::EntityId;
1763
1764        let mut microsystems = HashMap::new();
1765
1766        // Create shared member
1767        let shared_member = EntityId::new("shared_person").unwrap();
1768
1769        // Work context with shared member as peer
1770        let mut work = WorkContext::default();
1771        work.peer_ids = vec![shared_member.clone(), EntityId::new("coworker1").unwrap()];
1772        work.interaction_profile.interaction_frequency = 0.7;
1773        let work_id = MicrosystemId::new("work").unwrap();
1774        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1775
1776        // Social context with shared member as friend
1777        let mut social = SocialContext::default();
1778        social.close_friends = vec![
1779            shared_member.clone(),
1780            EntityId::new("other_friend").unwrap(),
1781        ];
1782        social.interaction_profile.interaction_frequency = 0.6;
1783        let social_id = MicrosystemId::new("social").unwrap();
1784        microsystems.insert(social_id.clone(), Microsystem::new_social(social));
1785
1786        // Verify linkages exist
1787        let cache = MesosystemCache::new();
1788        let linkages = cache.list_linkages(&microsystems);
1789
1790        // Should have 1 linkage (work-social)
1791        assert_eq!(linkages.len(), 1);
1792
1793        // Verify shared membership strength is positive (shared_person appears in both)
1794        let membership_strength =
1795            MesosystemCache::compute_shared_membership_strength(&microsystems);
1796
1797        // We have 3 unique people: shared_person, coworker1, other_friend
1798        // shared_person appears in 2 contexts
1799        // So 1 out of 3 people overlap: 1/3 = 0.333...
1800        assert!(membership_strength > 0.3);
1801
1802        // Without shared member, there would be no overlap
1803        let mut no_overlap_microsystems = HashMap::new();
1804        let mut work2 = WorkContext::default();
1805        work2.peer_ids = vec![EntityId::new("alice").unwrap()];
1806        work2.interaction_profile.interaction_frequency = 0.7;
1807        no_overlap_microsystems.insert(
1808            MicrosystemId::new("work").unwrap(),
1809            Microsystem::new_work(work2),
1810        );
1811
1812        let mut social2 = SocialContext::default();
1813        social2.close_friends = vec![EntityId::new("bob").unwrap()];
1814        social2.interaction_profile.interaction_frequency = 0.6;
1815        no_overlap_microsystems.insert(
1816            MicrosystemId::new("social").unwrap(),
1817            Microsystem::new_social(social2),
1818        );
1819
1820        let no_overlap_strength =
1821            MesosystemCache::compute_shared_membership_strength(&no_overlap_microsystems);
1822        assert!((no_overlap_strength - 0.0).abs() < f64::EPSILON);
1823    }
1824
1825    #[test]
1826    fn mesosystem_spillover_from_work_missing_family() {
1827        let cache = MesosystemCache::new();
1828        let mut microsystems = HashMap::new();
1829
1830        let mut work = WorkContext::default();
1831        work.workload_stress = 0.8;
1832        let work_id = MicrosystemId::new("work").unwrap();
1833        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1834
1835        let family_id = MicrosystemId::new("family").unwrap();
1836        // Note: family_id NOT in microsystems
1837
1838        let spillover = cache.get_spillover(&work_id, &family_id, &microsystems);
1839        assert!((spillover - 0.0).abs() < f64::EPSILON);
1840    }
1841
1842    #[test]
1843    fn mesosystem_work_family_conflict_with_non_zero_count() {
1844        let mut microsystems = HashMap::new();
1845
1846        let mut work = WorkContext::default();
1847        work.workload_stress = 0.8;
1848        let work_id = MicrosystemId::new("work").unwrap();
1849        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1850
1851        let mut family = FamilyContext::default();
1852        family.caregiving_burden = 0.7;
1853        let family_id = MicrosystemId::new("family").unwrap();
1854        microsystems.insert(family_id.clone(), Microsystem::new_family(family));
1855
1856        let state = MesosystemState::compute(&microsystems);
1857        let conflict = state.work_family_conflict;
1858        assert!(conflict >= 0.0);
1859    }
1860
1861    #[test]
1862    fn mesosystem_work_family_conflict_with_zero_count() {
1863        let mut microsystems = HashMap::new();
1864
1865        // Only work, no family - count will be 0
1866        let mut work = WorkContext::default();
1867        work.workload_stress = 0.8;
1868        let work_id = MicrosystemId::new("work").unwrap();
1869        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1870
1871        let state = MesosystemState::compute(&microsystems);
1872        // With no family contexts, work_family_conflict should be 0.0 (the else branch)
1873        assert_eq!(state.work_family_conflict, 0.0);
1874    }
1875
1876    #[test]
1877    fn mesosystem_spillover_below_threshold_returns_zero() {
1878        let cache = MesosystemCache::new();
1879        let mut microsystems = HashMap::new();
1880
1881        let mut work = WorkContext::default();
1882        work.workload_stress = 0.3; // Below 0.5 threshold
1883        let work_id = MicrosystemId::new("work").unwrap();
1884        microsystems.insert(work_id.clone(), Microsystem::new_work(work));
1885
1886        let family = FamilyContext::default();
1887        let family_id = MicrosystemId::new("family").unwrap();
1888        microsystems.insert(family_id.clone(), Microsystem::new_family(family));
1889
1890        let spillover = cache.get_spillover(&work_id, &family_id, &microsystems);
1891        assert!((spillover - 0.0).abs() < f64::EPSILON);
1892    }
1893}