Skip to main content

peat_protocol/cot/
types.rs

1//! Peat message types for TAK integration
2//!
3//! These types represent Peat messages that can be translated to/from CoT format
4//! for integration with TAK (Team Awareness Kit) systems.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Position with WGS84 coordinates and accuracy
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12pub struct Position {
13    /// Latitude in degrees (WGS84)
14    pub lat: f64,
15    /// Longitude in degrees (WGS84)
16    pub lon: f64,
17    /// Circular Error Probable in meters (accuracy)
18    pub cep_m: Option<f64>,
19    /// Height Above Ellipsoid in meters
20    pub hae: Option<f64>,
21}
22
23impl Position {
24    /// Create a new position
25    pub fn new(lat: f64, lon: f64) -> Self {
26        Self {
27            lat,
28            lon,
29            cep_m: None,
30            hae: None,
31        }
32    }
33
34    /// Create a position with accuracy
35    pub fn with_accuracy(lat: f64, lon: f64, cep_m: f64) -> Self {
36        Self {
37            lat,
38            lon,
39            cep_m: Some(cep_m),
40            hae: None,
41        }
42    }
43
44    /// Create a position with full 3D coordinates
45    pub fn with_altitude(lat: f64, lon: f64, hae: f64, cep_m: Option<f64>) -> Self {
46        Self {
47            lat,
48            lon,
49            cep_m,
50            hae: Some(hae),
51        }
52    }
53}
54
55/// Velocity with bearing and speed
56#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
57pub struct Velocity {
58    /// Bearing in degrees (0 = North, clockwise)
59    pub bearing: f64,
60    /// Speed in meters per second
61    pub speed_mps: f64,
62}
63
64impl Velocity {
65    /// Create a new velocity
66    pub fn new(bearing: f64, speed_mps: f64) -> Self {
67        Self { bearing, speed_mps }
68    }
69
70    /// Check if stationary (speed below threshold)
71    pub fn is_stationary(&self, threshold_mps: f64) -> bool {
72        self.speed_mps < threshold_mps
73    }
74}
75
76/// Track update from a Peat platform's sensor
77///
78/// Represents a detected entity (person, vehicle, etc.) being tracked by a Peat sensor.
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct TrackUpdate {
81    /// Unique track identifier
82    pub track_id: String,
83    /// Classification of tracked entity (person, vehicle, aircraft, etc.)
84    pub classification: String,
85    /// Detection confidence (0.0 - 1.0)
86    pub confidence: f64,
87    /// Current position
88    pub position: Position,
89    /// Current velocity (if available)
90    pub velocity: Option<Velocity>,
91    /// Custom attributes (key-value pairs)
92    pub attributes: HashMap<String, serde_json::Value>,
93    /// Platform that detected this track
94    pub source_platform: String,
95    /// AI model that generated detection
96    pub source_model: String,
97    /// Version of the AI model
98    pub model_version: String,
99    /// Timestamp of the update
100    pub timestamp: DateTime<Utc>,
101    /// Cell membership (if assigned)
102    pub cell_id: Option<String>,
103    /// Formation membership (if assigned)
104    pub formation_id: Option<String>,
105}
106
107impl TrackUpdate {
108    /// Create a new track update
109    pub fn new(
110        track_id: String,
111        classification: String,
112        confidence: f64,
113        position: Position,
114        source_platform: String,
115        source_model: String,
116        model_version: String,
117    ) -> Self {
118        Self {
119            track_id,
120            classification,
121            confidence: confidence.clamp(0.0, 1.0),
122            position,
123            velocity: None,
124            attributes: HashMap::new(),
125            source_platform,
126            source_model,
127            model_version,
128            timestamp: Utc::now(),
129            cell_id: None,
130            formation_id: None,
131        }
132    }
133
134    /// Add an attribute
135    pub fn with_attribute(mut self, key: &str, value: serde_json::Value) -> Self {
136        self.attributes.insert(key.to_string(), value);
137        self
138    }
139
140    /// Set velocity
141    pub fn with_velocity(mut self, velocity: Velocity) -> Self {
142        self.velocity = Some(velocity);
143        self
144    }
145
146    /// Set cell membership
147    pub fn with_cell(mut self, cell_id: String) -> Self {
148        self.cell_id = Some(cell_id);
149        self
150    }
151
152    /// Set formation membership
153    pub fn with_formation(mut self, formation_id: String) -> Self {
154        self.formation_id = Some(formation_id);
155        self
156    }
157}
158
159/// Operational status of a platform or capability
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
161pub enum OperationalStatus {
162    /// Platform is ready but not actively processing
163    Ready,
164    /// Platform is actively processing/sensing
165    Active,
166    /// Platform has reduced capability
167    Degraded,
168    /// Platform is offline
169    Offline,
170    /// Platform is loading/initializing
171    Loading,
172}
173
174impl OperationalStatus {
175    /// Convert to CoT-compatible string
176    pub fn as_str(&self) -> &'static str {
177        match self {
178            Self::Ready => "READY",
179            Self::Active => "ACTIVE",
180            Self::Degraded => "DEGRADED",
181            Self::Offline => "OFFLINE",
182            Self::Loading => "LOADING",
183        }
184    }
185}
186
187/// Capability advertisement from a Peat platform
188///
189/// Announces what a platform can do (sensor types, compute capabilities, etc.)
190#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
191pub struct CapabilityAdvertisement {
192    /// Platform identifier
193    pub platform_id: String,
194    /// Platform type (UGV, UAV, Soldier System, etc.)
195    pub platform_type: String,
196    /// Current position
197    pub position: Position,
198    /// Operational status
199    pub status: OperationalStatus,
200    /// Readiness level (0.0 - 1.0)
201    pub readiness: f64,
202    /// Capabilities offered by this platform
203    pub capabilities: Vec<CapabilityInfo>,
204    /// Cell membership (if assigned)
205    pub cell_id: Option<String>,
206    /// Formation membership (if assigned)
207    pub formation_id: Option<String>,
208    /// Timestamp of the advertisement
209    pub timestamp: DateTime<Utc>,
210}
211
212/// Information about a single capability
213#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
214pub struct CapabilityInfo {
215    /// Capability type (OBJECT_TRACKING, COMPUTE, COMMUNICATION, etc.)
216    pub capability_type: String,
217    /// Model or sensor name
218    pub model_name: String,
219    /// Version string
220    pub version: String,
221    /// Precision/confidence of this capability (0.0 - 1.0)
222    pub precision: f64,
223    /// Current status
224    pub status: OperationalStatus,
225}
226
227impl CapabilityAdvertisement {
228    /// Create a new capability advertisement
229    pub fn new(
230        platform_id: String,
231        platform_type: String,
232        position: Position,
233        status: OperationalStatus,
234        readiness: f64,
235    ) -> Self {
236        Self {
237            platform_id,
238            platform_type,
239            position,
240            status,
241            readiness: readiness.clamp(0.0, 1.0),
242            capabilities: Vec::new(),
243            cell_id: None,
244            formation_id: None,
245            timestamp: Utc::now(),
246        }
247    }
248
249    /// Add a capability
250    pub fn with_capability(mut self, capability: CapabilityInfo) -> Self {
251        self.capabilities.push(capability);
252        self
253    }
254
255    /// Set cell membership
256    pub fn with_cell(mut self, cell_id: String) -> Self {
257        self.cell_id = Some(cell_id);
258        self
259    }
260}
261
262/// State of a handoff operation
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
264pub enum HandoffState {
265    /// Handoff initiated, awaiting acceptance
266    Initiated,
267    /// Handoff accepted by receiving cell
268    Accepted,
269    /// Track custody transferred
270    Transferred,
271    /// Handoff completed successfully
272    Completed,
273    /// Handoff failed or rejected
274    Failed,
275}
276
277impl HandoffState {
278    /// Convert to string
279    pub fn as_str(&self) -> &'static str {
280        match self {
281            Self::Initiated => "INITIATED",
282            Self::Accepted => "ACCEPTED",
283            Self::Transferred => "TRANSFERRED",
284            Self::Completed => "COMPLETED",
285            Self::Failed => "FAILED",
286        }
287    }
288}
289
290/// Handoff message for track custody transfer between cells
291#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
292pub struct HandoffMessage {
293    /// Track being handed off
294    pub track_id: String,
295    /// Current track position
296    pub position: Position,
297    /// Source cell releasing the track
298    pub source_cell: String,
299    /// Target cell receiving the track
300    pub target_cell: String,
301    /// Current handoff state
302    pub state: HandoffState,
303    /// Reason for handoff (boundary crossing, capability match, etc.)
304    pub reason: String,
305    /// Priority level (1-5, with 1 being highest)
306    pub priority: u8,
307    /// Timestamp
308    pub timestamp: DateTime<Utc>,
309}
310
311impl HandoffMessage {
312    /// Create a new handoff message
313    pub fn new(
314        track_id: String,
315        position: Position,
316        source_cell: String,
317        target_cell: String,
318        reason: String,
319    ) -> Self {
320        Self {
321            track_id,
322            position,
323            source_cell,
324            target_cell,
325            state: HandoffState::Initiated,
326            reason,
327            priority: 3, // Default normal priority
328            timestamp: Utc::now(),
329        }
330    }
331
332    /// Set priority
333    pub fn with_priority(mut self, priority: u8) -> Self {
334        self.priority = priority.clamp(1, 5);
335        self
336    }
337
338    /// Update state
339    pub fn with_state(mut self, state: HandoffState) -> Self {
340        self.state = state;
341        self
342    }
343}
344
345/// Aggregated capability summary for a formation
346#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
347pub struct FormationCapabilitySummary {
348    /// Formation identifier
349    pub formation_id: String,
350    /// Formation name/callsign
351    pub callsign: String,
352    /// Center position of formation
353    pub center_position: Position,
354    /// Number of active platforms
355    pub platform_count: u32,
356    /// Number of cells in formation
357    pub cell_count: u32,
358    /// Aggregated capabilities
359    pub capabilities: Vec<AggregatedCapability>,
360    /// Overall formation readiness (0.0 - 1.0)
361    pub readiness: f64,
362    /// Timestamp
363    pub timestamp: DateTime<Utc>,
364}
365
366/// Aggregated capability across multiple platforms
367#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
368pub struct AggregatedCapability {
369    /// Capability type
370    pub capability_type: String,
371    /// Number of platforms with this capability
372    pub count: u32,
373    /// Average precision across platforms
374    pub avg_precision: f64,
375    /// Percentage of platforms that are active
376    pub availability: f64,
377}
378
379// =============================================================================
380// Mission Task Types (Issue #318: TAK → Peat direction)
381// =============================================================================
382
383/// Mission task type enumeration
384///
385/// Maps to CoT mission types as defined in CONTRACT_CORE_ATAK_TAK_BRIDGE.md
386#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
387pub enum MissionTaskType {
388    /// Track a specific target (CoT: t-x-m-c-c → TRACK_TARGET)
389    TrackTarget,
390    /// Search an area for targets (CoT: t-x-m-c-s → SEARCH_AREA)
391    SearchArea,
392    /// Monitor a zone continuously
393    MonitorZone,
394    /// Abort current mission (CoT: t-x-m-c-a)
395    Abort,
396}
397
398impl MissionTaskType {
399    /// Convert to string
400    pub fn as_str(&self) -> &'static str {
401        match self {
402            Self::TrackTarget => "TRACK_TARGET",
403            Self::SearchArea => "SEARCH_AREA",
404            Self::MonitorZone => "MONITOR_ZONE",
405            Self::Abort => "ABORT",
406        }
407    }
408
409    /// Parse from CoT type string
410    pub fn from_cot_type(cot_type: &str) -> Option<Self> {
411        match cot_type {
412            "t-x-m-c-c" => Some(Self::TrackTarget),
413            "t-x-m-c-s" => Some(Self::SearchArea),
414            "t-x-m-c-a" => Some(Self::Abort),
415            _ if cot_type.starts_with("t-x-m-c") => Some(Self::TrackTarget), // Default mission
416            _ => None,
417        }
418    }
419}
420
421/// Mission priority level
422#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
423pub enum MissionPriority {
424    Critical,
425    High,
426    #[default]
427    Normal,
428    Low,
429}
430
431impl MissionPriority {
432    pub fn as_str(&self) -> &'static str {
433        match self {
434            Self::Critical => "CRITICAL",
435            Self::High => "HIGH",
436            Self::Normal => "NORMAL",
437            Self::Low => "LOW",
438        }
439    }
440}
441
442/// Target information for a mission
443#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
444pub struct MissionTarget {
445    /// Description of the target
446    pub description: String,
447    /// Last known position
448    pub last_known_position: Option<Position>,
449}
450
451/// Boundary definition for area missions
452#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
453pub struct MissionBoundary {
454    /// Boundary type
455    pub boundary_type: BoundaryType,
456    /// Polygon coordinates (for polygon type)
457    pub coordinates: Vec<Position>,
458    /// Radius in meters (for circle type)
459    pub radius_m: Option<f64>,
460}
461
462/// Type of boundary
463#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
464pub enum BoundaryType {
465    Polygon,
466    Circle,
467}
468
469/// Mission task from C2/TAK Server (Issue #318)
470///
471/// Represents a mission tasking command received from TAK Server.
472/// This is the Peat representation of CoT mission task events.
473///
474/// CoT Event Types handled:
475/// - `t-x-m-c-c`: Track target command → TrackTarget
476/// - `t-x-m-c-s`: Search area command → SearchArea
477/// - `t-x-m-c-a`: Abort command → Abort
478#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
479pub struct MissionTask {
480    /// Unique task identifier (from CoT event@uid)
481    pub task_id: String,
482    /// Mission type
483    pub task_type: MissionTaskType,
484    /// When the task was issued (from CoT event@time)
485    pub issued_at: DateTime<Utc>,
486    /// Who issued the task (CoT source UID or callsign)
487    pub issued_by: String,
488    /// When the task expires (from CoT event@stale)
489    pub expires_at: DateTime<Utc>,
490    /// Target information (for TRACK_TARGET missions)
491    pub target: Option<MissionTarget>,
492    /// Boundary/area (for SEARCH_AREA, MONITOR_ZONE missions)
493    pub boundary: Option<MissionBoundary>,
494    /// Task priority
495    pub priority: MissionPriority,
496    /// Objective location (from CoT point)
497    pub objective_position: Option<Position>,
498    /// Raw remarks from CoT (for additional context)
499    pub remarks: Option<String>,
500}
501
502impl MissionTask {
503    /// Create a new mission task
504    pub fn new(
505        task_id: String,
506        task_type: MissionTaskType,
507        issued_by: String,
508        expires_at: DateTime<Utc>,
509    ) -> Self {
510        Self {
511            task_id,
512            task_type,
513            issued_at: Utc::now(),
514            issued_by,
515            expires_at,
516            target: None,
517            boundary: None,
518            priority: MissionPriority::Normal,
519            objective_position: None,
520            remarks: None,
521        }
522    }
523
524    /// Create from a CoT event (Issue #318)
525    ///
526    /// Converts a mission-type CoT event to Peat MissionTask format.
527    pub fn from_cot_event(event: &super::CotEvent) -> Result<Self, MissionTaskError> {
528        let task_type = MissionTaskType::from_cot_type(event.cot_type.as_str()).ok_or(
529            MissionTaskError::InvalidCotType(event.cot_type.as_str().to_string()),
530        )?;
531
532        let mut task = Self {
533            task_id: event.uid.clone(),
534            task_type,
535            issued_at: event.time,
536            issued_by: "TAK-Server".to_string(), // Could extract from CoT contact
537            expires_at: event.stale,
538            target: None,
539            boundary: None,
540            priority: MissionPriority::Normal,
541            objective_position: Some(Position::with_altitude(
542                event.point.lat,
543                event.point.lon,
544                event.point.hae,
545                Some(event.point.ce),
546            )),
547            remarks: event.detail.remarks.clone(),
548        };
549
550        // Extract target description from remarks if present
551        if let Some(ref remarks) = event.detail.remarks {
552            task.target = Some(MissionTarget {
553                description: remarks.clone(),
554                last_known_position: task.objective_position.clone(),
555            });
556        }
557
558        Ok(task)
559    }
560
561    /// Set target information
562    pub fn with_target(mut self, target: MissionTarget) -> Self {
563        self.target = Some(target);
564        self
565    }
566
567    /// Set boundary
568    pub fn with_boundary(mut self, boundary: MissionBoundary) -> Self {
569        self.boundary = Some(boundary);
570        self
571    }
572
573    /// Set priority
574    pub fn with_priority(mut self, priority: MissionPriority) -> Self {
575        self.priority = priority;
576        self
577    }
578
579    /// Set objective position
580    pub fn with_objective_position(mut self, position: Position) -> Self {
581        self.objective_position = Some(position);
582        self
583    }
584
585    /// Check if the mission is expired
586    pub fn is_expired(&self) -> bool {
587        Utc::now() > self.expires_at
588    }
589
590    /// Check if this is a mission-type CoT event
591    pub fn is_mission_cot_type(cot_type: &str) -> bool {
592        cot_type.starts_with("t-x-m-c")
593    }
594
595    /// Serialize to JSON for Automerge storage
596    pub fn to_json(&self) -> Result<String, serde_json::Error> {
597        serde_json::to_string(self)
598    }
599
600    /// Deserialize from JSON
601    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
602        serde_json::from_str(json)
603    }
604}
605
606/// Errors that can occur when creating a MissionTask
607#[derive(Debug, Clone, PartialEq)]
608pub enum MissionTaskError {
609    /// CoT type is not a mission type
610    InvalidCotType(String),
611    /// Missing required field
612    MissingField(&'static str),
613}
614
615impl std::fmt::Display for MissionTaskError {
616    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
617        match self {
618            Self::InvalidCotType(t) => write!(f, "Invalid CoT type for mission task: {}", t),
619            Self::MissingField(field) => write!(f, "Missing required field: {}", field),
620        }
621    }
622}
623
624impl std::error::Error for MissionTaskError {}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629
630    #[test]
631    fn test_position_creation() {
632        let pos = Position::new(33.7749, -84.3958);
633        assert_eq!(pos.lat, 33.7749);
634        assert_eq!(pos.lon, -84.3958);
635        assert!(pos.cep_m.is_none());
636        assert!(pos.hae.is_none());
637    }
638
639    #[test]
640    fn test_position_with_accuracy() {
641        let pos = Position::with_accuracy(33.7749, -84.3958, 2.5);
642        assert_eq!(pos.cep_m, Some(2.5));
643    }
644
645    #[test]
646    fn test_position_with_altitude() {
647        let pos = Position::with_altitude(33.7749, -84.3958, 100.0, Some(2.5));
648        assert_eq!(pos.hae, Some(100.0));
649        assert_eq!(pos.cep_m, Some(2.5));
650    }
651
652    #[test]
653    fn test_velocity_stationary() {
654        let moving = Velocity::new(45.0, 5.0);
655        assert!(!moving.is_stationary(0.5));
656
657        let stationary = Velocity::new(0.0, 0.1);
658        assert!(stationary.is_stationary(0.5));
659    }
660
661    #[test]
662    fn test_track_update_creation() {
663        let track = TrackUpdate::new(
664            "TRACK-001".to_string(),
665            "person".to_string(),
666            0.89,
667            Position::new(33.7749, -84.3958),
668            "Alpha-2".to_string(),
669            "object_tracker".to_string(),
670            "1.3.0".to_string(),
671        );
672
673        assert_eq!(track.track_id, "TRACK-001");
674        assert_eq!(track.confidence, 0.89);
675    }
676
677    #[test]
678    fn test_track_update_confidence_clamped() {
679        let track = TrackUpdate::new(
680            "TRACK-001".to_string(),
681            "person".to_string(),
682            1.5, // Should be clamped to 1.0
683            Position::new(0.0, 0.0),
684            "platform".to_string(),
685            "model".to_string(),
686            "1.0".to_string(),
687        );
688
689        assert_eq!(track.confidence, 1.0);
690    }
691
692    #[test]
693    fn test_track_update_with_attributes() {
694        let track = TrackUpdate::new(
695            "TRACK-001".to_string(),
696            "person".to_string(),
697            0.89,
698            Position::new(0.0, 0.0),
699            "platform".to_string(),
700            "model".to_string(),
701            "1.0".to_string(),
702        )
703        .with_attribute("jacket_color", serde_json::json!("blue"))
704        .with_attribute("has_backpack", serde_json::json!(true));
705
706        assert_eq!(track.attributes.len(), 2);
707        assert_eq!(track.attributes["jacket_color"], "blue");
708    }
709
710    #[test]
711    fn test_capability_advertisement() {
712        let cap = CapabilityAdvertisement::new(
713            "Alpha-3".to_string(),
714            "UGV".to_string(),
715            Position::new(33.7749, -84.3958),
716            OperationalStatus::Active,
717            0.91,
718        )
719        .with_capability(CapabilityInfo {
720            capability_type: "OBJECT_TRACKING".to_string(),
721            model_name: "object_tracker".to_string(),
722            version: "1.3.0".to_string(),
723            precision: 0.94,
724            status: OperationalStatus::Active,
725        });
726
727        assert_eq!(cap.capabilities.len(), 1);
728        assert_eq!(cap.status.as_str(), "ACTIVE");
729    }
730
731    #[test]
732    fn test_handoff_message() {
733        let handoff = HandoffMessage::new(
734            "TRACK-001".to_string(),
735            Position::new(33.78, -84.40),
736            "Alpha-Team".to_string(),
737            "Bravo-Team".to_string(),
738            "boundary_crossing".to_string(),
739        )
740        .with_priority(2);
741
742        assert_eq!(handoff.state, HandoffState::Initiated);
743        assert_eq!(handoff.priority, 2);
744    }
745
746    #[test]
747    fn test_handoff_priority_clamped() {
748        let handoff = HandoffMessage::new(
749            "TRACK-001".to_string(),
750            Position::new(0.0, 0.0),
751            "source".to_string(),
752            "target".to_string(),
753            "test".to_string(),
754        )
755        .with_priority(10); // Should be clamped to 5
756
757        assert_eq!(handoff.priority, 5);
758    }
759
760    #[test]
761    fn test_operational_status_strings() {
762        assert_eq!(OperationalStatus::Ready.as_str(), "READY");
763        assert_eq!(OperationalStatus::Active.as_str(), "ACTIVE");
764        assert_eq!(OperationalStatus::Degraded.as_str(), "DEGRADED");
765        assert_eq!(OperationalStatus::Offline.as_str(), "OFFLINE");
766        assert_eq!(OperationalStatus::Loading.as_str(), "LOADING");
767    }
768
769    #[test]
770    fn test_handoff_state_strings() {
771        assert_eq!(HandoffState::Initiated.as_str(), "INITIATED");
772        assert_eq!(HandoffState::Accepted.as_str(), "ACCEPTED");
773        assert_eq!(HandoffState::Transferred.as_str(), "TRANSFERRED");
774        assert_eq!(HandoffState::Completed.as_str(), "COMPLETED");
775        assert_eq!(HandoffState::Failed.as_str(), "FAILED");
776    }
777
778    // =======================================================================
779    // MissionTask Tests (Issue #318)
780    // =======================================================================
781
782    #[test]
783    fn test_mission_task_type_from_cot() {
784        assert_eq!(
785            MissionTaskType::from_cot_type("t-x-m-c-c"),
786            Some(MissionTaskType::TrackTarget)
787        );
788        assert_eq!(
789            MissionTaskType::from_cot_type("t-x-m-c-s"),
790            Some(MissionTaskType::SearchArea)
791        );
792        assert_eq!(
793            MissionTaskType::from_cot_type("t-x-m-c-a"),
794            Some(MissionTaskType::Abort)
795        );
796        // Generic mission defaults to TrackTarget
797        assert_eq!(
798            MissionTaskType::from_cot_type("t-x-m-c-z"),
799            Some(MissionTaskType::TrackTarget)
800        );
801        // Non-mission types return None
802        assert_eq!(MissionTaskType::from_cot_type("a-f-G-U-C"), None);
803    }
804
805    #[test]
806    fn test_mission_task_type_as_str() {
807        assert_eq!(MissionTaskType::TrackTarget.as_str(), "TRACK_TARGET");
808        assert_eq!(MissionTaskType::SearchArea.as_str(), "SEARCH_AREA");
809        assert_eq!(MissionTaskType::MonitorZone.as_str(), "MONITOR_ZONE");
810        assert_eq!(MissionTaskType::Abort.as_str(), "ABORT");
811    }
812
813    #[test]
814    fn test_mission_priority_default() {
815        let priority = MissionPriority::default();
816        assert_eq!(priority, MissionPriority::Normal);
817    }
818
819    #[test]
820    fn test_mission_priority_as_str() {
821        assert_eq!(MissionPriority::Critical.as_str(), "CRITICAL");
822        assert_eq!(MissionPriority::High.as_str(), "HIGH");
823        assert_eq!(MissionPriority::Normal.as_str(), "NORMAL");
824        assert_eq!(MissionPriority::Low.as_str(), "LOW");
825    }
826
827    #[test]
828    fn test_mission_task_new() {
829        let expires = Utc::now() + chrono::Duration::hours(2);
830        let task = MissionTask::new(
831            "MISSION-001".to_string(),
832            MissionTaskType::TrackTarget,
833            "CMD-ALPHA".to_string(),
834            expires,
835        );
836
837        assert_eq!(task.task_id, "MISSION-001");
838        assert_eq!(task.task_type, MissionTaskType::TrackTarget);
839        assert_eq!(task.issued_by, "CMD-ALPHA");
840        assert_eq!(task.priority, MissionPriority::Normal);
841        assert!(task.target.is_none());
842        assert!(task.boundary.is_none());
843    }
844
845    #[test]
846    fn test_mission_task_with_builder_methods() {
847        let expires = Utc::now() + chrono::Duration::hours(1);
848        let task = MissionTask::new(
849            "MISSION-002".to_string(),
850            MissionTaskType::SearchArea,
851            "CMD-BRAVO".to_string(),
852            expires,
853        )
854        .with_priority(MissionPriority::High)
855        .with_objective_position(Position::new(33.7749, -84.3958))
856        .with_target(MissionTarget {
857            description: "Suspicious vehicle".to_string(),
858            last_known_position: Some(Position::new(33.77, -84.39)),
859        });
860
861        assert_eq!(task.priority, MissionPriority::High);
862        assert!(task.objective_position.is_some());
863        assert!(task.target.is_some());
864        assert_eq!(
865            task.target.as_ref().unwrap().description,
866            "Suspicious vehicle"
867        );
868    }
869
870    #[test]
871    fn test_mission_task_is_expired() {
872        let past_expires = Utc::now() - chrono::Duration::hours(1);
873        let task = MissionTask::new(
874            "EXPIRED-001".to_string(),
875            MissionTaskType::TrackTarget,
876            "CMD".to_string(),
877            past_expires,
878        );
879        assert!(task.is_expired());
880
881        let future_expires = Utc::now() + chrono::Duration::hours(1);
882        let active_task = MissionTask::new(
883            "ACTIVE-001".to_string(),
884            MissionTaskType::TrackTarget,
885            "CMD".to_string(),
886            future_expires,
887        );
888        assert!(!active_task.is_expired());
889    }
890
891    #[test]
892    fn test_mission_task_is_mission_cot_type() {
893        assert!(MissionTask::is_mission_cot_type("t-x-m-c-c"));
894        assert!(MissionTask::is_mission_cot_type("t-x-m-c-s"));
895        assert!(MissionTask::is_mission_cot_type("t-x-m-c-a"));
896        assert!(!MissionTask::is_mission_cot_type("a-f-G-U-C"));
897        assert!(!MissionTask::is_mission_cot_type("b-m-p-s-p-l"));
898    }
899
900    #[test]
901    fn test_mission_task_json_roundtrip() {
902        let expires = Utc::now() + chrono::Duration::hours(2);
903        let task = MissionTask::new(
904            "JSON-001".to_string(),
905            MissionTaskType::SearchArea,
906            "CMD".to_string(),
907            expires,
908        )
909        .with_priority(MissionPriority::Critical)
910        .with_objective_position(Position::new(33.7749, -84.3958));
911
912        let json = task.to_json().expect("should serialize");
913        let restored = MissionTask::from_json(&json).expect("should deserialize");
914
915        assert_eq!(restored.task_id, task.task_id);
916        assert_eq!(restored.task_type, task.task_type);
917        assert_eq!(restored.priority, task.priority);
918    }
919
920    #[test]
921    fn test_mission_task_error_display() {
922        let err = MissionTaskError::InvalidCotType("a-f-G-U-C".to_string());
923        assert!(err.to_string().contains("Invalid CoT type"));
924
925        let err = MissionTaskError::MissingField("target");
926        assert!(err.to_string().contains("Missing required field"));
927    }
928
929    #[test]
930    fn test_mission_task_from_cot_event() {
931        use crate::cot::{CotEvent, CotPoint, CotType};
932
933        // Build a mission CoT event
934        let event = CotEvent {
935            version: "2.0".to_string(),
936            uid: "MISSION-TAK-001".to_string(),
937            cot_type: CotType::new("t-x-m-c-c"), // Track target
938            time: Utc::now(),
939            start: Utc::now(),
940            stale: Utc::now() + chrono::Duration::hours(2),
941            how: "m-g".to_string(),
942            point: CotPoint {
943                lat: 33.7749,
944                lon: -84.3958,
945                hae: 300.0,
946                ce: 10.0,
947                le: 10.0,
948            },
949            detail: crate::cot::CotDetail {
950                contact_callsign: Some("CMD-001".to_string()),
951                remarks: Some("Track suspicious vehicle in sector alpha".to_string()),
952                ..Default::default()
953            },
954        };
955
956        let task = MissionTask::from_cot_event(&event).expect("should convert");
957
958        assert_eq!(task.task_id, "MISSION-TAK-001");
959        assert_eq!(task.task_type, MissionTaskType::TrackTarget);
960        assert!(task.target.is_some());
961        assert_eq!(
962            task.target.as_ref().unwrap().description,
963            "Track suspicious vehicle in sector alpha"
964        );
965        assert!(task.objective_position.is_some());
966        let pos = task.objective_position.as_ref().unwrap();
967        assert_eq!(pos.lat, 33.7749);
968        assert_eq!(pos.lon, -84.3958);
969    }
970
971    #[test]
972    fn test_mission_task_from_cot_event_invalid_type() {
973        use crate::cot::{CotEvent, CotPoint, CotType};
974
975        // Build a non-mission CoT event
976        let event = CotEvent {
977            version: "2.0".to_string(),
978            uid: "UNIT-001".to_string(),
979            cot_type: CotType::new("a-f-G-U-C"), // Friendly ground unit, not a mission
980            time: Utc::now(),
981            start: Utc::now(),
982            stale: Utc::now() + chrono::Duration::hours(1),
983            how: "m-g".to_string(),
984            point: CotPoint::new(0.0, 0.0),
985            detail: Default::default(),
986        };
987
988        let result = MissionTask::from_cot_event(&event);
989        assert!(result.is_err());
990        assert!(matches!(
991            result.unwrap_err(),
992            MissionTaskError::InvalidCotType(_)
993        ));
994    }
995
996    /// End-to-end test: XML → CotEvent → MissionTask (Issue #318)
997    #[test]
998    fn test_mission_task_from_xml_end_to_end() {
999        use crate::cot::CotEvent;
1000
1001        // Realistic mission task CoT XML from TAK Server
1002        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1003            <event uid="TASK-20251208-001" type="t-x-m-c-c" time="2025-12-08T14:05:00Z"
1004                   start="2025-12-08T14:05:00Z" stale="2025-12-08T16:05:00Z" how="h-g-i-g-o">
1005                <point lat="33.7756" lon="-84.3963" hae="300" ce="50" le="50"/>
1006                <detail>
1007                    <contact callsign="CMD-ALPHA"/>
1008                    <remarks>Track suspicious vehicle in sector bravo, heading north on Main St</remarks>
1009                </detail>
1010            </event>"#;
1011
1012        // Parse XML to CotEvent
1013        let event = CotEvent::from_xml(xml).expect("should parse XML");
1014        assert_eq!(event.uid, "TASK-20251208-001");
1015        assert_eq!(event.cot_type.as_str(), "t-x-m-c-c");
1016
1017        // Convert to MissionTask
1018        let task = MissionTask::from_cot_event(&event).expect("should convert to MissionTask");
1019
1020        // Verify conversion
1021        assert_eq!(task.task_id, "TASK-20251208-001");
1022        assert_eq!(task.task_type, MissionTaskType::TrackTarget);
1023        assert_eq!(task.issued_by, "TAK-Server");
1024        assert!(task.target.is_some());
1025        assert!(task
1026            .target
1027            .as_ref()
1028            .unwrap()
1029            .description
1030            .contains("suspicious vehicle"));
1031        assert!(task.objective_position.is_some());
1032        let pos = task.objective_position.as_ref().unwrap();
1033        assert!((pos.lat - 33.7756).abs() < 0.0001);
1034        assert!((pos.lon - (-84.3963)).abs() < 0.0001);
1035    }
1036
1037    /// Test search area mission type
1038    #[test]
1039    fn test_mission_task_search_area_from_xml() {
1040        use crate::cot::CotEvent;
1041
1042        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1043            <event uid="SEARCH-001" type="t-x-m-c-s" time="2025-12-08T14:00:00Z"
1044                   start="2025-12-08T14:00:00Z" stale="2025-12-08T18:00:00Z" how="h-g-i-g-o">
1045                <point lat="33.80" lon="-84.40" hae="0" ce="500" le="500"/>
1046                <detail>
1047                    <remarks>Search grid sector 7 for missing hiker</remarks>
1048                </detail>
1049            </event>"#;
1050
1051        let event = CotEvent::from_xml(xml).expect("should parse");
1052        let task = MissionTask::from_cot_event(&event).expect("should convert");
1053
1054        assert_eq!(task.task_type, MissionTaskType::SearchArea);
1055        assert_eq!(task.task_id, "SEARCH-001");
1056    }
1057
1058    /// Test abort mission type
1059    #[test]
1060    fn test_mission_task_abort_from_xml() {
1061        use crate::cot::CotEvent;
1062
1063        let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1064            <event uid="ABORT-001" type="t-x-m-c-a" time="2025-12-08T14:00:00Z"
1065                   start="2025-12-08T14:00:00Z" stale="2025-12-08T14:30:00Z" how="h-g-i-g-o">
1066                <point lat="0" lon="0" hae="0" ce="999999" le="999999"/>
1067                <detail>
1068                    <remarks>Abort current mission - RTB immediately</remarks>
1069                </detail>
1070            </event>"#;
1071
1072        let event = CotEvent::from_xml(xml).expect("should parse");
1073        let task = MissionTask::from_cot_event(&event).expect("should convert");
1074
1075        assert_eq!(task.task_type, MissionTaskType::Abort);
1076        assert!(task.remarks.as_ref().unwrap().contains("RTB"));
1077    }
1078}