Skip to main content

peat_protocol/models/
domain.rs

1//! Domain layer data structures
2//!
3//! This module defines the operational domain (altitude layer) for nodes and capabilities.
4//! Domains affect sensor detection, engagement, and composition rules.
5
6use serde::{Deserialize, Serialize};
7
8/// Operational domain (altitude layer) for a node or capability
9///
10/// Domains define where a platform operates and what it can detect/engage.
11/// Cross-domain operations require specific capability combinations.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
13#[repr(i32)]
14pub enum Domain {
15    /// Unspecified domain (defaults to Surface)
16    #[default]
17    Unspecified = 0,
18    /// Subsurface operations (underwater, underground)
19    /// - Submarines, UUVs, tunneling systems
20    /// - Detectable only by sonar/acoustic sensors
21    /// - Cannot directly engage air targets
22    Subsurface = 1,
23    /// Surface operations (ground, water surface)
24    /// - Ground vehicles, ships, operators
25    /// - Terrain affects movement and LOS
26    /// - Can engage both subsurface (with ASW) and air (with ADA)
27    Surface = 2,
28    /// Air operations (airborne platforms)
29    /// - Aircraft, drones, missiles
30    /// - Full visibility, high mobility
31    /// - Cannot detect subsurface without specialized sensors
32    Air = 3,
33}
34
35impl Domain {
36    /// Get the domain name as a string
37    pub fn name(&self) -> &'static str {
38        match self {
39            Domain::Unspecified => "Unspecified",
40            Domain::Subsurface => "Subsurface",
41            Domain::Surface => "Surface",
42            Domain::Air => "Air",
43        }
44    }
45
46    /// Get the short code for display
47    pub fn code(&self) -> &'static str {
48        match self {
49            Domain::Unspecified => "UNK",
50            Domain::Subsurface => "SUB",
51            Domain::Surface => "SFC",
52            Domain::Air => "AIR",
53        }
54    }
55
56    /// Check if this domain can natively detect the target domain
57    ///
58    /// Returns true if sensors in this domain can typically detect targets
59    /// in the target domain without special equipment.
60    pub fn can_detect(&self, target: Domain) -> bool {
61        match (self, target) {
62            // Same domain - always detectable
63            (Domain::Subsurface, Domain::Subsurface) => true,
64            (Domain::Surface, Domain::Surface) => true,
65            (Domain::Air, Domain::Air) => true,
66
67            // Air can see surface (looking down)
68            (Domain::Air, Domain::Surface) => true,
69
70            // Surface can see air (looking up) with appropriate sensors
71            (Domain::Surface, Domain::Air) => true,
72
73            // Subsurface can detect surface (periscope, passive sonar)
74            (Domain::Subsurface, Domain::Surface) => true,
75
76            // Surface can detect subsurface (with sonar/ASW)
77            (Domain::Surface, Domain::Subsurface) => true,
78
79            // Air cannot directly detect subsurface
80            (Domain::Air, Domain::Subsurface) => false,
81
82            // Subsurface cannot directly detect air
83            (Domain::Subsurface, Domain::Air) => false,
84
85            // Unspecified defaults to surface behavior
86            (Domain::Unspecified, target) => Domain::Surface.can_detect(target),
87            (source, Domain::Unspecified) => source.can_detect(Domain::Surface),
88        }
89    }
90
91    /// Check if this domain can engage the target domain
92    ///
93    /// Returns true if weapons from this domain can reach targets in the target domain.
94    pub fn can_engage(&self, target: Domain) -> bool {
95        match (self, target) {
96            // Same domain - always engageable
97            (Domain::Subsurface, Domain::Subsurface) => true,
98            (Domain::Surface, Domain::Surface) => true,
99            (Domain::Air, Domain::Air) => true,
100
101            // Air can strike surface
102            (Domain::Air, Domain::Surface) => true,
103
104            // Surface can engage air (ADA)
105            (Domain::Surface, Domain::Air) => true,
106
107            // Surface can engage subsurface (ASW)
108            (Domain::Surface, Domain::Subsurface) => true,
109
110            // Subsurface can engage surface (torpedoes, missiles)
111            (Domain::Subsurface, Domain::Surface) => true,
112
113            // Air can engage subsurface (ASW aircraft, sonobuoys + torpedoes)
114            (Domain::Air, Domain::Subsurface) => true,
115
116            // Subsurface typically cannot engage air directly
117            (Domain::Subsurface, Domain::Air) => false,
118
119            // Unspecified defaults to surface behavior
120            (Domain::Unspecified, target) => Domain::Surface.can_engage(target),
121            (source, Domain::Unspecified) => source.can_engage(Domain::Surface),
122        }
123    }
124
125    /// Get all valid domains (excluding Unspecified)
126    pub fn all() -> &'static [Domain] {
127        &[Domain::Subsurface, Domain::Surface, Domain::Air]
128    }
129
130    /// Parse domain from string (case-insensitive)
131    pub fn parse(s: &str) -> Option<Domain> {
132        match s.to_lowercase().as_str() {
133            "subsurface" | "sub" | "underwater" | "underground" => Some(Domain::Subsurface),
134            "surface" | "sfc" | "ground" | "sea" => Some(Domain::Surface),
135            "air" | "airborne" | "aerial" | "sky" => Some(Domain::Air),
136            _ => None,
137        }
138    }
139}
140
141impl TryFrom<i32> for Domain {
142    type Error = ();
143
144    fn try_from(value: i32) -> Result<Self, Self::Error> {
145        match value {
146            0 => Ok(Domain::Unspecified),
147            1 => Ok(Domain::Subsurface),
148            2 => Ok(Domain::Surface),
149            3 => Ok(Domain::Air),
150            _ => Err(()),
151        }
152    }
153}
154
155impl std::fmt::Display for Domain {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        write!(f, "{}", self.name())
158    }
159}
160
161/// A set of domains that a capability or node can operate in
162#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
163pub struct DomainSet {
164    subsurface: bool,
165    surface: bool,
166    air: bool,
167}
168
169impl DomainSet {
170    /// Create an empty domain set
171    pub fn empty() -> Self {
172        Self::default()
173    }
174
175    /// Create a domain set with a single domain
176    pub fn single(domain: Domain) -> Self {
177        let mut set = Self::empty();
178        set.add(domain);
179        set
180    }
181
182    /// Create a domain set from multiple domains
183    pub fn from_domains(domains: &[Domain]) -> Self {
184        let mut set = Self::empty();
185        for domain in domains {
186            set.add(*domain);
187        }
188        set
189    }
190
191    /// Create a domain set covering all domains
192    pub fn all() -> Self {
193        Self {
194            subsurface: true,
195            surface: true,
196            air: true,
197        }
198    }
199
200    /// Add a domain to the set
201    pub fn add(&mut self, domain: Domain) {
202        match domain {
203            Domain::Subsurface => self.subsurface = true,
204            Domain::Surface => self.surface = true,
205            Domain::Air => self.air = true,
206            Domain::Unspecified => {} // No-op
207        }
208    }
209
210    /// Remove a domain from the set
211    pub fn remove(&mut self, domain: Domain) {
212        match domain {
213            Domain::Subsurface => self.subsurface = false,
214            Domain::Surface => self.surface = false,
215            Domain::Air => self.air = false,
216            Domain::Unspecified => {} // No-op
217        }
218    }
219
220    /// Check if the set contains a domain
221    pub fn contains(&self, domain: Domain) -> bool {
222        match domain {
223            Domain::Subsurface => self.subsurface,
224            Domain::Surface => self.surface,
225            Domain::Air => self.air,
226            Domain::Unspecified => true, // Unspecified matches any
227        }
228    }
229
230    /// Check if the set is empty
231    pub fn is_empty(&self) -> bool {
232        !self.subsurface && !self.surface && !self.air
233    }
234
235    /// Count the number of domains in the set
236    pub fn count(&self) -> usize {
237        (self.subsurface as usize) + (self.surface as usize) + (self.air as usize)
238    }
239
240    /// Check if this set covers multiple domains
241    pub fn is_multi_domain(&self) -> bool {
242        self.count() > 1
243    }
244
245    /// Get the intersection of two domain sets
246    pub fn intersection(&self, other: &DomainSet) -> DomainSet {
247        DomainSet {
248            subsurface: self.subsurface && other.subsurface,
249            surface: self.surface && other.surface,
250            air: self.air && other.air,
251        }
252    }
253
254    /// Get the union of two domain sets
255    pub fn union(&self, other: &DomainSet) -> DomainSet {
256        DomainSet {
257            subsurface: self.subsurface || other.subsurface,
258            surface: self.surface || other.surface,
259            air: self.air || other.air,
260        }
261    }
262
263    /// Iterate over the domains in the set
264    pub fn iter(&self) -> impl Iterator<Item = Domain> + '_ {
265        Domain::all().iter().copied().filter(|d| self.contains(*d))
266    }
267
268    /// Convert to a vector of domains
269    pub fn to_vec(&self) -> Vec<Domain> {
270        self.iter().collect()
271    }
272}
273
274impl std::fmt::Display for DomainSet {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        let domains: Vec<&str> = self.iter().map(|d| d.code()).collect();
277        if domains.is_empty() {
278            write!(f, "NONE")
279        } else {
280            write!(f, "{}", domains.join("+"))
281        }
282    }
283}
284
285/// Sensor type with associated domain constraints
286#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
287pub enum SensorType {
288    /// Electro-optical (visible light cameras)
289    /// Domains: Surface, Air (cannot penetrate water)
290    ElectroOptical,
291    /// Infrared (thermal imaging)
292    /// Domains: Surface, Air (cannot penetrate water)
293    Infrared,
294    /// Radar (radio detection and ranging)
295    /// Domains: Surface, Air (limited water penetration)
296    Radar,
297    /// Sonar (sound navigation and ranging)
298    /// Domains: Subsurface, Surface (underwater only)
299    Sonar,
300    /// Acoustic (passive sound detection)
301    /// Domains: All (sound propagates everywhere)
302    Acoustic,
303    /// SIGINT (signals intelligence)
304    /// Domains: All (radio waves propagate through air/space)
305    Sigint,
306    /// Magnetic Anomaly Detector
307    /// Domains: Air, Surface (for detecting subsurface)
308    Mad,
309}
310
311impl SensorType {
312    /// Get the domains this sensor type can operate in
313    pub fn operating_domains(&self) -> DomainSet {
314        match self {
315            SensorType::ElectroOptical => DomainSet::from_domains(&[Domain::Surface, Domain::Air]),
316            SensorType::Infrared => DomainSet::from_domains(&[Domain::Surface, Domain::Air]),
317            SensorType::Radar => DomainSet::from_domains(&[Domain::Surface, Domain::Air]),
318            SensorType::Sonar => DomainSet::from_domains(&[Domain::Subsurface, Domain::Surface]),
319            SensorType::Acoustic => DomainSet::all(),
320            SensorType::Sigint => DomainSet::all(),
321            SensorType::Mad => DomainSet::from_domains(&[Domain::Surface, Domain::Air]),
322        }
323    }
324
325    /// Get the domains this sensor type can detect targets in
326    pub fn detection_domains(&self) -> DomainSet {
327        match self {
328            SensorType::ElectroOptical => DomainSet::from_domains(&[Domain::Surface, Domain::Air]),
329            SensorType::Infrared => DomainSet::from_domains(&[Domain::Surface, Domain::Air]),
330            SensorType::Radar => DomainSet::from_domains(&[Domain::Surface, Domain::Air]),
331            SensorType::Sonar => DomainSet::from_domains(&[Domain::Subsurface, Domain::Surface]),
332            SensorType::Acoustic => DomainSet::all(),
333            SensorType::Sigint => DomainSet::all(),
334            // MAD specifically detects subsurface from air/surface
335            SensorType::Mad => DomainSet::single(Domain::Subsurface),
336        }
337    }
338
339    /// Get the sensor type name
340    pub fn name(&self) -> &'static str {
341        match self {
342            SensorType::ElectroOptical => "Electro-Optical",
343            SensorType::Infrared => "Infrared",
344            SensorType::Radar => "Radar",
345            SensorType::Sonar => "Sonar",
346            SensorType::Acoustic => "Acoustic",
347            SensorType::Sigint => "SIGINT",
348            SensorType::Mad => "MAD",
349        }
350    }
351
352    /// Get the short code for display
353    pub fn code(&self) -> &'static str {
354        match self {
355            SensorType::ElectroOptical => "EO",
356            SensorType::Infrared => "IR",
357            SensorType::Radar => "RAD",
358            SensorType::Sonar => "SON",
359            SensorType::Acoustic => "ACO",
360            SensorType::Sigint => "SIG",
361            SensorType::Mad => "MAD",
362        }
363    }
364}
365
366/// Domain-aware detection check result
367#[derive(Debug, Clone, PartialEq)]
368pub struct DetectionCheck {
369    /// Can the sensor detect the target?
370    pub can_detect: bool,
371    /// Reason for the result
372    pub reason: String,
373    /// Detection modifier based on cross-domain factors
374    pub modifier: i32,
375}
376
377impl DetectionCheck {
378    /// Check if a sensor in one domain can detect a target in another domain
379    pub fn check(
380        sensor_domain: Domain,
381        sensor_type: SensorType,
382        target_domain: Domain,
383    ) -> DetectionCheck {
384        let operating = sensor_type.operating_domains();
385        let detecting = sensor_type.detection_domains();
386
387        // Sensor must be able to operate in its domain
388        if !operating.contains(sensor_domain) {
389            return DetectionCheck {
390                can_detect: false,
391                reason: format!(
392                    "{} cannot operate in {} domain",
393                    sensor_type.name(),
394                    sensor_domain.name()
395                ),
396                modifier: 0,
397            };
398        }
399
400        // Sensor must be able to detect targets in the target domain
401        if !detecting.contains(target_domain) {
402            return DetectionCheck {
403                can_detect: false,
404                reason: format!(
405                    "{} cannot detect targets in {} domain",
406                    sensor_type.name(),
407                    target_domain.name()
408                ),
409                modifier: 0,
410            };
411        }
412
413        // Cross-domain detection has penalties
414        let modifier = if sensor_domain == target_domain {
415            0 // Same domain - no penalty
416        } else {
417            match (sensor_domain, target_domain) {
418                // Air looking down at surface - slight advantage
419                (Domain::Air, Domain::Surface) => 1,
420                // Surface looking up at air - slight penalty
421                (Domain::Surface, Domain::Air) => -1,
422                // Cross-domain ASW is harder
423                (Domain::Air, Domain::Subsurface) => -2,
424                (Domain::Surface, Domain::Subsurface) => -1,
425                // Subsurface detecting surface - moderate
426                (Domain::Subsurface, Domain::Surface) => -1,
427                _ => 0,
428            }
429        };
430
431        DetectionCheck {
432            can_detect: true,
433            reason: format!(
434                "{} in {} can detect {} targets",
435                sensor_type.name(),
436                sensor_domain.name(),
437                target_domain.name()
438            ),
439            modifier,
440        }
441    }
442}
443
444#[cfg(test)]
445mod tests {
446    use super::*;
447
448    #[test]
449    fn test_domain_names() {
450        assert_eq!(Domain::Subsurface.name(), "Subsurface");
451        assert_eq!(Domain::Surface.name(), "Surface");
452        assert_eq!(Domain::Air.name(), "Air");
453        assert_eq!(Domain::Unspecified.name(), "Unspecified");
454    }
455
456    #[test]
457    fn test_domain_codes() {
458        assert_eq!(Domain::Subsurface.code(), "SUB");
459        assert_eq!(Domain::Surface.code(), "SFC");
460        assert_eq!(Domain::Air.code(), "AIR");
461    }
462
463    #[test]
464    fn test_domain_same_domain_detection() {
465        assert!(Domain::Subsurface.can_detect(Domain::Subsurface));
466        assert!(Domain::Surface.can_detect(Domain::Surface));
467        assert!(Domain::Air.can_detect(Domain::Air));
468    }
469
470    #[test]
471    fn test_domain_cross_domain_detection() {
472        // Air can see surface
473        assert!(Domain::Air.can_detect(Domain::Surface));
474        // Surface can see air
475        assert!(Domain::Surface.can_detect(Domain::Air));
476        // Air cannot see subsurface directly
477        assert!(!Domain::Air.can_detect(Domain::Subsurface));
478        // Subsurface cannot see air
479        assert!(!Domain::Subsurface.can_detect(Domain::Air));
480        // Surface can detect subsurface (sonar)
481        assert!(Domain::Surface.can_detect(Domain::Subsurface));
482        // Subsurface can detect surface (periscope)
483        assert!(Domain::Subsurface.can_detect(Domain::Surface));
484    }
485
486    #[test]
487    fn test_domain_engagement() {
488        // Same domain engagement
489        assert!(Domain::Air.can_engage(Domain::Air));
490        assert!(Domain::Surface.can_engage(Domain::Surface));
491        assert!(Domain::Subsurface.can_engage(Domain::Subsurface));
492
493        // Air can strike surface
494        assert!(Domain::Air.can_engage(Domain::Surface));
495        // Air can engage subsurface (ASW)
496        assert!(Domain::Air.can_engage(Domain::Subsurface));
497        // Surface can engage air (ADA)
498        assert!(Domain::Surface.can_engage(Domain::Air));
499        // Surface can engage subsurface (ASW)
500        assert!(Domain::Surface.can_engage(Domain::Subsurface));
501        // Subsurface can engage surface
502        assert!(Domain::Subsurface.can_engage(Domain::Surface));
503        // Subsurface cannot engage air directly
504        assert!(!Domain::Subsurface.can_engage(Domain::Air));
505    }
506
507    #[test]
508    fn test_domain_from_i32() {
509        assert_eq!(Domain::try_from(0), Ok(Domain::Unspecified));
510        assert_eq!(Domain::try_from(1), Ok(Domain::Subsurface));
511        assert_eq!(Domain::try_from(2), Ok(Domain::Surface));
512        assert_eq!(Domain::try_from(3), Ok(Domain::Air));
513        assert!(Domain::try_from(99).is_err());
514    }
515
516    #[test]
517    fn test_domain_from_str() {
518        assert_eq!(Domain::parse("subsurface"), Some(Domain::Subsurface));
519        assert_eq!(Domain::parse("SUB"), Some(Domain::Subsurface));
520        assert_eq!(Domain::parse("underwater"), Some(Domain::Subsurface));
521        assert_eq!(Domain::parse("surface"), Some(Domain::Surface));
522        assert_eq!(Domain::parse("ground"), Some(Domain::Surface));
523        assert_eq!(Domain::parse("air"), Some(Domain::Air));
524        assert_eq!(Domain::parse("AIRBORNE"), Some(Domain::Air));
525        assert_eq!(Domain::parse("invalid"), None);
526    }
527
528    #[test]
529    fn test_domain_default() {
530        assert_eq!(Domain::default(), Domain::Unspecified);
531    }
532
533    #[test]
534    fn test_domain_all() {
535        let all = Domain::all();
536        assert_eq!(all.len(), 3);
537        assert!(all.contains(&Domain::Subsurface));
538        assert!(all.contains(&Domain::Surface));
539        assert!(all.contains(&Domain::Air));
540        assert!(!all.contains(&Domain::Unspecified));
541    }
542
543    // DomainSet tests
544
545    #[test]
546    fn test_domain_set_empty() {
547        let set = DomainSet::empty();
548        assert!(set.is_empty());
549        assert_eq!(set.count(), 0);
550        assert!(!set.is_multi_domain());
551    }
552
553    #[test]
554    fn test_domain_set_single() {
555        let set = DomainSet::single(Domain::Air);
556        assert!(!set.is_empty());
557        assert_eq!(set.count(), 1);
558        assert!(set.contains(Domain::Air));
559        assert!(!set.contains(Domain::Surface));
560        assert!(!set.is_multi_domain());
561    }
562
563    #[test]
564    fn test_domain_set_all() {
565        let set = DomainSet::all();
566        assert_eq!(set.count(), 3);
567        assert!(set.is_multi_domain());
568        assert!(set.contains(Domain::Subsurface));
569        assert!(set.contains(Domain::Surface));
570        assert!(set.contains(Domain::Air));
571    }
572
573    #[test]
574    fn test_domain_set_from_domains() {
575        let set = DomainSet::from_domains(&[Domain::Air, Domain::Surface]);
576        assert_eq!(set.count(), 2);
577        assert!(set.is_multi_domain());
578        assert!(set.contains(Domain::Air));
579        assert!(set.contains(Domain::Surface));
580        assert!(!set.contains(Domain::Subsurface));
581    }
582
583    #[test]
584    fn test_domain_set_add_remove() {
585        let mut set = DomainSet::empty();
586
587        set.add(Domain::Air);
588        assert!(set.contains(Domain::Air));
589        assert_eq!(set.count(), 1);
590
591        set.add(Domain::Surface);
592        assert_eq!(set.count(), 2);
593
594        set.remove(Domain::Air);
595        assert!(!set.contains(Domain::Air));
596        assert_eq!(set.count(), 1);
597    }
598
599    #[test]
600    fn test_domain_set_intersection() {
601        let set1 = DomainSet::from_domains(&[Domain::Air, Domain::Surface]);
602        let set2 = DomainSet::from_domains(&[Domain::Surface, Domain::Subsurface]);
603
604        let intersection = set1.intersection(&set2);
605        assert_eq!(intersection.count(), 1);
606        assert!(intersection.contains(Domain::Surface));
607    }
608
609    #[test]
610    fn test_domain_set_union() {
611        let set1 = DomainSet::from_domains(&[Domain::Air]);
612        let set2 = DomainSet::from_domains(&[Domain::Surface]);
613
614        let union = set1.union(&set2);
615        assert_eq!(union.count(), 2);
616        assert!(union.contains(Domain::Air));
617        assert!(union.contains(Domain::Surface));
618    }
619
620    #[test]
621    fn test_domain_set_iter() {
622        let set = DomainSet::from_domains(&[Domain::Air, Domain::Subsurface]);
623        let domains: Vec<Domain> = set.iter().collect();
624
625        assert_eq!(domains.len(), 2);
626        assert!(domains.contains(&Domain::Air));
627        assert!(domains.contains(&Domain::Subsurface));
628    }
629
630    #[test]
631    fn test_domain_set_display() {
632        assert_eq!(DomainSet::empty().to_string(), "NONE");
633        assert_eq!(DomainSet::single(Domain::Air).to_string(), "AIR");
634        assert_eq!(
635            DomainSet::from_domains(&[Domain::Air, Domain::Surface]).to_string(),
636            "SFC+AIR"
637        );
638    }
639
640    // SensorType tests
641
642    #[test]
643    fn test_sensor_type_eo_domains() {
644        let eo = SensorType::ElectroOptical;
645        let operating = eo.operating_domains();
646        let detecting = eo.detection_domains();
647
648        assert!(operating.contains(Domain::Surface));
649        assert!(operating.contains(Domain::Air));
650        assert!(!operating.contains(Domain::Subsurface));
651
652        assert!(detecting.contains(Domain::Surface));
653        assert!(detecting.contains(Domain::Air));
654        assert!(!detecting.contains(Domain::Subsurface));
655    }
656
657    #[test]
658    fn test_sensor_type_sonar_domains() {
659        let sonar = SensorType::Sonar;
660        let operating = sonar.operating_domains();
661        let detecting = sonar.detection_domains();
662
663        assert!(operating.contains(Domain::Subsurface));
664        assert!(operating.contains(Domain::Surface));
665        assert!(!operating.contains(Domain::Air));
666
667        assert!(detecting.contains(Domain::Subsurface));
668        assert!(detecting.contains(Domain::Surface));
669    }
670
671    #[test]
672    fn test_sensor_type_mad_domains() {
673        let mad = SensorType::Mad;
674        let operating = mad.operating_domains();
675        let detecting = mad.detection_domains();
676
677        // MAD operates from air/surface
678        assert!(operating.contains(Domain::Air));
679        assert!(operating.contains(Domain::Surface));
680        // MAD specifically detects subsurface
681        assert!(detecting.contains(Domain::Subsurface));
682        assert_eq!(detecting.count(), 1);
683    }
684
685    #[test]
686    fn test_sensor_type_acoustic_all_domains() {
687        let acoustic = SensorType::Acoustic;
688        let operating = acoustic.operating_domains();
689
690        assert_eq!(operating.count(), 3);
691        assert!(operating.contains(Domain::Subsurface));
692        assert!(operating.contains(Domain::Surface));
693        assert!(operating.contains(Domain::Air));
694    }
695
696    // DetectionCheck tests
697
698    #[test]
699    fn test_detection_check_same_domain() {
700        let check = DetectionCheck::check(Domain::Air, SensorType::Radar, Domain::Air);
701        assert!(check.can_detect);
702        assert_eq!(check.modifier, 0);
703    }
704
705    #[test]
706    fn test_detection_check_air_to_surface() {
707        let check = DetectionCheck::check(Domain::Air, SensorType::ElectroOptical, Domain::Surface);
708        assert!(check.can_detect);
709        assert_eq!(check.modifier, 1); // Advantage from altitude
710    }
711
712    #[test]
713    fn test_detection_check_surface_to_air() {
714        let check = DetectionCheck::check(Domain::Surface, SensorType::Radar, Domain::Air);
715        assert!(check.can_detect);
716        assert_eq!(check.modifier, -1); // Slight penalty looking up
717    }
718
719    #[test]
720    fn test_detection_check_eo_cannot_detect_subsurface() {
721        let check = DetectionCheck::check(
722            Domain::Surface,
723            SensorType::ElectroOptical,
724            Domain::Subsurface,
725        );
726        assert!(!check.can_detect);
727        assert!(check.reason.contains("cannot detect"));
728    }
729
730    #[test]
731    fn test_detection_check_sonar_cannot_operate_in_air() {
732        let check = DetectionCheck::check(Domain::Air, SensorType::Sonar, Domain::Subsurface);
733        assert!(!check.can_detect);
734        assert!(check.reason.contains("cannot operate"));
735    }
736
737    #[test]
738    fn test_detection_check_mad_from_air_to_subsurface() {
739        let check = DetectionCheck::check(Domain::Air, SensorType::Mad, Domain::Subsurface);
740        assert!(check.can_detect);
741        assert_eq!(check.modifier, -2); // Cross-domain ASW penalty
742    }
743
744    #[test]
745    fn test_detection_check_sonar_subsurface_to_surface() {
746        let check = DetectionCheck::check(Domain::Subsurface, SensorType::Sonar, Domain::Surface);
747        assert!(check.can_detect);
748        assert_eq!(check.modifier, -1); // Cross-domain penalty
749    }
750}