Skip to main content

peat_protocol/hierarchy/
routing_table.rs

1//! Routing table for hierarchical message routing
2//!
3//! This module provides the core routing table that manages the hierarchical
4//! relationships between nodes, cells, and zones. It uses CRDT semantics
5//! (Last-Write-Wins) for conflict resolution in distributed scenarios.
6
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use tracing::{debug, instrument};
10
11/// Routing table managing node→cell→zone hierarchy
12///
13/// This structure maintains the routing information for the entire system:
14/// - Which cell each node belongs to
15/// - Which zone each cell belongs to
16/// - Which node is the leader of each cell
17///
18/// All assignments use Last-Write-Wins (LWW) CRDT semantics for distributed
19/// conflict resolution.
20///
21/// # Example
22/// ```
23/// use peat_protocol::hierarchy::RoutingTable;
24///
25/// let mut table = RoutingTable::new();
26///
27/// // Assign nodes to cells
28/// table.assign_node("node1", "cell_alpha", 100);
29/// table.assign_node("node2", "cell_alpha", 101);
30///
31/// // Assign cell to zone
32/// table.assign_cell("cell_alpha", "zone_north", 102);
33///
34/// // Set cell leader
35/// table.set_cell_leader("cell_alpha", "node1", 103);
36///
37/// // Query routing
38/// assert_eq!(table.get_node_cell("node1"), Some("cell_alpha"));
39/// assert_eq!(table.get_cell_zone("cell_alpha"), Some("zone_north"));
40/// assert_eq!(table.get_node_zone("node1"), Some("zone_north"));
41/// assert!(table.is_cell_leader("node1", "cell_alpha"));
42/// ```
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct RoutingTable {
45    /// Maps node_id → (cell_id, timestamp)
46    node_to_cell: HashMap<String, (String, u64)>,
47    /// Maps cell_id → (zone_id, timestamp)
48    cell_to_zone: HashMap<String, (String, u64)>,
49    /// Maps cell_id → (leader_node_id, timestamp)
50    cell_leaders: HashMap<String, (String, u64)>,
51}
52
53impl RoutingTable {
54    /// Create a new empty routing table
55    pub fn new() -> Self {
56        Self {
57            node_to_cell: HashMap::new(),
58            cell_to_zone: HashMap::new(),
59            cell_leaders: HashMap::new(),
60        }
61    }
62
63    /// Assign a node to a cell (LWW-Register operation)
64    ///
65    /// # Arguments
66    /// * `node_id` - The node to assign
67    /// * `cell_id` - The cell to assign to
68    /// * `timestamp` - Logical timestamp for LWW conflict resolution
69    ///
70    /// # Returns
71    /// `true` if the assignment was applied, `false` if rejected due to older timestamp
72    #[instrument(skip(self))]
73    pub fn assign_node(&mut self, node_id: &str, cell_id: &str, timestamp: u64) -> bool {
74        if let Some((_, existing_ts)) = self.node_to_cell.get(node_id) {
75            if timestamp <= *existing_ts {
76                debug!(
77                    "Rejected node assignment (old timestamp): {} → {} @ {}",
78                    node_id, cell_id, timestamp
79                );
80                return false;
81            }
82        }
83
84        debug!(
85            "Assigning node to cell: {} → {} @ {}",
86            node_id, cell_id, timestamp
87        );
88        self.node_to_cell
89            .insert(node_id.to_string(), (cell_id.to_string(), timestamp));
90        true
91    }
92
93    /// Remove a node from its cell (LWW-Register deletion)
94    ///
95    /// # Arguments
96    /// * `node_id` - The node to remove
97    /// * `timestamp` - Logical timestamp for LWW conflict resolution
98    ///
99    /// # Returns
100    /// `true` if the removal was applied, `false` if rejected due to older timestamp
101    #[instrument(skip(self))]
102    pub fn remove_node(&mut self, node_id: &str, timestamp: u64) -> bool {
103        if let Some((_, existing_ts)) = self.node_to_cell.get(node_id) {
104            if timestamp < *existing_ts {
105                debug!(
106                    "Rejected node removal (old timestamp): {} @ {}",
107                    node_id, timestamp
108                );
109                return false;
110            }
111        }
112
113        debug!("Removing node from cell: {} @ {}", node_id, timestamp);
114        self.node_to_cell.remove(node_id);
115        true
116    }
117
118    /// Assign a cell to a zone (LWW-Register operation)
119    ///
120    /// # Arguments
121    /// * `cell_id` - The cell to assign
122    /// * `zone_id` - The zone to assign to
123    /// * `timestamp` - Logical timestamp for LWW conflict resolution
124    ///
125    /// # Returns
126    /// `true` if the assignment was applied, `false` if rejected due to older timestamp
127    #[instrument(skip(self))]
128    pub fn assign_cell(&mut self, cell_id: &str, zone_id: &str, timestamp: u64) -> bool {
129        if let Some((_, existing_ts)) = self.cell_to_zone.get(cell_id) {
130            if timestamp <= *existing_ts {
131                debug!(
132                    "Rejected cell assignment (old timestamp): {} → {} @ {}",
133                    cell_id, zone_id, timestamp
134                );
135                return false;
136            }
137        }
138
139        debug!(
140            "Assigning cell to zone: {} → {} @ {}",
141            cell_id, zone_id, timestamp
142        );
143        self.cell_to_zone
144            .insert(cell_id.to_string(), (zone_id.to_string(), timestamp));
145        true
146    }
147
148    /// Remove a cell from its zone (LWW-Register deletion)
149    ///
150    /// # Arguments
151    /// * `cell_id` - The cell to remove
152    /// * `timestamp` - Logical timestamp for LWW conflict resolution
153    ///
154    /// # Returns
155    /// `true` if the removal was applied, `false` if rejected due to older timestamp
156    #[instrument(skip(self))]
157    pub fn remove_cell(&mut self, cell_id: &str, timestamp: u64) -> bool {
158        if let Some((_, existing_ts)) = self.cell_to_zone.get(cell_id) {
159            if timestamp < *existing_ts {
160                debug!(
161                    "Rejected cell removal (old timestamp): {} @ {}",
162                    cell_id, timestamp
163                );
164                return false;
165            }
166        }
167
168        debug!("Removing cell from zone: {} @ {}", cell_id, timestamp);
169        self.cell_to_zone.remove(cell_id);
170        true
171    }
172
173    /// Set the leader of a cell (LWW-Register operation)
174    ///
175    /// # Arguments
176    /// * `cell_id` - The cell
177    /// * `leader_node_id` - The node to designate as leader
178    /// * `timestamp` - Logical timestamp for LWW conflict resolution
179    ///
180    /// # Returns
181    /// `true` if the assignment was applied, `false` if rejected due to older timestamp
182    #[instrument(skip(self))]
183    pub fn set_cell_leader(&mut self, cell_id: &str, leader_node_id: &str, timestamp: u64) -> bool {
184        if let Some((_, existing_ts)) = self.cell_leaders.get(cell_id) {
185            if timestamp <= *existing_ts {
186                debug!(
187                    "Rejected leader assignment (old timestamp): {} → {} @ {}",
188                    cell_id, leader_node_id, timestamp
189                );
190                return false;
191            }
192        }
193
194        debug!(
195            "Setting cell leader: {} → {} @ {}",
196            cell_id, leader_node_id, timestamp
197        );
198        self.cell_leaders
199            .insert(cell_id.to_string(), (leader_node_id.to_string(), timestamp));
200        true
201    }
202
203    /// Remove the leader designation from a cell (LWW-Register deletion)
204    ///
205    /// # Arguments
206    /// * `cell_id` - The cell to remove leader from
207    /// * `timestamp` - Logical timestamp for LWW conflict resolution
208    ///
209    /// # Returns
210    /// `true` if the removal was applied, `false` if rejected due to older timestamp
211    #[instrument(skip(self))]
212    pub fn remove_cell_leader(&mut self, cell_id: &str, timestamp: u64) -> bool {
213        if let Some((_, existing_ts)) = self.cell_leaders.get(cell_id) {
214            if timestamp < *existing_ts {
215                debug!(
216                    "Rejected leader removal (old timestamp): {} @ {}",
217                    cell_id, timestamp
218                );
219                return false;
220            }
221        }
222
223        debug!("Removing cell leader: {} @ {}", cell_id, timestamp);
224        self.cell_leaders.remove(cell_id);
225        true
226    }
227
228    /// Get the cell ID for a given node
229    ///
230    /// Returns None if the node is not assigned to any cell.
231    pub fn get_node_cell(&self, node_id: &str) -> Option<&str> {
232        self.node_to_cell
233            .get(node_id)
234            .map(|(cell_id, _)| cell_id.as_str())
235    }
236
237    /// Get the zone ID for a given cell
238    ///
239    /// Returns None if the cell is not assigned to any zone.
240    pub fn get_cell_zone(&self, cell_id: &str) -> Option<&str> {
241        self.cell_to_zone
242            .get(cell_id)
243            .map(|(zone_id, _)| zone_id.as_str())
244    }
245
246    /// Get the zone ID for a given node (by following node→cell→zone)
247    ///
248    /// Returns None if the node is not in a cell, or the cell is not in a zone.
249    pub fn get_node_zone(&self, node_id: &str) -> Option<&str> {
250        let cell_id = self.get_node_cell(node_id)?;
251        self.get_cell_zone(cell_id)
252    }
253
254    /// Check if a node is the leader of a given cell
255    ///
256    /// Returns true if the node is designated as the cell's leader.
257    pub fn is_cell_leader(&self, node_id: &str, cell_id: &str) -> bool {
258        self.cell_leaders
259            .get(cell_id)
260            .map(|(leader_id, _)| leader_id == node_id)
261            .unwrap_or(false)
262    }
263
264    /// Get the leader node ID for a given cell
265    ///
266    /// Returns None if the cell has no designated leader.
267    pub fn get_cell_leader(&self, cell_id: &str) -> Option<&str> {
268        self.cell_leaders
269            .get(cell_id)
270            .map(|(leader_id, _)| leader_id.as_str())
271    }
272
273    /// Merge another routing table into this one (CRDT merge)
274    ///
275    /// Applies Last-Write-Wins semantics for all entries. Higher timestamps win.
276    #[instrument(skip(self, other))]
277    pub fn merge(&mut self, other: &RoutingTable) {
278        debug!("Merging routing tables");
279
280        // Merge node→cell mappings
281        for (node_id, (cell_id, timestamp)) in &other.node_to_cell {
282            self.assign_node(node_id, cell_id, *timestamp);
283        }
284
285        // Merge cell→zone mappings
286        for (cell_id, (zone_id, timestamp)) in &other.cell_to_zone {
287            self.assign_cell(cell_id, zone_id, *timestamp);
288        }
289
290        // Merge cell leaders
291        for (cell_id, (leader_id, timestamp)) in &other.cell_leaders {
292            self.set_cell_leader(cell_id, leader_id, *timestamp);
293        }
294    }
295
296    /// Get statistics about the routing table
297    pub fn stats(&self) -> RoutingTableStats {
298        RoutingTableStats {
299            node_count: self.node_to_cell.len(),
300            cell_count: self.cell_to_zone.len(),
301            leader_count: self.cell_leaders.len(),
302        }
303    }
304
305    /// Get all nodes in a specific cell
306    pub fn get_cell_nodes(&self, cell_id: &str) -> Vec<&str> {
307        self.node_to_cell
308            .iter()
309            .filter(|(_, (cid, _))| cid == cell_id)
310            .map(|(node_id, _)| node_id.as_str())
311            .collect()
312    }
313
314    /// Get all cells in a specific zone
315    pub fn get_zone_cells(&self, zone_id: &str) -> Vec<&str> {
316        self.cell_to_zone
317            .iter()
318            .filter(|(_, (zid, _))| zid == zone_id)
319            .map(|(cell_id, _)| cell_id.as_str())
320            .collect()
321    }
322
323    /// Merge multiple cells into a new cell by reassigning all their nodes
324    ///
325    /// This operation is used during cell merging in hierarchy maintenance.
326    /// All nodes from the source cells are reassigned to the new merged cell.
327    /// The old cells are removed from the routing table.
328    ///
329    /// # Arguments
330    /// * `source_cell_ids` - IDs of cells to merge (will be removed)
331    /// * `merged_cell_id` - ID of the new merged cell
332    /// * `zone_id` - Zone ID for the merged cell (optional)
333    /// * `timestamp` - Logical timestamp for all operations
334    ///
335    /// # Returns
336    /// Number of nodes reassigned
337    #[instrument(skip(self))]
338    pub fn merge_cells(
339        &mut self,
340        source_cell_ids: &[&str],
341        merged_cell_id: &str,
342        zone_id: Option<&str>,
343        timestamp: u64,
344    ) -> usize {
345        debug!(
346            "Merging cells {:?} into {} @ {}",
347            source_cell_ids, merged_cell_id, timestamp
348        );
349
350        let mut reassigned_count = 0;
351
352        // Collect all nodes from source cells
353        let nodes_to_reassign: Vec<String> = source_cell_ids
354            .iter()
355            .flat_map(|cell_id| {
356                self.node_to_cell
357                    .iter()
358                    .filter(|(_, (cid, _))| cid == *cell_id)
359                    .map(|(node_id, _)| node_id.clone())
360                    .collect::<Vec<_>>()
361            })
362            .collect();
363
364        // Reassign all nodes to the merged cell
365        for node_id in nodes_to_reassign {
366            if self.assign_node(&node_id, merged_cell_id, timestamp) {
367                reassigned_count += 1;
368            }
369        }
370
371        // Assign merged cell to zone if provided
372        if let Some(zid) = zone_id {
373            self.assign_cell(merged_cell_id, zid, timestamp);
374        }
375
376        // Remove old cells from zone mapping
377        for cell_id in source_cell_ids {
378            self.remove_cell(cell_id, timestamp);
379            self.remove_cell_leader(cell_id, timestamp);
380        }
381
382        debug!(
383            "Merge complete: {} nodes reassigned to {}",
384            reassigned_count, merged_cell_id
385        );
386
387        reassigned_count
388    }
389
390    /// Split a cell into two new cells by partitioning its nodes
391    ///
392    /// This operation is used during cell splitting in hierarchy maintenance.
393    /// Nodes are partitioned between two new cells based on the provided split.
394    /// The old cell is removed from the routing table.
395    ///
396    /// # Arguments
397    /// * `source_cell_id` - ID of cell to split (will be removed)
398    /// * `cell_a_id` - ID of first new cell
399    /// * `cell_b_id` - ID of second new cell
400    /// * `nodes_for_a` - Node IDs to assign to cell A
401    /// * `nodes_for_b` - Node IDs to assign to cell B
402    /// * `zone_id` - Zone ID for both new cells (optional)
403    /// * `timestamp` - Logical timestamp for all operations
404    ///
405    /// # Returns
406    /// Tuple of (nodes_in_a, nodes_in_b)
407    #[instrument(skip(self, nodes_for_a, nodes_for_b))]
408    #[allow(clippy::too_many_arguments)]
409    pub fn split_cell(
410        &mut self,
411        source_cell_id: &str,
412        cell_a_id: &str,
413        cell_b_id: &str,
414        nodes_for_a: &[&str],
415        nodes_for_b: &[&str],
416        zone_id: Option<&str>,
417        timestamp: u64,
418    ) -> (usize, usize) {
419        debug!(
420            "Splitting cell {} into {} and {} @ {}",
421            source_cell_id, cell_a_id, cell_b_id, timestamp
422        );
423
424        let mut count_a = 0;
425        let mut count_b = 0;
426
427        // Assign nodes to cell A
428        for node_id in nodes_for_a {
429            if self.assign_node(node_id, cell_a_id, timestamp) {
430                count_a += 1;
431            }
432        }
433
434        // Assign nodes to cell B
435        for node_id in nodes_for_b {
436            if self.assign_node(node_id, cell_b_id, timestamp) {
437                count_b += 1;
438            }
439        }
440
441        // Assign both cells to zone if provided
442        if let Some(zid) = zone_id {
443            self.assign_cell(cell_a_id, zid, timestamp);
444            self.assign_cell(cell_b_id, zid, timestamp);
445        }
446
447        // Remove old cell from zone mapping and leadership
448        self.remove_cell(source_cell_id, timestamp);
449        self.remove_cell_leader(source_cell_id, timestamp);
450
451        debug!(
452            "Split complete: {} → {} nodes in {}, {} nodes in {}",
453            source_cell_id, count_a, cell_a_id, count_b, cell_b_id
454        );
455
456        (count_a, count_b)
457    }
458}
459
460impl Default for RoutingTable {
461    fn default() -> Self {
462        Self::new()
463    }
464}
465
466/// Statistics about the routing table
467#[derive(Debug, Clone)]
468pub struct RoutingTableStats {
469    /// Number of nodes with cell assignments
470    pub node_count: usize,
471    /// Number of cells with zone assignments
472    pub cell_count: usize,
473    /// Number of cells with designated leaders
474    pub leader_count: usize,
475}
476
477#[cfg(test)]
478mod tests {
479    use super::*;
480
481    #[test]
482    fn test_routing_table_creation() {
483        let table = RoutingTable::new();
484        let stats = table.stats();
485        assert_eq!(stats.node_count, 0);
486        assert_eq!(stats.cell_count, 0);
487        assert_eq!(stats.leader_count, 0);
488    }
489
490    #[test]
491    fn test_node_assignment() {
492        let mut table = RoutingTable::new();
493
494        // Assign node to cell
495        assert!(table.assign_node("node1", "cell_alpha", 100));
496        assert_eq!(table.get_node_cell("node1"), Some("cell_alpha"));
497
498        // Assign another node to same cell
499        assert!(table.assign_node("node2", "cell_alpha", 101));
500        assert_eq!(table.get_node_cell("node2"), Some("cell_alpha"));
501
502        // Check stats
503        let stats = table.stats();
504        assert_eq!(stats.node_count, 2);
505    }
506
507    #[test]
508    fn test_lww_semantics_node_assignment() {
509        let mut table = RoutingTable::new();
510
511        // Initial assignment
512        assert!(table.assign_node("node1", "cell_alpha", 100));
513        assert_eq!(table.get_node_cell("node1"), Some("cell_alpha"));
514
515        // Later assignment should win
516        assert!(table.assign_node("node1", "cell_beta", 200));
517        assert_eq!(table.get_node_cell("node1"), Some("cell_beta"));
518
519        // Earlier assignment should be rejected
520        assert!(!table.assign_node("node1", "cell_gamma", 150));
521        assert_eq!(table.get_node_cell("node1"), Some("cell_beta"));
522
523        // Equal timestamp should be rejected
524        assert!(!table.assign_node("node1", "cell_delta", 200));
525        assert_eq!(table.get_node_cell("node1"), Some("cell_beta"));
526    }
527
528    #[test]
529    fn test_cell_assignment() {
530        let mut table = RoutingTable::new();
531
532        // Assign cell to zone
533        assert!(table.assign_cell("cell_alpha", "zone_north", 100));
534        assert_eq!(table.get_cell_zone("cell_alpha"), Some("zone_north"));
535
536        let stats = table.stats();
537        assert_eq!(stats.cell_count, 1);
538    }
539
540    #[test]
541    fn test_lww_semantics_cell_assignment() {
542        let mut table = RoutingTable::new();
543
544        // Initial assignment
545        assert!(table.assign_cell("cell_alpha", "zone_north", 100));
546        assert_eq!(table.get_cell_zone("cell_alpha"), Some("zone_north"));
547
548        // Later assignment should win
549        assert!(table.assign_cell("cell_alpha", "zone_south", 200));
550        assert_eq!(table.get_cell_zone("cell_alpha"), Some("zone_south"));
551
552        // Earlier assignment should be rejected
553        assert!(!table.assign_cell("cell_alpha", "zone_east", 150));
554        assert_eq!(table.get_cell_zone("cell_alpha"), Some("zone_south"));
555    }
556
557    #[test]
558    fn test_cell_leader() {
559        let mut table = RoutingTable::new();
560
561        // Assign nodes to cell
562        table.assign_node("node1", "cell_alpha", 100);
563        table.assign_node("node2", "cell_alpha", 101);
564
565        // Set leader
566        assert!(table.set_cell_leader("cell_alpha", "node1", 102));
567        assert!(table.is_cell_leader("node1", "cell_alpha"));
568        assert!(!table.is_cell_leader("node2", "cell_alpha"));
569        assert_eq!(table.get_cell_leader("cell_alpha"), Some("node1"));
570
571        let stats = table.stats();
572        assert_eq!(stats.leader_count, 1);
573    }
574
575    #[test]
576    fn test_lww_semantics_leader() {
577        let mut table = RoutingTable::new();
578
579        // Initial leader
580        assert!(table.set_cell_leader("cell_alpha", "node1", 100));
581        assert!(table.is_cell_leader("node1", "cell_alpha"));
582
583        // Later assignment should win
584        assert!(table.set_cell_leader("cell_alpha", "node2", 200));
585        assert!(!table.is_cell_leader("node1", "cell_alpha"));
586        assert!(table.is_cell_leader("node2", "cell_alpha"));
587
588        // Earlier assignment should be rejected
589        assert!(!table.set_cell_leader("cell_alpha", "node3", 150));
590        assert!(table.is_cell_leader("node2", "cell_alpha"));
591    }
592
593    #[test]
594    fn test_node_zone_lookup() {
595        let mut table = RoutingTable::new();
596
597        // Set up hierarchy: node1 → cell_alpha → zone_north
598        table.assign_node("node1", "cell_alpha", 100);
599        table.assign_cell("cell_alpha", "zone_north", 101);
600
601        // Should traverse the hierarchy
602        assert_eq!(table.get_node_zone("node1"), Some("zone_north"));
603
604        // Node without cell
605        assert_eq!(table.get_node_zone("node2"), None);
606
607        // Node in cell without zone
608        table.assign_node("node3", "cell_beta", 102);
609        assert_eq!(table.get_node_zone("node3"), None);
610    }
611
612    #[test]
613    fn test_get_cell_nodes() {
614        let mut table = RoutingTable::new();
615
616        table.assign_node("node1", "cell_alpha", 100);
617        table.assign_node("node2", "cell_alpha", 101);
618        table.assign_node("node3", "cell_beta", 102);
619
620        let mut alpha_nodes = table.get_cell_nodes("cell_alpha");
621        alpha_nodes.sort();
622        assert_eq!(alpha_nodes, vec!["node1", "node2"]);
623
624        let beta_nodes = table.get_cell_nodes("cell_beta");
625        assert_eq!(beta_nodes, vec!["node3"]);
626
627        let empty_nodes = table.get_cell_nodes("cell_gamma");
628        assert_eq!(empty_nodes.len(), 0);
629    }
630
631    #[test]
632    fn test_get_zone_cells() {
633        let mut table = RoutingTable::new();
634
635        table.assign_cell("cell_alpha", "zone_north", 100);
636        table.assign_cell("cell_beta", "zone_north", 101);
637        table.assign_cell("cell_gamma", "zone_south", 102);
638
639        let mut north_cells = table.get_zone_cells("zone_north");
640        north_cells.sort();
641        assert_eq!(north_cells, vec!["cell_alpha", "cell_beta"]);
642
643        let south_cells = table.get_zone_cells("zone_south");
644        assert_eq!(south_cells, vec!["cell_gamma"]);
645
646        let empty_cells = table.get_zone_cells("zone_east");
647        assert_eq!(empty_cells.len(), 0);
648    }
649
650    #[test]
651    fn test_node_removal() {
652        let mut table = RoutingTable::new();
653
654        table.assign_node("node1", "cell_alpha", 100);
655        assert_eq!(table.get_node_cell("node1"), Some("cell_alpha"));
656
657        // Remove node with old timestamp should fail
658        assert!(!table.remove_node("node1", 50));
659        assert_eq!(table.get_node_cell("node1"), Some("cell_alpha"));
660
661        // Remove node with newer timestamp should succeed
662        assert!(table.remove_node("node1", 200));
663        assert_eq!(table.get_node_cell("node1"), None);
664    }
665
666    #[test]
667    fn test_cell_removal() {
668        let mut table = RoutingTable::new();
669
670        table.assign_cell("cell_alpha", "zone_north", 100);
671        assert_eq!(table.get_cell_zone("cell_alpha"), Some("zone_north"));
672
673        // Remove cell with old timestamp should fail
674        assert!(!table.remove_cell("cell_alpha", 50));
675        assert_eq!(table.get_cell_zone("cell_alpha"), Some("zone_north"));
676
677        // Remove cell with newer timestamp should succeed
678        assert!(table.remove_cell("cell_alpha", 200));
679        assert_eq!(table.get_cell_zone("cell_alpha"), None);
680    }
681
682    #[test]
683    fn test_leader_removal() {
684        let mut table = RoutingTable::new();
685
686        table.set_cell_leader("cell_alpha", "node1", 100);
687        assert_eq!(table.get_cell_leader("cell_alpha"), Some("node1"));
688
689        // Remove leader with old timestamp should fail
690        assert!(!table.remove_cell_leader("cell_alpha", 50));
691        assert_eq!(table.get_cell_leader("cell_alpha"), Some("node1"));
692
693        // Remove leader with newer timestamp should succeed
694        assert!(table.remove_cell_leader("cell_alpha", 200));
695        assert_eq!(table.get_cell_leader("cell_alpha"), None);
696    }
697
698    #[test]
699    fn test_merge() {
700        let mut table1 = RoutingTable::new();
701        let mut table2 = RoutingTable::new();
702
703        // Table 1 has older data
704        table1.assign_node("node1", "cell_alpha", 100);
705        table1.assign_cell("cell_alpha", "zone_north", 100);
706
707        // Table 2 has newer data for node1 and different data for node2
708        table2.assign_node("node1", "cell_beta", 200);
709        table2.assign_node("node2", "cell_gamma", 150);
710        table2.assign_cell("cell_beta", "zone_south", 200);
711
712        // Merge table2 into table1
713        table1.merge(&table2);
714
715        // Newer timestamp should win for node1
716        assert_eq!(table1.get_node_cell("node1"), Some("cell_beta"));
717
718        // Node2 should be added
719        assert_eq!(table1.get_node_cell("node2"), Some("cell_gamma"));
720
721        // Cell assignments should be merged
722        assert_eq!(table1.get_cell_zone("cell_beta"), Some("zone_south"));
723        assert_eq!(table1.get_cell_zone("cell_alpha"), Some("zone_north"));
724    }
725
726    #[test]
727    fn test_merge_leaders() {
728        let mut table1 = RoutingTable::new();
729        let mut table2 = RoutingTable::new();
730
731        table1.set_cell_leader("cell_alpha", "node1", 100);
732        table2.set_cell_leader("cell_alpha", "node2", 200);
733
734        table1.merge(&table2);
735
736        // Newer timestamp should win
737        assert!(table1.is_cell_leader("node2", "cell_alpha"));
738        assert!(!table1.is_cell_leader("node1", "cell_alpha"));
739    }
740
741    #[test]
742    fn test_merge_cells() {
743        let mut table = RoutingTable::new();
744
745        // Create two cells with nodes
746        table.assign_node("node1", "cell_alpha", 100);
747        table.assign_node("node2", "cell_alpha", 101);
748        table.assign_node("node3", "cell_beta", 102);
749        table.assign_node("node4", "cell_beta", 103);
750
751        table.assign_cell("cell_alpha", "zone_north", 100);
752        table.assign_cell("cell_beta", "zone_north", 101);
753
754        table.set_cell_leader("cell_alpha", "node1", 100);
755        table.set_cell_leader("cell_beta", "node3", 101);
756
757        // Merge cells
758        let count = table.merge_cells(
759            &["cell_alpha", "cell_beta"],
760            "cell_merged",
761            Some("zone_north"),
762            200,
763        );
764
765        // All 4 nodes should be reassigned
766        assert_eq!(count, 4);
767
768        // Verify all nodes are now in merged cell
769        assert_eq!(table.get_node_cell("node1"), Some("cell_merged"));
770        assert_eq!(table.get_node_cell("node2"), Some("cell_merged"));
771        assert_eq!(table.get_node_cell("node3"), Some("cell_merged"));
772        assert_eq!(table.get_node_cell("node4"), Some("cell_merged"));
773
774        // Verify merged cell is in zone
775        assert_eq!(table.get_cell_zone("cell_merged"), Some("zone_north"));
776
777        // Verify old cells are removed
778        assert_eq!(table.get_cell_zone("cell_alpha"), None);
779        assert_eq!(table.get_cell_zone("cell_beta"), None);
780
781        // Verify old leaders are removed
782        assert_eq!(table.get_cell_leader("cell_alpha"), None);
783        assert_eq!(table.get_cell_leader("cell_beta"), None);
784
785        // Verify merged cell has no leader yet
786        assert_eq!(table.get_cell_leader("cell_merged"), None);
787    }
788
789    #[test]
790    fn test_split_cell() {
791        let mut table = RoutingTable::new();
792
793        // Create a cell with nodes
794        table.assign_node("node1", "cell_alpha", 100);
795        table.assign_node("node2", "cell_alpha", 101);
796        table.assign_node("node3", "cell_alpha", 102);
797        table.assign_node("node4", "cell_alpha", 103);
798
799        table.assign_cell("cell_alpha", "zone_north", 100);
800        table.set_cell_leader("cell_alpha", "node1", 100);
801
802        // Split cell
803        let (count_a, count_b) = table.split_cell(
804            "cell_alpha",
805            "cell_a",
806            "cell_b",
807            &["node1", "node2"],
808            &["node3", "node4"],
809            Some("zone_north"),
810            200,
811        );
812
813        // Verify split counts
814        assert_eq!(count_a, 2);
815        assert_eq!(count_b, 2);
816
817        // Verify nodes are in correct cells
818        assert_eq!(table.get_node_cell("node1"), Some("cell_a"));
819        assert_eq!(table.get_node_cell("node2"), Some("cell_a"));
820        assert_eq!(table.get_node_cell("node3"), Some("cell_b"));
821        assert_eq!(table.get_node_cell("node4"), Some("cell_b"));
822
823        // Verify both cells are in zone
824        assert_eq!(table.get_cell_zone("cell_a"), Some("zone_north"));
825        assert_eq!(table.get_cell_zone("cell_b"), Some("zone_north"));
826
827        // Verify old cell is removed
828        assert_eq!(table.get_cell_zone("cell_alpha"), None);
829
830        // Verify old leader is removed
831        assert_eq!(table.get_cell_leader("cell_alpha"), None);
832
833        // Verify new cells have no leaders yet
834        assert_eq!(table.get_cell_leader("cell_a"), None);
835        assert_eq!(table.get_cell_leader("cell_b"), None);
836    }
837
838    #[test]
839    fn test_merge_cells_without_zone() {
840        let mut table = RoutingTable::new();
841
842        table.assign_node("node1", "cell_alpha", 100);
843        table.assign_node("node2", "cell_beta", 101);
844
845        // Merge without zone assignment
846        let count = table.merge_cells(&["cell_alpha", "cell_beta"], "cell_merged", None, 200);
847
848        assert_eq!(count, 2);
849        assert_eq!(table.get_node_cell("node1"), Some("cell_merged"));
850        assert_eq!(table.get_node_cell("node2"), Some("cell_merged"));
851
852        // Verify merged cell has no zone
853        assert_eq!(table.get_cell_zone("cell_merged"), None);
854    }
855
856    #[test]
857    fn test_split_cell_without_zone() {
858        let mut table = RoutingTable::new();
859
860        table.assign_node("node1", "cell_alpha", 100);
861        table.assign_node("node2", "cell_alpha", 101);
862
863        // Split without zone assignment
864        let (count_a, count_b) = table.split_cell(
865            "cell_alpha",
866            "cell_a",
867            "cell_b",
868            &["node1"],
869            &["node2"],
870            None,
871            200,
872        );
873
874        assert_eq!(count_a, 1);
875        assert_eq!(count_b, 1);
876
877        // Verify new cells have no zone
878        assert_eq!(table.get_cell_zone("cell_a"), None);
879        assert_eq!(table.get_cell_zone("cell_b"), None);
880    }
881}