Skip to main content

ipfrs_network/
adaptive_routing_engine.rs

1//! Adaptive Routing Engine
2//!
3//! Dynamically adjusts routing decisions based on observed network conditions.
4//! Supports multiple routing policies, multi-path routing, EWMA-based metrics,
5//! and periodic route probing/adaptation cycles.
6
7use std::collections::HashMap;
8use std::time::{SystemTime, UNIX_EPOCH};
9
10// ─── PRNG / Hashing helpers ──────────────────────────────────────────────────
11
12#[inline]
13fn xorshift64(state: &mut u64) -> u64 {
14    let mut x = *state;
15    x ^= x << 13;
16    x ^= x >> 7;
17    x ^= x << 17;
18    *state = x;
19    x
20}
21
22#[inline]
23fn fnv1a_64(data: &[u8]) -> u64 {
24    let mut h: u64 = 14_695_981_039_346_656_037;
25    for &b in data {
26        h ^= b as u64;
27        h = h.wrapping_mul(1_099_511_628_211);
28    }
29    h
30}
31
32// ─── Public type aliases (Are prefix) ────────────────────────────────────────
33
34/// Type alias – route key (src + dst pair).
35pub type AreRouteKey = RouteKey;
36/// Type alias – a single route entry.
37pub type AreRouteEntry = RouteEntry;
38/// Type alias – routing policy selector.
39pub type AreRoutingPolicy = RoutingPolicy;
40/// Type alias – engine configuration.
41pub type AreRoutingConfig = AdaptiveRoutingConfig;
42/// Type alias – engine statistics snapshot.
43pub type AreRoutingStats = AdaptiveRoutingStats;
44
45// ─── RouteKey ────────────────────────────────────────────────────────────────
46
47/// Identifies a source → destination pair for multi-path routing.
48#[derive(Debug, Clone, PartialEq, Eq, Hash)]
49pub struct RouteKey {
50    /// Source peer identifier (32 bytes).
51    pub src: [u8; 32],
52    /// Destination peer identifier (32 bytes).
53    pub dst: [u8; 32],
54}
55
56impl RouteKey {
57    /// Create a new `RouteKey` from raw byte arrays.
58    pub fn new(src: [u8; 32], dst: [u8; 32]) -> Self {
59        Self { src, dst }
60    }
61
62    /// Compute a deterministic u64 hash of the key using FNV-1a.
63    pub fn hash_u64(&self) -> u64 {
64        let mut buf = [0u8; 64];
65        buf[..32].copy_from_slice(&self.src);
66        buf[32..].copy_from_slice(&self.dst);
67        fnv1a_64(&buf)
68    }
69}
70
71// ─── RouteEntry ──────────────────────────────────────────────────────────────
72
73/// A single next-hop route with associated metrics.
74#[derive(Debug, Clone)]
75pub struct RouteEntry {
76    /// Next-hop peer identifier (32 bytes).
77    pub next_hop: [u8; 32],
78    /// Current routing weight (higher is better).
79    pub weight: f64,
80    /// Exponentially weighted moving-average RTT in milliseconds.
81    pub rtt_ms: f64,
82    /// Estimated packet loss rate in the range [0.0, 1.0].
83    pub loss_rate: f64,
84    /// Unix timestamp (seconds) when this entry was last updated.
85    pub last_updated: u64,
86    /// Estimated available bandwidth in kbps.
87    pub bandwidth_kbps: f64,
88}
89
90impl RouteEntry {
91    /// Construct a new `RouteEntry` with default metric estimates.
92    pub fn new(next_hop: [u8; 32]) -> Self {
93        Self {
94            next_hop,
95            weight: 1.0,
96            rtt_ms: 100.0,
97            loss_rate: 0.0,
98            last_updated: now_secs(),
99            bandwidth_kbps: 1000.0,
100        }
101    }
102
103    /// Returns `true` if the entry is considered healthy (loss < 50 %, RTT < 5 s).
104    pub fn is_healthy(&self) -> bool {
105        self.loss_rate < 0.5 && self.rtt_ms < 5_000.0
106    }
107}
108
109// ─── RoutingPolicy ───────────────────────────────────────────────────────────
110
111/// Policy governing how the best next-hop is selected from candidate routes.
112#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
113pub enum RoutingPolicy {
114    /// Minimize hop count (use pre-computed weight).
115    #[default]
116    ShortestPath,
117    /// Prefer routes with the lowest RTT.
118    LowestLatency,
119    /// Prefer routes with the highest estimated bandwidth.
120    HighestBandwidth,
121    /// Distribute traffic across routes proportional to their weights.
122    LoadBalanced,
123    /// Consider both latency and loss rate (QoS composite score).
124    QoSAware,
125}
126
127impl RoutingPolicy {
128    /// Return a human-readable label for the policy.
129    pub fn label(&self) -> &'static str {
130        match self {
131            RoutingPolicy::ShortestPath => "shortest_path",
132            RoutingPolicy::LowestLatency => "lowest_latency",
133            RoutingPolicy::HighestBandwidth => "highest_bandwidth",
134            RoutingPolicy::LoadBalanced => "load_balanced",
135            RoutingPolicy::QoSAware => "qos_aware",
136        }
137    }
138}
139
140// ─── AdaptiveRoutingConfig ───────────────────────────────────────────────────
141
142/// Configuration for the `AdaptiveRoutingEngine`.
143#[derive(Debug, Clone)]
144pub struct AdaptiveRoutingConfig {
145    /// Default routing policy applied when none is specified per-call.
146    pub policy: RoutingPolicy,
147    /// EWMA smoothing factor α ∈ (0, 1].  Higher → more weight on recent samples.
148    pub alpha: f64,
149    /// Maximum number of route entries retained per `RouteKey`.
150    pub max_routes_per_key: usize,
151    /// Interval between synthetic probe rounds (seconds).
152    pub probe_interval_secs: u64,
153}
154
155impl Default for AdaptiveRoutingConfig {
156    fn default() -> Self {
157        Self {
158            policy: RoutingPolicy::LowestLatency,
159            alpha: 0.125,
160            max_routes_per_key: 8,
161            probe_interval_secs: 30,
162        }
163    }
164}
165
166// ─── AdaptiveRoutingStats ────────────────────────────────────────────────────
167
168/// Snapshot of key statistics for the routing engine.
169#[derive(Debug, Clone, Default)]
170pub struct AdaptiveRoutingStats {
171    /// Total number of unique route entries across all keys.
172    pub total_routes: usize,
173    /// Arithmetic mean RTT across all route entries (ms).
174    pub avg_rtt_ms: f64,
175    /// Arithmetic mean packet loss rate across all route entries.
176    pub avg_loss_rate: f64,
177    /// Number of times the active routing policy has been switched.
178    pub policy_switches: u64,
179    /// Number of route entries pruned in the last adaptation cycle.
180    pub pruned_last_cycle: usize,
181    /// Total probe rounds executed since engine creation.
182    pub total_probe_rounds: u64,
183}
184
185// ─── AdaptiveRoutingEngine ───────────────────────────────────────────────────
186
187/// An adaptive, multi-path routing engine for IPFRS network nodes.
188///
189/// Maintains per-destination route tables with real-time metrics updated via
190/// EWMA and selects the best next-hop according to a configurable
191/// [`RoutingPolicy`].
192pub struct AdaptiveRoutingEngine {
193    /// Route table: one or more entries per (src, dst) key.
194    route_table: HashMap<RouteKey, Vec<RouteEntry>>,
195    /// Engine configuration.
196    config: AdaptiveRoutingConfig,
197    /// Currently active policy (may differ from config default after a switch).
198    active_policy: RoutingPolicy,
199    /// Accumulated statistics.
200    stats: AdaptiveRoutingStats,
201    /// Pseudo-random state for synthetic probes.
202    prng_state: u64,
203}
204
205impl AdaptiveRoutingEngine {
206    // ── Construction ─────────────────────────────────────────────────────────
207
208    /// Create a new engine with the supplied configuration.
209    pub fn new(config: AdaptiveRoutingConfig) -> Self {
210        let active_policy = config.policy;
211        // Seed with FNV-1a of the config policy label.
212        let seed = fnv1a_64(active_policy.label().as_bytes());
213        Self {
214            route_table: HashMap::new(),
215            config,
216            active_policy,
217            stats: AdaptiveRoutingStats::default(),
218            prng_state: seed | 1, // ensure non-zero
219        }
220    }
221
222    /// Create an engine with default configuration.
223    pub fn with_defaults() -> Self {
224        Self::new(AdaptiveRoutingConfig::default())
225    }
226
227    // ── Route management ─────────────────────────────────────────────────────
228
229    /// Add or overwrite a route entry for the given key.
230    ///
231    /// If `max_routes_per_key` is reached the entry with the worst score is
232    /// replaced.
233    pub fn add_route(&mut self, key: RouteKey, entry: RouteEntry) {
234        let entries = self.route_table.entry(key).or_default();
235        // If next_hop already exists update in-place.
236        if let Some(existing) = entries.iter_mut().find(|e| e.next_hop == entry.next_hop) {
237            *existing = entry;
238            return;
239        }
240        if entries.len() >= self.config.max_routes_per_key {
241            // Evict the worst-scoring entry.
242            if let Some(idx) = worst_entry_index(entries) {
243                entries.remove(idx);
244            }
245        }
246        entries.push(entry);
247    }
248
249    /// Remove all routes for the given key that use `next_hop`.
250    pub fn remove_route(&mut self, key: &RouteKey, next_hop: &[u8; 32]) {
251        if let Some(entries) = self.route_table.get_mut(key) {
252            entries.retain(|e| &e.next_hop != next_hop);
253        }
254    }
255
256    /// Remove every route entry associated with `key`.
257    pub fn clear_routes(&mut self, key: &RouteKey) {
258        self.route_table.remove(key);
259    }
260
261    /// Return an immutable slice of route entries for a key, if any.
262    pub fn routes_for(&self, key: &RouteKey) -> Option<&[RouteEntry]> {
263        self.route_table.get(key).map(|v| v.as_slice())
264    }
265
266    // ── Metric updates ───────────────────────────────────────────────────────
267
268    /// Apply an EWMA update to RTT and loss metrics for a specific next-hop.
269    ///
270    /// # Errors
271    ///
272    /// Returns `Err` if the key or next-hop is not found in the route table.
273    pub fn update_metrics(
274        &mut self,
275        key: &RouteKey,
276        next_hop: &[u8; 32],
277        rtt_ms: f64,
278        loss: f64,
279    ) -> Result<(), RoutingEngineError> {
280        let entries = self
281            .route_table
282            .get_mut(key)
283            .ok_or(RoutingEngineError::KeyNotFound)?;
284        let entry = entries
285            .iter_mut()
286            .find(|e| &e.next_hop == next_hop)
287            .ok_or(RoutingEngineError::NextHopNotFound)?;
288
289        let alpha = self.config.alpha;
290        entry.rtt_ms = alpha * rtt_ms + (1.0 - alpha) * entry.rtt_ms;
291        entry.loss_rate = (alpha * loss + (1.0 - alpha) * entry.loss_rate).clamp(0.0, 1.0);
292        entry.last_updated = now_secs();
293        recompute_weight(entry);
294        Ok(())
295    }
296
297    /// Update only the bandwidth estimate for a specific next-hop.
298    pub fn update_bandwidth(
299        &mut self,
300        key: &RouteKey,
301        next_hop: &[u8; 32],
302        bandwidth_kbps: f64,
303    ) -> Result<(), RoutingEngineError> {
304        let entries = self
305            .route_table
306            .get_mut(key)
307            .ok_or(RoutingEngineError::KeyNotFound)?;
308        let entry = entries
309            .iter_mut()
310            .find(|e| &e.next_hop == next_hop)
311            .ok_or(RoutingEngineError::NextHopNotFound)?;
312        let alpha = self.config.alpha;
313        entry.bandwidth_kbps = alpha * bandwidth_kbps + (1.0 - alpha) * entry.bandwidth_kbps;
314        entry.last_updated = now_secs();
315        recompute_weight(entry);
316        Ok(())
317    }
318
319    // ── Next-hop selection ───────────────────────────────────────────────────
320
321    /// Select the best next-hop peer for `key` according to `policy`.
322    ///
323    /// Returns `None` if no routes are recorded for the key or all entries are
324    /// unhealthy.
325    pub fn select_next_hop(&self, key: &RouteKey, policy: RoutingPolicy) -> Option<[u8; 32]> {
326        let entries = self.route_table.get(key)?;
327        let healthy: Vec<&RouteEntry> = entries.iter().filter(|e| e.is_healthy()).collect();
328        if healthy.is_empty() {
329            // Fall back to any entry if no healthy ones remain.
330            return entries.first().map(|e| e.next_hop);
331        }
332        match policy {
333            RoutingPolicy::ShortestPath => healthy
334                .iter()
335                .max_by(|a, b| {
336                    a.weight
337                        .partial_cmp(&b.weight)
338                        .unwrap_or(std::cmp::Ordering::Equal)
339                })
340                .map(|e| e.next_hop),
341            RoutingPolicy::LowestLatency => healthy
342                .iter()
343                .min_by(|a, b| {
344                    a.rtt_ms
345                        .partial_cmp(&b.rtt_ms)
346                        .unwrap_or(std::cmp::Ordering::Equal)
347                })
348                .map(|e| e.next_hop),
349            RoutingPolicy::HighestBandwidth => healthy
350                .iter()
351                .max_by(|a, b| {
352                    a.bandwidth_kbps
353                        .partial_cmp(&b.bandwidth_kbps)
354                        .unwrap_or(std::cmp::Ordering::Equal)
355                })
356                .map(|e| e.next_hop),
357            RoutingPolicy::LoadBalanced => self.select_load_balanced(&healthy),
358            RoutingPolicy::QoSAware => healthy
359                .iter()
360                .max_by(|a, b| {
361                    qos_score(a)
362                        .partial_cmp(&qos_score(b))
363                        .unwrap_or(std::cmp::Ordering::Equal)
364                })
365                .map(|e| e.next_hop),
366        }
367    }
368
369    /// Select using the engine's current active policy.
370    pub fn select_next_hop_default(&self, key: &RouteKey) -> Option<[u8; 32]> {
371        self.select_next_hop(key, self.active_policy)
372    }
373
374    /// Weighted-random selection proportional to route weights (load-balanced).
375    fn select_load_balanced(&self, entries: &[&RouteEntry]) -> Option<[u8; 32]> {
376        if entries.is_empty() {
377            return None;
378        }
379        let total_weight: f64 = entries.iter().map(|e| e.weight.max(0.0)).sum();
380        if total_weight <= 0.0 {
381            return entries.first().map(|e| e.next_hop);
382        }
383        // Deterministic seed derived from current time and route key hash.
384        let mut state = fnv1a_64(&now_secs().to_le_bytes()) | 1;
385        let r = (xorshift64(&mut state) as f64 / u64::MAX as f64) * total_weight;
386        let mut acc = 0.0;
387        for entry in entries {
388            acc += entry.weight.max(0.0);
389            if acc >= r {
390                return Some(entry.next_hop);
391            }
392        }
393        entries.last().map(|e| e.next_hop)
394    }
395
396    // ── Policy management ────────────────────────────────────────────────────
397
398    /// Switch the active routing policy, incrementing the policy-switch counter.
399    pub fn set_policy(&mut self, policy: RoutingPolicy) {
400        if policy != self.active_policy {
401            self.stats.policy_switches += 1;
402            self.active_policy = policy;
403        }
404    }
405
406    /// Return the currently active routing policy.
407    pub fn active_policy(&self) -> RoutingPolicy {
408        self.active_policy
409    }
410
411    // ── Probing ──────────────────────────────────────────────────────────────
412
413    /// Simulate a probe round: apply synthetic RTT perturbations to all routes
414    /// using xorshift64-based PRNG.
415    ///
416    /// In production this would send real probe packets; here we perturb
417    /// existing RTT estimates to model jitter and update weights accordingly.
418    pub fn probe_routes(&mut self) {
419        let alpha = self.config.alpha;
420        for entries in self.route_table.values_mut() {
421            for entry in entries.iter_mut() {
422                // Generate synthetic delta RTT: ±10 % of current RTT.
423                let r = xorshift64(&mut self.prng_state);
424                let jitter_fraction = (r as f64 / u64::MAX as f64) * 0.20 - 0.10; // [-0.10, +0.10]
425                let synthetic_rtt = (entry.rtt_ms * (1.0 + jitter_fraction)).max(0.5);
426                // Apply EWMA with a smaller alpha for probes to avoid over-reacting.
427                let probe_alpha = alpha * 0.5;
428                entry.rtt_ms = probe_alpha * synthetic_rtt + (1.0 - probe_alpha) * entry.rtt_ms;
429                entry.last_updated = now_secs();
430                recompute_weight(entry);
431            }
432        }
433        self.stats.total_probe_rounds += 1;
434    }
435
436    // ── Adaptation cycle ─────────────────────────────────────────────────────
437
438    /// Prune stale route entries and recompute all weights.
439    ///
440    /// An entry is considered stale if it hasn't been updated for more than
441    /// `probe_interval_secs × 3` seconds.
442    pub fn run_adaptation_cycle(&mut self) {
443        let stale_threshold = self.config.probe_interval_secs.saturating_mul(3);
444        let now = now_secs();
445        let mut pruned = 0usize;
446
447        for entries in self.route_table.values_mut() {
448            let before = entries.len();
449            entries.retain(|e| {
450                let age = now.saturating_sub(e.last_updated);
451                age < stale_threshold
452            });
453            pruned += before - entries.len();
454            for entry in entries.iter_mut() {
455                recompute_weight(entry);
456            }
457        }
458        // Remove keys with no remaining routes.
459        self.route_table.retain(|_, v| !v.is_empty());
460
461        self.stats.pruned_last_cycle = pruned;
462    }
463
464    // ── Statistics ───────────────────────────────────────────────────────────
465
466    /// Return a snapshot of current engine statistics.
467    pub fn routing_stats(&self) -> AdaptiveRoutingStats {
468        let all_entries: Vec<&RouteEntry> =
469            self.route_table.values().flat_map(|v| v.iter()).collect();
470        let total_routes = all_entries.len();
471        let (avg_rtt_ms, avg_loss_rate) = if total_routes == 0 {
472            (0.0, 0.0)
473        } else {
474            let sum_rtt: f64 = all_entries.iter().map(|e| e.rtt_ms).sum();
475            let sum_loss: f64 = all_entries.iter().map(|e| e.loss_rate).sum();
476            (
477                sum_rtt / total_routes as f64,
478                sum_loss / total_routes as f64,
479            )
480        };
481
482        AdaptiveRoutingStats {
483            total_routes,
484            avg_rtt_ms,
485            avg_loss_rate,
486            policy_switches: self.stats.policy_switches,
487            pruned_last_cycle: self.stats.pruned_last_cycle,
488            total_probe_rounds: self.stats.total_probe_rounds,
489        }
490    }
491
492    /// Return the number of unique `RouteKey`s tracked.
493    pub fn key_count(&self) -> usize {
494        self.route_table.len()
495    }
496
497    /// Return the total number of route entries across all keys.
498    pub fn entry_count(&self) -> usize {
499        self.route_table.values().map(|v| v.len()).sum()
500    }
501
502    /// Check whether a route exists for the given key and next-hop.
503    pub fn has_route(&self, key: &RouteKey, next_hop: &[u8; 32]) -> bool {
504        self.route_table
505            .get(key)
506            .map(|entries| entries.iter().any(|e| &e.next_hop == next_hop))
507            .unwrap_or(false)
508    }
509
510    /// Return all distinct next-hops recorded for `key`.
511    pub fn next_hops_for(&self, key: &RouteKey) -> Vec<[u8; 32]> {
512        self.route_table
513            .get(key)
514            .map(|entries| entries.iter().map(|e| e.next_hop).collect())
515            .unwrap_or_default()
516    }
517
518    /// Return the best route entry for `key` under the given policy (clone).
519    pub fn best_entry(&self, key: &RouteKey, policy: RoutingPolicy) -> Option<RouteEntry> {
520        let next_hop = self.select_next_hop(key, policy)?;
521        self.route_table
522            .get(key)?
523            .iter()
524            .find(|e| e.next_hop == next_hop)
525            .cloned()
526    }
527
528    /// Return a reference to the underlying route table.
529    pub fn route_table(&self) -> &HashMap<RouteKey, Vec<RouteEntry>> {
530        &self.route_table
531    }
532
533    /// Merge another engine's route table into this one.
534    pub fn merge_from(&mut self, other: &AdaptiveRoutingEngine) {
535        for (key, entries) in &other.route_table {
536            for entry in entries {
537                self.add_route(key.clone(), entry.clone());
538            }
539        }
540    }
541
542    /// Resize `max_routes_per_key` and evict excess entries if needed.
543    pub fn resize_max_routes(&mut self, new_max: usize) {
544        self.config.max_routes_per_key = new_max;
545        for entries in self.route_table.values_mut() {
546            while entries.len() > new_max {
547                if let Some(idx) = worst_entry_index(entries) {
548                    entries.remove(idx);
549                } else {
550                    break;
551                }
552            }
553        }
554    }
555
556    /// Update the EWMA alpha factor.
557    ///
558    /// # Errors
559    ///
560    /// Returns `Err` if `alpha` is outside (0.0, 1.0].
561    pub fn set_alpha(&mut self, alpha: f64) -> Result<(), RoutingEngineError> {
562        if alpha <= 0.0 || alpha > 1.0 {
563            return Err(RoutingEngineError::InvalidAlpha(alpha));
564        }
565        self.config.alpha = alpha;
566        Ok(())
567    }
568
569    /// Update the probe interval.
570    pub fn set_probe_interval(&mut self, secs: u64) {
571        self.config.probe_interval_secs = secs;
572    }
573
574    /// Drain all routes and reset statistics.
575    pub fn reset(&mut self) {
576        self.route_table.clear();
577        self.stats = AdaptiveRoutingStats::default();
578    }
579}
580
581// ─── RoutingEngineError ───────────────────────────────────────────────────────
582
583/// Errors returned by `AdaptiveRoutingEngine` operations.
584#[derive(Debug, Clone, PartialEq)]
585pub enum RoutingEngineError {
586    /// No entry found for the requested `RouteKey`.
587    KeyNotFound,
588    /// The specified `next_hop` is not recorded under the given key.
589    NextHopNotFound,
590    /// The supplied EWMA alpha factor is invalid.
591    InvalidAlpha(f64),
592}
593
594impl std::fmt::Display for RoutingEngineError {
595    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
596        match self {
597            RoutingEngineError::KeyNotFound => write!(f, "route key not found"),
598            RoutingEngineError::NextHopNotFound => write!(f, "next-hop not found for key"),
599            RoutingEngineError::InvalidAlpha(a) => {
600                write!(f, "invalid EWMA alpha {a}: must be in (0, 1]")
601            }
602        }
603    }
604}
605
606impl std::error::Error for RoutingEngineError {}
607
608// ─── Private helpers ─────────────────────────────────────────────────────────
609
610/// Current Unix timestamp in whole seconds (saturating at u64::MAX).
611fn now_secs() -> u64 {
612    SystemTime::now()
613        .duration_since(UNIX_EPOCH)
614        .map(|d| d.as_secs())
615        .unwrap_or(0)
616}
617
618/// Recompute the composite weight for a route entry.
619///
620/// weight = bandwidth_kbps / (rtt_ms.max(1) * (1 + loss_rate)²)
621fn recompute_weight(entry: &mut RouteEntry) {
622    let rtt = entry.rtt_ms.max(1.0);
623    let loss_factor = (1.0 + entry.loss_rate).powi(2);
624    entry.weight = entry.bandwidth_kbps / (rtt * loss_factor);
625}
626
627/// QoS composite score: higher is better.
628///
629/// qos = (1 − loss_rate) × bandwidth_kbps / rtt_ms.max(1)
630fn qos_score(entry: &RouteEntry) -> f64 {
631    let availability = (1.0 - entry.loss_rate).max(0.0);
632    availability * entry.bandwidth_kbps / entry.rtt_ms.max(1.0)
633}
634
635/// Return the index of the worst-scoring entry in a non-empty slice.
636fn worst_entry_index(entries: &[RouteEntry]) -> Option<usize> {
637    entries
638        .iter()
639        .enumerate()
640        .min_by(|(_, a), (_, b)| {
641            a.weight
642                .partial_cmp(&b.weight)
643                .unwrap_or(std::cmp::Ordering::Equal)
644        })
645        .map(|(i, _)| i)
646}
647
648// ─── Tests ───────────────────────────────────────────────────────────────────
649
650#[cfg(test)]
651mod tests {
652    use super::*;
653
654    // ── Helpers ──────────────────────────────────────────────────────────────
655
656    fn make_key(src_byte: u8, dst_byte: u8) -> RouteKey {
657        let mut src = [0u8; 32];
658        let mut dst = [0u8; 32];
659        src[0] = src_byte;
660        dst[0] = dst_byte;
661        RouteKey::new(src, dst)
662    }
663
664    fn make_hop(byte: u8) -> [u8; 32] {
665        let mut h = [0u8; 32];
666        h[0] = byte;
667        h
668    }
669
670    fn entry_with_metrics(hop_byte: u8, rtt: f64, loss: f64, bw: f64) -> RouteEntry {
671        let mut e = RouteEntry::new(make_hop(hop_byte));
672        e.rtt_ms = rtt;
673        e.loss_rate = loss;
674        e.bandwidth_kbps = bw;
675        recompute_weight(&mut e);
676        e
677    }
678
679    fn engine_default() -> AdaptiveRoutingEngine {
680        AdaptiveRoutingEngine::with_defaults()
681    }
682
683    // ── xorshift64 ───────────────────────────────────────────────────────────
684
685    #[test]
686    fn test_xorshift64_non_zero_output() {
687        let mut state = 12345u64;
688        let v = xorshift64(&mut state);
689        assert_ne!(v, 0);
690    }
691
692    #[test]
693    fn test_xorshift64_state_changes() {
694        let mut state = 99u64;
695        let before = state;
696        xorshift64(&mut state);
697        assert_ne!(state, before);
698    }
699
700    #[test]
701    fn test_xorshift64_sequence_unique() {
702        let mut state = 1u64;
703        let a = xorshift64(&mut state);
704        let b = xorshift64(&mut state);
705        let c = xorshift64(&mut state);
706        assert_ne!(a, b);
707        assert_ne!(b, c);
708    }
709
710    #[test]
711    fn test_xorshift64_deterministic() {
712        let mut s1 = 42u64;
713        let mut s2 = 42u64;
714        assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
715    }
716
717    // ── fnv1a_64 ─────────────────────────────────────────────────────────────
718
719    #[test]
720    fn test_fnv1a_64_empty() {
721        let h = fnv1a_64(&[]);
722        assert_eq!(h, 14_695_981_039_346_656_037u64);
723    }
724
725    #[test]
726    fn test_fnv1a_64_known_value() {
727        // "foobar" FNV-1a 64-bit = 0x85944171f73967e8
728        let h = fnv1a_64(b"foobar");
729        assert_eq!(h, 0x85944171f73967e8);
730    }
731
732    #[test]
733    fn test_fnv1a_64_different_inputs() {
734        assert_ne!(fnv1a_64(b"abc"), fnv1a_64(b"xyz"));
735    }
736
737    #[test]
738    fn test_fnv1a_64_single_byte() {
739        let h = fnv1a_64(&[0xAB]);
740        assert_ne!(h, 0);
741    }
742
743    // ── RouteKey ─────────────────────────────────────────────────────────────
744
745    #[test]
746    fn test_route_key_equality() {
747        let k1 = make_key(1, 2);
748        let k2 = make_key(1, 2);
749        assert_eq!(k1, k2);
750    }
751
752    #[test]
753    fn test_route_key_inequality() {
754        let k1 = make_key(1, 2);
755        let k2 = make_key(1, 3);
756        assert_ne!(k1, k2);
757    }
758
759    #[test]
760    fn test_route_key_hash_deterministic() {
761        let k = make_key(5, 10);
762        assert_eq!(k.hash_u64(), k.hash_u64());
763    }
764
765    #[test]
766    fn test_route_key_hash_distinct() {
767        assert_ne!(make_key(1, 2).hash_u64(), make_key(2, 1).hash_u64());
768    }
769
770    #[test]
771    fn test_route_key_hashmap_usage() {
772        let mut map: HashMap<RouteKey, u32> = HashMap::new();
773        let k = make_key(7, 8);
774        map.insert(k.clone(), 42);
775        assert_eq!(map.get(&k), Some(&42));
776    }
777
778    // ── RouteEntry ────────────────────────────────────────────────────────────
779
780    #[test]
781    fn test_route_entry_defaults() {
782        let e = RouteEntry::new(make_hop(1));
783        assert_eq!(e.rtt_ms, 100.0);
784        assert_eq!(e.loss_rate, 0.0);
785        assert_eq!(e.bandwidth_kbps, 1000.0);
786        assert!(e.weight > 0.0);
787    }
788
789    #[test]
790    fn test_route_entry_is_healthy_nominal() {
791        let e = RouteEntry::new(make_hop(1));
792        assert!(e.is_healthy());
793    }
794
795    #[test]
796    fn test_route_entry_unhealthy_high_loss() {
797        let mut e = RouteEntry::new(make_hop(1));
798        e.loss_rate = 0.9;
799        assert!(!e.is_healthy());
800    }
801
802    #[test]
803    fn test_route_entry_unhealthy_high_rtt() {
804        let mut e = RouteEntry::new(make_hop(1));
805        e.rtt_ms = 6000.0;
806        assert!(!e.is_healthy());
807    }
808
809    #[test]
810    fn test_recompute_weight_high_bw_low_rtt() {
811        let e = entry_with_metrics(1, 10.0, 0.0, 10_000.0);
812        let w1 = e.weight;
813        let mut e2 = entry_with_metrics(2, 100.0, 0.0, 10_000.0);
814        let w2 = e2.weight;
815        // Lower RTT → higher weight.
816        assert!(w1 > w2, "w1={w1} w2={w2}");
817        // Avoid unused warning.
818        let _ = &mut e2;
819        let _ = e.weight;
820    }
821
822    #[test]
823    fn test_recompute_weight_increases_with_bandwidth() {
824        let mut e1 = entry_with_metrics(1, 50.0, 0.0, 500.0);
825        let mut e2 = entry_with_metrics(2, 50.0, 0.0, 5000.0);
826        assert!(e2.weight > e1.weight);
827        // suppress unused warnings
828        let _ = &mut e1;
829        let _ = &mut e2;
830    }
831
832    // ── RoutingPolicy ─────────────────────────────────────────────────────────
833
834    #[test]
835    fn test_routing_policy_labels_unique() {
836        let policies = [
837            RoutingPolicy::ShortestPath,
838            RoutingPolicy::LowestLatency,
839            RoutingPolicy::HighestBandwidth,
840            RoutingPolicy::LoadBalanced,
841            RoutingPolicy::QoSAware,
842        ];
843        let labels: std::collections::HashSet<_> = policies.iter().map(|p| p.label()).collect();
844        assert_eq!(labels.len(), 5);
845    }
846
847    #[test]
848    fn test_routing_policy_default_is_shortest_path() {
849        assert_eq!(RoutingPolicy::default(), RoutingPolicy::ShortestPath);
850    }
851
852    // ── AdaptiveRoutingConfig ─────────────────────────────────────────────────
853
854    #[test]
855    fn test_config_defaults_valid() {
856        let cfg = AdaptiveRoutingConfig::default();
857        assert!(cfg.alpha > 0.0 && cfg.alpha <= 1.0);
858        assert!(cfg.max_routes_per_key > 0);
859        assert!(cfg.probe_interval_secs > 0);
860    }
861
862    // ── Engine construction ───────────────────────────────────────────────────
863
864    #[test]
865    fn test_engine_new_empty() {
866        let e = engine_default();
867        assert_eq!(e.key_count(), 0);
868        assert_eq!(e.entry_count(), 0);
869    }
870
871    #[test]
872    fn test_engine_active_policy_matches_config() {
873        let cfg = AdaptiveRoutingConfig {
874            policy: RoutingPolicy::QoSAware,
875            ..AdaptiveRoutingConfig::default()
876        };
877        let eng = AdaptiveRoutingEngine::new(cfg);
878        assert_eq!(eng.active_policy(), RoutingPolicy::QoSAware);
879    }
880
881    // ── add_route / remove_route ──────────────────────────────────────────────
882
883    #[test]
884    fn test_add_route_increases_count() {
885        let mut eng = engine_default();
886        let key = make_key(1, 2);
887        eng.add_route(key, RouteEntry::new(make_hop(10)));
888        assert_eq!(eng.entry_count(), 1);
889    }
890
891    #[test]
892    fn test_add_route_duplicate_hop_overwrites() {
893        let mut eng = engine_default();
894        let key = make_key(1, 2);
895        eng.add_route(key.clone(), RouteEntry::new(make_hop(10)));
896        eng.add_route(key.clone(), RouteEntry::new(make_hop(10)));
897        assert_eq!(eng.entry_count(), 1);
898    }
899
900    #[test]
901    fn test_add_multiple_hops() {
902        let mut eng = engine_default();
903        let key = make_key(1, 2);
904        eng.add_route(key.clone(), RouteEntry::new(make_hop(10)));
905        eng.add_route(key.clone(), RouteEntry::new(make_hop(11)));
906        assert_eq!(eng.entry_count(), 2);
907    }
908
909    #[test]
910    fn test_remove_route() {
911        let mut eng = engine_default();
912        let key = make_key(1, 2);
913        eng.add_route(key.clone(), RouteEntry::new(make_hop(10)));
914        eng.remove_route(&key, &make_hop(10));
915        assert_eq!(eng.entry_count(), 0);
916    }
917
918    #[test]
919    fn test_remove_nonexistent_route_no_panic() {
920        let mut eng = engine_default();
921        let key = make_key(1, 2);
922        // Should be a no-op, not a panic.
923        eng.remove_route(&key, &make_hop(99));
924    }
925
926    #[test]
927    fn test_max_routes_per_key_evicts_worst() {
928        let cfg = AdaptiveRoutingConfig {
929            max_routes_per_key: 2,
930            ..AdaptiveRoutingConfig::default()
931        };
932        let mut eng = AdaptiveRoutingEngine::new(cfg);
933        let key = make_key(0, 1);
934        // Add 3 entries; third should evict the worst.
935        eng.add_route(key.clone(), entry_with_metrics(1, 50.0, 0.0, 1000.0));
936        eng.add_route(key.clone(), entry_with_metrics(2, 100.0, 0.2, 1000.0));
937        eng.add_route(key.clone(), entry_with_metrics(3, 30.0, 0.0, 2000.0));
938        assert_eq!(eng.routes_for(&key).map(|s| s.len()), Some(2));
939    }
940
941    #[test]
942    fn test_clear_routes() {
943        let mut eng = engine_default();
944        let key = make_key(1, 2);
945        eng.add_route(key.clone(), RouteEntry::new(make_hop(1)));
946        eng.add_route(key.clone(), RouteEntry::new(make_hop(2)));
947        eng.clear_routes(&key);
948        assert!(eng.routes_for(&key).is_none());
949    }
950
951    #[test]
952    fn test_has_route_true() {
953        let mut eng = engine_default();
954        let key = make_key(1, 2);
955        eng.add_route(key.clone(), RouteEntry::new(make_hop(10)));
956        assert!(eng.has_route(&key, &make_hop(10)));
957    }
958
959    #[test]
960    fn test_has_route_false() {
961        let eng = engine_default();
962        let key = make_key(1, 2);
963        assert!(!eng.has_route(&key, &make_hop(10)));
964    }
965
966    // ── update_metrics ────────────────────────────────────────────────────────
967
968    #[test]
969    fn test_update_metrics_ok() {
970        let mut eng = engine_default();
971        let key = make_key(1, 2);
972        eng.add_route(key.clone(), RouteEntry::new(make_hop(10)));
973        let result = eng.update_metrics(&key, &make_hop(10), 20.0, 0.01);
974        assert!(result.is_ok());
975    }
976
977    #[test]
978    fn test_update_metrics_rtt_decreases_with_low_sample() {
979        let mut eng = engine_default();
980        let key = make_key(1, 2);
981        let mut entry = RouteEntry::new(make_hop(10));
982        entry.rtt_ms = 200.0;
983        eng.add_route(key.clone(), entry);
984        eng.update_metrics(&key, &make_hop(10), 10.0, 0.0).ok();
985        let rtt = eng
986            .routes_for(&key)
987            .and_then(|s| s.first())
988            .map(|e| e.rtt_ms)
989            .unwrap_or(0.0);
990        assert!(rtt < 200.0, "rtt should decrease: {rtt}");
991    }
992
993    #[test]
994    fn test_update_metrics_loss_clamped() {
995        let mut eng = engine_default();
996        let key = make_key(1, 2);
997        eng.add_route(key.clone(), RouteEntry::new(make_hop(10)));
998        eng.update_metrics(&key, &make_hop(10), 50.0, 2.0).ok();
999        let loss = eng
1000            .routes_for(&key)
1001            .and_then(|s| s.first())
1002            .map(|e| e.loss_rate)
1003            .unwrap_or(1.1);
1004        assert!(loss <= 1.0, "loss clamped to 1.0: {loss}");
1005    }
1006
1007    #[test]
1008    fn test_update_metrics_key_not_found() {
1009        let mut eng = engine_default();
1010        let key = make_key(9, 9);
1011        let res = eng.update_metrics(&key, &make_hop(1), 10.0, 0.0);
1012        assert_eq!(res, Err(RoutingEngineError::KeyNotFound));
1013    }
1014
1015    #[test]
1016    fn test_update_metrics_hop_not_found() {
1017        let mut eng = engine_default();
1018        let key = make_key(1, 2);
1019        eng.add_route(key.clone(), RouteEntry::new(make_hop(1)));
1020        let res = eng.update_metrics(&key, &make_hop(99), 10.0, 0.0);
1021        assert_eq!(res, Err(RoutingEngineError::NextHopNotFound));
1022    }
1023
1024    // ── select_next_hop ───────────────────────────────────────────────────────
1025
1026    #[test]
1027    fn test_select_next_hop_no_routes_returns_none() {
1028        let eng = engine_default();
1029        let key = make_key(1, 2);
1030        assert!(eng
1031            .select_next_hop(&key, RoutingPolicy::LowestLatency)
1032            .is_none());
1033    }
1034
1035    #[test]
1036    fn test_select_next_hop_single_entry() {
1037        let mut eng = engine_default();
1038        let key = make_key(1, 2);
1039        eng.add_route(key.clone(), RouteEntry::new(make_hop(7)));
1040        let hop = eng.select_next_hop(&key, RoutingPolicy::LowestLatency);
1041        assert_eq!(hop, Some(make_hop(7)));
1042    }
1043
1044    #[test]
1045    fn test_select_lowest_latency_picks_best() {
1046        let mut eng = engine_default();
1047        let key = make_key(1, 2);
1048        eng.add_route(key.clone(), entry_with_metrics(1, 200.0, 0.0, 1000.0));
1049        eng.add_route(key.clone(), entry_with_metrics(2, 50.0, 0.0, 1000.0));
1050        eng.add_route(key.clone(), entry_with_metrics(3, 500.0, 0.0, 1000.0));
1051        let hop = eng.select_next_hop(&key, RoutingPolicy::LowestLatency);
1052        assert_eq!(hop, Some(make_hop(2)));
1053    }
1054
1055    #[test]
1056    fn test_select_highest_bandwidth_picks_best() {
1057        let mut eng = engine_default();
1058        let key = make_key(1, 2);
1059        eng.add_route(key.clone(), entry_with_metrics(1, 50.0, 0.0, 500.0));
1060        eng.add_route(key.clone(), entry_with_metrics(2, 50.0, 0.0, 5000.0));
1061        eng.add_route(key.clone(), entry_with_metrics(3, 50.0, 0.0, 100.0));
1062        let hop = eng.select_next_hop(&key, RoutingPolicy::HighestBandwidth);
1063        assert_eq!(hop, Some(make_hop(2)));
1064    }
1065
1066    #[test]
1067    fn test_select_shortest_path_picks_highest_weight() {
1068        let mut eng = engine_default();
1069        let key = make_key(1, 2);
1070        // Lower RTT + higher BW = higher weight.
1071        eng.add_route(key.clone(), entry_with_metrics(1, 100.0, 0.0, 1000.0));
1072        eng.add_route(key.clone(), entry_with_metrics(2, 10.0, 0.0, 10_000.0));
1073        let hop = eng.select_next_hop(&key, RoutingPolicy::ShortestPath);
1074        assert_eq!(hop, Some(make_hop(2)));
1075    }
1076
1077    #[test]
1078    fn test_select_qos_aware_picks_best_composite() {
1079        let mut eng = engine_default();
1080        let key = make_key(1, 2);
1081        // Hop 1: good bw, low latency, low loss → should win QoS.
1082        eng.add_route(key.clone(), entry_with_metrics(1, 30.0, 0.01, 5000.0));
1083        // Hop 2: very high bandwidth but also very high loss.
1084        eng.add_route(key.clone(), entry_with_metrics(2, 30.0, 0.8, 50_000.0));
1085        let hop = eng.select_next_hop(&key, RoutingPolicy::QoSAware);
1086        assert_eq!(hop, Some(make_hop(1)));
1087    }
1088
1089    #[test]
1090    fn test_select_load_balanced_returns_valid_hop() {
1091        let mut eng = engine_default();
1092        let key = make_key(1, 2);
1093        eng.add_route(key.clone(), entry_with_metrics(1, 50.0, 0.0, 1000.0));
1094        eng.add_route(key.clone(), entry_with_metrics(2, 60.0, 0.0, 2000.0));
1095        let hop = eng.select_next_hop(&key, RoutingPolicy::LoadBalanced);
1096        assert!(hop == Some(make_hop(1)) || hop == Some(make_hop(2)));
1097    }
1098
1099    #[test]
1100    fn test_select_default_uses_active_policy() {
1101        let mut eng = engine_default();
1102        eng.set_policy(RoutingPolicy::HighestBandwidth);
1103        let key = make_key(1, 2);
1104        eng.add_route(key.clone(), entry_with_metrics(1, 50.0, 0.0, 100.0));
1105        eng.add_route(key.clone(), entry_with_metrics(2, 50.0, 0.0, 9000.0));
1106        let hop = eng.select_next_hop_default(&key);
1107        assert_eq!(hop, Some(make_hop(2)));
1108    }
1109
1110    #[test]
1111    fn test_select_unhealthy_all_falls_back_to_first() {
1112        let mut eng = engine_default();
1113        let key = make_key(1, 2);
1114        // Both entries are unhealthy.
1115        eng.add_route(key.clone(), entry_with_metrics(1, 6000.0, 0.9, 1000.0));
1116        eng.add_route(key.clone(), entry_with_metrics(2, 6000.0, 0.9, 1000.0));
1117        let hop = eng.select_next_hop(&key, RoutingPolicy::LowestLatency);
1118        assert!(hop.is_some());
1119    }
1120
1121    // ── set_policy ────────────────────────────────────────────────────────────
1122
1123    #[test]
1124    fn test_set_policy_increments_switch_count() {
1125        let mut eng = engine_default();
1126        eng.set_policy(RoutingPolicy::LowestLatency); // already default → no switch
1127                                                      // Change to a different policy.
1128        eng.set_policy(RoutingPolicy::QoSAware);
1129        assert_eq!(eng.routing_stats().policy_switches, 1);
1130    }
1131
1132    #[test]
1133    fn test_set_policy_same_no_increment() {
1134        let mut eng = engine_default();
1135        let p = eng.active_policy();
1136        eng.set_policy(p);
1137        assert_eq!(eng.routing_stats().policy_switches, 0);
1138    }
1139
1140    // ── probe_routes ──────────────────────────────────────────────────────────
1141
1142    #[test]
1143    fn test_probe_routes_increments_counter() {
1144        let mut eng = engine_default();
1145        let key = make_key(1, 2);
1146        eng.add_route(key, RouteEntry::new(make_hop(1)));
1147        eng.probe_routes();
1148        assert_eq!(eng.routing_stats().total_probe_rounds, 1);
1149    }
1150
1151    #[test]
1152    fn test_probe_routes_modifies_rtt() {
1153        let mut eng = engine_default();
1154        let key = make_key(1, 2);
1155        let mut entry = RouteEntry::new(make_hop(1));
1156        entry.rtt_ms = 100.0;
1157        eng.add_route(key.clone(), entry);
1158        eng.probe_routes();
1159        let rtt = eng
1160            .routes_for(&key)
1161            .and_then(|s| s.first())
1162            .map(|e| e.rtt_ms)
1163            .unwrap_or(100.0);
1164        // RTT should have changed (jitter applied).
1165        // Because probe alpha is small, change could be tiny; just verify range.
1166        assert!(rtt > 0.0);
1167    }
1168
1169    #[test]
1170    fn test_probe_routes_empty_table_no_panic() {
1171        let mut eng = engine_default();
1172        eng.probe_routes(); // should not panic
1173        assert_eq!(eng.routing_stats().total_probe_rounds, 1);
1174    }
1175
1176    // ── run_adaptation_cycle ──────────────────────────────────────────────────
1177
1178    #[test]
1179    fn test_adaptation_cycle_prunes_stale() {
1180        let cfg = AdaptiveRoutingConfig {
1181            probe_interval_secs: 1,
1182            ..AdaptiveRoutingConfig::default()
1183        };
1184        // Very short stale threshold: interval=1 → stale after 3 secs.
1185        let mut eng = AdaptiveRoutingEngine::new(cfg);
1186        let key = make_key(1, 2);
1187        let mut entry = RouteEntry::new(make_hop(1));
1188        // Force last_updated to ancient past.
1189        entry.last_updated = 0;
1190        eng.route_table.entry(key.clone()).or_default().push(entry);
1191
1192        eng.run_adaptation_cycle();
1193        assert_eq!(eng.entry_count(), 0, "stale entry should be pruned");
1194    }
1195
1196    #[test]
1197    fn test_adaptation_cycle_keeps_fresh() {
1198        let mut eng = engine_default();
1199        let key = make_key(1, 2);
1200        eng.add_route(key.clone(), RouteEntry::new(make_hop(1)));
1201        eng.run_adaptation_cycle();
1202        assert_eq!(eng.entry_count(), 1, "fresh entry must survive");
1203    }
1204
1205    #[test]
1206    fn test_adaptation_cycle_prune_count_recorded() {
1207        let cfg = AdaptiveRoutingConfig {
1208            probe_interval_secs: 1,
1209            ..AdaptiveRoutingConfig::default()
1210        };
1211        let mut eng = AdaptiveRoutingEngine::new(cfg);
1212        let key = make_key(3, 4);
1213        let mut e = RouteEntry::new(make_hop(5));
1214        e.last_updated = 0;
1215        eng.route_table.entry(key).or_default().push(e);
1216        eng.run_adaptation_cycle();
1217        assert_eq!(eng.routing_stats().pruned_last_cycle, 1);
1218    }
1219
1220    // ── routing_stats ─────────────────────────────────────────────────────────
1221
1222    #[test]
1223    fn test_routing_stats_empty_engine() {
1224        let eng = engine_default();
1225        let s = eng.routing_stats();
1226        assert_eq!(s.total_routes, 0);
1227        assert_eq!(s.avg_rtt_ms, 0.0);
1228        assert_eq!(s.avg_loss_rate, 0.0);
1229    }
1230
1231    #[test]
1232    fn test_routing_stats_avg_rtt() {
1233        let mut eng = engine_default();
1234        let key = make_key(1, 2);
1235        eng.add_route(key.clone(), entry_with_metrics(1, 100.0, 0.0, 1000.0));
1236        eng.add_route(key.clone(), entry_with_metrics(2, 200.0, 0.0, 1000.0));
1237        let s = eng.routing_stats();
1238        assert!(
1239            (s.avg_rtt_ms - 150.0).abs() < 1.0,
1240            "avg_rtt={}",
1241            s.avg_rtt_ms
1242        );
1243    }
1244
1245    #[test]
1246    fn test_routing_stats_total_routes() {
1247        let mut eng = engine_default();
1248        for i in 0u8..5 {
1249            let key = make_key(i, i + 1);
1250            eng.add_route(key, RouteEntry::new(make_hop(i)));
1251        }
1252        assert_eq!(eng.routing_stats().total_routes, 5);
1253    }
1254
1255    // ── next_hops_for ─────────────────────────────────────────────────────────
1256
1257    #[test]
1258    fn test_next_hops_for_empty() {
1259        let eng = engine_default();
1260        let key = make_key(1, 2);
1261        assert!(eng.next_hops_for(&key).is_empty());
1262    }
1263
1264    #[test]
1265    fn test_next_hops_for_multiple() {
1266        let mut eng = engine_default();
1267        let key = make_key(1, 2);
1268        eng.add_route(key.clone(), RouteEntry::new(make_hop(10)));
1269        eng.add_route(key.clone(), RouteEntry::new(make_hop(20)));
1270        let hops = eng.next_hops_for(&key);
1271        assert_eq!(hops.len(), 2);
1272        assert!(hops.contains(&make_hop(10)));
1273        assert!(hops.contains(&make_hop(20)));
1274    }
1275
1276    // ── best_entry ────────────────────────────────────────────────────────────
1277
1278    #[test]
1279    fn test_best_entry_returns_lowest_latency() {
1280        let mut eng = engine_default();
1281        let key = make_key(1, 2);
1282        eng.add_route(key.clone(), entry_with_metrics(1, 300.0, 0.0, 1000.0));
1283        eng.add_route(key.clone(), entry_with_metrics(2, 20.0, 0.0, 1000.0));
1284        let best = eng.best_entry(&key, RoutingPolicy::LowestLatency);
1285        assert!(best.is_some());
1286        assert_eq!(
1287            best.expect("test: best_entry must return Some for LowestLatency with two routes")
1288                .next_hop,
1289            make_hop(2)
1290        );
1291    }
1292
1293    #[test]
1294    fn test_best_entry_missing_key_none() {
1295        let eng = engine_default();
1296        assert!(eng
1297            .best_entry(&make_key(9, 9), RoutingPolicy::ShortestPath)
1298            .is_none());
1299    }
1300
1301    // ── set_alpha ─────────────────────────────────────────────────────────────
1302
1303    #[test]
1304    fn test_set_alpha_valid() {
1305        let mut eng = engine_default();
1306        assert!(eng.set_alpha(0.5).is_ok());
1307    }
1308
1309    #[test]
1310    fn test_set_alpha_zero_is_error() {
1311        let mut eng = engine_default();
1312        assert!(eng.set_alpha(0.0).is_err());
1313    }
1314
1315    #[test]
1316    fn test_set_alpha_negative_is_error() {
1317        let mut eng = engine_default();
1318        assert!(eng.set_alpha(-0.1).is_err());
1319    }
1320
1321    #[test]
1322    fn test_set_alpha_one_is_valid() {
1323        let mut eng = engine_default();
1324        assert!(eng.set_alpha(1.0).is_ok());
1325    }
1326
1327    // ── update_bandwidth ──────────────────────────────────────────────────────
1328
1329    #[test]
1330    fn test_update_bandwidth_ok() {
1331        let mut eng = engine_default();
1332        let key = make_key(1, 2);
1333        eng.add_route(key.clone(), RouteEntry::new(make_hop(1)));
1334        assert!(eng.update_bandwidth(&key, &make_hop(1), 5000.0).is_ok());
1335    }
1336
1337    #[test]
1338    fn test_update_bandwidth_increases_bw() {
1339        let mut eng = engine_default();
1340        let key = make_key(1, 2);
1341        let mut e = RouteEntry::new(make_hop(1));
1342        e.bandwidth_kbps = 100.0;
1343        eng.add_route(key.clone(), e);
1344        eng.update_bandwidth(&key, &make_hop(1), 10_000.0).ok();
1345        let bw = eng
1346            .routes_for(&key)
1347            .and_then(|s| s.first())
1348            .map(|e| e.bandwidth_kbps)
1349            .unwrap_or(0.0);
1350        assert!(bw > 100.0, "bw should increase: {bw}");
1351    }
1352
1353    #[test]
1354    fn test_update_bandwidth_key_not_found() {
1355        let mut eng = engine_default();
1356        let res = eng.update_bandwidth(&make_key(9, 9), &make_hop(1), 5000.0);
1357        assert_eq!(res, Err(RoutingEngineError::KeyNotFound));
1358    }
1359
1360    // ── merge_from ────────────────────────────────────────────────────────────
1361
1362    #[test]
1363    fn test_merge_from_adds_routes() {
1364        let mut eng1 = engine_default();
1365        let mut eng2 = engine_default();
1366        let key = make_key(1, 2);
1367        eng2.add_route(key.clone(), RouteEntry::new(make_hop(42)));
1368        eng1.merge_from(&eng2);
1369        assert!(eng1.has_route(&key, &make_hop(42)));
1370    }
1371
1372    #[test]
1373    fn test_merge_from_no_duplicates_for_same_hop() {
1374        let mut eng1 = engine_default();
1375        let mut eng2 = engine_default();
1376        let key = make_key(1, 2);
1377        eng1.add_route(key.clone(), RouteEntry::new(make_hop(1)));
1378        eng2.add_route(key.clone(), RouteEntry::new(make_hop(1)));
1379        eng1.merge_from(&eng2);
1380        assert_eq!(eng1.entry_count(), 1);
1381    }
1382
1383    // ── resize_max_routes ─────────────────────────────────────────────────────
1384
1385    #[test]
1386    fn test_resize_max_routes_evicts_when_shrinking() {
1387        let cfg = AdaptiveRoutingConfig {
1388            max_routes_per_key: 5,
1389            ..AdaptiveRoutingConfig::default()
1390        };
1391        let mut eng = AdaptiveRoutingEngine::new(cfg);
1392        let key = make_key(1, 2);
1393        for i in 0u8..5 {
1394            eng.add_route(key.clone(), RouteEntry::new(make_hop(i + 1)));
1395        }
1396        assert_eq!(eng.entry_count(), 5);
1397        eng.resize_max_routes(2);
1398        assert!(eng.entry_count() <= 2, "count={}", eng.entry_count());
1399    }
1400
1401    // ── reset ─────────────────────────────────────────────────────────────────
1402
1403    #[test]
1404    fn test_reset_clears_routes() {
1405        let mut eng = engine_default();
1406        let key = make_key(1, 2);
1407        eng.add_route(key, RouteEntry::new(make_hop(1)));
1408        eng.reset();
1409        assert_eq!(eng.entry_count(), 0);
1410    }
1411
1412    #[test]
1413    fn test_reset_clears_stats() {
1414        let mut eng = engine_default();
1415        eng.set_policy(RoutingPolicy::QoSAware);
1416        eng.reset();
1417        assert_eq!(eng.routing_stats().policy_switches, 0);
1418    }
1419
1420    // ── set_probe_interval ────────────────────────────────────────────────────
1421
1422    #[test]
1423    fn test_set_probe_interval() {
1424        let mut eng = engine_default();
1425        eng.set_probe_interval(60);
1426        assert_eq!(eng.config.probe_interval_secs, 60);
1427    }
1428
1429    // ── Type aliases ──────────────────────────────────────────────────────────
1430
1431    #[test]
1432    fn test_are_type_aliases_usable() {
1433        let key: AreRouteKey = make_key(1, 2);
1434        let entry: AreRouteEntry = RouteEntry::new(make_hop(5));
1435        let policy: AreRoutingPolicy = RoutingPolicy::QoSAware;
1436        let config: AreRoutingConfig = AdaptiveRoutingConfig::default();
1437        let stats: AreRoutingStats = AdaptiveRoutingStats::default();
1438
1439        assert_eq!(key.src[0], 1);
1440        assert!(entry.rtt_ms > 0.0);
1441        assert_eq!(policy, RoutingPolicy::QoSAware);
1442        assert!(config.alpha > 0.0);
1443        assert_eq!(stats.total_routes, 0);
1444    }
1445
1446    // ── Edge / stress ─────────────────────────────────────────────────────────
1447
1448    #[test]
1449    fn test_many_keys() {
1450        let mut eng = engine_default();
1451        for i in 0u8..200 {
1452            let key = make_key(i, 255 - i);
1453            eng.add_route(key, RouteEntry::new(make_hop(i)));
1454        }
1455        assert_eq!(eng.key_count(), 200);
1456    }
1457
1458    #[test]
1459    fn test_multiple_probe_rounds() {
1460        let mut eng = engine_default();
1461        let key = make_key(1, 2);
1462        eng.add_route(key, RouteEntry::new(make_hop(1)));
1463        for _ in 0..10 {
1464            eng.probe_routes();
1465        }
1466        assert_eq!(eng.routing_stats().total_probe_rounds, 10);
1467    }
1468
1469    #[test]
1470    fn test_update_metrics_ewma_convergence() {
1471        let mut eng = engine_default();
1472        let key = make_key(1, 2);
1473        let mut e = RouteEntry::new(make_hop(1));
1474        e.rtt_ms = 1000.0;
1475        eng.add_route(key.clone(), e);
1476        // Apply many updates with low RTT.
1477        for _ in 0..100 {
1478            eng.update_metrics(&key, &make_hop(1), 10.0, 0.0).ok();
1479        }
1480        let rtt = eng
1481            .routes_for(&key)
1482            .and_then(|s| s.first())
1483            .map(|e| e.rtt_ms)
1484            .unwrap_or(1000.0);
1485        assert!(rtt < 50.0, "EWMA should converge: {rtt}");
1486    }
1487
1488    #[test]
1489    fn test_qos_score_better_for_low_loss() {
1490        let e_low_loss = entry_with_metrics(1, 50.0, 0.01, 1000.0);
1491        let e_high_loss = entry_with_metrics(2, 50.0, 0.9, 1000.0);
1492        assert!(qos_score(&e_low_loss) > qos_score(&e_high_loss));
1493    }
1494
1495    #[test]
1496    fn test_routing_engine_error_display() {
1497        let e = RoutingEngineError::InvalidAlpha(0.0);
1498        assert!(e.to_string().contains("alpha"));
1499        let e2 = RoutingEngineError::KeyNotFound;
1500        assert!(e2.to_string().contains("key"));
1501    }
1502}