Skip to main content

ipfrs_network/
network_topology_mapper.rs

1//! Network Topology Mapper
2//!
3//! A live, production-quality topology mapper that tracks peer connections and
4//! infers structural properties of the overlay network.
5//!
6//! # Features
7//!
8//! - Fixed-size `[u8; 32]` node identifiers and `u64` edge identifiers.
9//! - Directed graph with per-edge latency and bandwidth annotations.
10//! - Dijkstra shortest-path (minimising latency), BFS diameter, clustering
11//!   coefficient, and Brandes betweenness centrality.
12//! - Bounded snapshot history (max 20 entries, oldest dropped).
13//! - Stale-entry pruning via configurable TTL.
14//! - No external crates beyond those already in `ipfrs-network`.
15//! - Zero `unwrap()` calls throughout.
16
17use std::cmp::Reverse;
18use std::collections::{BinaryHeap, HashMap, HashSet, VecDeque};
19
20// ─── Type Aliases ─────────────────────────────────────────────────────────────
21
22/// Opaque 32-byte node identifier.
23pub type NtmNodeId = [u8; 32];
24
25/// Opaque 64-bit edge identifier (derived from src XOR dst XOR counter).
26pub type NtmEdgeId = u64;
27
28/// Convenient top-level alias for the mapper itself.
29pub type NtmNetworkTopologyMapper = NetworkTopologyMapper;
30
31// ─── PRNG helpers (pure inline, no external crates) ──────────────────────────
32
33#[inline]
34fn xorshift64(state: &mut u64) -> u64 {
35    let mut x = *state;
36    x ^= x << 13;
37    x ^= x >> 7;
38    x ^= x << 17;
39    *state = x;
40    x
41}
42
43#[inline]
44fn fnv1a_64(data: &[u8]) -> u64 {
45    let mut h: u64 = 14_695_981_039_346_656_037;
46    for &b in data {
47        h ^= b as u64;
48        h = h.wrapping_mul(1_099_511_628_211);
49    }
50    h
51}
52
53// ─── Configuration ────────────────────────────────────────────────────────────
54
55/// Configuration for `NetworkTopologyMapper`.
56#[derive(Debug, Clone)]
57pub struct NtmMapperConfig {
58    /// Maximum number of nodes the graph may hold.
59    pub max_nodes: usize,
60    /// Maximum number of edges the graph may hold.
61    pub max_edges: usize,
62    /// Minimum seconds between automatic snapshots (0 = never auto-snapshot).
63    pub snapshot_interval_secs: u64,
64    /// Seconds after which a node that has not been seen is pruned.
65    pub prune_disconnected_after_secs: u64,
66}
67
68impl Default for NtmMapperConfig {
69    fn default() -> Self {
70        Self {
71            max_nodes: 4_096,
72            max_edges: 65_536,
73            snapshot_interval_secs: 60,
74            prune_disconnected_after_secs: 300,
75        }
76    }
77}
78
79// ─── Error type ───────────────────────────────────────────────────────────────
80
81/// Errors produced by `NetworkTopologyMapper` operations.
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum NtmMapperError {
84    /// Node or edge not found.
85    NotFound(String),
86    /// The graph is at capacity.
87    CapacityExceeded(String),
88    /// A duplicate key was inserted.
89    Duplicate(String),
90    /// Generic internal error.
91    Internal(String),
92}
93
94impl std::fmt::Display for NtmMapperError {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            Self::NotFound(s) => write!(f, "not found: {s}"),
98            Self::CapacityExceeded(s) => write!(f, "capacity exceeded: {s}"),
99            Self::Duplicate(s) => write!(f, "duplicate: {s}"),
100            Self::Internal(s) => write!(f, "internal error: {s}"),
101        }
102    }
103}
104
105impl std::error::Error for NtmMapperError {}
106
107// ─── Core graph data types ────────────────────────────────────────────────────
108
109/// A peer node in the topology graph.
110#[derive(Debug, Clone)]
111pub struct NtmNode {
112    /// 32-byte opaque node identifier.
113    pub id: NtmNodeId,
114    /// Human-readable network address (multiaddr or socket string).
115    pub addr: String,
116    /// Optional geographic region hint.
117    pub region: Option<String>,
118    /// Latest round-trip time in milliseconds.
119    pub rtt_ms: f64,
120    /// Unix-epoch seconds when this node was last observed alive.
121    pub last_seen: u64,
122    /// Number of neighbours (undirected degree = out-degree + in-degree).
123    pub degree: u32,
124    /// Whether this node is a bootstrap / well-known anchor.
125    pub is_bootstrap: bool,
126}
127
128/// A directed edge between two peers.
129#[derive(Debug, Clone)]
130pub struct NtmEdge {
131    /// Unique edge identifier.
132    pub id: NtmEdgeId,
133    /// Source node identifier.
134    pub src: NtmNodeId,
135    /// Destination node identifier.
136    pub dst: NtmNodeId,
137    /// One-way (or round-trip) latency estimate in milliseconds.
138    pub latency_ms: f64,
139    /// Measured bandwidth in kbps.
140    pub bandwidth_kbps: f64,
141    /// Unix-epoch seconds when this edge was last observed.
142    pub observed_at: u64,
143}
144
145/// Point-in-time snapshot of key topology metrics.
146#[derive(Debug, Clone)]
147pub struct NtmSnapshot {
148    /// Unix-epoch seconds when the snapshot was taken.
149    pub ts: u64,
150    /// Number of nodes at snapshot time.
151    pub node_count: usize,
152    /// Number of edges at snapshot time.
153    pub edge_count: usize,
154    /// Average node degree at snapshot time.
155    pub avg_degree: f64,
156    /// Graph diameter (longest shortest path) at snapshot time.
157    pub diameter: u32,
158    /// Average clustering coefficient at snapshot time.
159    pub clustering_coeff: f64,
160}
161
162/// Derived topology metrics computed on demand.
163#[derive(Debug, Clone)]
164pub struct NtmTopologyMetrics {
165    /// Graph density: `|E| / (|V| * (|V| - 1))`.
166    pub density: f64,
167    /// Average of all pairwise shortest-path lengths.
168    pub avg_path_length: f64,
169    /// Betweenness centrality per node (Brandes algorithm).
170    pub betweenness: HashMap<NtmNodeId, f64>,
171    /// Degree centrality per node: `degree / (|V| - 1)`.
172    pub centrality: HashMap<NtmNodeId, f64>,
173}
174
175// ─── Internal adjacency helpers ───────────────────────────────────────────────
176
177/// Compact ordered pair used as the key for the adjacency set.
178type EdgeKey = (NtmNodeId, NtmNodeId);
179
180// ─── Main struct ──────────────────────────────────────────────────────────────
181
182/// Live network topology mapper.
183///
184/// Tracks peer nodes and directed edges, computes graph algorithms, and
185/// maintains a bounded ring-buffer of snapshots.
186pub struct NetworkTopologyMapper {
187    /// All known nodes keyed by their 32-byte id.
188    nodes: HashMap<NtmNodeId, NtmNode>,
189    /// All known edges keyed by their `NtmEdgeId`.
190    edges: HashMap<NtmEdgeId, NtmEdge>,
191    /// Adjacency index: `(src, dst) → edge_id` for fast lookup.
192    adj: HashMap<EdgeKey, NtmEdgeId>,
193    /// Out-edge list: `src → Vec<edge_id>` for fast neighbour iteration.
194    out_edges: HashMap<NtmNodeId, Vec<NtmEdgeId>>,
195    /// In-edge list: `dst → Vec<edge_id>` for efficient reverse traversal.
196    in_edges: HashMap<NtmNodeId, Vec<NtmEdgeId>>,
197    /// Bounded snapshot ring-buffer (capacity = 20).
198    snapshots: VecDeque<NtmSnapshot>,
199    /// Mapper configuration.
200    config: NtmMapperConfig,
201    /// Monotonic PRNG state for edge-id generation.
202    prng_state: u64,
203    /// Edge counter used to salt edge-id hashing.
204    edge_counter: u64,
205}
206
207impl NetworkTopologyMapper {
208    // ── Construction ──────────────────────────────────────────────────────────
209
210    /// Create a new mapper with the given `config`.
211    pub fn new(config: NtmMapperConfig) -> Self {
212        // Seed the PRNG with the FNV-1a hash of a fixed string so it is
213        // deterministic yet non-trivial.
214        let seed = fnv1a_64(b"NetworkTopologyMapper:v1");
215        Self {
216            nodes: HashMap::new(),
217            edges: HashMap::new(),
218            adj: HashMap::new(),
219            out_edges: HashMap::new(),
220            in_edges: HashMap::new(),
221            snapshots: VecDeque::with_capacity(20),
222            config,
223            prng_state: seed | 1, // ensure non-zero
224            edge_counter: 0,
225        }
226    }
227
228    /// Create a mapper with default configuration.
229    pub fn with_defaults() -> Self {
230        Self::new(NtmMapperConfig::default())
231    }
232
233    // ── Node operations ───────────────────────────────────────────────────────
234
235    /// Insert or update a node.
236    ///
237    /// If the node already exists its `addr`, `region`, `rtt_ms`, and
238    /// `last_seen` fields are refreshed.  Returns `Err(CapacityExceeded)`
239    /// when the node is new and the graph is full.
240    pub fn add_node(
241        &mut self,
242        id: NtmNodeId,
243        addr: impl Into<String>,
244        region: Option<String>,
245        rtt_ms: f64,
246        last_seen: u64,
247    ) -> Result<(), NtmMapperError> {
248        if let Some(existing) = self.nodes.get_mut(&id) {
249            existing.addr = addr.into();
250            existing.region = region;
251            existing.rtt_ms = rtt_ms;
252            existing.last_seen = last_seen;
253            return Ok(());
254        }
255        if self.nodes.len() >= self.config.max_nodes {
256            return Err(NtmMapperError::CapacityExceeded(format!(
257                "node limit {} reached",
258                self.config.max_nodes
259            )));
260        }
261        self.nodes.insert(
262            id,
263            NtmNode {
264                id,
265                addr: addr.into(),
266                region,
267                rtt_ms,
268                last_seen,
269                degree: 0,
270                is_bootstrap: false,
271            },
272        );
273        Ok(())
274    }
275
276    /// Remove a node and all edges incident to it.
277    pub fn remove_node(&mut self, id: &NtmNodeId) -> Result<(), NtmMapperError> {
278        if !self.nodes.contains_key(id) {
279            return Err(NtmMapperError::NotFound(format!("{id:?}")));
280        }
281        // Collect all edge ids touching this node before mutating.
282        let mut to_remove: Vec<NtmEdgeId> = Vec::new();
283        if let Some(outs) = self.out_edges.get(id) {
284            to_remove.extend_from_slice(outs);
285        }
286        if let Some(ins) = self.in_edges.get(id) {
287            to_remove.extend_from_slice(ins);
288        }
289        for eid in to_remove {
290            let _ = self.remove_edge(eid);
291        }
292        self.out_edges.remove(id);
293        self.in_edges.remove(id);
294        self.nodes.remove(id);
295        Ok(())
296    }
297
298    /// Update the RTT measurement for an existing node.
299    pub fn update_node_rtt(&mut self, id: &NtmNodeId, rtt_ms: f64) -> Result<(), NtmMapperError> {
300        let node = self
301            .nodes
302            .get_mut(id)
303            .ok_or_else(|| NtmMapperError::NotFound(format!("{id:?}")))?;
304        node.rtt_ms = rtt_ms;
305        Ok(())
306    }
307
308    /// Mark a node as a bootstrap / well-known anchor.
309    pub fn set_bootstrap(&mut self, id: &NtmNodeId, flag: bool) -> Result<(), NtmMapperError> {
310        let node = self
311            .nodes
312            .get_mut(id)
313            .ok_or_else(|| NtmMapperError::NotFound(format!("{id:?}")))?;
314        node.is_bootstrap = flag;
315        Ok(())
316    }
317
318    /// Return an immutable reference to a node.
319    pub fn get_node(&self, id: &NtmNodeId) -> Option<&NtmNode> {
320        self.nodes.get(id)
321    }
322
323    /// Iterate over all nodes.
324    pub fn nodes(&self) -> impl Iterator<Item = &NtmNode> {
325        self.nodes.values()
326    }
327
328    // ── Edge operations ───────────────────────────────────────────────────────
329
330    /// Insert a directed edge `src → dst`.
331    ///
332    /// If an edge with the same `(src, dst)` pair already exists, its
333    /// `latency_ms`, `bandwidth_kbps`, and `observed_at` fields are updated.
334    /// Returns the edge identifier.
335    pub fn add_edge(
336        &mut self,
337        src: NtmNodeId,
338        dst: NtmNodeId,
339        latency_ms: f64,
340        bandwidth_kbps: f64,
341        observed_at: u64,
342    ) -> Result<NtmEdgeId, NtmMapperError> {
343        if !self.nodes.contains_key(&src) {
344            return Err(NtmMapperError::NotFound(format!("src {:?}", src)));
345        }
346        if !self.nodes.contains_key(&dst) {
347            return Err(NtmMapperError::NotFound(format!("dst {:?}", dst)));
348        }
349
350        // Update existing edge.
351        if let Some(&eid) = self.adj.get(&(src, dst)) {
352            if let Some(edge) = self.edges.get_mut(&eid) {
353                edge.latency_ms = latency_ms;
354                edge.bandwidth_kbps = bandwidth_kbps;
355                edge.observed_at = observed_at;
356            }
357            return Ok(eid);
358        }
359
360        if self.edges.len() >= self.config.max_edges {
361            return Err(NtmMapperError::CapacityExceeded(format!(
362                "edge limit {} reached",
363                self.config.max_edges
364            )));
365        }
366
367        let eid = self.gen_edge_id(&src, &dst);
368        let edge = NtmEdge {
369            id: eid,
370            src,
371            dst,
372            latency_ms,
373            bandwidth_kbps,
374            observed_at,
375        };
376        self.edges.insert(eid, edge);
377        self.adj.insert((src, dst), eid);
378        self.out_edges.entry(src).or_default().push(eid);
379        self.in_edges.entry(dst).or_default().push(eid);
380
381        // Update degrees.
382        self.recompute_degree(&src);
383        self.recompute_degree(&dst);
384
385        Ok(eid)
386    }
387
388    /// Remove a directed edge by its identifier.
389    pub fn remove_edge(&mut self, id: NtmEdgeId) -> Result<(), NtmMapperError> {
390        let edge = self
391            .edges
392            .remove(&id)
393            .ok_or_else(|| NtmMapperError::NotFound(format!("edge {id}")))?;
394        self.adj.remove(&(edge.src, edge.dst));
395        if let Some(list) = self.out_edges.get_mut(&edge.src) {
396            list.retain(|&e| e != id);
397        }
398        if let Some(list) = self.in_edges.get_mut(&edge.dst) {
399            list.retain(|&e| e != id);
400        }
401        self.recompute_degree(&edge.src);
402        self.recompute_degree(&edge.dst);
403        Ok(())
404    }
405
406    /// Update the latency measurement for an existing edge.
407    pub fn update_edge_latency(
408        &mut self,
409        id: NtmEdgeId,
410        latency_ms: f64,
411    ) -> Result<(), NtmMapperError> {
412        let edge = self
413            .edges
414            .get_mut(&id)
415            .ok_or_else(|| NtmMapperError::NotFound(format!("edge {id}")))?;
416        edge.latency_ms = latency_ms;
417        Ok(())
418    }
419
420    /// Return an immutable reference to an edge.
421    pub fn get_edge(&self, id: NtmEdgeId) -> Option<&NtmEdge> {
422        self.edges.get(&id)
423    }
424
425    /// Return the edge identifier for the directed pair `(src, dst)`, if any.
426    pub fn find_edge(&self, src: &NtmNodeId, dst: &NtmNodeId) -> Option<NtmEdgeId> {
427        self.adj.get(&(*src, *dst)).copied()
428    }
429
430    /// Iterate over all edges.
431    pub fn edges(&self) -> impl Iterator<Item = &NtmEdge> {
432        self.edges.values()
433    }
434
435    // ── Graph algorithms ──────────────────────────────────────────────────────
436
437    /// Return the neighbour node ids reachable via outgoing edges from `node_id`.
438    pub fn neighbors(&self, node_id: &NtmNodeId) -> Vec<NtmNodeId> {
439        match self.out_edges.get(node_id) {
440            None => Vec::new(),
441            Some(eids) => eids
442                .iter()
443                .filter_map(|eid| self.edges.get(eid).map(|e| e.dst))
444                .collect(),
445        }
446    }
447
448    /// Dijkstra shortest path from `src` to `dst` weighted by `latency_ms`.
449    ///
450    /// Returns `None` if either node is unknown or no path exists.
451    pub fn shortest_path(&self, src: &NtmNodeId, dst: &NtmNodeId) -> Option<Vec<NtmNodeId>> {
452        if !self.nodes.contains_key(src) || !self.nodes.contains_key(dst) {
453            return None;
454        }
455        if src == dst {
456            return Some(vec![*src]);
457        }
458
459        // dist stored as ordered bits of f64 (positive, so bit-cast works for
460        // ordering): we use `u64` distances in the heap via `f64::to_bits`.
461        let mut dist: HashMap<NtmNodeId, f64> = HashMap::new();
462        let mut prev: HashMap<NtmNodeId, NtmNodeId> = HashMap::new();
463        // BinaryHeap<Reverse<(dist_bits, node_id)>>
464        let mut heap: BinaryHeap<Reverse<(u64, NtmNodeId)>> = BinaryHeap::new();
465
466        dist.insert(*src, 0.0);
467        heap.push(Reverse((0u64, *src)));
468
469        while let Some(Reverse((d_bits, u))) = heap.pop() {
470            let d = f64::from_bits(d_bits);
471            // Skip stale entries.
472            if let Some(&best) = dist.get(&u) {
473                if d > best + f64::EPSILON {
474                    continue;
475                }
476            }
477            if &u == dst {
478                // Reconstruct path.
479                let mut path = vec![u];
480                let mut cur = u;
481                while let Some(&p) = prev.get(&cur) {
482                    path.push(p);
483                    cur = p;
484                }
485                path.reverse();
486                return Some(path);
487            }
488            if let Some(eids) = self.out_edges.get(&u) {
489                for &eid in eids {
490                    if let Some(edge) = self.edges.get(&eid) {
491                        let new_d = d + edge.latency_ms.max(0.0);
492                        let better = match dist.get(&edge.dst) {
493                            None => true,
494                            Some(&old) => new_d < old - f64::EPSILON,
495                        };
496                        if better {
497                            dist.insert(edge.dst, new_d);
498                            prev.insert(edge.dst, u);
499                            heap.push(Reverse((new_d.to_bits(), edge.dst)));
500                        }
501                    }
502                }
503            }
504        }
505        None
506    }
507
508    /// BFS-based shortest path counting hops (ignores weights).
509    ///
510    /// Returns `None` if no path exists.
511    pub fn bfs_distance(&self, src: &NtmNodeId, dst: &NtmNodeId) -> Option<u32> {
512        if !self.nodes.contains_key(src) || !self.nodes.contains_key(dst) {
513            return None;
514        }
515        if src == dst {
516            return Some(0);
517        }
518        let mut visited: HashSet<NtmNodeId> = HashSet::new();
519        let mut queue: VecDeque<(NtmNodeId, u32)> = VecDeque::new();
520        queue.push_back((*src, 0));
521        visited.insert(*src);
522        while let Some((cur, d)) = queue.pop_front() {
523            for nb in self.neighbors(&cur) {
524                if &nb == dst {
525                    return Some(d + 1);
526                }
527                if visited.insert(nb) {
528                    queue.push_back((nb, d + 1));
529                }
530            }
531        }
532        None
533    }
534
535    /// Graph diameter: maximum of all pairwise BFS distances.
536    ///
537    /// Returns 0 when the graph has fewer than two nodes.
538    pub fn diameter(&self) -> u32 {
539        let ids: Vec<NtmNodeId> = self.nodes.keys().copied().collect();
540        if ids.len() < 2 {
541            return 0;
542        }
543        let mut max_d = 0u32;
544        for &start in &ids {
545            // BFS from start.
546            let mut dist: HashMap<NtmNodeId, u32> = HashMap::new();
547            let mut queue: VecDeque<NtmNodeId> = VecDeque::new();
548            dist.insert(start, 0);
549            queue.push_back(start);
550            while let Some(cur) = queue.pop_front() {
551                let d = dist[&cur];
552                for nb in self.neighbors(&cur) {
553                    if let std::collections::hash_map::Entry::Vacant(e) = dist.entry(nb) {
554                        e.insert(d + 1);
555                        queue.push_back(nb);
556                    }
557                }
558            }
559            for &v in dist.values() {
560                if v > max_d {
561                    max_d = v;
562                }
563            }
564        }
565        max_d
566    }
567
568    /// Local clustering coefficient for `node_id`.
569    ///
570    /// `C(u) = triangles / (k * (k-1))` where `k = degree(u)`.
571    /// Returns `0.0` for nodes with degree < 2.
572    pub fn clustering_coefficient(&self, node_id: &NtmNodeId) -> f64 {
573        let nbs: Vec<NtmNodeId> = self.neighbors(node_id);
574        let k = nbs.len();
575        if k < 2 {
576            return 0.0;
577        }
578        let mut triangles = 0u64;
579        for &nb in &nbs {
580            for &nb2 in &nbs {
581                if nb == nb2 {
582                    continue;
583                }
584                if self.adj.contains_key(&(nb, nb2)) {
585                    triangles += 1;
586                }
587            }
588        }
589        // Each triangle counted twice (both orderings of the pair).
590        triangles as f64 / (k as f64 * (k as f64 - 1.0))
591    }
592
593    /// Brandes algorithm for betweenness centrality on the directed graph.
594    ///
595    /// Complexity O(V * E) — suitable for moderate-sized graphs (≤ 4096 nodes).
596    pub fn compute_betweenness_centrality(&self) -> HashMap<NtmNodeId, f64> {
597        let mut cb: HashMap<NtmNodeId, f64> = self.nodes.keys().map(|&id| (id, 0.0)).collect();
598
599        let all_nodes: Vec<NtmNodeId> = self.nodes.keys().copied().collect();
600
601        for &s in &all_nodes {
602            // BFS from s.
603            let mut stack: Vec<NtmNodeId> = Vec::new();
604            let mut pred: HashMap<NtmNodeId, Vec<NtmNodeId>> = HashMap::new();
605            let mut sigma: HashMap<NtmNodeId, f64> = HashMap::new();
606            let mut dist_map: HashMap<NtmNodeId, i64> = HashMap::new();
607
608            for &v in &all_nodes {
609                pred.insert(v, Vec::new());
610                sigma.insert(v, 0.0);
611                dist_map.insert(v, -1);
612            }
613            if let Some(sig) = sigma.get_mut(&s) {
614                *sig = 1.0;
615            }
616            if let Some(d) = dist_map.get_mut(&s) {
617                *d = 0;
618            }
619
620            let mut queue: VecDeque<NtmNodeId> = VecDeque::new();
621            queue.push_back(s);
622
623            while let Some(v) = queue.pop_front() {
624                stack.push(v);
625                let dv = dist_map[&v];
626                let sig_v = sigma[&v];
627                if let Some(eids) = self.out_edges.get(&v) {
628                    for &eid in eids {
629                        if let Some(edge) = self.edges.get(&eid) {
630                            let w = edge.dst;
631                            if dist_map[&w] < 0 {
632                                queue.push_back(w);
633                                if let Some(d) = dist_map.get_mut(&w) {
634                                    *d = dv + 1;
635                                }
636                            }
637                            if dist_map[&w] == dv + 1 {
638                                if let Some(sig) = sigma.get_mut(&w) {
639                                    *sig += sig_v;
640                                }
641                                if let Some(p) = pred.get_mut(&w) {
642                                    p.push(v);
643                                }
644                            }
645                        }
646                    }
647                }
648            }
649
650            let mut delta: HashMap<NtmNodeId, f64> = all_nodes.iter().map(|&v| (v, 0.0)).collect();
651
652            while let Some(w) = stack.pop() {
653                let sig_w = sigma[&w];
654                let delta_w = delta[&w];
655                let preds = pred[&w].clone();
656                for v in preds {
657                    let coeff = (sigma[&v] / sig_w) * (1.0 + delta_w);
658                    if let Some(d) = delta.get_mut(&v) {
659                        *d += coeff;
660                    }
661                }
662                if w != s {
663                    if let Some(c) = cb.get_mut(&w) {
664                        *c += delta_w;
665                    }
666                }
667            }
668        }
669
670        // Normalise by (V-1)(V-2) for directed graphs.
671        let n = all_nodes.len() as f64;
672        if n > 2.0 {
673            let norm = (n - 1.0) * (n - 2.0);
674            for v in cb.values_mut() {
675                *v /= norm;
676            }
677        }
678
679        cb
680    }
681
682    // ── Snapshot ──────────────────────────────────────────────────────────────
683
684    /// Capture the current state into a snapshot and store it.
685    ///
686    /// If the snapshot buffer is full (20 entries) the oldest entry is
687    /// dropped before the new one is inserted.
688    pub fn take_snapshot(&mut self, ts: u64) -> NtmSnapshot {
689        let node_count = self.nodes.len();
690        let edge_count = self.edges.len();
691        let avg_degree = if node_count == 0 {
692            0.0
693        } else {
694            self.nodes.values().map(|n| n.degree as f64).sum::<f64>() / node_count as f64
695        };
696        let diameter = self.diameter();
697        let clustering_coeff = if node_count == 0 {
698            0.0
699        } else {
700            let sum: f64 = self
701                .nodes
702                .keys()
703                .map(|id| self.clustering_coefficient(id))
704                .sum();
705            sum / node_count as f64
706        };
707        let snap = NtmSnapshot {
708            ts,
709            node_count,
710            edge_count,
711            avg_degree,
712            diameter,
713            clustering_coeff,
714        };
715        if self.snapshots.len() == 20 {
716            self.snapshots.pop_front();
717        }
718        self.snapshots.push_back(snap.clone());
719        snap
720    }
721
722    /// Access the snapshot history (oldest first).
723    pub fn snapshots(&self) -> &VecDeque<NtmSnapshot> {
724        &self.snapshots
725    }
726
727    // ── Pruning ───────────────────────────────────────────────────────────────
728
729    /// Remove all nodes (and their edges) whose `last_seen` is older than
730    /// `now_ts - config.prune_disconnected_after_secs`.
731    pub fn prune_stale(&mut self, now_ts: u64) {
732        let threshold = now_ts.saturating_sub(self.config.prune_disconnected_after_secs);
733        let stale: Vec<NtmNodeId> = self
734            .nodes
735            .values()
736            .filter(|n| n.last_seen < threshold)
737            .map(|n| n.id)
738            .collect();
739        for id in stale {
740            let _ = self.remove_node(&id);
741        }
742    }
743
744    // ── Derived metrics ───────────────────────────────────────────────────────
745
746    /// Compute the full suite of topology metrics.
747    pub fn topology_stats(&self) -> NtmTopologyMetrics {
748        let n = self.nodes.len();
749        let e = self.edges.len();
750
751        let density = if n > 1 {
752            e as f64 / (n as f64 * (n as f64 - 1.0))
753        } else {
754            0.0
755        };
756
757        let avg_path_length = self.compute_avg_path_length();
758        let betweenness = self.compute_betweenness_centrality();
759
760        let centrality: HashMap<NtmNodeId, f64> = if n > 1 {
761            self.nodes
762                .values()
763                .map(|node| (node.id, node.degree as f64 / (n as f64 - 1.0)))
764                .collect()
765        } else {
766            self.nodes.values().map(|n| (n.id, 0.0)).collect()
767        };
768
769        NtmTopologyMetrics {
770            density,
771            avg_path_length,
772            betweenness,
773            centrality,
774        }
775    }
776
777    // ── Accessors ─────────────────────────────────────────────────────────────
778
779    /// Return the number of nodes.
780    pub fn node_count(&self) -> usize {
781        self.nodes.len()
782    }
783
784    /// Return the number of edges.
785    pub fn edge_count(&self) -> usize {
786        self.edges.len()
787    }
788
789    /// Return the current configuration.
790    pub fn config(&self) -> &NtmMapperConfig {
791        &self.config
792    }
793
794    // ── Private helpers ───────────────────────────────────────────────────────
795
796    /// Generate a collision-resistant edge identifier.
797    fn gen_edge_id(&mut self, src: &NtmNodeId, dst: &NtmNodeId) -> NtmEdgeId {
798        self.edge_counter += 1;
799        // Mix src bytes, dst bytes, and a fresh PRNG value.
800        let src_h = fnv1a_64(src);
801        let dst_h = fnv1a_64(dst);
802        let rnd = xorshift64(&mut self.prng_state);
803        src_h ^ dst_h.rotate_left(31) ^ rnd ^ self.edge_counter.wrapping_mul(0x9e3779b97f4a7c15)
804    }
805
806    /// Recompute the `degree` field (out-degree + in-degree) for `id`.
807    fn recompute_degree(&mut self, id: &NtmNodeId) {
808        let out = self.out_edges.get(id).map(|v| v.len()).unwrap_or(0);
809        let inc = self.in_edges.get(id).map(|v| v.len()).unwrap_or(0);
810        if let Some(node) = self.nodes.get_mut(id) {
811            node.degree = (out + inc) as u32;
812        }
813    }
814
815    /// Average pairwise shortest-path length via BFS (unweighted).
816    fn compute_avg_path_length(&self) -> f64 {
817        let ids: Vec<NtmNodeId> = self.nodes.keys().copied().collect();
818        let n = ids.len();
819        if n < 2 {
820            return 0.0;
821        }
822        let mut total = 0u64;
823        let mut pairs = 0u64;
824        for &s in &ids {
825            // BFS from s.
826            let mut dist: HashMap<NtmNodeId, u32> = HashMap::new();
827            let mut queue: VecDeque<NtmNodeId> = VecDeque::new();
828            dist.insert(s, 0);
829            queue.push_back(s);
830            while let Some(cur) = queue.pop_front() {
831                let d = dist[&cur];
832                for nb in self.neighbors(&cur) {
833                    if let std::collections::hash_map::Entry::Vacant(e) = dist.entry(nb) {
834                        e.insert(d + 1);
835                        queue.push_back(nb);
836                    }
837                }
838            }
839            for (&v, &d) in &dist {
840                if v != s {
841                    total += d as u64;
842                    pairs += 1;
843                }
844            }
845        }
846        if pairs == 0 {
847            0.0
848        } else {
849            total as f64 / pairs as f64
850        }
851    }
852}
853
854// ─── Tests ────────────────────────────────────────────────────────────────────
855
856#[cfg(test)]
857mod tests {
858    use super::*;
859
860    // ── helpers ───────────────────────────────────────────────────────────────
861
862    fn make_id(v: u8) -> NtmNodeId {
863        let mut id = [0u8; 32];
864        id[0] = v;
865        id
866    }
867
868    fn make_mapper() -> NetworkTopologyMapper {
869        NetworkTopologyMapper::with_defaults()
870    }
871
872    fn add_n(m: &mut NetworkTopologyMapper, v: u8) {
873        m.add_node(make_id(v), format!("127.0.0.{v}:4001"), None, 1.0, 100)
874            .expect("add_node failed");
875    }
876
877    fn add_e(m: &mut NetworkTopologyMapper, u: u8, v: u8, lat: f64) -> NtmEdgeId {
878        m.add_edge(make_id(u), make_id(v), lat, 1000.0, 100)
879            .expect("add_edge failed")
880    }
881
882    // ── Config ────────────────────────────────────────────────────────────────
883
884    #[test]
885    fn test_config_defaults() {
886        let c = NtmMapperConfig::default();
887        assert_eq!(c.max_nodes, 4_096);
888        assert_eq!(c.max_edges, 65_536);
889        assert_eq!(c.snapshot_interval_secs, 60);
890        assert_eq!(c.prune_disconnected_after_secs, 300);
891    }
892
893    #[test]
894    fn test_config_custom() {
895        let c = NtmMapperConfig {
896            max_nodes: 10,
897            max_edges: 20,
898            snapshot_interval_secs: 5,
899            prune_disconnected_after_secs: 15,
900        };
901        assert_eq!(c.max_nodes, 10);
902    }
903
904    // ── Construction ──────────────────────────────────────────────────────────
905
906    #[test]
907    fn test_new_mapper_empty() {
908        let m = make_mapper();
909        assert_eq!(m.node_count(), 0);
910        assert_eq!(m.edge_count(), 0);
911    }
912
913    #[test]
914    fn test_with_defaults() {
915        let m = NetworkTopologyMapper::with_defaults();
916        assert_eq!(m.node_count(), 0);
917    }
918
919    // ── add_node ──────────────────────────────────────────────────────────────
920
921    #[test]
922    fn test_add_single_node() {
923        let mut m = make_mapper();
924        add_n(&mut m, 1);
925        assert_eq!(m.node_count(), 1);
926    }
927
928    #[test]
929    fn test_add_node_idempotent() {
930        let mut m = make_mapper();
931        add_n(&mut m, 1);
932        add_n(&mut m, 1); // second call updates, not duplicates
933        assert_eq!(m.node_count(), 1);
934    }
935
936    #[test]
937    fn test_add_node_updates_rtt() {
938        let mut m = make_mapper();
939        m.add_node(make_id(1), "a:1", None, 5.0, 100)
940            .expect("test: add_node for node 1 (initial)");
941        m.add_node(make_id(1), "a:1", None, 99.0, 200)
942            .expect("test: add_node for node 1 (update rtt)");
943        assert!(
944            (m.get_node(&make_id(1))
945                .expect("test: get_node for node 1 to check rtt")
946                .rtt_ms
947                - 99.0)
948                .abs()
949                < f64::EPSILON
950        );
951    }
952
953    #[test]
954    fn test_add_node_capacity_exceeded() {
955        let config = NtmMapperConfig {
956            max_nodes: 2,
957            ..Default::default()
958        };
959        let mut m = NetworkTopologyMapper::new(config);
960        add_n(&mut m, 1);
961        add_n(&mut m, 2);
962        let r = m.add_node(make_id(3), "x", None, 1.0, 0);
963        assert!(matches!(r, Err(NtmMapperError::CapacityExceeded(_))));
964    }
965
966    #[test]
967    fn test_add_node_with_region() {
968        let mut m = make_mapper();
969        m.add_node(make_id(5), "addr", Some("eu-west".into()), 1.0, 0)
970            .expect("test: add_node for node 5 with region");
971        assert_eq!(
972            m.get_node(&make_id(5))
973                .expect("test: get_node for node 5 to check region")
974                .region
975                .as_deref(),
976            Some("eu-west")
977        );
978    }
979
980    // ── get_node ──────────────────────────────────────────────────────────────
981
982    #[test]
983    fn test_get_node_existing() {
984        let mut m = make_mapper();
985        add_n(&mut m, 7);
986        let n = m.get_node(&make_id(7));
987        assert!(n.is_some());
988    }
989
990    #[test]
991    fn test_get_node_missing() {
992        let m = make_mapper();
993        assert!(m.get_node(&make_id(99)).is_none());
994    }
995
996    // ── remove_node ───────────────────────────────────────────────────────────
997
998    #[test]
999    fn test_remove_node_basic() {
1000        let mut m = make_mapper();
1001        add_n(&mut m, 1);
1002        m.remove_node(&make_id(1))
1003            .expect("test: remove_node should succeed for existing node 1");
1004        assert_eq!(m.node_count(), 0);
1005    }
1006
1007    #[test]
1008    fn test_remove_node_missing() {
1009        let mut m = make_mapper();
1010        let r = m.remove_node(&make_id(42));
1011        assert!(matches!(r, Err(NtmMapperError::NotFound(_))));
1012    }
1013
1014    #[test]
1015    fn test_remove_node_cascades_edges() {
1016        let mut m = make_mapper();
1017        add_n(&mut m, 1);
1018        add_n(&mut m, 2);
1019        add_e(&mut m, 1, 2, 5.0);
1020        m.remove_node(&make_id(1))
1021            .expect("test: remove_node should cascade edges for node 1");
1022        assert_eq!(m.edge_count(), 0);
1023    }
1024
1025    // ── update_node_rtt ───────────────────────────────────────────────────────
1026
1027    #[test]
1028    fn test_update_rtt_ok() {
1029        let mut m = make_mapper();
1030        add_n(&mut m, 3);
1031        m.update_node_rtt(&make_id(3), 42.0)
1032            .expect("test: update_node_rtt should succeed for existing node 3");
1033        assert!(
1034            (m.get_node(&make_id(3))
1035                .expect("test: get_node should return node 3 after rtt update")
1036                .rtt_ms
1037                - 42.0)
1038                .abs()
1039                < f64::EPSILON
1040        );
1041    }
1042
1043    #[test]
1044    fn test_update_rtt_missing() {
1045        let mut m = make_mapper();
1046        let r = m.update_node_rtt(&make_id(99), 1.0);
1047        assert!(matches!(r, Err(NtmMapperError::NotFound(_))));
1048    }
1049
1050    // ── set_bootstrap ─────────────────────────────────────────────────────────
1051
1052    #[test]
1053    fn test_set_bootstrap_true() {
1054        let mut m = make_mapper();
1055        add_n(&mut m, 1);
1056        m.set_bootstrap(&make_id(1), true)
1057            .expect("test: set_bootstrap true should succeed for node 1");
1058        assert!(
1059            m.get_node(&make_id(1))
1060                .expect("test: get_node should return node 1 after set_bootstrap")
1061                .is_bootstrap
1062        );
1063    }
1064
1065    #[test]
1066    fn test_set_bootstrap_false() {
1067        let mut m = make_mapper();
1068        add_n(&mut m, 1);
1069        m.set_bootstrap(&make_id(1), true)
1070            .expect("test: set_bootstrap true should succeed");
1071        m.set_bootstrap(&make_id(1), false)
1072            .expect("test: set_bootstrap false should succeed");
1073        assert!(
1074            !m.get_node(&make_id(1))
1075                .expect("test: get_node should return node 1 after set_bootstrap false")
1076                .is_bootstrap
1077        );
1078    }
1079
1080    // ── add_edge ──────────────────────────────────────────────────────────────
1081
1082    #[test]
1083    fn test_add_edge_basic() {
1084        let mut m = make_mapper();
1085        add_n(&mut m, 1);
1086        add_n(&mut m, 2);
1087        add_e(&mut m, 1, 2, 10.0);
1088        assert_eq!(m.edge_count(), 1);
1089    }
1090
1091    #[test]
1092    fn test_add_edge_updates_latency() {
1093        let mut m = make_mapper();
1094        add_n(&mut m, 1);
1095        add_n(&mut m, 2);
1096        let eid = add_e(&mut m, 1, 2, 10.0);
1097        m.add_edge(make_id(1), make_id(2), 99.0, 500.0, 200)
1098            .expect("test: add_edge update should succeed");
1099        assert!(
1100            (m.get_edge(eid)
1101                .expect("test: get_edge should return edge after latency update")
1102                .latency_ms
1103                - 99.0)
1104                .abs()
1105                < f64::EPSILON
1106        );
1107    }
1108
1109    #[test]
1110    fn test_add_edge_missing_src() {
1111        let mut m = make_mapper();
1112        add_n(&mut m, 2);
1113        let r = m.add_edge(make_id(1), make_id(2), 1.0, 1.0, 0);
1114        assert!(matches!(r, Err(NtmMapperError::NotFound(_))));
1115    }
1116
1117    #[test]
1118    fn test_add_edge_missing_dst() {
1119        let mut m = make_mapper();
1120        add_n(&mut m, 1);
1121        let r = m.add_edge(make_id(1), make_id(2), 1.0, 1.0, 0);
1122        assert!(matches!(r, Err(NtmMapperError::NotFound(_))));
1123    }
1124
1125    #[test]
1126    fn test_add_edge_capacity_exceeded() {
1127        let config = NtmMapperConfig {
1128            max_edges: 1,
1129            ..Default::default()
1130        };
1131        let mut m = NetworkTopologyMapper::new(config);
1132        add_n(&mut m, 1);
1133        add_n(&mut m, 2);
1134        add_n(&mut m, 3);
1135        add_e(&mut m, 1, 2, 1.0);
1136        let r = m.add_edge(make_id(2), make_id(3), 1.0, 1.0, 0);
1137        assert!(matches!(r, Err(NtmMapperError::CapacityExceeded(_))));
1138    }
1139
1140    // ── remove_edge ───────────────────────────────────────────────────────────
1141
1142    #[test]
1143    fn test_remove_edge_ok() {
1144        let mut m = make_mapper();
1145        add_n(&mut m, 1);
1146        add_n(&mut m, 2);
1147        let eid = add_e(&mut m, 1, 2, 1.0);
1148        m.remove_edge(eid)
1149            .expect("test: remove_edge should succeed for existing edge");
1150        assert_eq!(m.edge_count(), 0);
1151    }
1152
1153    #[test]
1154    fn test_remove_edge_missing() {
1155        let mut m = make_mapper();
1156        let r = m.remove_edge(0xdeadbeef);
1157        assert!(matches!(r, Err(NtmMapperError::NotFound(_))));
1158    }
1159
1160    #[test]
1161    fn test_remove_edge_updates_degree() {
1162        let mut m = make_mapper();
1163        add_n(&mut m, 1);
1164        add_n(&mut m, 2);
1165        let eid = add_e(&mut m, 1, 2, 1.0);
1166        assert_eq!(
1167            m.get_node(&make_id(1))
1168                .expect("test: get_node should return node 1 before remove_edge")
1169                .degree,
1170            1
1171        );
1172        m.remove_edge(eid)
1173            .expect("test: remove_edge should succeed when updating degree");
1174        assert_eq!(
1175            m.get_node(&make_id(1))
1176                .expect("test: get_node should return node 1 after remove_edge")
1177                .degree,
1178            0
1179        );
1180    }
1181
1182    // ── update_edge_latency ───────────────────────────────────────────────────
1183
1184    #[test]
1185    fn test_update_edge_latency_ok() {
1186        let mut m = make_mapper();
1187        add_n(&mut m, 1);
1188        add_n(&mut m, 2);
1189        let eid = add_e(&mut m, 1, 2, 5.0);
1190        m.update_edge_latency(eid, 77.0)
1191            .expect("test: update_edge_latency should succeed");
1192        assert!(
1193            (m.get_edge(eid)
1194                .expect("test: get_edge should return edge after latency update")
1195                .latency_ms
1196                - 77.0)
1197                .abs()
1198                < f64::EPSILON
1199        );
1200    }
1201
1202    #[test]
1203    fn test_update_edge_latency_missing() {
1204        let mut m = make_mapper();
1205        let r = m.update_edge_latency(999, 1.0);
1206        assert!(matches!(r, Err(NtmMapperError::NotFound(_))));
1207    }
1208
1209    // ── find_edge ─────────────────────────────────────────────────────────────
1210
1211    #[test]
1212    fn test_find_edge_exists() {
1213        let mut m = make_mapper();
1214        add_n(&mut m, 1);
1215        add_n(&mut m, 2);
1216        let eid = add_e(&mut m, 1, 2, 1.0);
1217        assert_eq!(m.find_edge(&make_id(1), &make_id(2)), Some(eid));
1218    }
1219
1220    #[test]
1221    fn test_find_edge_not_exists() {
1222        let m = make_mapper();
1223        assert!(m.find_edge(&make_id(1), &make_id(2)).is_none());
1224    }
1225
1226    // ── neighbors ─────────────────────────────────────────────────────────────
1227
1228    #[test]
1229    fn test_neighbors_empty() {
1230        let mut m = make_mapper();
1231        add_n(&mut m, 1);
1232        assert!(m.neighbors(&make_id(1)).is_empty());
1233    }
1234
1235    #[test]
1236    fn test_neighbors_single() {
1237        let mut m = make_mapper();
1238        add_n(&mut m, 1);
1239        add_n(&mut m, 2);
1240        add_e(&mut m, 1, 2, 1.0);
1241        assert_eq!(m.neighbors(&make_id(1)), vec![make_id(2)]);
1242    }
1243
1244    #[test]
1245    fn test_neighbors_multiple() {
1246        let mut m = make_mapper();
1247        for v in 1..=4 {
1248            add_n(&mut m, v);
1249        }
1250        add_e(&mut m, 1, 2, 1.0);
1251        add_e(&mut m, 1, 3, 2.0);
1252        add_e(&mut m, 1, 4, 3.0);
1253        let nbs = m.neighbors(&make_id(1));
1254        assert_eq!(nbs.len(), 3);
1255    }
1256
1257    // ── shortest_path ─────────────────────────────────────────────────────────
1258
1259    #[test]
1260    fn test_shortest_path_direct() {
1261        let mut m = make_mapper();
1262        add_n(&mut m, 1);
1263        add_n(&mut m, 2);
1264        add_e(&mut m, 1, 2, 5.0);
1265        let path = m
1266            .shortest_path(&make_id(1), &make_id(2))
1267            .expect("test: shortest_path should find direct path");
1268        assert_eq!(path, vec![make_id(1), make_id(2)]);
1269    }
1270
1271    #[test]
1272    fn test_shortest_path_multi_hop() {
1273        let mut m = make_mapper();
1274        for v in 1..=4 {
1275            add_n(&mut m, v);
1276        }
1277        // 1→2 (100), 1→3 (1), 3→4 (1), 2→4 (1)
1278        add_e(&mut m, 1, 2, 100.0);
1279        add_e(&mut m, 1, 3, 1.0);
1280        add_e(&mut m, 3, 4, 1.0);
1281        add_e(&mut m, 2, 4, 1.0);
1282        let path = m
1283            .shortest_path(&make_id(1), &make_id(4))
1284            .expect("test: shortest_path should find multi-hop path");
1285        // Should go 1→3→4 (cost 2) not 1→2→4 (cost 101).
1286        assert_eq!(path, vec![make_id(1), make_id(3), make_id(4)]);
1287    }
1288
1289    #[test]
1290    fn test_shortest_path_no_path() {
1291        let mut m = make_mapper();
1292        add_n(&mut m, 1);
1293        add_n(&mut m, 2);
1294        assert!(m.shortest_path(&make_id(1), &make_id(2)).is_none());
1295    }
1296
1297    #[test]
1298    fn test_shortest_path_same_node() {
1299        let mut m = make_mapper();
1300        add_n(&mut m, 1);
1301        let path = m
1302            .shortest_path(&make_id(1), &make_id(1))
1303            .expect("test: shortest_path to self should return single node");
1304        assert_eq!(path, vec![make_id(1)]);
1305    }
1306
1307    #[test]
1308    fn test_shortest_path_missing_node() {
1309        let m = make_mapper();
1310        assert!(m.shortest_path(&make_id(1), &make_id(2)).is_none());
1311    }
1312
1313    // ── bfs_distance ──────────────────────────────────────────────────────────
1314
1315    #[test]
1316    fn test_bfs_distance_direct() {
1317        let mut m = make_mapper();
1318        add_n(&mut m, 1);
1319        add_n(&mut m, 2);
1320        add_e(&mut m, 1, 2, 1.0);
1321        assert_eq!(m.bfs_distance(&make_id(1), &make_id(2)), Some(1));
1322    }
1323
1324    #[test]
1325    fn test_bfs_distance_two_hops() {
1326        let mut m = make_mapper();
1327        add_n(&mut m, 1);
1328        add_n(&mut m, 2);
1329        add_n(&mut m, 3);
1330        add_e(&mut m, 1, 2, 1.0);
1331        add_e(&mut m, 2, 3, 1.0);
1332        assert_eq!(m.bfs_distance(&make_id(1), &make_id(3)), Some(2));
1333    }
1334
1335    #[test]
1336    fn test_bfs_distance_self() {
1337        let mut m = make_mapper();
1338        add_n(&mut m, 1);
1339        assert_eq!(m.bfs_distance(&make_id(1), &make_id(1)), Some(0));
1340    }
1341
1342    #[test]
1343    fn test_bfs_distance_unreachable() {
1344        let mut m = make_mapper();
1345        add_n(&mut m, 1);
1346        add_n(&mut m, 2);
1347        assert!(m.bfs_distance(&make_id(1), &make_id(2)).is_none());
1348    }
1349
1350    // ── diameter ──────────────────────────────────────────────────────────────
1351
1352    #[test]
1353    fn test_diameter_empty() {
1354        let m = make_mapper();
1355        assert_eq!(m.diameter(), 0);
1356    }
1357
1358    #[test]
1359    fn test_diameter_single_node() {
1360        let mut m = make_mapper();
1361        add_n(&mut m, 1);
1362        assert_eq!(m.diameter(), 0);
1363    }
1364
1365    #[test]
1366    fn test_diameter_two_connected() {
1367        let mut m = make_mapper();
1368        add_n(&mut m, 1);
1369        add_n(&mut m, 2);
1370        add_e(&mut m, 1, 2, 1.0);
1371        assert_eq!(m.diameter(), 1);
1372    }
1373
1374    #[test]
1375    fn test_diameter_chain() {
1376        // 1→2→3→4: diameter = 3 (1 to 4)
1377        let mut m = make_mapper();
1378        for v in 1..=4 {
1379            add_n(&mut m, v);
1380        }
1381        add_e(&mut m, 1, 2, 1.0);
1382        add_e(&mut m, 2, 3, 1.0);
1383        add_e(&mut m, 3, 4, 1.0);
1384        assert_eq!(m.diameter(), 3);
1385    }
1386
1387    // ── clustering_coefficient ────────────────────────────────────────────────
1388
1389    #[test]
1390    fn test_clustering_low_degree() {
1391        let mut m = make_mapper();
1392        add_n(&mut m, 1);
1393        assert!((m.clustering_coefficient(&make_id(1)) - 0.0).abs() < f64::EPSILON);
1394    }
1395
1396    #[test]
1397    fn test_clustering_triangle() {
1398        // Fully connected triangle: C = 1.0.
1399        let mut m = make_mapper();
1400        add_n(&mut m, 1);
1401        add_n(&mut m, 2);
1402        add_n(&mut m, 3);
1403        add_e(&mut m, 1, 2, 1.0);
1404        add_e(&mut m, 1, 3, 1.0);
1405        add_e(&mut m, 2, 3, 1.0); // 2→3
1406        add_e(&mut m, 3, 2, 1.0); // 3→2
1407                                  // Node 1 has neighbours 2 and 3; 2→3 and 3→2 exist → both pairs satisfied.
1408        let c = m.clustering_coefficient(&make_id(1));
1409        assert!(c > 0.0);
1410    }
1411
1412    #[test]
1413    fn test_clustering_no_edges_between_neighbours() {
1414        let mut m = make_mapper();
1415        add_n(&mut m, 1);
1416        add_n(&mut m, 2);
1417        add_n(&mut m, 3);
1418        add_e(&mut m, 1, 2, 1.0);
1419        add_e(&mut m, 1, 3, 1.0);
1420        // No edge between 2 and 3.
1421        let c = m.clustering_coefficient(&make_id(1));
1422        assert!((c - 0.0).abs() < f64::EPSILON);
1423    }
1424
1425    // ── take_snapshot ─────────────────────────────────────────────────────────
1426
1427    #[test]
1428    fn test_snapshot_basic() {
1429        let mut m = make_mapper();
1430        add_n(&mut m, 1);
1431        add_n(&mut m, 2);
1432        let snap = m.take_snapshot(1000);
1433        assert_eq!(snap.node_count, 2);
1434        assert_eq!(snap.ts, 1000);
1435    }
1436
1437    #[test]
1438    fn test_snapshot_stored() {
1439        let mut m = make_mapper();
1440        m.take_snapshot(1);
1441        assert_eq!(m.snapshots().len(), 1);
1442    }
1443
1444    #[test]
1445    fn test_snapshot_ring_bounded() {
1446        let mut m = make_mapper();
1447        for i in 0..25u64 {
1448            m.take_snapshot(i);
1449        }
1450        assert_eq!(m.snapshots().len(), 20);
1451    }
1452
1453    #[test]
1454    fn test_snapshot_oldest_dropped() {
1455        let mut m = make_mapper();
1456        for i in 0..21u64 {
1457            m.take_snapshot(i);
1458        }
1459        // The oldest snapshot (ts = 0) should have been dropped.
1460        assert_eq!(
1461            m.snapshots()
1462                .front()
1463                .expect("test: snapshots should have at least one entry after 21 takes")
1464                .ts,
1465            1
1466        );
1467    }
1468
1469    #[test]
1470    fn test_snapshot_edge_count() {
1471        let mut m = make_mapper();
1472        add_n(&mut m, 1);
1473        add_n(&mut m, 2);
1474        add_e(&mut m, 1, 2, 1.0);
1475        let snap = m.take_snapshot(0);
1476        assert_eq!(snap.edge_count, 1);
1477    }
1478
1479    // ── prune_stale ───────────────────────────────────────────────────────────
1480
1481    #[test]
1482    fn test_prune_stale_removes_old() {
1483        let config = NtmMapperConfig {
1484            prune_disconnected_after_secs: 100,
1485            ..Default::default()
1486        };
1487        let mut m = NetworkTopologyMapper::new(config);
1488        m.add_node(make_id(1), "a", None, 1.0, 0)
1489            .expect("test: add_node for stale node 1 should succeed"); // last_seen = 0
1490        m.add_node(make_id(2), "b", None, 1.0, 500)
1491            .expect("test: add_node for fresh node 2 should succeed"); // last_seen = 500
1492        m.prune_stale(600); // threshold = 500
1493        assert_eq!(m.node_count(), 1);
1494        assert!(m.get_node(&make_id(2)).is_some());
1495    }
1496
1497    #[test]
1498    fn test_prune_stale_keeps_recent() {
1499        let config = NtmMapperConfig {
1500            prune_disconnected_after_secs: 300,
1501            ..Default::default()
1502        };
1503        let mut m = NetworkTopologyMapper::new(config);
1504        m.add_node(make_id(1), "a", None, 1.0, 1000)
1505            .expect("test: add_node for recent node should succeed");
1506        m.prune_stale(1200); // threshold = 900; 1000 > 900 → kept
1507        assert_eq!(m.node_count(), 1);
1508    }
1509
1510    #[test]
1511    fn test_prune_stale_cascades_edges() {
1512        let config = NtmMapperConfig {
1513            prune_disconnected_after_secs: 100,
1514            ..Default::default()
1515        };
1516        let mut m = NetworkTopologyMapper::new(config);
1517        m.add_node(make_id(1), "a", None, 1.0, 0)
1518            .expect("test: add_node for cascade edge test node 1 should succeed");
1519        m.add_node(make_id(2), "b", None, 1.0, 0)
1520            .expect("test: add_node for cascade edge test node 2 should succeed");
1521        add_e(&mut m, 1, 2, 1.0);
1522        m.prune_stale(1000);
1523        assert_eq!(m.edge_count(), 0);
1524    }
1525
1526    // ── topology_stats ────────────────────────────────────────────────────────
1527
1528    #[test]
1529    fn test_topology_stats_empty() {
1530        let m = make_mapper();
1531        let s = m.topology_stats();
1532        assert!((s.density - 0.0).abs() < f64::EPSILON);
1533    }
1534
1535    #[test]
1536    fn test_topology_stats_density() {
1537        // 2 nodes, 1 directed edge → density = 1/2.
1538        let mut m = make_mapper();
1539        add_n(&mut m, 1);
1540        add_n(&mut m, 2);
1541        add_e(&mut m, 1, 2, 1.0);
1542        let s = m.topology_stats();
1543        assert!((s.density - 0.5).abs() < 1e-9);
1544    }
1545
1546    #[test]
1547    fn test_topology_stats_centrality_present() {
1548        let mut m = make_mapper();
1549        add_n(&mut m, 1);
1550        add_n(&mut m, 2);
1551        add_n(&mut m, 3);
1552        add_e(&mut m, 1, 2, 1.0);
1553        add_e(&mut m, 2, 3, 1.0);
1554        let s = m.topology_stats();
1555        assert!(s.centrality.contains_key(&make_id(1)));
1556        assert!(s.centrality.contains_key(&make_id(2)));
1557        assert!(s.centrality.contains_key(&make_id(3)));
1558    }
1559
1560    // ── betweenness centrality ────────────────────────────────────────────────
1561
1562    #[test]
1563    fn test_betweenness_single_node() {
1564        let mut m = make_mapper();
1565        add_n(&mut m, 1);
1566        let b = m.compute_betweenness_centrality();
1567        assert!((b[&make_id(1)] - 0.0).abs() < f64::EPSILON);
1568    }
1569
1570    #[test]
1571    fn test_betweenness_chain() {
1572        // In a directed chain 1→2→3, node 2 is the only intermediary.
1573        let mut m = make_mapper();
1574        add_n(&mut m, 1);
1575        add_n(&mut m, 2);
1576        add_n(&mut m, 3);
1577        add_e(&mut m, 1, 2, 1.0);
1578        add_e(&mut m, 2, 3, 1.0);
1579        let b = m.compute_betweenness_centrality();
1580        // Node 2 has higher betweenness than node 1 or 3.
1581        assert!(b[&make_id(2)] >= b[&make_id(1)]);
1582        assert!(b[&make_id(2)] >= b[&make_id(3)]);
1583    }
1584
1585    // ── PRNG / hash helpers ───────────────────────────────────────────────────
1586
1587    #[test]
1588    fn test_xorshift64_nonzero() {
1589        let mut state = 12345u64;
1590        let v = xorshift64(&mut state);
1591        assert_ne!(v, 0);
1592    }
1593
1594    #[test]
1595    fn test_xorshift64_changes_state() {
1596        let mut state = 1u64;
1597        let first = xorshift64(&mut state);
1598        let second = xorshift64(&mut state);
1599        assert_ne!(first, second);
1600    }
1601
1602    #[test]
1603    fn test_fnv1a_64_empty() {
1604        let h = fnv1a_64(b"");
1605        assert_eq!(h, 14_695_981_039_346_656_037u64);
1606    }
1607
1608    #[test]
1609    fn test_fnv1a_64_known() {
1610        // "hello" FNV-1a 64-bit
1611        let h = fnv1a_64(b"hello");
1612        assert_ne!(h, 0);
1613        assert_ne!(h, 14_695_981_039_346_656_037u64);
1614    }
1615
1616    #[test]
1617    fn test_fnv1a_64_different_inputs() {
1618        let h1 = fnv1a_64(b"foo");
1619        let h2 = fnv1a_64(b"bar");
1620        assert_ne!(h1, h2);
1621    }
1622
1623    // ── edge id uniqueness ────────────────────────────────────────────────────
1624
1625    #[test]
1626    fn test_edge_ids_unique() {
1627        let mut m = make_mapper();
1628        for v in 1..=5 {
1629            add_n(&mut m, v);
1630        }
1631        let e12 = add_e(&mut m, 1, 2, 1.0);
1632        let e13 = add_e(&mut m, 1, 3, 1.0);
1633        let e14 = add_e(&mut m, 1, 4, 1.0);
1634        assert_ne!(e12, e13);
1635        assert_ne!(e12, e14);
1636        assert_ne!(e13, e14);
1637    }
1638
1639    // ── degree counting ───────────────────────────────────────────────────────
1640
1641    #[test]
1642    fn test_degree_after_add_edge() {
1643        let mut m = make_mapper();
1644        add_n(&mut m, 1);
1645        add_n(&mut m, 2);
1646        add_e(&mut m, 1, 2, 1.0);
1647        // src gets out-degree 1, dst gets in-degree 1.
1648        assert_eq!(
1649            m.get_node(&make_id(1))
1650                .expect("test: get_node for node 1 degree after add_edge")
1651                .degree,
1652            1
1653        );
1654        assert_eq!(
1655            m.get_node(&make_id(2))
1656                .expect("test: get_node for node 2 degree after add_edge")
1657                .degree,
1658            1
1659        );
1660    }
1661
1662    #[test]
1663    fn test_degree_bidirectional() {
1664        let mut m = make_mapper();
1665        add_n(&mut m, 1);
1666        add_n(&mut m, 2);
1667        add_e(&mut m, 1, 2, 1.0);
1668        add_e(&mut m, 2, 1, 1.0);
1669        assert_eq!(
1670            m.get_node(&make_id(1))
1671                .expect("test: get_node for node 1 degree after bidirectional edges")
1672                .degree,
1673            2
1674        );
1675        assert_eq!(
1676            m.get_node(&make_id(2))
1677                .expect("test: get_node for node 2 degree after bidirectional edges")
1678                .degree,
1679            2
1680        );
1681    }
1682
1683    // ── nodes() / edges() iterators ───────────────────────────────────────────
1684
1685    #[test]
1686    fn test_nodes_iterator() {
1687        let mut m = make_mapper();
1688        add_n(&mut m, 1);
1689        add_n(&mut m, 2);
1690        let ids: Vec<NtmNodeId> = m.nodes().map(|n| n.id).collect();
1691        assert_eq!(ids.len(), 2);
1692    }
1693
1694    #[test]
1695    fn test_edges_iterator() {
1696        let mut m = make_mapper();
1697        add_n(&mut m, 1);
1698        add_n(&mut m, 2);
1699        add_e(&mut m, 1, 2, 1.0);
1700        let eids: Vec<NtmEdgeId> = m.edges().map(|e| e.id).collect();
1701        assert_eq!(eids.len(), 1);
1702    }
1703
1704    // ── NtmMapperError display ────────────────────────────────────────────────
1705
1706    #[test]
1707    fn test_error_display_not_found() {
1708        let e = NtmMapperError::NotFound("x".into());
1709        assert!(e.to_string().contains("not found"));
1710    }
1711
1712    #[test]
1713    fn test_error_display_capacity() {
1714        let e = NtmMapperError::CapacityExceeded("full".into());
1715        assert!(e.to_string().contains("capacity exceeded"));
1716    }
1717
1718    #[test]
1719    fn test_error_display_internal() {
1720        let e = NtmMapperError::Internal("oops".into());
1721        assert!(e.to_string().contains("internal error"));
1722    }
1723
1724    // ── avg_path_length ───────────────────────────────────────────────────────
1725
1726    #[test]
1727    fn test_avg_path_length_empty() {
1728        let m = make_mapper();
1729        let s = m.topology_stats();
1730        assert!((s.avg_path_length - 0.0).abs() < f64::EPSILON);
1731    }
1732
1733    #[test]
1734    fn test_avg_path_length_two_nodes_connected() {
1735        let mut m = make_mapper();
1736        add_n(&mut m, 1);
1737        add_n(&mut m, 2);
1738        add_e(&mut m, 1, 2, 1.0);
1739        let s = m.topology_stats();
1740        // Only path: 1→2 = 1 hop, 2→1 unreachable.
1741        assert!(s.avg_path_length >= 0.0);
1742    }
1743
1744    // ── miscellaneous edge cases ───────────────────────────────────────────────
1745
1746    #[test]
1747    fn test_remove_nonexistent_edge_error() {
1748        let mut m = make_mapper();
1749        assert!(m.remove_edge(42).is_err());
1750    }
1751
1752    #[test]
1753    fn test_snapshot_avg_degree() {
1754        let mut m = make_mapper();
1755        add_n(&mut m, 1);
1756        add_n(&mut m, 2);
1757        add_e(&mut m, 1, 2, 1.0);
1758        // Both nodes have degree 1; avg = 1.0.
1759        let snap = m.take_snapshot(0);
1760        assert!((snap.avg_degree - 1.0).abs() < f64::EPSILON);
1761    }
1762
1763    #[test]
1764    fn test_many_nodes_and_edges() {
1765        let mut m = make_mapper();
1766        for v in 0..10u8 {
1767            add_n(&mut m, v);
1768        }
1769        for u in 0..9u8 {
1770            add_e(&mut m, u, u + 1, (u + 1) as f64);
1771        }
1772        assert_eq!(m.node_count(), 10);
1773        assert_eq!(m.edge_count(), 9);
1774        let path = m.shortest_path(&make_id(0), &make_id(9));
1775        assert!(path.is_some());
1776    }
1777
1778    #[test]
1779    fn test_type_alias_ntm_network_topology_mapper() {
1780        let _m: NtmNetworkTopologyMapper = NtmNetworkTopologyMapper::with_defaults();
1781    }
1782}