Skip to main content

peat_btle/sync/
crdt.rs

1// Copyright (c) 2025-2026 (r)evolve - Revolve Team LLC
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! CRDT (Conflict-free Replicated Data Types) for Peat-Lite
17//!
18//! Provides lightweight CRDT implementations optimized for BLE sync:
19//! - LWW-Register: Last-Writer-Wins for single values
20//! - G-Counter: Grow-only counter for metrics
21//!
22//! These are designed for minimal memory footprint and efficient
23//! serialization over constrained BLE connections.
24
25#[cfg(not(feature = "std"))]
26use alloc::{collections::BTreeMap, string::String, string::ToString, vec, vec::Vec};
27#[cfg(feature = "std")]
28use std::collections::BTreeMap;
29
30use crate::NodeId;
31
32// ============================================================================
33// Security Validation Constants
34// ============================================================================
35// These constants define bounds for validating decoded data to prevent
36// spoofing, injection attacks, and garbage data from corrupting state.
37
38/// Minimum valid timestamp: 2020-01-01 00:00:00 UTC (milliseconds)
39/// Any timestamp before this is rejected as invalid/spoofed.
40pub const MIN_VALID_TIMESTAMP: u64 = 1577836800000;
41
42/// Maximum valid battery percentage (100%)
43pub const MAX_BATTERY_PERCENT: u8 = 100;
44
45/// Minimum valid heart rate (BPM) - anything below is likely garbage
46pub const MIN_HEART_RATE: u8 = 20;
47
48/// Maximum valid heart rate (BPM) - anything above is likely garbage
49pub const MAX_HEART_RATE: u8 = 250;
50
51/// Maximum number of ACK entries in an emergency event (prevents DoS)
52pub const MAX_EMERGENCY_ACKS: usize = 256;
53
54/// Maximum number of counter entries (prevents DoS)
55pub const MAX_COUNTER_ENTRIES: usize = 256;
56
57/// Timestamp for CRDT operations (milliseconds since epoch or monotonic)
58pub type Timestamp = u64;
59
60/// A Last-Writer-Wins Register
61///
62/// Stores a single value where concurrent writes are resolved by timestamp.
63/// Higher timestamp wins. In case of tie, higher node ID wins.
64#[derive(Debug, Clone, PartialEq)]
65pub struct LwwRegister<T: Clone> {
66    /// Current value
67    value: T,
68    /// Timestamp when value was set
69    timestamp: Timestamp,
70    /// Node that set the value
71    node_id: NodeId,
72}
73
74impl<T: Clone + Default> Default for LwwRegister<T> {
75    fn default() -> Self {
76        Self {
77            value: T::default(),
78            timestamp: 0,
79            node_id: NodeId::default(),
80        }
81    }
82}
83
84impl<T: Clone> LwwRegister<T> {
85    /// Create a new register with an initial value
86    pub fn new(value: T, timestamp: Timestamp, node_id: NodeId) -> Self {
87        Self {
88            value,
89            timestamp,
90            node_id,
91        }
92    }
93
94    /// Get the current value
95    pub fn get(&self) -> &T {
96        &self.value
97    }
98
99    /// Get the timestamp
100    pub fn timestamp(&self) -> Timestamp {
101        self.timestamp
102    }
103
104    /// Get the node that set the value
105    pub fn node_id(&self) -> &NodeId {
106        &self.node_id
107    }
108
109    /// Set a new value if it has a higher timestamp
110    ///
111    /// Returns true if the value was updated
112    pub fn set(&mut self, value: T, timestamp: Timestamp, node_id: NodeId) -> bool {
113        if self.should_update(timestamp, &node_id) {
114            self.value = value;
115            self.timestamp = timestamp;
116            self.node_id = node_id;
117            true
118        } else {
119            false
120        }
121    }
122
123    /// Merge with another register (LWW semantics)
124    ///
125    /// Returns true if our value was updated
126    pub fn merge(&mut self, other: &LwwRegister<T>) -> bool {
127        if self.should_update(other.timestamp, &other.node_id) {
128            self.value = other.value.clone();
129            self.timestamp = other.timestamp;
130            self.node_id = other.node_id;
131            true
132        } else {
133            false
134        }
135    }
136
137    /// Check if we should update based on timestamp/node_id
138    fn should_update(&self, timestamp: Timestamp, node_id: &NodeId) -> bool {
139        timestamp > self.timestamp
140            || (timestamp == self.timestamp && node_id.as_u32() > self.node_id.as_u32())
141    }
142}
143
144/// A Grow-only Counter (G-Counter)
145///
146/// Each node maintains its own count, total is the sum of all counts.
147/// Only supports increment operations.
148#[derive(Debug, Clone, Default)]
149pub struct GCounter {
150    /// Per-node counts
151    counts: BTreeMap<u32, u64>,
152}
153
154impl GCounter {
155    /// Create a new empty counter
156    pub fn new() -> Self {
157        Self {
158            counts: BTreeMap::new(),
159        }
160    }
161
162    /// Get the total count
163    pub fn value(&self) -> u64 {
164        self.counts.values().sum()
165    }
166
167    /// Increment the counter for a node
168    pub fn increment(&mut self, node_id: &NodeId, amount: u64) {
169        let count = self.counts.entry(node_id.as_u32()).or_insert(0);
170        *count = count.saturating_add(amount);
171    }
172
173    /// Get the count for a specific node
174    pub fn node_count(&self, node_id: &NodeId) -> u64 {
175        self.counts.get(&node_id.as_u32()).copied().unwrap_or(0)
176    }
177
178    /// Merge with another counter
179    ///
180    /// Takes the max of each node's count
181    pub fn merge(&mut self, other: &GCounter) {
182        for (&node_id, &count) in &other.counts {
183            let our_count = self.counts.entry(node_id).or_insert(0);
184            *our_count = (*our_count).max(count);
185        }
186    }
187
188    /// Get the number of nodes that have contributed
189    pub fn node_count_total(&self) -> usize {
190        self.counts.len()
191    }
192
193    /// Get all entries as (node_id, count) pairs
194    ///
195    /// Returns an iterator over all nodes and their counts.
196    /// Used for building delta documents.
197    pub fn entries(&self) -> impl Iterator<Item = (u32, u64)> + '_ {
198        self.counts.iter().map(|(&k, &v)| (k, v))
199    }
200
201    /// Encode to bytes for transmission
202    pub fn encode(&self) -> Vec<u8> {
203        let mut buf = Vec::with_capacity(4 + self.counts.len() * 12);
204        // Number of entries
205        buf.extend_from_slice(&(self.counts.len() as u32).to_le_bytes());
206        // Each entry: node_id (4 bytes) + count (8 bytes)
207        for (&node_id, &count) in &self.counts {
208            buf.extend_from_slice(&node_id.to_le_bytes());
209            buf.extend_from_slice(&count.to_le_bytes());
210        }
211        buf
212    }
213
214    /// Decode from bytes with validation
215    ///
216    /// # Security
217    /// Limits number of entries to prevent DoS via huge allocations.
218    /// Skips zero node IDs.
219    pub fn decode(data: &[u8]) -> Option<Self> {
220        if data.len() < 4 {
221            return None;
222        }
223        let num_entries = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
224
225        // Security: Limit entries to prevent DoS via huge allocations
226        if num_entries > MAX_COUNTER_ENTRIES {
227            return None;
228        }
229
230        if data.len() < 4 + num_entries * 12 {
231            return None;
232        }
233
234        let mut counts = BTreeMap::new();
235        let mut offset = 4;
236        for _ in 0..num_entries {
237            let node_id = u32::from_le_bytes([
238                data[offset],
239                data[offset + 1],
240                data[offset + 2],
241                data[offset + 3],
242            ]);
243            let count = u64::from_le_bytes([
244                data[offset + 4],
245                data[offset + 5],
246                data[offset + 6],
247                data[offset + 7],
248                data[offset + 8],
249                data[offset + 9],
250                data[offset + 10],
251                data[offset + 11],
252            ]);
253            // Security: Skip zero node IDs
254            if node_id != 0 {
255                counts.insert(node_id, count);
256            }
257            offset += 12;
258        }
259
260        Some(Self { counts })
261    }
262}
263
264/// Position data with LWW semantics
265#[derive(Debug, Clone, Default, PartialEq)]
266pub struct Position {
267    /// Latitude in degrees
268    pub latitude: f32,
269    /// Longitude in degrees
270    pub longitude: f32,
271    /// Altitude in meters (optional)
272    pub altitude: Option<f32>,
273    /// Accuracy in meters (optional)
274    pub accuracy: Option<f32>,
275}
276
277impl Position {
278    /// Create a new position
279    pub fn new(latitude: f32, longitude: f32) -> Self {
280        Self {
281            latitude,
282            longitude,
283            altitude: None,
284            accuracy: None,
285        }
286    }
287
288    /// Create with altitude
289    pub fn with_altitude(mut self, altitude: f32) -> Self {
290        self.altitude = Some(altitude);
291        self
292    }
293
294    /// Create with accuracy
295    pub fn with_accuracy(mut self, accuracy: f32) -> Self {
296        self.accuracy = Some(accuracy);
297        self
298    }
299
300    /// Encode to bytes (12-20 bytes)
301    pub fn encode(&self) -> Vec<u8> {
302        let mut buf = Vec::with_capacity(20);
303        buf.extend_from_slice(&self.latitude.to_le_bytes());
304        buf.extend_from_slice(&self.longitude.to_le_bytes());
305
306        // Flags byte: bit 0 = has altitude, bit 1 = has accuracy
307        let mut flags = 0u8;
308        if self.altitude.is_some() {
309            flags |= 0x01;
310        }
311        if self.accuracy.is_some() {
312            flags |= 0x02;
313        }
314        buf.push(flags);
315
316        if let Some(alt) = self.altitude {
317            buf.extend_from_slice(&alt.to_le_bytes());
318        }
319        if let Some(acc) = self.accuracy {
320            buf.extend_from_slice(&acc.to_le_bytes());
321        }
322        buf
323    }
324
325    /// Decode from bytes with validation
326    ///
327    /// # Security
328    /// Validates that latitude/longitude are within valid ranges and not NaN/Inf.
329    /// Rejects garbage data that could indicate spoofing or corruption.
330    pub fn decode(data: &[u8]) -> Option<Self> {
331        if data.len() < 9 {
332            return None;
333        }
334
335        let latitude = f32::from_le_bytes([data[0], data[1], data[2], data[3]]);
336        let longitude = f32::from_le_bytes([data[4], data[5], data[6], data[7]]);
337        let flags = data[8];
338
339        // Security: Reject NaN, Inf, or out-of-range coordinates
340        if !latitude.is_finite() || !longitude.is_finite() {
341            return None;
342        }
343        if !(-90.0..=90.0).contains(&latitude) {
344            return None;
345        }
346        if !(-180.0..=180.0).contains(&longitude) {
347            return None;
348        }
349
350        let mut pos = Self::new(latitude, longitude);
351        let mut offset = 9;
352
353        if flags & 0x01 != 0 {
354            if data.len() < offset + 4 {
355                return None;
356            }
357            let alt = f32::from_le_bytes([
358                data[offset],
359                data[offset + 1],
360                data[offset + 2],
361                data[offset + 3],
362            ]);
363            // Security: Reject NaN/Inf altitude, allow wide range for valid Earth elevations
364            if !alt.is_finite() || !(-1000.0..=100000.0).contains(&alt) {
365                return None;
366            }
367            pos.altitude = Some(alt);
368            offset += 4;
369        }
370
371        if flags & 0x02 != 0 {
372            if data.len() < offset + 4 {
373                return None;
374            }
375            let acc = f32::from_le_bytes([
376                data[offset],
377                data[offset + 1],
378                data[offset + 2],
379                data[offset + 3],
380            ]);
381            // Security: Reject NaN/Inf/negative accuracy
382            if !acc.is_finite() || acc < 0.0 {
383                return None;
384            }
385            pos.accuracy = Some(acc);
386        }
387
388        Some(pos)
389    }
390}
391
392/// Health status data with LWW semantics
393#[derive(Debug, Clone, Default, PartialEq)]
394pub struct HealthStatus {
395    /// Battery percentage (0-100)
396    pub battery_percent: u8,
397    /// Heart rate BPM (optional)
398    pub heart_rate: Option<u8>,
399    /// Activity level — non-validated `u8` render hint (see
400    /// `HealthStatus::decode`). Current emitters use 0=still/standing,
401    /// 3=PossibleFall; 4+ is reserved for sender-side extensions and
402    /// receivers treat unknown values as "no animation."
403    pub activity: u8,
404    /// Alert status flags
405    pub alerts: u8,
406}
407
408impl HealthStatus {
409    /// Alert flag: Man down
410    pub const ALERT_MAN_DOWN: u8 = 0x01;
411    /// Alert flag: Low battery
412    pub const ALERT_LOW_BATTERY: u8 = 0x02;
413    /// Alert flag: Out of range
414    pub const ALERT_OUT_OF_RANGE: u8 = 0x04;
415    /// Alert flag: Custom alert 1
416    pub const ALERT_CUSTOM_1: u8 = 0x08;
417
418    /// Create a new health status
419    pub fn new(battery_percent: u8) -> Self {
420        Self {
421            battery_percent,
422            heart_rate: None,
423            activity: 0,
424            alerts: 0,
425        }
426    }
427
428    /// Set heart rate
429    pub fn with_heart_rate(mut self, hr: u8) -> Self {
430        self.heart_rate = Some(hr);
431        self
432    }
433
434    /// Set activity level
435    pub fn with_activity(mut self, activity: u8) -> Self {
436        self.activity = activity;
437        self
438    }
439
440    /// Set alert flag
441    pub fn set_alert(&mut self, flag: u8) {
442        self.alerts |= flag;
443    }
444
445    /// Clear alert flag
446    pub fn clear_alert(&mut self, flag: u8) {
447        self.alerts &= !flag;
448    }
449
450    /// Check if alert is set
451    pub fn has_alert(&self, flag: u8) -> bool {
452        self.alerts & flag != 0
453    }
454
455    /// Encode to bytes (3-4 bytes)
456    pub fn encode(&self) -> Vec<u8> {
457        vec![
458            self.battery_percent,
459            self.activity,
460            self.alerts,
461            // Heart rate: 0 means not present, otherwise value
462            self.heart_rate.unwrap_or(0),
463        ]
464    }
465
466    /// Decode from bytes with validation
467    ///
468    /// # Security
469    /// Validates battery percentage and heart rate are within physiological bounds.
470    /// Rejects garbage data that could indicate spoofing or corruption.
471    ///
472    /// **Activity is intentionally not validated** — it's a `u8` render
473    /// hint (animation/icon selection), not a security-load-bearing
474    /// discriminator. Receivers should treat unknown values as "no
475    /// animation" and keep the rest of the peripheral intact. Re-adding
476    /// a range check here silently drops entire peripherals (callsign,
477    /// battery, location) for any peer reporting a value outside the
478    /// known set; that was the failure mode that motivated this carve-out.
479    pub fn decode(data: &[u8]) -> Option<Self> {
480        if data.len() < 4 {
481            return None;
482        }
483
484        let battery_percent = data[0];
485        let activity = data[1];
486        let alerts = data[2];
487        let heart_rate_raw = data[3];
488
489        // Security: Reject battery percentage > 100%
490        if battery_percent > MAX_BATTERY_PERCENT {
491            return None;
492        }
493
494        // Activity is a u8 render hint, not a security-load-bearing
495        // discriminator: receivers should treat unknown values as "no
496        // animation / unknown state" and keep the rest of the peripheral
497        // intact. Rejecting here used to silently drop entire peripherals
498        // any time a peer reported a value outside the original 0-3 range
499        // (notably the M5Stack Prone=4 extension), losing callsign,
500        // battery, and location alongside an unrelated render hint.
501
502        let mut status = Self::new(battery_percent);
503        status.activity = activity;
504        status.alerts = alerts;
505
506        // Security: Validate heart rate is in physiological range if present
507        if heart_rate_raw > 0 {
508            if !(MIN_HEART_RATE..=MAX_HEART_RATE).contains(&heart_rate_raw) {
509                return None;
510            }
511            status.heart_rate = Some(heart_rate_raw);
512        }
513
514        Some(status)
515    }
516}
517
518// ============================================================================
519// Peripheral (Sub-node) Types - for soldier-attached devices like M5Stack Core2
520// ============================================================================
521
522/// Type of peripheral device
523#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
524#[repr(u8)]
525pub enum PeripheralType {
526    /// Unknown/unspecified
527    #[default]
528    Unknown = 0,
529    /// Soldier-worn sensor (e.g., M5Stack Core2)
530    SoldierSensor = 1,
531    /// Fixed/stationary sensor
532    FixedSensor = 2,
533    /// Mesh relay only (no sensors)
534    Relay = 3,
535}
536
537impl PeripheralType {
538    /// Convert from u8 value
539    pub fn from_u8(v: u8) -> Self {
540        match v {
541            1 => Self::SoldierSensor,
542            2 => Self::FixedSensor,
543            3 => Self::Relay,
544            _ => Self::Unknown,
545        }
546    }
547}
548
549/// Event types that a peripheral can emit (e.g., from tap input)
550#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
551#[repr(u8)]
552pub enum EventType {
553    /// No event / cleared
554    #[default]
555    None = 0,
556    /// "I'm OK" ping
557    Ping = 1,
558    /// Request assistance
559    NeedAssist = 2,
560    /// Emergency / SOS
561    Emergency = 3,
562    /// Moving / in transit
563    Moving = 4,
564    /// In position / stationary
565    InPosition = 5,
566    /// Acknowledged / copy
567    Ack = 6,
568}
569
570impl EventType {
571    /// Convert from u8 value
572    pub fn from_u8(v: u8) -> Self {
573        match v {
574            1 => Self::Ping,
575            2 => Self::NeedAssist,
576            3 => Self::Emergency,
577            4 => Self::Moving,
578            5 => Self::InPosition,
579            6 => Self::Ack,
580            _ => Self::None,
581        }
582    }
583
584    /// Human-readable label for display
585    pub fn label(&self) -> &'static str {
586        match self {
587            Self::None => "",
588            Self::Ping => "PING",
589            Self::NeedAssist => "NEED ASSIST",
590            Self::Emergency => "EMERGENCY",
591            Self::Moving => "MOVING",
592            Self::InPosition => "IN POSITION",
593            Self::Ack => "ACK",
594        }
595    }
596}
597
598/// An event emitted by a peripheral (e.g., tap on Core2)
599#[derive(Debug, Clone, Default, PartialEq)]
600pub struct PeripheralEvent {
601    /// Type of event
602    pub event_type: EventType,
603    /// Timestamp when event occurred (ms since epoch or boot)
604    pub timestamp: u64,
605}
606
607impl PeripheralEvent {
608    /// Create a new peripheral event
609    pub fn new(event_type: EventType, timestamp: u64) -> Self {
610        Self {
611            event_type,
612            timestamp,
613        }
614    }
615
616    /// Encode to bytes (9 bytes)
617    pub fn encode(&self) -> Vec<u8> {
618        let mut buf = Vec::with_capacity(9);
619        buf.push(self.event_type as u8);
620        buf.extend_from_slice(&self.timestamp.to_le_bytes());
621        buf
622    }
623
624    /// Decode from bytes with validation
625    ///
626    /// # Security
627    /// Validates timestamp is within reasonable bounds.
628    /// Rejects garbage data that could indicate spoofing or corruption.
629    pub fn decode(data: &[u8]) -> Option<Self> {
630        if data.len() < 9 {
631            return None;
632        }
633
634        let event_type = EventType::from_u8(data[0]);
635        let timestamp = u64::from_le_bytes([
636            data[1], data[2], data[3], data[4], data[5], data[6], data[7], data[8],
637        ]);
638
639        // Security: Reject events with timestamps before 2020
640        // Note: timestamp could be 0 for "no event" scenarios, so we allow that
641        if timestamp != 0 && timestamp < MIN_VALID_TIMESTAMP {
642            return None;
643        }
644
645        Some(Self {
646            event_type,
647            timestamp,
648        })
649    }
650}
651
652/// An emergency event with acknowledgment tracking (CRDT)
653///
654/// Represents a single emergency incident with distributed ACK tracking.
655/// Each node in the mesh can acknowledge the emergency, and this state
656/// is replicated across all nodes using CRDT semantics.
657///
658/// ## CRDT Semantics
659///
660/// - **Identity**: Events are uniquely identified by (source_node, timestamp)
661/// - **Merge for same event**: ACK maps merge with OR (once acked, stays acked)
662/// - **Merge for different events**: Higher timestamp wins (newer emergency replaces older)
663/// - **Monotonic**: ACK state only moves from false → true, never back
664///
665/// ## Wire Format
666///
667/// ```text
668/// source_node: 4 bytes (LE)
669/// timestamp:   8 bytes (LE)
670/// num_acks:    4 bytes (LE)
671/// acks[N]:
672///   node_id:   4 bytes (LE)
673///   acked:     1 byte (0 or 1)
674/// ```
675#[derive(Debug, Clone, PartialEq, Default)]
676pub struct EmergencyEvent {
677    /// Node that triggered the emergency
678    source_node: u32,
679    /// Timestamp when emergency was triggered (for uniqueness)
680    timestamp: u64,
681    /// ACK status for each known peer: node_id -> has_acked
682    acks: BTreeMap<u32, bool>,
683}
684
685impl EmergencyEvent {
686    /// Create a new emergency event
687    ///
688    /// # Arguments
689    /// * `source_node` - Node ID that triggered the emergency
690    /// * `timestamp` - When the emergency was triggered
691    /// * `known_peers` - List of peer node IDs to track for ACKs
692    ///
693    /// The source node is automatically marked as acknowledged.
694    pub fn new(source_node: u32, timestamp: u64, known_peers: &[u32]) -> Self {
695        let mut acks = BTreeMap::new();
696
697        // Source node implicitly ACKs their own emergency
698        acks.insert(source_node, true);
699
700        // All other known peers start as not-acked
701        for &peer_id in known_peers {
702            if peer_id != source_node {
703                acks.entry(peer_id).or_insert(false);
704            }
705        }
706
707        Self {
708            source_node,
709            timestamp,
710            acks,
711        }
712    }
713
714    /// Get the source node that triggered the emergency
715    pub fn source_node(&self) -> u32 {
716        self.source_node
717    }
718
719    /// Get the timestamp when the emergency was triggered
720    pub fn timestamp(&self) -> u64 {
721        self.timestamp
722    }
723
724    /// Check if a specific node has acknowledged
725    pub fn has_acked(&self, node_id: u32) -> bool {
726        self.acks.get(&node_id).copied().unwrap_or(false)
727    }
728
729    /// Record an acknowledgment from a node
730    ///
731    /// Returns true if this was a new ACK (state changed)
732    pub fn ack(&mut self, node_id: u32) -> bool {
733        let entry = self.acks.entry(node_id).or_insert(false);
734        if !*entry {
735            *entry = true;
736            true
737        } else {
738            false
739        }
740    }
741
742    /// Add a peer to track (if not already present)
743    ///
744    /// New peers start as not-acked. This is useful when discovering
745    /// new peers after the emergency was created.
746    pub fn add_peer(&mut self, node_id: u32) {
747        self.acks.entry(node_id).or_insert(false);
748    }
749
750    /// Get list of nodes that have acknowledged
751    pub fn acked_nodes(&self) -> Vec<u32> {
752        self.acks
753            .iter()
754            .filter(|(_, &acked)| acked)
755            .map(|(&node_id, _)| node_id)
756            .collect()
757    }
758
759    /// Get list of nodes that have NOT acknowledged
760    pub fn pending_nodes(&self) -> Vec<u32> {
761        self.acks
762            .iter()
763            .filter(|(_, &acked)| !acked)
764            .map(|(&node_id, _)| node_id)
765            .collect()
766    }
767
768    /// Get all tracked node IDs (both acked and pending)
769    pub fn all_nodes(&self) -> Vec<u32> {
770        self.acks.keys().copied().collect()
771    }
772
773    /// Check if all tracked nodes have acknowledged
774    pub fn all_acked(&self) -> bool {
775        !self.acks.is_empty() && self.acks.values().all(|&acked| acked)
776    }
777
778    /// Get the total number of tracked nodes
779    pub fn peer_count(&self) -> usize {
780        self.acks.len()
781    }
782
783    /// Get the number of nodes that have acknowledged
784    pub fn ack_count(&self) -> usize {
785        self.acks.values().filter(|&&acked| acked).count()
786    }
787
788    /// Merge with another emergency event (CRDT semantics)
789    ///
790    /// # Returns
791    /// `true` if our state changed
792    ///
793    /// # Semantics
794    /// - Same event (source_node, timestamp): merge ACK maps with OR
795    /// - Different event: take the one with higher timestamp
796    pub fn merge(&mut self, other: &EmergencyEvent) -> bool {
797        // Different emergency - take newer one
798        if self.source_node != other.source_node || self.timestamp != other.timestamp {
799            if other.timestamp > self.timestamp {
800                *self = other.clone();
801                return true;
802            }
803            return false;
804        }
805
806        // Same emergency - merge ACK maps with OR
807        let mut changed = false;
808        for (&node_id, &other_acked) in &other.acks {
809            let entry = self.acks.entry(node_id).or_insert(false);
810            if other_acked && !*entry {
811                *entry = true;
812                changed = true;
813            }
814        }
815        changed
816    }
817
818    /// Encode to bytes for transmission
819    ///
820    /// Format: source_node(4) + timestamp(8) + num_acks(4) + acks[N](5 each)
821    pub fn encode(&self) -> Vec<u8> {
822        let mut buf = Vec::with_capacity(16 + self.acks.len() * 5);
823
824        buf.extend_from_slice(&self.source_node.to_le_bytes());
825        buf.extend_from_slice(&self.timestamp.to_le_bytes());
826        buf.extend_from_slice(&(self.acks.len() as u32).to_le_bytes());
827
828        for (&node_id, &acked) in &self.acks {
829            buf.extend_from_slice(&node_id.to_le_bytes());
830            buf.push(if acked { 1 } else { 0 });
831        }
832
833        buf
834    }
835
836    /// Decode from bytes with validation
837    ///
838    /// # Security
839    /// Validates source_node, timestamp, and limits ACK entries to prevent DoS.
840    /// Rejects garbage data that could indicate spoofing or corruption.
841    pub fn decode(data: &[u8]) -> Option<Self> {
842        if data.len() < 16 {
843            return None;
844        }
845
846        let source_node = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
847        let timestamp = u64::from_le_bytes([
848            data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
849        ]);
850        let num_acks = u32::from_le_bytes([data[12], data[13], data[14], data[15]]) as usize;
851
852        // Security: Reject zero source_node (invalid)
853        if source_node == 0 {
854            return None;
855        }
856
857        // Security: Reject timestamps before 2020
858        if timestamp < MIN_VALID_TIMESTAMP {
859            return None;
860        }
861
862        // Security: Limit ACK entries to prevent DoS via huge allocations
863        if num_acks > MAX_EMERGENCY_ACKS {
864            return None;
865        }
866
867        if data.len() < 16 + num_acks * 5 {
868            return None;
869        }
870
871        let mut acks = BTreeMap::new();
872        let mut offset = 16;
873        for _ in 0..num_acks {
874            let node_id = u32::from_le_bytes([
875                data[offset],
876                data[offset + 1],
877                data[offset + 2],
878                data[offset + 3],
879            ]);
880            // Security: Skip zero node_ids in ACK list
881            if node_id != 0 {
882                let acked = data[offset + 4] != 0;
883                acks.insert(node_id, acked);
884            }
885            offset += 5;
886        }
887
888        Some(Self {
889            source_node,
890            timestamp,
891            acks,
892        })
893    }
894}
895
896/// A peripheral device attached to a Node (soldier)
897///
898/// Peripherals are sub-tier devices that augment a soldier's capabilities
899/// with sensors and input (e.g., M5Stack Core2 wearable).
900#[derive(Debug, Clone, Default)]
901pub struct Peripheral {
902    /// Unique peripheral ID (derived from device MAC or similar)
903    pub id: u32,
904    /// Parent Node ID this peripheral is attached to (0 if not paired)
905    pub parent_node: u32,
906    /// Type of peripheral
907    pub peripheral_type: PeripheralType,
908    /// Callsign/name (inherited from parent or configured)
909    pub callsign: [u8; 12],
910    /// Current health status
911    pub health: HealthStatus,
912    /// Most recent event (if any)
913    pub last_event: Option<PeripheralEvent>,
914    /// Current location (if available)
915    pub location: Option<Position>,
916    /// Last update timestamp
917    pub timestamp: u64,
918}
919
920impl Peripheral {
921    /// Create a new peripheral
922    pub fn new(id: u32, peripheral_type: PeripheralType) -> Self {
923        Self {
924            id,
925            parent_node: 0,
926            peripheral_type,
927            callsign: [0u8; 12],
928            health: HealthStatus::default(),
929            last_event: None,
930            location: None,
931            timestamp: 0,
932        }
933    }
934
935    /// Set the callsign (truncated to 12 bytes)
936    pub fn with_callsign(mut self, callsign: &str) -> Self {
937        let bytes = callsign.as_bytes();
938        let len = bytes.len().min(12);
939        self.callsign[..len].copy_from_slice(&bytes[..len]);
940        self
941    }
942
943    /// Update callsign in place
944    pub fn set_callsign(&mut self, callsign: &str) {
945        self.callsign = [0u8; 12];
946        let bytes = callsign.as_bytes();
947        let len = bytes.len().min(12);
948        self.callsign[..len].copy_from_slice(&bytes[..len]);
949    }
950
951    /// Get callsign as string
952    pub fn callsign_str(&self) -> &str {
953        let len = self.callsign.iter().position(|&b| b == 0).unwrap_or(12);
954        core::str::from_utf8(&self.callsign[..len]).unwrap_or("")
955    }
956
957    /// Set parent node
958    pub fn with_parent(mut self, parent_node: u32) -> Self {
959        self.parent_node = parent_node;
960        self
961    }
962
963    /// Set location (builder pattern)
964    pub fn with_location(mut self, location: Position) -> Self {
965        self.location = Some(location);
966        self
967    }
968
969    /// Update location in place
970    pub fn set_location(&mut self, latitude: f32, longitude: f32, altitude: Option<f32>) {
971        let mut pos = Position::new(latitude, longitude);
972        if let Some(alt) = altitude {
973            pos = pos.with_altitude(alt);
974        }
975        self.location = Some(pos);
976    }
977
978    /// Clear location
979    pub fn clear_location(&mut self) {
980        self.location = None;
981    }
982
983    /// Record an event
984    pub fn set_event(&mut self, event_type: EventType, timestamp: u64) {
985        self.last_event = Some(PeripheralEvent::new(event_type, timestamp));
986        self.timestamp = timestamp;
987    }
988
989    /// Clear the last event
990    pub fn clear_event(&mut self) {
991        self.last_event = None;
992    }
993
994    /// Encode to bytes for BLE transmission
995    /// Format: [id:4][parent:4][type:1][callsign:12][health:4][has_event:1][event:9?][timestamp:8][has_location:1][location:9-20?]
996    /// Size: 35 bytes minimum (no event, no location), up to 64 bytes with both
997    pub fn encode(&self) -> Vec<u8> {
998        let mut buf = Vec::with_capacity(64);
999        buf.extend_from_slice(&self.id.to_le_bytes());
1000        buf.extend_from_slice(&self.parent_node.to_le_bytes());
1001        buf.push(self.peripheral_type as u8);
1002        buf.extend_from_slice(&self.callsign);
1003        buf.extend_from_slice(&self.health.encode());
1004
1005        if let Some(ref event) = self.last_event {
1006            buf.push(1); // has event
1007            buf.extend_from_slice(&event.encode());
1008        } else {
1009            buf.push(0); // no event
1010        }
1011
1012        buf.extend_from_slice(&self.timestamp.to_le_bytes());
1013
1014        // Location encoding (added in v0.1.0-rc.7)
1015        if let Some(ref location) = self.location {
1016            buf.push(1); // has location
1017            buf.extend_from_slice(&location.encode());
1018        } else {
1019            buf.push(0); // no location
1020        }
1021
1022        buf
1023    }
1024
1025    /// Decode from bytes with validation
1026    ///
1027    /// # Security
1028    /// Validates peripheral ID, callsign UTF-8, and timestamp.
1029    /// Rejects garbage data that could indicate spoofing or corruption.
1030    pub fn decode(data: &[u8]) -> Option<Self> {
1031        if data.len() < 34 {
1032            return None;
1033        }
1034
1035        let id = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
1036        let parent_node = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
1037        let peripheral_type = PeripheralType::from_u8(data[8]);
1038
1039        // Security: Reject zero peripheral ID (invalid)
1040        if id == 0 {
1041            return None;
1042        }
1043
1044        let mut callsign = [0u8; 12];
1045        callsign.copy_from_slice(&data[9..21]);
1046
1047        // Security: Validate callsign is valid UTF-8
1048        // Find the actual length (up to first null or end)
1049        let callsign_len = callsign.iter().position(|&b| b == 0).unwrap_or(12);
1050        if callsign_len > 0 && core::str::from_utf8(&callsign[..callsign_len]).is_err() {
1051            return None;
1052        }
1053
1054        // HealthStatus::decode now validates internally
1055        let health = HealthStatus::decode(&data[21..25])?;
1056
1057        let has_event = data[25] != 0;
1058        let (last_event, timestamp_offset) = if has_event {
1059            if data.len() < 43 {
1060                return None;
1061            }
1062            // PeripheralEvent::decode now validates timestamp internally
1063            (PeripheralEvent::decode(&data[26..35]), 35)
1064        } else {
1065            (None, 26)
1066        };
1067
1068        if data.len() < timestamp_offset + 8 {
1069            return None;
1070        }
1071
1072        let timestamp = u64::from_le_bytes([
1073            data[timestamp_offset],
1074            data[timestamp_offset + 1],
1075            data[timestamp_offset + 2],
1076            data[timestamp_offset + 3],
1077            data[timestamp_offset + 4],
1078            data[timestamp_offset + 5],
1079            data[timestamp_offset + 6],
1080            data[timestamp_offset + 7],
1081        ]);
1082
1083        // Security: Reject timestamps before 2020 (0 allowed for "not set")
1084        if timestamp != 0 && timestamp < MIN_VALID_TIMESTAMP {
1085            return None;
1086        }
1087
1088        // Decode location if present (added in v0.1.0-rc.7)
1089        // For backward compatibility, treat missing has_location byte as no location
1090        let location_offset = timestamp_offset + 8;
1091        let location = if data.len() > location_offset {
1092            let has_location = data[location_offset] != 0;
1093            if has_location && data.len() > location_offset + 1 {
1094                Position::decode(&data[location_offset + 1..])
1095            } else {
1096                None
1097            }
1098        } else {
1099            None
1100        };
1101
1102        Some(Self {
1103            id,
1104            parent_node,
1105            peripheral_type,
1106            callsign,
1107            health,
1108            last_event,
1109            location,
1110            timestamp,
1111        })
1112    }
1113}
1114
1115// ============================================================================
1116// ChatCRDT - Add-only set of chat messages for mesh-wide messaging
1117// (gated behind "legacy-chat" feature, deprecated in favor of CannedMessage)
1118// ============================================================================
1119
1120#[cfg(feature = "legacy-chat")]
1121/// Maximum message text length in bytes
1122pub const CHAT_MAX_TEXT_LEN: usize = 128;
1123
1124#[cfg(feature = "legacy-chat")]
1125/// Maximum sender name length in bytes
1126pub const CHAT_MAX_SENDER_LEN: usize = 12;
1127
1128#[cfg(feature = "legacy-chat")]
1129/// Maximum number of messages to retain in the CRDT
1130///
1131/// Older messages are pruned to keep memory bounded on embedded devices.
1132pub const CHAT_MAX_MESSAGES: usize = 32;
1133
1134#[cfg(feature = "legacy-chat")]
1135/// Maximum number of chat messages to include in sync documents
1136///
1137/// BLE GATT notifications have a limited MTU (typically 512 bytes).
1138/// To avoid exceeding this limit, sync documents only include the
1139/// most recent messages. The full history is kept in the local CRDT.
1140pub const CHAT_SYNC_LIMIT: usize = 8;
1141
1142#[cfg(feature = "legacy-chat")]
1143/// A single chat message in the mesh
1144///
1145/// Messages are uniquely identified by `(origin_node, timestamp)`.
1146/// This allows deduplication across mesh sync while preserving message ordering.
1147#[derive(Debug, Clone, PartialEq)]
1148pub struct ChatMessage {
1149    /// Node that originated this message
1150    pub origin_node: u32,
1151    /// Timestamp when message was created (ms since epoch)
1152    pub timestamp: u64,
1153    /// Sender name/callsign (up to 12 bytes)
1154    sender: [u8; CHAT_MAX_SENDER_LEN],
1155    sender_len: u8,
1156    /// Message text (up to 128 bytes)
1157    text: [u8; CHAT_MAX_TEXT_LEN],
1158    text_len: u8,
1159    /// Whether this is a broadcast message (vs directed)
1160    pub is_broadcast: bool,
1161    /// Whether ACK is requested
1162    pub requires_ack: bool,
1163    /// Reply-to: origin node of the message being replied to (0 = not a reply)
1164    pub reply_to_node: u32,
1165    /// Reply-to: timestamp of the message being replied to (0 = not a reply)
1166    pub reply_to_timestamp: u64,
1167}
1168
1169#[cfg(feature = "legacy-chat")]
1170impl Default for ChatMessage {
1171    fn default() -> Self {
1172        Self {
1173            origin_node: 0,
1174            timestamp: 0,
1175            sender: [0u8; CHAT_MAX_SENDER_LEN],
1176            sender_len: 0,
1177            text: [0u8; CHAT_MAX_TEXT_LEN],
1178            text_len: 0,
1179            is_broadcast: true,
1180            requires_ack: false,
1181            reply_to_node: 0,
1182            reply_to_timestamp: 0,
1183        }
1184    }
1185}
1186
1187#[cfg(feature = "legacy-chat")]
1188impl ChatMessage {
1189    /// Create a new chat message
1190    pub fn new(origin_node: u32, timestamp: u64, sender: &str, text: &str) -> Self {
1191        let mut msg = Self {
1192            origin_node,
1193            timestamp,
1194            ..Default::default()
1195        };
1196        msg.set_sender(sender);
1197        msg.set_text(text);
1198        msg
1199    }
1200
1201    /// Set the sender name (truncated to 12 bytes)
1202    pub fn set_sender(&mut self, sender: &str) {
1203        let bytes = sender.as_bytes();
1204        let len = bytes.len().min(CHAT_MAX_SENDER_LEN);
1205        self.sender[..len].copy_from_slice(&bytes[..len]);
1206        self.sender_len = len as u8;
1207    }
1208
1209    /// Get the sender name as a string
1210    pub fn sender(&self) -> &str {
1211        core::str::from_utf8(&self.sender[..self.sender_len as usize]).unwrap_or("")
1212    }
1213
1214    /// Set the message text (truncated to 128 bytes)
1215    pub fn set_text(&mut self, text: &str) {
1216        let bytes = text.as_bytes();
1217        let len = bytes.len().min(CHAT_MAX_TEXT_LEN);
1218        self.text[..len].copy_from_slice(&bytes[..len]);
1219        self.text_len = len as u8;
1220    }
1221
1222    /// Get the message text as a string
1223    pub fn text(&self) -> &str {
1224        core::str::from_utf8(&self.text[..self.text_len as usize]).unwrap_or("")
1225    }
1226
1227    /// Set reply-to information
1228    pub fn set_reply_to(&mut self, node: u32, timestamp: u64) {
1229        self.reply_to_node = node;
1230        self.reply_to_timestamp = timestamp;
1231    }
1232
1233    /// Check if this is a reply to another message
1234    pub fn is_reply(&self) -> bool {
1235        self.reply_to_node != 0 || self.reply_to_timestamp != 0
1236    }
1237
1238    /// Get the unique message ID (combines origin_node and timestamp)
1239    ///
1240    /// Format: `(origin_node as u64) << 32 | (timestamp & 0xFFFFFFFF)`
1241    /// This provides a sortable key where messages from same node are ordered by time.
1242    pub fn message_id(&self) -> u64 {
1243        ((self.origin_node as u64) << 32) | (self.timestamp & 0xFFFFFFFF)
1244    }
1245
1246    /// Encode to bytes for transmission
1247    ///
1248    /// Wire format:
1249    /// ```text
1250    /// origin_node:       4 bytes (LE)
1251    /// timestamp:         8 bytes (LE)
1252    /// sender_len:        1 byte
1253    /// sender:            sender_len bytes
1254    /// text_len:          1 byte
1255    /// text:              text_len bytes
1256    /// flags:             1 byte (bit 0: is_broadcast, bit 1: requires_ack)
1257    /// reply_to_node:     4 bytes (LE)
1258    /// reply_to_timestamp: 8 bytes (LE)
1259    /// ```
1260    pub fn encode(&self) -> Vec<u8> {
1261        let size = 4 + 8 + 1 + self.sender_len as usize + 1 + self.text_len as usize + 1 + 4 + 8;
1262        let mut buf = Vec::with_capacity(size);
1263
1264        buf.extend_from_slice(&self.origin_node.to_le_bytes());
1265        buf.extend_from_slice(&self.timestamp.to_le_bytes());
1266        buf.push(self.sender_len);
1267        buf.extend_from_slice(&self.sender[..self.sender_len as usize]);
1268        buf.push(self.text_len);
1269        buf.extend_from_slice(&self.text[..self.text_len as usize]);
1270
1271        let mut flags = 0u8;
1272        if self.is_broadcast {
1273            flags |= 0x01;
1274        }
1275        if self.requires_ack {
1276            flags |= 0x02;
1277        }
1278        buf.push(flags);
1279
1280        buf.extend_from_slice(&self.reply_to_node.to_le_bytes());
1281        buf.extend_from_slice(&self.reply_to_timestamp.to_le_bytes());
1282
1283        buf
1284    }
1285
1286    /// Decode from bytes with strict validation
1287    ///
1288    /// Returns `None` if the data is malformed or fails validation checks.
1289    /// This is a security-critical function - malformed messages could be
1290    /// spoofed or part of an attack.
1291    ///
1292    /// # Validation checks:
1293    /// - origin_node must be non-zero
1294    /// - timestamp must be non-zero and within reasonable bounds
1295    /// - sender must be non-empty and valid UTF-8
1296    /// - text must be valid UTF-8 (can be empty for ACK messages)
1297    /// - All length fields must be within bounds
1298    pub fn decode(data: &[u8]) -> Option<(Self, usize)> {
1299        if data.len() < 14 {
1300            // Minimum: 4 + 8 + 1 + 0 + 1 + 0 + 0 (no reply fields in old format)
1301            return None;
1302        }
1303
1304        let origin_node = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
1305        let timestamp = u64::from_le_bytes([
1306            data[4], data[5], data[6], data[7], data[8], data[9], data[10], data[11],
1307        ]);
1308
1309        // Security: Reject messages with zero origin_node (invalid/spoofed)
1310        if origin_node == 0 {
1311            return None;
1312        }
1313
1314        // Security: Reject messages with timestamps before 2020
1315        if timestamp < MIN_VALID_TIMESTAMP {
1316            return None;
1317        }
1318
1319        let sender_len = data[12] as usize;
1320        if sender_len > CHAT_MAX_SENDER_LEN || data.len() < 13 + sender_len + 1 {
1321            return None;
1322        }
1323
1324        // Security: Reject messages with empty sender (required field)
1325        if sender_len == 0 {
1326            return None;
1327        }
1328
1329        let mut sender = [0u8; CHAT_MAX_SENDER_LEN];
1330        sender[..sender_len].copy_from_slice(&data[13..13 + sender_len]);
1331
1332        // Security: Validate sender is valid UTF-8 (reject garbage/binary data)
1333        if core::str::from_utf8(&sender[..sender_len]).is_err() {
1334            return None;
1335        }
1336
1337        let text_offset = 13 + sender_len;
1338        let text_len = data[text_offset] as usize;
1339        if text_len > CHAT_MAX_TEXT_LEN || data.len() < text_offset + 1 + text_len + 1 {
1340            return None;
1341        }
1342
1343        let mut text = [0u8; CHAT_MAX_TEXT_LEN];
1344        text[..text_len].copy_from_slice(&data[text_offset + 1..text_offset + 1 + text_len]);
1345
1346        // Security: Validate text is valid UTF-8 (reject garbage/binary data)
1347        // Empty text is allowed (e.g., for ACK messages)
1348        if text_len > 0 && core::str::from_utf8(&text[..text_len]).is_err() {
1349            return None;
1350        }
1351
1352        let flags_offset = text_offset + 1 + text_len;
1353        let flags = data[flags_offset];
1354        let is_broadcast = flags & 0x01 != 0;
1355        let requires_ack = flags & 0x02 != 0;
1356
1357        // Reply-to fields (optional for backward compat)
1358        let mut reply_to_node = 0u32;
1359        let mut reply_to_timestamp = 0u64;
1360        let mut total_len = flags_offset + 1;
1361
1362        if data.len() >= flags_offset + 1 + 12 {
1363            reply_to_node = u32::from_le_bytes([
1364                data[flags_offset + 1],
1365                data[flags_offset + 2],
1366                data[flags_offset + 3],
1367                data[flags_offset + 4],
1368            ]);
1369            reply_to_timestamp = u64::from_le_bytes([
1370                data[flags_offset + 5],
1371                data[flags_offset + 6],
1372                data[flags_offset + 7],
1373                data[flags_offset + 8],
1374                data[flags_offset + 9],
1375                data[flags_offset + 10],
1376                data[flags_offset + 11],
1377                data[flags_offset + 12],
1378            ]);
1379            total_len = flags_offset + 13;
1380        }
1381
1382        Some((
1383            Self {
1384                origin_node,
1385                timestamp,
1386                sender,
1387                sender_len: sender_len as u8,
1388                text,
1389                text_len: text_len as u8,
1390                is_broadcast,
1391                requires_ack,
1392                reply_to_node,
1393                reply_to_timestamp,
1394            },
1395            total_len,
1396        ))
1397    }
1398}
1399
1400#[cfg(feature = "legacy-chat")]
1401/// Chat CRDT - Add-only set of messages
1402///
1403/// Implements add-only set semantics where messages are identified by
1404/// `(origin_node, timestamp)`. Once a message is added, it cannot be removed
1405/// (tombstone-free design optimized for mesh networks).
1406///
1407/// ## CRDT Semantics
1408///
1409/// - **Merge**: Union of all messages from both sets
1410/// - **Identity**: `(origin_node, timestamp)` - duplicates are ignored
1411/// - **Ordering**: Messages are stored sorted by message_id for efficient iteration
1412/// - **Pruning**: Oldest messages are removed when exceeding `CHAT_MAX_MESSAGES`
1413///
1414/// ## Wire Format
1415///
1416/// ```text
1417/// num_messages: 2 bytes (LE)
1418/// messages[N]:  variable (see ChatMessage::encode)
1419/// ```
1420#[derive(Debug, Clone, Default)]
1421pub struct ChatCRDT {
1422    /// Messages indexed by message_id for deduplication
1423    messages: BTreeMap<u64, ChatMessage>,
1424}
1425
1426#[cfg(feature = "legacy-chat")]
1427impl ChatCRDT {
1428    /// Create a new empty chat CRDT
1429    pub fn new() -> Self {
1430        Self {
1431            messages: BTreeMap::new(),
1432        }
1433    }
1434
1435    /// Add a message to the chat
1436    ///
1437    /// Returns `true` if the message was new (not a duplicate)
1438    pub fn add_message(&mut self, message: ChatMessage) -> bool {
1439        let id = message.message_id();
1440        if self.messages.contains_key(&id) {
1441            return false;
1442        }
1443
1444        self.messages.insert(id, message);
1445        self.prune_if_needed();
1446        true
1447    }
1448
1449    /// Create and add a new message
1450    pub fn send_message(
1451        &mut self,
1452        origin_node: u32,
1453        timestamp: u64,
1454        sender: &str,
1455        text: &str,
1456    ) -> bool {
1457        let msg = ChatMessage::new(origin_node, timestamp, sender, text);
1458        self.add_message(msg)
1459    }
1460
1461    /// Get a message by its ID
1462    pub fn get_message(&self, origin_node: u32, timestamp: u64) -> Option<&ChatMessage> {
1463        let id = ((origin_node as u64) << 32) | (timestamp & 0xFFFFFFFF);
1464        self.messages.get(&id)
1465    }
1466
1467    /// Get all messages, ordered by message_id
1468    pub fn messages(&self) -> impl Iterator<Item = &ChatMessage> {
1469        self.messages.values()
1470    }
1471
1472    /// Get messages newer than a given timestamp
1473    pub fn messages_since(&self, since_timestamp: u64) -> impl Iterator<Item = &ChatMessage> {
1474        self.messages
1475            .values()
1476            .filter(move |m| m.timestamp > since_timestamp)
1477    }
1478
1479    /// Get the number of messages
1480    pub fn len(&self) -> usize {
1481        self.messages.len()
1482    }
1483
1484    /// Check if there are no messages
1485    pub fn is_empty(&self) -> bool {
1486        self.messages.is_empty()
1487    }
1488
1489    /// Get the newest message timestamp (if any)
1490    pub fn newest_timestamp(&self) -> Option<u64> {
1491        self.messages.values().map(|m| m.timestamp).max()
1492    }
1493
1494    /// Merge with another ChatCRDT
1495    ///
1496    /// Returns `true` if any new messages were added
1497    pub fn merge(&mut self, other: &ChatCRDT) -> bool {
1498        let mut changed = false;
1499        for (id, msg) in &other.messages {
1500            if !self.messages.contains_key(id) {
1501                self.messages.insert(*id, msg.clone());
1502                changed = true;
1503            }
1504        }
1505        if changed {
1506            self.prune_if_needed();
1507        }
1508        changed
1509    }
1510
1511    /// Prune oldest messages if we exceed the limit
1512    fn prune_if_needed(&mut self) {
1513        while self.messages.len() > CHAT_MAX_MESSAGES {
1514            // Remove the entry with the lowest timestamp
1515            if let Some(&oldest_id) = self.messages.keys().next() {
1516                self.messages.remove(&oldest_id);
1517            }
1518        }
1519    }
1520
1521    /// Encode to bytes for transmission
1522    pub fn encode(&self) -> Vec<u8> {
1523        let mut buf = Vec::new();
1524
1525        // Number of messages
1526        buf.extend_from_slice(&(self.messages.len() as u16).to_le_bytes());
1527
1528        // Each message
1529        for msg in self.messages.values() {
1530            buf.extend_from_slice(&msg.encode());
1531        }
1532
1533        buf
1534    }
1535
1536    /// Decode from bytes
1537    pub fn decode(data: &[u8]) -> Option<Self> {
1538        if data.len() < 2 {
1539            return None;
1540        }
1541
1542        let num_messages = u16::from_le_bytes([data[0], data[1]]) as usize;
1543        let mut messages = BTreeMap::new();
1544        let mut offset = 2;
1545
1546        for _ in 0..num_messages {
1547            if offset >= data.len() {
1548                break;
1549            }
1550            if let Some((msg, len)) = ChatMessage::decode(&data[offset..]) {
1551                let id = msg.message_id();
1552                messages.insert(id, msg);
1553                offset += len;
1554            } else {
1555                break;
1556            }
1557        }
1558
1559        Some(Self { messages })
1560    }
1561
1562    /// Get the encoded size of this CRDT
1563    pub fn encoded_size(&self) -> usize {
1564        2 + self
1565            .messages
1566            .values()
1567            .map(|m| m.encode().len())
1568            .sum::<usize>()
1569    }
1570
1571    /// Create a copy limited to the most recent messages for sync
1572    ///
1573    /// Returns a new ChatCRDT containing only the N most recent messages,
1574    /// where N is `CHAT_SYNC_LIMIT`. This is used when building sync documents
1575    /// to avoid exceeding BLE MTU limits.
1576    ///
1577    /// The local CRDT retains all messages up to `CHAT_MAX_MESSAGES`.
1578    pub fn for_sync(&self) -> Self {
1579        if self.messages.len() <= CHAT_SYNC_LIMIT {
1580            return self.clone();
1581        }
1582
1583        // BTreeMap is ordered by key (message_id), and message_id encodes
1584        // timestamp in lower bits, so we take from the end for newest
1585        let messages: BTreeMap<u64, ChatMessage> = self
1586            .messages
1587            .iter()
1588            .rev()
1589            .take(CHAT_SYNC_LIMIT)
1590            .map(|(&k, v)| (k, v.clone()))
1591            .collect();
1592
1593        Self { messages }
1594    }
1595}
1596
1597/// CRDT operation types for sync
1598#[derive(Debug, Clone)]
1599pub enum CrdtOperation {
1600    /// Update a position register
1601    UpdatePosition {
1602        /// Node ID that owns this position
1603        node_id: NodeId,
1604        /// Position data
1605        position: Position,
1606        /// Timestamp of the update
1607        timestamp: Timestamp,
1608    },
1609    /// Update health status register
1610    UpdateHealth {
1611        /// Node ID that owns this status
1612        node_id: NodeId,
1613        /// Health status data
1614        status: HealthStatus,
1615        /// Timestamp of the update
1616        timestamp: Timestamp,
1617    },
1618    /// Increment a counter
1619    IncrementCounter {
1620        /// Counter identifier
1621        counter_id: u8,
1622        /// Node performing the increment
1623        node_id: NodeId,
1624        /// Amount to increment
1625        amount: u64,
1626    },
1627    /// Generic LWW update (key-value)
1628    UpdateRegister {
1629        /// Key for the register
1630        key: String,
1631        /// Value data
1632        value: Vec<u8>,
1633        /// Timestamp of the update
1634        timestamp: Timestamp,
1635        /// Node that set the value
1636        node_id: NodeId,
1637    },
1638}
1639
1640impl CrdtOperation {
1641    /// Get the approximate size in bytes
1642    pub fn size(&self) -> usize {
1643        match self {
1644            CrdtOperation::UpdatePosition { position, .. } => 4 + 8 + position.encode().len(),
1645            CrdtOperation::UpdateHealth { status, .. } => 4 + 8 + status.encode().len(),
1646            CrdtOperation::IncrementCounter { .. } => 1 + 4 + 8,
1647            CrdtOperation::UpdateRegister { key, value, .. } => 4 + 8 + key.len() + value.len(),
1648        }
1649    }
1650
1651    /// Encode to bytes
1652    pub fn encode(&self) -> Vec<u8> {
1653        let mut buf = Vec::new();
1654        match self {
1655            CrdtOperation::UpdatePosition {
1656                node_id,
1657                position,
1658                timestamp,
1659            } => {
1660                buf.push(0x01); // Type tag
1661                buf.extend_from_slice(&node_id.as_u32().to_le_bytes());
1662                buf.extend_from_slice(&timestamp.to_le_bytes());
1663                buf.extend_from_slice(&position.encode());
1664            }
1665            CrdtOperation::UpdateHealth {
1666                node_id,
1667                status,
1668                timestamp,
1669            } => {
1670                buf.push(0x02); // Type tag
1671                buf.extend_from_slice(&node_id.as_u32().to_le_bytes());
1672                buf.extend_from_slice(&timestamp.to_le_bytes());
1673                buf.extend_from_slice(&status.encode());
1674            }
1675            CrdtOperation::IncrementCounter {
1676                counter_id,
1677                node_id,
1678                amount,
1679            } => {
1680                buf.push(0x03); // Type tag
1681                buf.push(*counter_id);
1682                buf.extend_from_slice(&node_id.as_u32().to_le_bytes());
1683                buf.extend_from_slice(&amount.to_le_bytes());
1684            }
1685            CrdtOperation::UpdateRegister {
1686                key,
1687                value,
1688                timestamp,
1689                node_id,
1690            } => {
1691                buf.push(0x04); // Type tag
1692                buf.extend_from_slice(&node_id.as_u32().to_le_bytes());
1693                buf.extend_from_slice(&timestamp.to_le_bytes());
1694                buf.push(key.len() as u8);
1695                buf.extend_from_slice(key.as_bytes());
1696                buf.extend_from_slice(&(value.len() as u16).to_le_bytes());
1697                buf.extend_from_slice(value);
1698            }
1699        }
1700        buf
1701    }
1702
1703    /// Decode from bytes
1704    pub fn decode(data: &[u8]) -> Option<Self> {
1705        if data.is_empty() {
1706            return None;
1707        }
1708
1709        match data[0] {
1710            0x01 => {
1711                // UpdatePosition
1712                if data.len() < 13 {
1713                    return None;
1714                }
1715                let node_id = NodeId::new(u32::from_le_bytes([data[1], data[2], data[3], data[4]]));
1716                let timestamp = u64::from_le_bytes([
1717                    data[5], data[6], data[7], data[8], data[9], data[10], data[11], data[12],
1718                ]);
1719                let position = Position::decode(&data[13..])?;
1720                Some(CrdtOperation::UpdatePosition {
1721                    node_id,
1722                    position,
1723                    timestamp,
1724                })
1725            }
1726            0x02 => {
1727                // UpdateHealth
1728                if data.len() < 13 {
1729                    return None;
1730                }
1731                let node_id = NodeId::new(u32::from_le_bytes([data[1], data[2], data[3], data[4]]));
1732                let timestamp = u64::from_le_bytes([
1733                    data[5], data[6], data[7], data[8], data[9], data[10], data[11], data[12],
1734                ]);
1735                let status = HealthStatus::decode(&data[13..])?;
1736                Some(CrdtOperation::UpdateHealth {
1737                    node_id,
1738                    status,
1739                    timestamp,
1740                })
1741            }
1742            0x03 => {
1743                // IncrementCounter
1744                if data.len() < 14 {
1745                    return None;
1746                }
1747                let counter_id = data[1];
1748                let node_id = NodeId::new(u32::from_le_bytes([data[2], data[3], data[4], data[5]]));
1749                let amount = u64::from_le_bytes([
1750                    data[6], data[7], data[8], data[9], data[10], data[11], data[12], data[13],
1751                ]);
1752                Some(CrdtOperation::IncrementCounter {
1753                    counter_id,
1754                    node_id,
1755                    amount,
1756                })
1757            }
1758            0x04 => {
1759                // UpdateRegister
1760                if data.len() < 14 {
1761                    return None;
1762                }
1763                let node_id = NodeId::new(u32::from_le_bytes([data[1], data[2], data[3], data[4]]));
1764                let timestamp = u64::from_le_bytes([
1765                    data[5], data[6], data[7], data[8], data[9], data[10], data[11], data[12],
1766                ]);
1767                let key_len = data[13] as usize;
1768                if data.len() < 14 + key_len + 2 {
1769                    return None;
1770                }
1771                let key = core::str::from_utf8(&data[14..14 + key_len])
1772                    .ok()?
1773                    .to_string();
1774                let value_len =
1775                    u16::from_le_bytes([data[14 + key_len], data[15 + key_len]]) as usize;
1776                if data.len() < 16 + key_len + value_len {
1777                    return None;
1778                }
1779                let value = data[16 + key_len..16 + key_len + value_len].to_vec();
1780                Some(CrdtOperation::UpdateRegister {
1781                    key,
1782                    value,
1783                    timestamp,
1784                    node_id,
1785                })
1786            }
1787            _ => None,
1788        }
1789    }
1790}
1791
1792#[cfg(test)]
1793mod tests {
1794    use super::*;
1795
1796    // Valid timestamp for tests: 2024-01-15 00:00:00 UTC
1797    // All tests should use this or timestamps derived from it
1798    const TEST_TIMESTAMP: u64 = 1705276800000;
1799
1800    #[test]
1801    fn test_lww_register_basic() {
1802        let mut reg = LwwRegister::new(42u32, 100, NodeId::new(1));
1803        assert_eq!(*reg.get(), 42);
1804        assert_eq!(reg.timestamp(), 100);
1805
1806        // Higher timestamp wins
1807        assert!(reg.set(99, 200, NodeId::new(2)));
1808        assert_eq!(*reg.get(), 99);
1809
1810        // Lower timestamp loses
1811        assert!(!reg.set(50, 150, NodeId::new(3)));
1812        assert_eq!(*reg.get(), 99);
1813    }
1814
1815    #[test]
1816    fn test_lww_register_tiebreak() {
1817        let mut reg = LwwRegister::new(1u32, 100, NodeId::new(1));
1818
1819        // Same timestamp, higher node_id wins
1820        assert!(reg.set(2, 100, NodeId::new(2)));
1821        assert_eq!(*reg.get(), 2);
1822
1823        // Same timestamp, lower node_id loses
1824        assert!(!reg.set(3, 100, NodeId::new(1)));
1825        assert_eq!(*reg.get(), 2);
1826    }
1827
1828    #[test]
1829    fn test_lww_register_merge() {
1830        let mut reg1 = LwwRegister::new(1u32, 100, NodeId::new(1));
1831        let reg2 = LwwRegister::new(2u32, 200, NodeId::new(2));
1832
1833        assert!(reg1.merge(&reg2));
1834        assert_eq!(*reg1.get(), 2);
1835    }
1836
1837    #[test]
1838    fn test_gcounter_basic() {
1839        let mut counter = GCounter::new();
1840        let node1 = NodeId::new(1);
1841        let node2 = NodeId::new(2);
1842
1843        counter.increment(&node1, 5);
1844        counter.increment(&node2, 3);
1845        counter.increment(&node1, 2);
1846
1847        assert_eq!(counter.value(), 10);
1848        assert_eq!(counter.node_count(&node1), 7);
1849        assert_eq!(counter.node_count(&node2), 3);
1850    }
1851
1852    #[test]
1853    fn test_gcounter_merge() {
1854        let mut counter1 = GCounter::new();
1855        let mut counter2 = GCounter::new();
1856        let node1 = NodeId::new(1);
1857        let node2 = NodeId::new(2);
1858
1859        counter1.increment(&node1, 5);
1860        counter2.increment(&node1, 3);
1861        counter2.increment(&node2, 4);
1862
1863        counter1.merge(&counter2);
1864
1865        assert_eq!(counter1.value(), 9); // max(5,3) + 4
1866        assert_eq!(counter1.node_count(&node1), 5);
1867        assert_eq!(counter1.node_count(&node2), 4);
1868    }
1869
1870    #[test]
1871    fn test_gcounter_encode_decode() {
1872        let mut counter = GCounter::new();
1873        counter.increment(&NodeId::new(1), 5);
1874        counter.increment(&NodeId::new(2), 10);
1875
1876        let encoded = counter.encode();
1877        let decoded = GCounter::decode(&encoded).unwrap();
1878
1879        assert_eq!(decoded.value(), counter.value());
1880        assert_eq!(decoded.node_count(&NodeId::new(1)), 5);
1881        assert_eq!(decoded.node_count(&NodeId::new(2)), 10);
1882    }
1883
1884    #[test]
1885    fn test_position_encode_decode() {
1886        let pos = Position::new(37.7749, -122.4194)
1887            .with_altitude(100.0)
1888            .with_accuracy(5.0);
1889
1890        let encoded = pos.encode();
1891        let decoded = Position::decode(&encoded).unwrap();
1892
1893        assert_eq!(decoded.latitude, pos.latitude);
1894        assert_eq!(decoded.longitude, pos.longitude);
1895        assert_eq!(decoded.altitude, pos.altitude);
1896        assert_eq!(decoded.accuracy, pos.accuracy);
1897    }
1898
1899    #[test]
1900    fn test_position_minimal_encode() {
1901        let pos = Position::new(0.0, 0.0);
1902        let encoded = pos.encode();
1903        assert_eq!(encoded.len(), 9); // lat + lon + flags
1904
1905        let pos_with_alt = Position::new(0.0, 0.0).with_altitude(0.0);
1906        let encoded_alt = pos_with_alt.encode();
1907        assert_eq!(encoded_alt.len(), 13);
1908    }
1909
1910    #[test]
1911    fn test_health_status() {
1912        let mut status = HealthStatus::new(85).with_heart_rate(72).with_activity(1);
1913
1914        assert_eq!(status.battery_percent, 85);
1915        assert_eq!(status.heart_rate, Some(72));
1916        assert!(!status.has_alert(HealthStatus::ALERT_MAN_DOWN));
1917
1918        status.set_alert(HealthStatus::ALERT_MAN_DOWN);
1919        assert!(status.has_alert(HealthStatus::ALERT_MAN_DOWN));
1920
1921        let encoded = status.encode();
1922        let decoded = HealthStatus::decode(&encoded).unwrap();
1923        assert_eq!(decoded.battery_percent, 85);
1924        assert_eq!(decoded.heart_rate, Some(72));
1925        assert!(decoded.has_alert(HealthStatus::ALERT_MAN_DOWN));
1926    }
1927
1928    #[test]
1929    fn test_health_status_decode_unknown_activity_preserves_peripheral() {
1930        // Activity values outside the original 0-3 enum (e.g. M5Stack's
1931        // Prone=4 extension) must not cause peripheral-wide rejection.
1932        // The receiver's renderer is responsible for treating unknown
1933        // values as "no animation"; the decoder must keep callsign,
1934        // battery, and other fields intact.
1935        let status = HealthStatus::new(87).with_activity(4);
1936        let encoded = status.encode();
1937        let decoded = HealthStatus::decode(&encoded)
1938            .expect("activity=4 must roundtrip without rejecting the peripheral");
1939        assert_eq!(decoded.battery_percent, 87);
1940        assert_eq!(decoded.activity, 4);
1941    }
1942
1943    #[test]
1944    fn test_crdt_operation_position() {
1945        let op = CrdtOperation::UpdatePosition {
1946            node_id: NodeId::new(0x1234),
1947            position: Position::new(37.0, -122.0),
1948            timestamp: 1000,
1949        };
1950
1951        let encoded = op.encode();
1952        let decoded = CrdtOperation::decode(&encoded).unwrap();
1953
1954        if let CrdtOperation::UpdatePosition {
1955            node_id,
1956            position,
1957            timestamp,
1958        } = decoded
1959        {
1960            assert_eq!(node_id.as_u32(), 0x1234);
1961            assert_eq!(timestamp, 1000);
1962            assert_eq!(position.latitude, 37.0);
1963        } else {
1964            panic!("Wrong operation type");
1965        }
1966    }
1967
1968    #[test]
1969    fn test_crdt_operation_counter() {
1970        let op = CrdtOperation::IncrementCounter {
1971            counter_id: 1,
1972            node_id: NodeId::new(0x5678),
1973            amount: 42,
1974        };
1975
1976        let encoded = op.encode();
1977        let decoded = CrdtOperation::decode(&encoded).unwrap();
1978
1979        if let CrdtOperation::IncrementCounter {
1980            counter_id,
1981            node_id,
1982            amount,
1983        } = decoded
1984        {
1985            assert_eq!(counter_id, 1);
1986            assert_eq!(node_id.as_u32(), 0x5678);
1987            assert_eq!(amount, 42);
1988        } else {
1989            panic!("Wrong operation type");
1990        }
1991    }
1992
1993    #[test]
1994    fn test_crdt_operation_size() {
1995        let pos_op = CrdtOperation::UpdatePosition {
1996            node_id: NodeId::new(1),
1997            position: Position::new(0.0, 0.0),
1998            timestamp: 0,
1999        };
2000        assert!(pos_op.size() > 0);
2001
2002        let counter_op = CrdtOperation::IncrementCounter {
2003            counter_id: 0,
2004            node_id: NodeId::new(1),
2005            amount: 1,
2006        };
2007        assert_eq!(counter_op.size(), 13);
2008    }
2009
2010    // ============================================================================
2011    // Peripheral Tests
2012    // ============================================================================
2013
2014    #[test]
2015    fn test_peripheral_type_from_u8() {
2016        assert_eq!(PeripheralType::from_u8(0), PeripheralType::Unknown);
2017        assert_eq!(PeripheralType::from_u8(1), PeripheralType::SoldierSensor);
2018        assert_eq!(PeripheralType::from_u8(2), PeripheralType::FixedSensor);
2019        assert_eq!(PeripheralType::from_u8(3), PeripheralType::Relay);
2020        assert_eq!(PeripheralType::from_u8(99), PeripheralType::Unknown);
2021    }
2022
2023    #[test]
2024    fn test_event_type_from_u8() {
2025        assert_eq!(EventType::from_u8(0), EventType::None);
2026        assert_eq!(EventType::from_u8(1), EventType::Ping);
2027        assert_eq!(EventType::from_u8(2), EventType::NeedAssist);
2028        assert_eq!(EventType::from_u8(3), EventType::Emergency);
2029        assert_eq!(EventType::from_u8(4), EventType::Moving);
2030        assert_eq!(EventType::from_u8(5), EventType::InPosition);
2031        assert_eq!(EventType::from_u8(6), EventType::Ack);
2032        assert_eq!(EventType::from_u8(99), EventType::None);
2033    }
2034
2035    #[test]
2036    fn test_event_type_labels() {
2037        assert_eq!(EventType::None.label(), "");
2038        assert_eq!(EventType::Emergency.label(), "EMERGENCY");
2039        assert_eq!(EventType::Ping.label(), "PING");
2040    }
2041
2042    #[test]
2043    fn test_peripheral_event_encode_decode() {
2044        let event = PeripheralEvent::new(EventType::Emergency, TEST_TIMESTAMP);
2045        let encoded = event.encode();
2046        assert_eq!(encoded.len(), 9);
2047
2048        let decoded = PeripheralEvent::decode(&encoded).unwrap();
2049        assert_eq!(decoded.event_type, EventType::Emergency);
2050        assert_eq!(decoded.timestamp, TEST_TIMESTAMP);
2051    }
2052
2053    #[test]
2054    fn test_peripheral_new() {
2055        let peripheral = Peripheral::new(0x12345678, PeripheralType::SoldierSensor);
2056        assert_eq!(peripheral.id, 0x12345678);
2057        assert_eq!(peripheral.peripheral_type, PeripheralType::SoldierSensor);
2058        assert_eq!(peripheral.parent_node, 0);
2059        assert!(peripheral.last_event.is_none());
2060    }
2061
2062    #[test]
2063    fn test_peripheral_with_callsign() {
2064        let peripheral = Peripheral::new(1, PeripheralType::SoldierSensor).with_callsign("ALPHA-1");
2065        assert_eq!(peripheral.callsign_str(), "ALPHA-1");
2066
2067        // Test truncation
2068        let peripheral2 = Peripheral::new(2, PeripheralType::SoldierSensor)
2069            .with_callsign("THIS_IS_A_VERY_LONG_CALLSIGN");
2070        assert_eq!(peripheral2.callsign_str(), "THIS_IS_A_VE");
2071    }
2072
2073    #[test]
2074    fn test_peripheral_set_event() {
2075        let mut peripheral = Peripheral::new(1, PeripheralType::SoldierSensor);
2076        peripheral.set_event(EventType::Emergency, TEST_TIMESTAMP);
2077
2078        assert!(peripheral.last_event.is_some());
2079        let event = peripheral.last_event.as_ref().unwrap();
2080        assert_eq!(event.event_type, EventType::Emergency);
2081        assert_eq!(event.timestamp, TEST_TIMESTAMP);
2082        assert_eq!(peripheral.timestamp, TEST_TIMESTAMP);
2083
2084        peripheral.clear_event();
2085        assert!(peripheral.last_event.is_none());
2086    }
2087
2088    #[test]
2089    fn test_peripheral_encode_decode_without_event() {
2090        let peripheral = Peripheral::new(0xAABBCCDD, PeripheralType::SoldierSensor)
2091            .with_callsign("BRAVO-2")
2092            .with_parent(0x11223344);
2093
2094        let encoded = peripheral.encode();
2095        assert_eq!(encoded.len(), 35); // No event + 1 byte has_location flag
2096
2097        let decoded = Peripheral::decode(&encoded).unwrap();
2098        assert_eq!(decoded.id, 0xAABBCCDD);
2099        assert_eq!(decoded.parent_node, 0x11223344);
2100        assert_eq!(decoded.peripheral_type, PeripheralType::SoldierSensor);
2101        assert_eq!(decoded.callsign_str(), "BRAVO-2");
2102        assert!(decoded.last_event.is_none());
2103        assert!(decoded.location.is_none());
2104    }
2105
2106    #[test]
2107    fn test_peripheral_encode_decode_with_event() {
2108        let mut peripheral = Peripheral::new(0x12345678, PeripheralType::SoldierSensor)
2109            .with_callsign("CHARLIE")
2110            .with_parent(0x87654321);
2111        peripheral.health = HealthStatus::new(85);
2112        peripheral.set_event(EventType::NeedAssist, TEST_TIMESTAMP);
2113
2114        let encoded = peripheral.encode();
2115        assert_eq!(encoded.len(), 44); // With event + 1 byte has_location flag
2116
2117        let decoded = Peripheral::decode(&encoded).unwrap();
2118        assert_eq!(decoded.id, 0x12345678);
2119        assert_eq!(decoded.parent_node, 0x87654321);
2120        assert_eq!(decoded.callsign_str(), "CHARLIE");
2121        assert_eq!(decoded.health.battery_percent, 85);
2122        assert!(decoded.last_event.is_some());
2123        let event = decoded.last_event.as_ref().unwrap();
2124        assert_eq!(event.event_type, EventType::NeedAssist);
2125        assert_eq!(event.timestamp, TEST_TIMESTAMP);
2126        assert!(decoded.location.is_none());
2127    }
2128
2129    #[test]
2130    fn test_peripheral_encode_decode_with_location() {
2131        let location = Position::new(37.7749, -122.4194).with_altitude(10.0);
2132        let peripheral = Peripheral::new(0x12345678, PeripheralType::SoldierSensor)
2133            .with_callsign("DELTA")
2134            .with_location(location);
2135
2136        let encoded = peripheral.encode();
2137        // 35 base + 13 location bytes (lat:4 + lon:4 + flags:1 + alt:4)
2138        assert_eq!(encoded.len(), 48);
2139
2140        let decoded = Peripheral::decode(&encoded).unwrap();
2141        assert_eq!(decoded.id, 0x12345678);
2142        assert_eq!(decoded.callsign_str(), "DELTA");
2143        assert!(decoded.location.is_some());
2144
2145        let loc = decoded.location.unwrap();
2146        assert!((loc.latitude - 37.7749).abs() < 0.0001);
2147        assert!((loc.longitude - (-122.4194)).abs() < 0.0001);
2148        assert!(loc.altitude.is_some());
2149        assert!((loc.altitude.unwrap() - 10.0).abs() < 1.0);
2150    }
2151
2152    #[test]
2153    fn test_peripheral_decode_invalid_data() {
2154        // Too short
2155        assert!(Peripheral::decode(&[0u8; 10]).is_none());
2156
2157        // Valid length but id=0 is rejected
2158        let mut data = vec![0u8; 34];
2159        data[25] = 0; // no event flag
2160        assert!(Peripheral::decode(&data).is_none()); // id=0 rejected
2161
2162        // Valid id but no event - should succeed
2163        data[0..4].copy_from_slice(&1u32.to_le_bytes()); // id=1
2164        assert!(Peripheral::decode(&data).is_some());
2165
2166        // Claims to have event but too short
2167        data[25] = 1; // has event flag
2168        assert!(Peripheral::decode(&data).is_none());
2169    }
2170
2171    // ============================================================================
2172    // EmergencyEvent Tests
2173    // ============================================================================
2174
2175    #[test]
2176    fn test_emergency_event_new() {
2177        let peers = vec![0x22222222, 0x33333333];
2178        let event = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP, &peers);
2179
2180        assert_eq!(event.source_node(), 0x11111111);
2181        assert_eq!(event.timestamp(), TEST_TIMESTAMP);
2182        assert_eq!(event.peer_count(), 3); // source + 2 peers
2183
2184        // Source is auto-acked
2185        assert!(event.has_acked(0x11111111));
2186        // Others are not
2187        assert!(!event.has_acked(0x22222222));
2188        assert!(!event.has_acked(0x33333333));
2189    }
2190
2191    #[test]
2192    fn test_emergency_event_ack() {
2193        let peers = vec![0x22222222, 0x33333333];
2194        let mut event = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP, &peers);
2195
2196        assert_eq!(event.ack_count(), 1); // just source
2197        assert!(!event.all_acked());
2198
2199        // ACK from first peer
2200        assert!(event.ack(0x22222222)); // returns true - new ack
2201        assert_eq!(event.ack_count(), 2);
2202        assert!(!event.all_acked());
2203
2204        // Duplicate ACK
2205        assert!(!event.ack(0x22222222)); // returns false - already acked
2206
2207        // ACK from second peer
2208        assert!(event.ack(0x33333333));
2209        assert_eq!(event.ack_count(), 3);
2210        assert!(event.all_acked());
2211    }
2212
2213    #[test]
2214    fn test_emergency_event_pending_nodes() {
2215        let peers = vec![0x22222222, 0x33333333];
2216        let mut event = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP, &peers);
2217
2218        let pending = event.pending_nodes();
2219        assert_eq!(pending.len(), 2);
2220        assert!(pending.contains(&0x22222222));
2221        assert!(pending.contains(&0x33333333));
2222
2223        event.ack(0x22222222);
2224        let pending = event.pending_nodes();
2225        assert_eq!(pending.len(), 1);
2226        assert!(pending.contains(&0x33333333));
2227    }
2228
2229    #[test]
2230    fn test_emergency_event_encode_decode() {
2231        let peers = vec![0x22222222, 0x33333333];
2232        let mut event = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP, &peers);
2233        event.ack(0x22222222);
2234
2235        let encoded = event.encode();
2236        let decoded = EmergencyEvent::decode(&encoded).unwrap();
2237
2238        assert_eq!(decoded.source_node(), 0x11111111);
2239        assert_eq!(decoded.timestamp(), TEST_TIMESTAMP);
2240        assert!(decoded.has_acked(0x11111111));
2241        assert!(decoded.has_acked(0x22222222));
2242        assert!(!decoded.has_acked(0x33333333));
2243    }
2244
2245    #[test]
2246    fn test_emergency_event_merge_same_event() {
2247        // Two nodes have the same emergency, different ack states
2248        let peers = vec![0x22222222, 0x33333333];
2249        let mut event1 = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP, &peers);
2250        let mut event2 = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP, &peers);
2251
2252        event1.ack(0x22222222);
2253        event2.ack(0x33333333);
2254
2255        // Merge event2 into event1
2256        let changed = event1.merge(&event2);
2257        assert!(changed);
2258        assert!(event1.has_acked(0x22222222));
2259        assert!(event1.has_acked(0x33333333));
2260        assert!(event1.all_acked());
2261    }
2262
2263    #[test]
2264    fn test_emergency_event_merge_different_events() {
2265        // Old emergency
2266        let mut old_event = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP, &[0x22222222]);
2267        old_event.ack(0x22222222);
2268
2269        // New emergency from different source
2270        let new_event =
2271            EmergencyEvent::new(0x33333333, TEST_TIMESTAMP + 1000, &[0x11111111, 0x22222222]);
2272
2273        // Merge new into old - should replace
2274        let changed = old_event.merge(&new_event);
2275        assert!(changed);
2276        assert_eq!(old_event.source_node(), 0x33333333);
2277        assert_eq!(old_event.timestamp(), TEST_TIMESTAMP + 1000);
2278        // Old ack state should be gone
2279        assert!(!old_event.has_acked(0x22222222));
2280    }
2281
2282    #[test]
2283    fn test_emergency_event_merge_older_event_ignored() {
2284        // Current emergency
2285        let mut current = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP + 1000, &[0x22222222]);
2286
2287        // Older emergency
2288        let older = EmergencyEvent::new(0x33333333, TEST_TIMESTAMP, &[0x11111111]);
2289
2290        // Merge older into current - should NOT replace
2291        let changed = current.merge(&older);
2292        assert!(!changed);
2293        assert_eq!(current.source_node(), 0x11111111);
2294        assert_eq!(current.timestamp(), TEST_TIMESTAMP + 1000);
2295    }
2296
2297    #[test]
2298    fn test_emergency_event_add_peer() {
2299        let mut event = EmergencyEvent::new(0x11111111, TEST_TIMESTAMP, &[]);
2300
2301        // Add a peer discovered after emergency started
2302        event.add_peer(0x22222222);
2303        assert!(!event.has_acked(0x22222222));
2304        assert_eq!(event.peer_count(), 2);
2305
2306        // Adding same peer again doesn't change ack status
2307        event.ack(0x22222222);
2308        event.add_peer(0x22222222);
2309        assert!(event.has_acked(0x22222222)); // still acked
2310    }
2311
2312    #[test]
2313    fn test_emergency_event_decode_invalid() {
2314        // Too short
2315        assert!(EmergencyEvent::decode(&[0u8; 10]).is_none());
2316
2317        // Valid header but claims more acks than data
2318        let mut data = vec![0u8; 16];
2319        data[12] = 5; // claims 5 ack entries
2320        assert!(EmergencyEvent::decode(&data).is_none());
2321    }
2322
2323    // ============================================================================
2324    // ChatMessage Tests (gated behind legacy-chat)
2325    // ============================================================================
2326
2327    #[cfg(feature = "legacy-chat")]
2328    mod chat_tests {
2329        use super::*;
2330
2331        #[test]
2332        fn test_chat_message_new() {
2333            let msg = ChatMessage::new(0x12345678, TEST_TIMESTAMP, "ALPHA-1", "Hello mesh!");
2334            assert_eq!(msg.origin_node, 0x12345678);
2335            assert_eq!(msg.timestamp, TEST_TIMESTAMP);
2336            assert_eq!(msg.sender(), "ALPHA-1");
2337            assert_eq!(msg.text(), "Hello mesh!");
2338            assert!(msg.is_broadcast);
2339            assert!(!msg.requires_ack);
2340            assert!(!msg.is_reply());
2341        }
2342
2343        #[test]
2344        fn test_chat_message_reply_to() {
2345            let mut msg =
2346                ChatMessage::new(0x12345678, TEST_TIMESTAMP + 1000, "BRAVO", "Roger that");
2347            msg.set_reply_to(0xAABBCCDD, TEST_TIMESTAMP);
2348
2349            assert!(msg.is_reply());
2350            assert_eq!(msg.reply_to_node, 0xAABBCCDD);
2351            assert_eq!(msg.reply_to_timestamp, TEST_TIMESTAMP);
2352        }
2353
2354        #[test]
2355        fn test_chat_message_truncation() {
2356            // Test sender truncation (max 12 bytes)
2357            let msg = ChatMessage::new(0x1, TEST_TIMESTAMP, "VERY_LONG_CALLSIGN", "Hi");
2358            assert_eq!(msg.sender(), "VERY_LONG_CA"); // 12 chars
2359
2360            // Test text truncation (max 128 bytes)
2361            let long_text = "A".repeat(200);
2362            let msg = ChatMessage::new(0x1, TEST_TIMESTAMP, "X", &long_text);
2363            assert_eq!(msg.text().len(), 128);
2364        }
2365
2366        #[test]
2367        fn test_chat_message_id() {
2368            let msg = ChatMessage::new(0x12345678, 0x18D4A51_ABCDEF01, "X", "Y");
2369            let id = msg.message_id();
2370            // ID = (origin << 32) | (timestamp & 0xFFFFFFFF)
2371            assert_eq!(id, (0x12345678u64 << 32) | 0xABCDEF01);
2372        }
2373
2374        #[test]
2375        fn test_chat_message_encode_decode() {
2376            let mut msg = ChatMessage::new(0x12345678, TEST_TIMESTAMP, "CHARLIE", "Test message");
2377            msg.is_broadcast = true;
2378            msg.requires_ack = true;
2379            msg.set_reply_to(0xAABBCCDD, TEST_TIMESTAMP - 1000);
2380
2381            let encoded = msg.encode();
2382            let (decoded, len) = ChatMessage::decode(&encoded).unwrap();
2383
2384            assert_eq!(len, encoded.len());
2385            assert_eq!(decoded.origin_node, 0x12345678);
2386            assert_eq!(decoded.timestamp, TEST_TIMESTAMP);
2387            assert_eq!(decoded.sender(), "CHARLIE");
2388            assert_eq!(decoded.text(), "Test message");
2389            assert!(decoded.is_broadcast);
2390            assert!(decoded.requires_ack);
2391            assert_eq!(decoded.reply_to_node, 0xAABBCCDD);
2392            assert_eq!(decoded.reply_to_timestamp, TEST_TIMESTAMP - 1000);
2393        }
2394
2395        #[test]
2396        fn test_chat_message_decode_empty_text() {
2397            // Message with valid sender but empty text (allowed for ACK messages)
2398            let msg = ChatMessage::new(0x1, TEST_TIMESTAMP, "ACK-NODE", "");
2399            let encoded = msg.encode();
2400            let (decoded, _) = ChatMessage::decode(&encoded).unwrap();
2401            assert_eq!(decoded.sender(), "ACK-NODE");
2402            assert_eq!(decoded.text(), "");
2403        }
2404
2405        // ============================================================================
2406        // ChatMessage Validation/Security Tests
2407        // ============================================================================
2408
2409        #[test]
2410        fn test_chat_message_decode_rejects_zero_origin() {
2411            // Build message bytes manually with origin_node = 0
2412            let msg = ChatMessage::new(0x12345678, TEST_TIMESTAMP, "TEST", "msg");
2413            let mut encoded = msg.encode();
2414            // Zero out the origin_node (first 4 bytes)
2415            encoded[0] = 0;
2416            encoded[1] = 0;
2417            encoded[2] = 0;
2418            encoded[3] = 0;
2419
2420            assert!(ChatMessage::decode(&encoded).is_none());
2421        }
2422
2423        #[test]
2424        fn test_chat_message_decode_rejects_old_timestamp() {
2425            // Build message bytes manually with timestamp before 2020
2426            let msg = ChatMessage::new(0x12345678, TEST_TIMESTAMP, "TEST", "msg");
2427            let mut encoded = msg.encode();
2428            // Set timestamp to 1000 (way before 2020)
2429            let old_ts: u64 = 1000;
2430            encoded[4..12].copy_from_slice(&old_ts.to_le_bytes());
2431
2432            assert!(ChatMessage::decode(&encoded).is_none());
2433        }
2434
2435        #[test]
2436        fn test_chat_message_decode_rejects_empty_sender() {
2437            // Build message bytes manually with sender_len = 0
2438            let msg = ChatMessage::new(0x12345678, TEST_TIMESTAMP, "X", "msg");
2439            let mut encoded = msg.encode();
2440            // Set sender_len to 0 at offset 12
2441            encoded[12] = 0;
2442            // Adjust text position (move text_len and text to right after sender_len)
2443            // This is tricky - we need to rebuild the encoding properly
2444            // Actually easier to just build raw bytes:
2445            let mut raw = Vec::new();
2446            raw.extend_from_slice(&0x12345678u32.to_le_bytes()); // origin_node
2447            raw.extend_from_slice(&TEST_TIMESTAMP.to_le_bytes()); // timestamp
2448            raw.push(0); // sender_len = 0 (INVALID)
2449            raw.push(3); // text_len = 3
2450            raw.extend_from_slice(b"msg"); // text
2451            raw.push(0x01); // flags
2452
2453            assert!(ChatMessage::decode(&raw).is_none());
2454        }
2455
2456        #[test]
2457        fn test_chat_message_decode_rejects_invalid_utf8_sender() {
2458            // Build message with invalid UTF-8 in sender
2459            let mut raw = Vec::new();
2460            raw.extend_from_slice(&0x12345678u32.to_le_bytes()); // origin_node
2461            raw.extend_from_slice(&TEST_TIMESTAMP.to_le_bytes()); // timestamp
2462            raw.push(4); // sender_len = 4
2463            raw.extend_from_slice(&[0x66, 0x59, 0xFF, 0xFE]); // "fY" + invalid UTF-8
2464            raw.push(3); // text_len = 3
2465            raw.extend_from_slice(b"msg"); // text
2466            raw.push(0x01); // flags
2467            raw.extend_from_slice(&0u32.to_le_bytes()); // reply_to_node
2468            raw.extend_from_slice(&0u64.to_le_bytes()); // reply_to_timestamp
2469
2470            assert!(ChatMessage::decode(&raw).is_none());
2471        }
2472
2473        #[test]
2474        fn test_chat_message_decode_rejects_invalid_utf8_text() {
2475            // Build message with invalid UTF-8 in text
2476            let mut raw = Vec::new();
2477            raw.extend_from_slice(&0x12345678u32.to_le_bytes()); // origin_node
2478            raw.extend_from_slice(&TEST_TIMESTAMP.to_le_bytes()); // timestamp
2479            raw.push(4); // sender_len = 4
2480            raw.extend_from_slice(b"TEST"); // valid sender
2481            raw.push(4); // text_len = 4
2482            raw.extend_from_slice(&[0x80, 0x81, 0x82, 0x83]); // invalid UTF-8
2483            raw.push(0x01); // flags
2484            raw.extend_from_slice(&0u32.to_le_bytes()); // reply_to_node
2485            raw.extend_from_slice(&0u64.to_le_bytes()); // reply_to_timestamp
2486
2487            assert!(ChatMessage::decode(&raw).is_none());
2488        }
2489
2490        #[test]
2491        fn test_chat_message_decode_accepts_valid_utf8() {
2492            // Build message with valid UTF-8 including unicode
2493            let mut raw = Vec::new();
2494            raw.extend_from_slice(&0x12345678u32.to_le_bytes()); // origin_node
2495            raw.extend_from_slice(&TEST_TIMESTAMP.to_le_bytes()); // timestamp
2496            raw.push(6); // sender_len = 6 (UTF-8 encoded)
2497            raw.extend_from_slice("TËST".as_bytes()); // valid UTF-8 with umlaut (5 bytes: T, Ë=2bytes, S, T)
2498                                                      // Wait, "TËST" is 5 bytes not 6. Let me fix:
2499            let sender_bytes = "TËST1".as_bytes(); // T(1) + Ë(2) + S(1) + T(1) + 1(1) = 6 bytes
2500            raw[12] = sender_bytes.len() as u8;
2501            raw.truncate(13);
2502            raw.extend_from_slice(sender_bytes);
2503            raw.push(4); // text_len = 4
2504            raw.extend_from_slice(b"test"); // text
2505            raw.push(0x01); // flags
2506            raw.extend_from_slice(&0u32.to_le_bytes()); // reply_to_node
2507            raw.extend_from_slice(&0u64.to_le_bytes()); // reply_to_timestamp
2508
2509            let result = ChatMessage::decode(&raw);
2510            assert!(result.is_some());
2511            let (msg, _) = result.unwrap();
2512            assert_eq!(msg.sender(), "TËST1");
2513        }
2514
2515        // ============================================================================
2516        // ChatCRDT Tests
2517        // ============================================================================
2518
2519        #[test]
2520        fn test_chat_crdt_new() {
2521            let chat = ChatCRDT::new();
2522            assert!(chat.is_empty());
2523            assert_eq!(chat.len(), 0);
2524        }
2525
2526        #[test]
2527        fn test_chat_crdt_add_message() {
2528            let mut chat = ChatCRDT::new();
2529
2530            let msg = ChatMessage::new(0x12345678, TEST_TIMESTAMP, "ALPHA", "Hello");
2531            assert!(chat.add_message(msg.clone()));
2532            assert_eq!(chat.len(), 1);
2533
2534            // Duplicate should be rejected
2535            assert!(!chat.add_message(msg));
2536            assert_eq!(chat.len(), 1);
2537        }
2538
2539        #[test]
2540        fn test_chat_crdt_send_message() {
2541            let mut chat = ChatCRDT::new();
2542
2543            assert!(chat.send_message(0x1, TEST_TIMESTAMP, "ALPHA", "First"));
2544            assert!(chat.send_message(0x2, TEST_TIMESTAMP + 1, "BRAVO", "Second"));
2545            assert_eq!(chat.len(), 2);
2546
2547            // Same node, same timestamp = duplicate
2548            assert!(!chat.send_message(0x1, TEST_TIMESTAMP, "ALPHA", "Duplicate"));
2549            assert_eq!(chat.len(), 2);
2550        }
2551
2552        #[test]
2553        fn test_chat_crdt_get_message() {
2554            let mut chat = ChatCRDT::new();
2555            chat.send_message(0x12345678, TEST_TIMESTAMP, "ALPHA", "Test");
2556
2557            let msg = chat.get_message(0x12345678, TEST_TIMESTAMP);
2558            assert!(msg.is_some());
2559            assert_eq!(msg.unwrap().text(), "Test");
2560
2561            // Non-existent message
2562            assert!(chat.get_message(0x99999999, TEST_TIMESTAMP).is_none());
2563        }
2564
2565        #[test]
2566        fn test_chat_crdt_merge() {
2567            let mut chat1 = ChatCRDT::new();
2568            let mut chat2 = ChatCRDT::new();
2569
2570            chat1.send_message(0x1, TEST_TIMESTAMP, "ALPHA", "From 1");
2571            chat2.send_message(0x2, TEST_TIMESTAMP + 1, "BRAVO", "From 2");
2572
2573            // Merge chat2 into chat1
2574            let changed = chat1.merge(&chat2);
2575            assert!(changed);
2576            assert_eq!(chat1.len(), 2);
2577
2578            // Merge again - no changes
2579            let changed = chat1.merge(&chat2);
2580            assert!(!changed);
2581            assert_eq!(chat1.len(), 2);
2582        }
2583
2584        #[test]
2585        fn test_chat_crdt_merge_duplicates() {
2586            let mut chat1 = ChatCRDT::new();
2587            let mut chat2 = ChatCRDT::new();
2588
2589            // Both have the same message
2590            chat1.send_message(0x1, TEST_TIMESTAMP, "ALPHA", "Same message");
2591            chat2.send_message(0x1, TEST_TIMESTAMP, "ALPHA", "Same message");
2592
2593            // Merge should not create duplicates
2594            chat1.merge(&chat2);
2595            assert_eq!(chat1.len(), 1);
2596        }
2597
2598        #[test]
2599        fn test_chat_crdt_pruning() {
2600            let mut chat = ChatCRDT::new();
2601
2602            // Add more than CHAT_MAX_MESSAGES with valid timestamps
2603            for i in 0..(CHAT_MAX_MESSAGES + 10) {
2604                chat.send_message(i as u32 + 1, TEST_TIMESTAMP + i as u64, "X", "Y");
2605            }
2606
2607            // Should be pruned to max
2608            assert_eq!(chat.len(), CHAT_MAX_MESSAGES);
2609
2610            // Oldest messages should be removed (first 10)
2611            // Message IDs are (node << 32) | (timestamp & 0xFFFFFFFF)
2612            // node=1, ts=TEST_TIMESTAMP has lowest ID, etc.
2613            assert!(chat.get_message(1, TEST_TIMESTAMP).is_none());
2614            assert!(chat.get_message(10, TEST_TIMESTAMP + 9).is_none());
2615            // Newer messages should remain
2616            assert!(chat.get_message(11, TEST_TIMESTAMP + 10).is_some());
2617        }
2618
2619        #[test]
2620        fn test_chat_crdt_encode_decode() {
2621            let mut chat = ChatCRDT::new();
2622            chat.send_message(0x12345678, TEST_TIMESTAMP, "ALPHA", "First message");
2623            chat.send_message(0xAABBCCDD, TEST_TIMESTAMP + 1000, "BRAVO", "Second message");
2624
2625            let encoded = chat.encode();
2626            let decoded = ChatCRDT::decode(&encoded).unwrap();
2627
2628            assert_eq!(decoded.len(), 2);
2629            assert!(decoded.get_message(0x12345678, TEST_TIMESTAMP).is_some());
2630            assert!(decoded
2631                .get_message(0xAABBCCDD, TEST_TIMESTAMP + 1000)
2632                .is_some());
2633        }
2634
2635        #[test]
2636        fn test_chat_crdt_messages_since() {
2637            let mut chat = ChatCRDT::new();
2638            chat.send_message(0x1, TEST_TIMESTAMP, "A", "Old");
2639            chat.send_message(0x2, TEST_TIMESTAMP + 1000, "B", "Mid");
2640            chat.send_message(0x3, TEST_TIMESTAMP + 2000, "C", "New");
2641
2642            let recent: Vec<_> = chat.messages_since(TEST_TIMESTAMP + 500).collect();
2643            assert_eq!(recent.len(), 2);
2644        }
2645
2646        #[test]
2647        fn test_chat_crdt_newest_timestamp() {
2648            let mut chat = ChatCRDT::new();
2649            assert!(chat.newest_timestamp().is_none());
2650
2651            chat.send_message(0x1, TEST_TIMESTAMP, "A", "1");
2652            assert_eq!(chat.newest_timestamp(), Some(TEST_TIMESTAMP));
2653
2654            chat.send_message(0x2, TEST_TIMESTAMP + 2000, "B", "2");
2655            assert_eq!(chat.newest_timestamp(), Some(TEST_TIMESTAMP + 2000));
2656
2657            chat.send_message(0x3, TEST_TIMESTAMP + 1000, "C", "3"); // older timestamp
2658            assert_eq!(chat.newest_timestamp(), Some(TEST_TIMESTAMP + 2000));
2659        }
2660
2661        // ============================================================================
2662        // ChatCRDT Security/Validation Tests
2663        // ============================================================================
2664
2665        #[test]
2666        fn test_chat_crdt_decode_skips_invalid_messages() {
2667            // Build a CRDT with 2 messages, one valid and one invalid
2668            let mut valid_chat = ChatCRDT::new();
2669            valid_chat.send_message(0x12345678, TEST_TIMESTAMP, "VALID", "Good message");
2670
2671            let encoded = valid_chat.encode();
2672
2673            // Decode should work and contain the valid message
2674            let decoded = ChatCRDT::decode(&encoded).unwrap();
2675            assert_eq!(decoded.len(), 1);
2676            assert!(decoded.get_message(0x12345678, TEST_TIMESTAMP).is_some());
2677        }
2678
2679        #[test]
2680        fn test_chat_crdt_decode_handles_truncated_data() {
2681            let mut chat = ChatCRDT::new();
2682            chat.send_message(0x12345678, TEST_TIMESTAMP, "TEST", "Message");
2683
2684            let encoded = chat.encode();
2685
2686            // Truncate to just the message count
2687            let truncated = &encoded[..2];
2688            let decoded = ChatCRDT::decode(truncated);
2689            assert!(decoded.is_some());
2690            assert_eq!(decoded.unwrap().len(), 0); // No messages decoded
2691        }
2692    } // mod chat_tests
2693}