hive_btle/
document.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//! HIVE Document wire format for BLE mesh sync
17//!
18//! This module provides the unified document format used across all platforms
19//! (iOS, Android, ESP32) for mesh synchronization. The format is designed for
20//! efficient BLE transmission while supporting CRDT semantics.
21//!
22//! ## Wire Format
23//!
24//! ```text
25//! Header (8 bytes):
26//!   version:  4 bytes (LE) - document version number
27//!   node_id:  4 bytes (LE) - source node identifier
28//!
29//! GCounter (4 + N*12 bytes):
30//!   num_entries: 4 bytes (LE)
31//!   entries[N]:
32//!     node_id: 4 bytes (LE)
33//!     count:   8 bytes (LE)
34//!
35//! Extended Section (optional, when peripheral data present):
36//!   marker:         1 byte (0xAB)
37//!   reserved:       1 byte
38//!   peripheral_len: 2 bytes (LE)
39//!   peripheral:     variable (34-43 bytes)
40//! ```
41
42#[cfg(not(feature = "std"))]
43use alloc::vec::Vec;
44
45use crate::sync::crdt::{
46    ChatCRDT, EmergencyEvent, EventType, GCounter, Peripheral, PeripheralEvent,
47};
48use crate::NodeId;
49
50/// Marker byte indicating extended section with peripheral data
51pub const EXTENDED_MARKER: u8 = 0xAB;
52
53/// Marker byte indicating emergency event section
54pub const EMERGENCY_MARKER: u8 = 0xAC;
55
56/// Marker byte indicating chat CRDT section
57///
58/// Used to include persisted chat messages in the document for CRDT sync.
59///
60/// ```text
61/// marker:   1 byte (0xAD)
62/// reserved: 1 byte (0x00)
63/// len:      2 bytes (LE) - length of chat CRDT data
64/// chat:     variable - ChatCRDT encoded data
65/// ```
66pub const CHAT_MARKER: u8 = 0xAD;
67
68/// Marker byte indicating encrypted document (mesh-wide)
69///
70/// When present, the entire document payload following the marker is encrypted
71/// using ChaCha20-Poly1305. The marker format is:
72///
73/// ```text
74/// marker:   1 byte (0xAE)
75/// reserved: 1 byte (0x00)
76/// payload:  12 bytes nonce + variable ciphertext (includes 16-byte auth tag)
77/// ```
78///
79/// Encryption happens at the HiveMesh layer before transmission, and decryption
80/// happens upon receipt before document parsing.
81pub const ENCRYPTED_MARKER: u8 = 0xAE;
82
83/// Marker byte indicating per-peer E2EE message
84///
85/// Used for end-to-end encrypted messages between specific peer pairs.
86/// Only the sender and recipient (who share a session key) can decrypt.
87///
88/// ```text
89/// marker:     1 byte (0xAF)
90/// flags:      1 byte (bit 0: key_exchange, bit 1: forward_secrecy)
91/// recipient:  4 bytes (LE) - recipient node ID
92/// sender:     4 bytes (LE) - sender node ID
93/// counter:    8 bytes (LE) - message counter for replay protection
94/// nonce:      12 bytes
95/// ciphertext: variable (includes 16-byte auth tag)
96/// ```
97pub const PEER_E2EE_MARKER: u8 = 0xAF;
98
99/// Marker byte indicating key exchange message for per-peer E2EE
100///
101/// Used to establish E2EE sessions between peers via X25519 key exchange.
102///
103/// ```text
104/// marker:     1 byte (0xB0)
105/// sender:     4 bytes (LE) - sender node ID
106/// flags:      1 byte (bit 0: is_ephemeral)
107/// public_key: 32 bytes - X25519 public key
108/// ```
109pub const KEY_EXCHANGE_MARKER: u8 = 0xB0;
110
111/// Marker byte indicating relay envelope for multi-hop transmission
112///
113/// Used to wrap documents for multi-hop relay with deduplication and TTL.
114/// See [`crate::relay`] module for details.
115///
116/// ```text
117/// marker:        1 byte (0xB1)
118/// flags:         1 byte (bit 0: requires_ack, bit 1: is_broadcast)
119/// message_id:    16 bytes (UUID for deduplication)
120/// hop_count:     1 byte (current hop count)
121/// max_hops:      1 byte (TTL)
122/// origin_node:   4 bytes (LE) - original sender node ID
123/// payload_len:   4 bytes (LE)
124/// payload:       variable (encrypted document)
125/// ```
126pub const RELAY_ENVELOPE_MARKER: u8 = 0xB1;
127
128/// Marker byte indicating delta document for bandwidth-efficient sync
129///
130/// Used to send only changed operations instead of full state snapshots.
131/// See [`crate::sync::delta_document`] module for details.
132///
133/// ```text
134/// marker:        1 byte (0xB2)
135/// flags:         1 byte (bit 0: has_vector_clock, bit 1: is_response)
136/// origin_node:   4 bytes (LE) - origin node ID
137/// timestamp_ms:  8 bytes (LE) - creation timestamp
138/// vector_clock:  variable (if flag set)
139/// op_count:      2 bytes (LE) - number of operations
140/// operations:    variable
141/// ```
142pub const DELTA_DOCUMENT_MARKER: u8 = 0xB2;
143
144/// Minimum document size (header only, no counter entries)
145pub const MIN_DOCUMENT_SIZE: usize = 8;
146
147/// Maximum recommended mesh size for reliable single-packet sync
148///
149/// Beyond this, documents may exceed typical BLE MTU (244 bytes).
150/// Size calculation: 8 (header) + 4 + (N × 12) (GCounter) + 42 (Peripheral)
151///   20 nodes = 8 + 244 + 42 = 294 bytes
152pub const MAX_MESH_SIZE: usize = 20;
153
154/// Target document size for single-packet transmission
155///
156/// Based on typical negotiated BLE MTU (247 bytes - 3 ATT overhead).
157pub const TARGET_DOCUMENT_SIZE: usize = 244;
158
159/// Hard limit before fragmentation would be required
160///
161/// BLE 5.0+ supports up to 517 byte MTU, but 512 is practical max payload.
162pub const MAX_DOCUMENT_SIZE: usize = 512;
163
164/// A HIVE document for mesh synchronization
165///
166/// Contains header information, a CRDT G-Counter for tracking mesh activity,
167/// optional peripheral data for events, optional emergency event with ACK tracking,
168/// and optional chat CRDT for mesh-wide messaging.
169#[derive(Debug, Clone)]
170pub struct HiveDocument {
171    /// Document version (incremented on each change)
172    pub version: u32,
173
174    /// Source node ID that created/last modified this document
175    pub node_id: NodeId,
176
177    /// CRDT G-Counter tracking activity across the mesh
178    pub counter: GCounter,
179
180    /// Optional peripheral data (sensor info, events)
181    pub peripheral: Option<Peripheral>,
182
183    /// Optional active emergency event with distributed ACK tracking
184    pub emergency: Option<EmergencyEvent>,
185
186    /// Optional chat CRDT for mesh-wide messaging
187    ///
188    /// Contains persisted chat messages that sync across the mesh using
189    /// add-only set semantics. Messages are identified by (origin_node, timestamp)
190    /// and automatically deduplicated during merge.
191    pub chat: Option<ChatCRDT>,
192}
193
194impl Default for HiveDocument {
195    fn default() -> Self {
196        Self {
197            version: 1,
198            node_id: NodeId::default(),
199            counter: GCounter::new(),
200            peripheral: None,
201            emergency: None,
202            chat: None,
203        }
204    }
205}
206
207impl HiveDocument {
208    /// Create a new document for the given node
209    pub fn new(node_id: NodeId) -> Self {
210        Self {
211            version: 1,
212            node_id,
213            counter: GCounter::new(),
214            peripheral: None,
215            emergency: None,
216            chat: None,
217        }
218    }
219
220    /// Create with an initial peripheral
221    pub fn with_peripheral(mut self, peripheral: Peripheral) -> Self {
222        self.peripheral = Some(peripheral);
223        self
224    }
225
226    /// Create with an initial emergency event
227    pub fn with_emergency(mut self, emergency: EmergencyEvent) -> Self {
228        self.emergency = Some(emergency);
229        self
230    }
231
232    /// Create with an initial chat CRDT
233    pub fn with_chat(mut self, chat: ChatCRDT) -> Self {
234        self.chat = Some(chat);
235        self
236    }
237
238    /// Increment the document version
239    pub fn increment_version(&mut self) {
240        self.version = self.version.wrapping_add(1);
241    }
242
243    /// Increment the counter for this node
244    pub fn increment_counter(&mut self) {
245        self.counter.increment(&self.node_id, 1);
246        self.increment_version();
247    }
248
249    /// Set an event on the peripheral
250    pub fn set_event(&mut self, event_type: EventType, timestamp: u64) {
251        if let Some(ref mut peripheral) = self.peripheral {
252            peripheral.set_event(event_type, timestamp);
253            self.increment_counter();
254        }
255    }
256
257    /// Clear the event from the peripheral
258    pub fn clear_event(&mut self) {
259        if let Some(ref mut peripheral) = self.peripheral {
260            peripheral.clear_event();
261            self.increment_version();
262        }
263    }
264
265    /// Set an emergency event
266    ///
267    /// Creates a new emergency event with the given source node, timestamp,
268    /// and list of known peers to track for ACKs.
269    pub fn set_emergency(&mut self, source_node: u32, timestamp: u64, known_peers: &[u32]) {
270        self.emergency = Some(EmergencyEvent::new(source_node, timestamp, known_peers));
271        self.increment_counter();
272    }
273
274    /// Record an ACK for the current emergency
275    ///
276    /// Returns true if the ACK was new (state changed)
277    pub fn ack_emergency(&mut self, node_id: u32) -> bool {
278        if let Some(ref mut emergency) = self.emergency {
279            if emergency.ack(node_id) {
280                self.increment_version();
281                return true;
282            }
283        }
284        false
285    }
286
287    /// Clear the emergency event
288    pub fn clear_emergency(&mut self) {
289        if self.emergency.is_some() {
290            self.emergency = None;
291            self.increment_version();
292        }
293    }
294
295    /// Get the current emergency event (if any)
296    pub fn get_emergency(&self) -> Option<&EmergencyEvent> {
297        self.emergency.as_ref()
298    }
299
300    /// Check if there's an active emergency
301    pub fn has_emergency(&self) -> bool {
302        self.emergency.is_some()
303    }
304
305    // --- Chat CRDT methods ---
306
307    /// Get the chat CRDT (if any)
308    pub fn get_chat(&self) -> Option<&ChatCRDT> {
309        self.chat.as_ref()
310    }
311
312    /// Get mutable reference to the chat CRDT, creating it if needed
313    pub fn get_or_create_chat(&mut self) -> &mut ChatCRDT {
314        if self.chat.is_none() {
315            self.chat = Some(ChatCRDT::new());
316        }
317        self.chat.as_mut().unwrap()
318    }
319
320    /// Add a chat message to the document
321    ///
322    /// Returns true if the message was new (not a duplicate)
323    pub fn add_chat_message(
324        &mut self,
325        origin_node: u32,
326        timestamp: u64,
327        sender: &str,
328        text: &str,
329    ) -> bool {
330        use crate::sync::crdt::ChatMessage;
331
332        let mut msg = ChatMessage::new(origin_node, timestamp, sender, text);
333        msg.is_broadcast = true;
334
335        let chat = self.get_or_create_chat();
336        if chat.add_message(msg) {
337            self.increment_counter();
338            true
339        } else {
340            false
341        }
342    }
343
344    /// Add a chat message with reply-to information
345    pub fn add_chat_reply(
346        &mut self,
347        origin_node: u32,
348        timestamp: u64,
349        sender: &str,
350        text: &str,
351        reply_to_node: u32,
352        reply_to_timestamp: u64,
353    ) -> bool {
354        use crate::sync::crdt::ChatMessage;
355
356        let mut msg = ChatMessage::new(origin_node, timestamp, sender, text);
357        msg.is_broadcast = true;
358        msg.set_reply_to(reply_to_node, reply_to_timestamp);
359
360        let chat = self.get_or_create_chat();
361        if chat.add_message(msg) {
362            self.increment_counter();
363            true
364        } else {
365            false
366        }
367    }
368
369    /// Check if the document has any chat messages
370    pub fn has_chat(&self) -> bool {
371        self.chat.as_ref().is_some_and(|c| !c.is_empty())
372    }
373
374    /// Get the number of chat messages
375    pub fn chat_count(&self) -> usize {
376        self.chat.as_ref().map_or(0, |c| c.len())
377    }
378
379    /// Merge with another document using CRDT semantics
380    ///
381    /// Returns true if our state changed (useful for triggering re-broadcast)
382    pub fn merge(&mut self, other: &HiveDocument) -> bool {
383        let mut changed = false;
384
385        // Merge counter
386        let old_value = self.counter.value();
387        self.counter.merge(&other.counter);
388        if self.counter.value() != old_value {
389            changed = true;
390        }
391
392        // Merge emergency event
393        if let Some(ref other_emergency) = other.emergency {
394            match &mut self.emergency {
395                Some(ref mut our_emergency) => {
396                    if our_emergency.merge(other_emergency) {
397                        changed = true;
398                    }
399                }
400                None => {
401                    self.emergency = Some(other_emergency.clone());
402                    changed = true;
403                }
404            }
405        }
406
407        // Merge chat CRDT
408        if let Some(ref other_chat) = other.chat {
409            match &mut self.chat {
410                Some(ref mut our_chat) => {
411                    if our_chat.merge(other_chat) {
412                        changed = true;
413                    }
414                }
415                None => {
416                    if !other_chat.is_empty() {
417                        self.chat = Some(other_chat.clone());
418                        changed = true;
419                    }
420                }
421            }
422        }
423
424        if changed {
425            self.increment_version();
426        }
427        changed
428    }
429
430    /// Get the current event type (if any)
431    pub fn current_event(&self) -> Option<EventType> {
432        self.peripheral
433            .as_ref()
434            .and_then(|p| p.last_event.as_ref())
435            .map(|e| e.event_type)
436    }
437
438    /// Encode to bytes for BLE transmission
439    ///
440    /// Alias: [`Self::to_bytes()`]
441    pub fn encode(&self) -> Vec<u8> {
442        let counter_data = self.counter.encode();
443        let peripheral_data = self.peripheral.as_ref().map(|p| p.encode());
444        let emergency_data = self.emergency.as_ref().map(|e| e.encode());
445        let chat_data = self
446            .chat
447            .as_ref()
448            .filter(|c| !c.is_empty())
449            .map(|c| c.encode());
450
451        // Calculate total size
452        let mut size = 8 + counter_data.len(); // header + counter
453        if let Some(ref pdata) = peripheral_data {
454            size += 4 + pdata.len(); // marker + reserved + len + peripheral
455        }
456        if let Some(ref edata) = emergency_data {
457            size += 4 + edata.len(); // marker + reserved + len + emergency
458        }
459        if let Some(ref cdata) = chat_data {
460            size += 4 + cdata.len(); // marker + reserved + len + chat
461        }
462
463        let mut buf = Vec::with_capacity(size);
464
465        // Header
466        buf.extend_from_slice(&self.version.to_le_bytes());
467        buf.extend_from_slice(&self.node_id.as_u32().to_le_bytes());
468
469        // GCounter
470        buf.extend_from_slice(&counter_data);
471
472        // Extended section (if peripheral present)
473        if let Some(pdata) = peripheral_data {
474            buf.push(EXTENDED_MARKER);
475            buf.push(0); // reserved
476            buf.extend_from_slice(&(pdata.len() as u16).to_le_bytes());
477            buf.extend_from_slice(&pdata);
478        }
479
480        // Emergency section (if emergency present)
481        if let Some(edata) = emergency_data {
482            buf.push(EMERGENCY_MARKER);
483            buf.push(0); // reserved
484            buf.extend_from_slice(&(edata.len() as u16).to_le_bytes());
485            buf.extend_from_slice(&edata);
486        }
487
488        // Chat section (if chat has messages)
489        if let Some(cdata) = chat_data {
490            buf.push(CHAT_MARKER);
491            buf.push(0); // reserved
492            buf.extend_from_slice(&(cdata.len() as u16).to_le_bytes());
493            buf.extend_from_slice(&cdata);
494        }
495
496        buf
497    }
498
499    /// Encode to bytes for transmission (alias for [`Self::encode()`])
500    ///
501    /// This is the conventional name used by external crates like hive-ffi
502    /// for transport-agnostic document serialization.
503    #[inline]
504    pub fn to_bytes(&self) -> Vec<u8> {
505        self.encode()
506    }
507
508    /// Decode from bytes received over BLE
509    ///
510    /// Alias: [`Self::from_bytes()`]
511    pub fn decode(data: &[u8]) -> Option<Self> {
512        if data.len() < MIN_DOCUMENT_SIZE {
513            return None;
514        }
515
516        // Header
517        let version = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
518        let node_id = NodeId::new(u32::from_le_bytes([data[4], data[5], data[6], data[7]]));
519
520        // GCounter (starts at offset 8)
521        let counter = GCounter::decode(&data[8..])?;
522
523        // Calculate where counter ends
524        let num_entries = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
525        let mut offset = 8 + 4 + num_entries * 12;
526
527        let mut peripheral = None;
528        let mut emergency = None;
529        let mut chat = None;
530
531        // Parse extended sections (can have peripheral, emergency, and/or chat)
532        while offset < data.len() {
533            let marker = data[offset];
534
535            if marker == EXTENDED_MARKER {
536                // Parse peripheral section
537                if data.len() < offset + 4 {
538                    break;
539                }
540                let _reserved = data[offset + 1];
541                let section_len = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize;
542
543                let section_start = offset + 4;
544                if data.len() < section_start + section_len {
545                    break;
546                }
547
548                peripheral = Peripheral::decode(&data[section_start..section_start + section_len]);
549                offset = section_start + section_len;
550            } else if marker == EMERGENCY_MARKER {
551                // Parse emergency section
552                if data.len() < offset + 4 {
553                    break;
554                }
555                let _reserved = data[offset + 1];
556                let section_len = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize;
557
558                let section_start = offset + 4;
559                if data.len() < section_start + section_len {
560                    break;
561                }
562
563                emergency =
564                    EmergencyEvent::decode(&data[section_start..section_start + section_len]);
565                offset = section_start + section_len;
566            } else if marker == CHAT_MARKER {
567                // Parse chat section
568                if data.len() < offset + 4 {
569                    break;
570                }
571                let _reserved = data[offset + 1];
572                let section_len = u16::from_le_bytes([data[offset + 2], data[offset + 3]]) as usize;
573
574                let section_start = offset + 4;
575                if data.len() < section_start + section_len {
576                    break;
577                }
578
579                chat = ChatCRDT::decode(&data[section_start..section_start + section_len]);
580                offset = section_start + section_len;
581            } else {
582                // Unknown marker, stop parsing
583                break;
584            }
585        }
586
587        Some(Self {
588            version,
589            node_id,
590            counter,
591            peripheral,
592            emergency,
593            chat,
594        })
595    }
596
597    /// Decode from bytes (alias for [`Self::decode()`])
598    ///
599    /// This is the conventional name used by external crates like hive-ffi
600    /// for transport-agnostic document deserialization.
601    #[inline]
602    pub fn from_bytes(data: &[u8]) -> Option<Self> {
603        Self::decode(data)
604    }
605
606    /// Get the total counter value
607    pub fn total_count(&self) -> u64 {
608        self.counter.value()
609    }
610
611    /// Get the encoded size of this document
612    ///
613    /// Use this to check if the document fits within BLE MTU constraints.
614    pub fn encoded_size(&self) -> usize {
615        let counter_size = 4 + self.counter.node_count_total() * 12;
616        let peripheral_size = self.peripheral.as_ref().map_or(0, |p| 4 + p.encode().len());
617        let emergency_size = self.emergency.as_ref().map_or(0, |e| 4 + e.encode().len());
618        let chat_size = self
619            .chat
620            .as_ref()
621            .filter(|c| !c.is_empty())
622            .map_or(0, |c| 4 + c.encoded_size());
623        8 + counter_size + peripheral_size + emergency_size + chat_size
624    }
625
626    /// Check if the document exceeds the target size for single-packet transmission
627    ///
628    /// Returns `true` if the document is larger than [`TARGET_DOCUMENT_SIZE`].
629    pub fn exceeds_target_size(&self) -> bool {
630        self.encoded_size() > TARGET_DOCUMENT_SIZE
631    }
632
633    /// Check if the document exceeds the maximum size
634    ///
635    /// Returns `true` if the document is larger than [`MAX_DOCUMENT_SIZE`].
636    pub fn exceeds_max_size(&self) -> bool {
637        self.encoded_size() > MAX_DOCUMENT_SIZE
638    }
639}
640
641/// Result from merging a received document
642#[derive(Debug, Clone)]
643pub struct MergeResult {
644    /// Node ID that sent this document
645    pub source_node: NodeId,
646
647    /// Event contained in the document (if any)
648    pub event: Option<PeripheralEvent>,
649
650    /// Whether the counter changed (indicates new data)
651    pub counter_changed: bool,
652
653    /// Whether the emergency state changed (new emergency or ACK updates)
654    pub emergency_changed: bool,
655
656    /// Whether chat messages changed (new messages received)
657    pub chat_changed: bool,
658
659    /// Updated total count after merge
660    pub total_count: u64,
661}
662
663impl MergeResult {
664    /// Check if this result contains an emergency event
665    pub fn is_emergency(&self) -> bool {
666        self.event
667            .as_ref()
668            .is_some_and(|e| e.event_type == EventType::Emergency)
669    }
670
671    /// Check if this result contains an ACK event
672    pub fn is_ack(&self) -> bool {
673        self.event
674            .as_ref()
675            .is_some_and(|e| e.event_type == EventType::Ack)
676    }
677}
678
679#[cfg(test)]
680mod tests {
681    use super::*;
682    use crate::sync::crdt::PeripheralType;
683
684    #[test]
685    fn test_document_encode_decode_minimal() {
686        let node_id = NodeId::new(0x12345678);
687        let doc = HiveDocument::new(node_id);
688
689        let encoded = doc.encode();
690        assert_eq!(encoded.len(), 12); // 8 header + 4 counter (0 entries)
691
692        let decoded = HiveDocument::decode(&encoded).unwrap();
693        assert_eq!(decoded.version, 1);
694        assert_eq!(decoded.node_id.as_u32(), 0x12345678);
695        assert_eq!(decoded.counter.value(), 0);
696        assert!(decoded.peripheral.is_none());
697    }
698
699    #[test]
700    fn test_document_encode_decode_with_counter() {
701        let node_id = NodeId::new(0x12345678);
702        let mut doc = HiveDocument::new(node_id);
703        doc.increment_counter();
704        doc.increment_counter();
705
706        let encoded = doc.encode();
707        // 8 header + 4 num_entries + 1 entry (12 bytes) = 24
708        assert_eq!(encoded.len(), 24);
709
710        let decoded = HiveDocument::decode(&encoded).unwrap();
711        assert_eq!(decoded.counter.value(), 2);
712    }
713
714    #[test]
715    fn test_document_encode_decode_with_peripheral() {
716        let node_id = NodeId::new(0x12345678);
717        let peripheral =
718            Peripheral::new(0xAABBCCDD, PeripheralType::SoldierSensor).with_callsign("ALPHA-1");
719
720        let doc = HiveDocument::new(node_id).with_peripheral(peripheral);
721
722        let encoded = doc.encode();
723        let decoded = HiveDocument::decode(&encoded).unwrap();
724
725        assert!(decoded.peripheral.is_some());
726        let p = decoded.peripheral.unwrap();
727        assert_eq!(p.id, 0xAABBCCDD);
728        assert_eq!(p.callsign_str(), "ALPHA-1");
729    }
730
731    #[test]
732    fn test_document_encode_decode_with_event() {
733        let node_id = NodeId::new(0x12345678);
734        let mut peripheral = Peripheral::new(0xAABBCCDD, PeripheralType::SoldierSensor);
735        peripheral.set_event(EventType::Emergency, TEST_TIMESTAMP);
736
737        let doc = HiveDocument::new(node_id).with_peripheral(peripheral);
738
739        let encoded = doc.encode();
740        let decoded = HiveDocument::decode(&encoded).unwrap();
741
742        assert!(decoded.peripheral.is_some());
743        let p = decoded.peripheral.unwrap();
744        assert!(p.last_event.is_some());
745        let event = p.last_event.unwrap();
746        assert_eq!(event.event_type, EventType::Emergency);
747        assert_eq!(event.timestamp, TEST_TIMESTAMP);
748    }
749
750    #[test]
751    fn test_document_merge() {
752        let node1 = NodeId::new(0x11111111);
753        let node2 = NodeId::new(0x22222222);
754
755        let mut doc1 = HiveDocument::new(node1);
756        doc1.increment_counter();
757
758        let mut doc2 = HiveDocument::new(node2);
759        doc2.counter.increment(&node2, 3);
760
761        // Merge doc2 into doc1
762        let changed = doc1.merge(&doc2);
763        assert!(changed);
764        assert_eq!(doc1.counter.value(), 4); // 1 + 3
765    }
766
767    #[test]
768    fn test_merge_result_helpers() {
769        let emergency_event = PeripheralEvent::new(EventType::Emergency, 123);
770        let result = MergeResult {
771            source_node: NodeId::new(0x12345678),
772            event: Some(emergency_event),
773            counter_changed: true,
774            emergency_changed: false,
775            chat_changed: false,
776            total_count: 10,
777        };
778
779        assert!(result.is_emergency());
780        assert!(!result.is_ack());
781
782        let ack_event = PeripheralEvent::new(EventType::Ack, 456);
783        let result = MergeResult {
784            source_node: NodeId::new(0x12345678),
785            event: Some(ack_event),
786            counter_changed: false,
787            emergency_changed: false,
788            chat_changed: false,
789            total_count: 10,
790        };
791
792        assert!(!result.is_emergency());
793        assert!(result.is_ack());
794    }
795
796    #[test]
797    fn test_document_size_calculation() {
798        use crate::sync::crdt::PeripheralType;
799
800        let node_id = NodeId::new(0x12345678);
801
802        // Minimal document: 8 header + 4 counter (0 entries) = 12 bytes
803        let doc = HiveDocument::new(node_id);
804        assert_eq!(doc.encoded_size(), 12);
805        assert!(!doc.exceeds_target_size());
806
807        // With one counter entry: 8 + (4 + 12) = 24 bytes
808        let mut doc = HiveDocument::new(node_id);
809        doc.increment_counter();
810        assert_eq!(doc.encoded_size(), 24);
811
812        // With peripheral: adds ~42 bytes (4 marker/len + 38 data)
813        let peripheral = Peripheral::new(0xAABBCCDD, PeripheralType::SoldierSensor);
814        let doc = HiveDocument::new(node_id).with_peripheral(peripheral);
815        let encoded = doc.encode();
816        assert_eq!(doc.encoded_size(), encoded.len());
817
818        // Verify size stays under target for reasonable mesh
819        let mut doc = HiveDocument::new(node_id);
820        for i in 0..10 {
821            doc.counter.increment(&NodeId::new(i), 1);
822        }
823        assert!(doc.encoded_size() < TARGET_DOCUMENT_SIZE);
824        assert!(!doc.exceeds_max_size());
825    }
826
827    // ============================================================================
828    // Chat CRDT Document Tests
829    // ============================================================================
830
831    // Valid timestamp for tests: 2024-01-15 00:00:00 UTC
832    const TEST_TIMESTAMP: u64 = 1705276800000;
833
834    #[test]
835    fn test_document_add_chat_message() {
836        let node_id = NodeId::new(0x12345678);
837        let mut doc = HiveDocument::new(node_id);
838
839        assert!(!doc.has_chat());
840        assert_eq!(doc.chat_count(), 0);
841
842        // Add a message
843        assert!(doc.add_chat_message(0x12345678, TEST_TIMESTAMP, "ALPHA", "Hello mesh!"));
844        assert!(doc.has_chat());
845        assert_eq!(doc.chat_count(), 1);
846
847        // Duplicate should be rejected
848        assert!(!doc.add_chat_message(0x12345678, TEST_TIMESTAMP, "ALPHA", "Hello mesh!"));
849        assert_eq!(doc.chat_count(), 1);
850
851        // Different message should be accepted
852        assert!(doc.add_chat_message(0x12345678, TEST_TIMESTAMP + 1000, "ALPHA", "Second message"));
853        assert_eq!(doc.chat_count(), 2);
854    }
855
856    #[test]
857    fn test_document_add_chat_reply() {
858        let node_id = NodeId::new(0x12345678);
859        let mut doc = HiveDocument::new(node_id);
860
861        // Add original message
862        doc.add_chat_message(0xAABBCCDD, TEST_TIMESTAMP, "BRAVO", "Need assistance");
863
864        // Add reply
865        assert!(doc.add_chat_reply(
866            0x12345678,
867            TEST_TIMESTAMP + 1000,
868            "ALPHA",
869            "Copy that",
870            0xAABBCCDD,     // reply to node
871            TEST_TIMESTAMP  // reply to timestamp
872        ));
873
874        assert_eq!(doc.chat_count(), 2);
875
876        // Verify reply-to info
877        let chat = doc.get_chat().unwrap();
878        let reply = chat.get_message(0x12345678, TEST_TIMESTAMP + 1000).unwrap();
879        assert!(reply.is_reply());
880        assert_eq!(reply.reply_to_node, 0xAABBCCDD);
881        assert_eq!(reply.reply_to_timestamp, TEST_TIMESTAMP);
882    }
883
884    #[test]
885    fn test_document_encode_decode_with_chat() {
886        let node_id = NodeId::new(0x12345678);
887        let mut doc = HiveDocument::new(node_id);
888
889        doc.add_chat_message(0x12345678, TEST_TIMESTAMP, "ALPHA", "First message");
890        doc.add_chat_message(0xAABBCCDD, TEST_TIMESTAMP + 1000, "BRAVO", "Second message");
891
892        let encoded = doc.encode();
893        let decoded = HiveDocument::decode(&encoded).unwrap();
894
895        assert!(decoded.has_chat());
896        assert_eq!(decoded.chat_count(), 2);
897
898        let chat = decoded.get_chat().unwrap();
899        let msg1 = chat.get_message(0x12345678, TEST_TIMESTAMP).unwrap();
900        assert_eq!(msg1.sender(), "ALPHA");
901        assert_eq!(msg1.text(), "First message");
902
903        let msg2 = chat.get_message(0xAABBCCDD, TEST_TIMESTAMP + 1000).unwrap();
904        assert_eq!(msg2.sender(), "BRAVO");
905        assert_eq!(msg2.text(), "Second message");
906    }
907
908    #[test]
909    fn test_document_merge_with_chat() {
910        let node1 = NodeId::new(0x11111111);
911        let node2 = NodeId::new(0x22222222);
912
913        let mut doc1 = HiveDocument::new(node1);
914        doc1.add_chat_message(0x11111111, TEST_TIMESTAMP, "ALPHA", "From node 1");
915
916        let mut doc2 = HiveDocument::new(node2);
917        doc2.add_chat_message(0x22222222, TEST_TIMESTAMP + 1000, "BRAVO", "From node 2");
918
919        // Merge doc2 into doc1
920        let changed = doc1.merge(&doc2);
921        assert!(changed);
922        assert_eq!(doc1.chat_count(), 2);
923
924        // Merge again - no changes
925        let changed = doc1.merge(&doc2);
926        assert!(!changed);
927
928        // Verify both messages present
929        let chat = doc1.get_chat().unwrap();
930        assert!(chat.get_message(0x11111111, TEST_TIMESTAMP).is_some());
931        assert!(chat
932            .get_message(0x22222222, TEST_TIMESTAMP + 1000)
933            .is_some());
934    }
935
936    #[test]
937    fn test_document_chat_encoded_size() {
938        let node_id = NodeId::new(0x12345678);
939        let mut doc = HiveDocument::new(node_id);
940
941        let base_size = doc.encoded_size();
942
943        // Add a message
944        doc.add_chat_message(0x12345678, TEST_TIMESTAMP, "ALPHA", "Test");
945
946        // Size should increase
947        let with_chat_size = doc.encoded_size();
948        assert!(with_chat_size > base_size);
949
950        // Encoded size should match actual encoded length
951        let encoded = doc.encode();
952        assert_eq!(doc.encoded_size(), encoded.len());
953    }
954}