Skip to main content

ipfrs_network/
flood_sub_router.rs

1//! FloodSubRouter — topic-based message flooding router with subscription
2//! management, message deduplication, TTL-based expiry, and per-peer forwarding
3//! history to prevent routing loops.
4//!
5//! ## Design
6//!
7//! - **Topic subscriptions**: many peers → one topic via `FloodTopic`.
8//! - **Deduplication**: every seen `FloodMessageId` is cached with its arrival
9//!   timestamp for `dedup_window_secs`; duplicates are immediately dropped.
10//! - **Loop prevention**: `forwarded_to` tracks which peers already received a
11//!   given message so the router never re-sends to the same peer.
12//! - **TTL enforcement**: messages whose TTL falls at or below `min_ttl` are
13//!   dropped before any forwarding occurs.
14//! - **No `unwrap()`**: all fallible operations are handled explicitly.
15
16use std::collections::{HashMap, HashSet};
17
18// ─── FNV-1a ──────────────────────────────────────────────────────────────────
19
20/// FNV-1a 64-bit hash over an arbitrary byte slice.
21///
22/// Used for `FloodMessageId` computation without requiring any external crate.
23#[inline]
24fn fnv1a_64(data: &[u8]) -> u64 {
25    let mut hash: u64 = 14_695_981_039_346_656_037;
26    for &b in data {
27        hash ^= b as u64;
28        hash = hash.wrapping_mul(1_099_511_628_211);
29    }
30    hash
31}
32
33// ─── xorshift64 PRNG ─────────────────────────────────────────────────────────
34
35/// xorshift64 PRNG — used wherever deterministic pseudo-randomness is needed
36/// inside this module (e.g. test helpers) without pulling in external crates.
37#[allow(dead_code)]
38#[inline]
39fn xorshift64(state: &mut u64) -> u64 {
40    let mut x = *state;
41    x ^= x << 13;
42    x ^= x >> 7;
43    x ^= x << 17;
44    *state = x;
45    x
46}
47
48// ═══════════════════════════════════════════════════════════════════════════════
49// Core types
50// ═══════════════════════════════════════════════════════════════════════════════
51
52/// Newtype wrapper for a topic identifier string.
53#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54pub struct FloodTopic(pub String);
55
56impl FloodTopic {
57    /// Create a new `FloodTopic` from any string-like value.
58    pub fn new(s: impl Into<String>) -> Self {
59        FloodTopic(s.into())
60    }
61
62    /// Return the inner topic string as a `&str`.
63    pub fn as_str(&self) -> &str {
64        &self.0
65    }
66}
67
68impl std::fmt::Display for FloodTopic {
69    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
70        f.write_str(&self.0)
71    }
72}
73
74// ─────────────────────────────────────────────────────────────────────────────
75
76/// 16-byte message ID derived from FNV-1a over content + timestamp + peer.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
78pub struct FloodMessageId(pub [u8; 16]);
79
80impl FloodMessageId {
81    /// Return the raw bytes of this ID.
82    pub fn as_bytes(&self) -> &[u8; 16] {
83        &self.0
84    }
85
86    /// Return a hex-encoded representation.
87    pub fn to_hex(&self) -> String {
88        self.0.iter().map(|b| format!("{b:02x}")).collect()
89    }
90}
91
92impl std::fmt::Display for FloodMessageId {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        write!(f, "{}", self.to_hex())
95    }
96}
97
98// ─────────────────────────────────────────────────────────────────────────────
99
100/// A flood-routed message with all routing metadata.
101#[derive(Debug, Clone)]
102pub struct FloodMessage {
103    /// Unique message identifier (derived via FNV-1a).
104    pub id: FloodMessageId,
105    /// Topic this message belongs to.
106    pub topic: FloodTopic,
107    /// Application-level payload bytes.
108    pub payload: Vec<u8>,
109    /// Time-to-live in hops; decremented by the receiving router before
110    /// forwarding.  When `ttl <= min_ttl` the message is dropped.
111    pub ttl: u8,
112    /// Peer ID of the originator of this message.
113    pub origin_peer: String,
114    /// Unix-epoch timestamp (seconds) at message creation.
115    pub created_at: u64,
116}
117
118// ─────────────────────────────────────────────────────────────────────────────
119
120/// Record of a peer's subscription to a topic.
121#[derive(Debug, Clone)]
122pub struct SubscriptionRecord {
123    /// Peer that subscribed.
124    pub peer_id: String,
125    /// The topic subscribed to.
126    pub topic: FloodTopic,
127    /// Unix-epoch timestamp when the subscription was registered.
128    pub subscribed_at: u64,
129    /// Cumulative count of messages delivered to this subscriber.
130    pub message_count: u64,
131}
132
133// ─────────────────────────────────────────────────────────────────────────────
134
135/// Decision returned by [`FloodSubRouter::route`].
136#[derive(Debug, Clone, PartialEq)]
137pub enum ForwardDecision {
138    /// Forward this message to the listed peers.
139    Forward {
140        /// Peers that should receive this message next.
141        to_peers: Vec<String>,
142    },
143    /// Drop the message; `reason` explains why.
144    Drop {
145        /// Human-readable reason for dropping.
146        reason: String,
147    },
148    /// The origin peer is the only subscriber — the message "loops back" to
149    /// its sender without needing further forwarding.
150    Loopback,
151}
152
153// ─────────────────────────────────────────────────────────────────────────────
154
155/// Configuration for a [`FloodSubRouter`] instance.
156#[derive(Debug, Clone)]
157pub struct RouterConfig {
158    /// Maximum number of peers allowed per topic.
159    pub max_peers_per_topic: usize,
160    /// Maximum number of message IDs held in the dedup cache.
161    pub max_message_cache: usize,
162    /// Minimum TTL value; messages at or below this are dropped.
163    pub min_ttl: u8,
164    /// Duration (seconds) for which a message ID is kept in the dedup cache.
165    pub dedup_window_secs: u64,
166    /// Maximum number of distinct topics.
167    pub max_topics: usize,
168}
169
170impl Default for RouterConfig {
171    fn default() -> Self {
172        RouterConfig {
173            max_peers_per_topic: 100,
174            max_message_cache: 10_000,
175            min_ttl: 1,
176            dedup_window_secs: 60,
177            max_topics: 256,
178        }
179    }
180}
181
182// ─────────────────────────────────────────────────────────────────────────────
183
184/// Snapshot of operational statistics for a [`FloodSubRouter`].
185#[derive(Debug, Clone, Default)]
186pub struct FsrRouterStats {
187    /// Total messages forwarded to at least one peer.
188    pub messages_forwarded: u64,
189    /// Total messages dropped (duplicate, TTL, no-subscribers, etc.).
190    pub messages_dropped: u64,
191    /// Total messages returned as `Loopback`.
192    pub messages_looped: u64,
193    /// Current number of active subscriptions (peer × topic pairs).
194    pub subscriptions: usize,
195    /// Current number of distinct active topics.
196    pub topics: usize,
197    /// Current number of entries in the dedup cache.
198    pub cache_size: usize,
199}
200
201// ═══════════════════════════════════════════════════════════════════════════════
202// FloodSubRouter
203// ═══════════════════════════════════════════════════════════════════════════════
204
205/// Topic-based message flooding router.
206///
207/// # Responsibilities
208///
209/// 1. Maintain per-peer topic subscriptions.
210/// 2. Deduplicate messages using a bounded, TTL-based ID cache.
211/// 3. Track which peers have already received a given message to prevent loops.
212/// 4. Compute forward sets excluding the origin peer and previously-forwarded
213///    peers.
214///
215/// # Thread safety
216///
217/// `FloodSubRouter` is *not* `Send + Sync` by default; callers should wrap it
218/// in a `Mutex` or `RwLock` when sharing across threads.
219pub struct FloodSubRouter {
220    /// Runtime configuration.
221    config: RouterConfig,
222    /// Active subscriptions keyed by `"peer_id:topic"`.
223    subscriptions: HashMap<String, SubscriptionRecord>,
224    /// Dedup cache: message ID → arrival timestamp (seconds).
225    message_cache: HashMap<FloodMessageId, u64>,
226    /// Per-message forward log: message ID → set of peer IDs already forwarded.
227    forwarded_to: HashMap<FloodMessageId, HashSet<String>>,
228    /// Operational statistics.
229    stats: FsrRouterStats,
230}
231
232impl FloodSubRouter {
233    // ── Construction ─────────────────────────────────────────────────────────
234
235    /// Create a new router with the supplied configuration.
236    pub fn new(config: RouterConfig) -> Self {
237        FloodSubRouter {
238            config,
239            subscriptions: HashMap::new(),
240            message_cache: HashMap::new(),
241            forwarded_to: HashMap::new(),
242            stats: FsrRouterStats::default(),
243        }
244    }
245
246    // ── Subscription management ───────────────────────────────────────────────
247
248    /// Subscribe `peer_id` to `topic`.
249    ///
250    /// Returns `true` if this was a *new* subscription, `false` if the peer
251    /// was already subscribed to this topic.
252    ///
253    /// The subscription is rejected (returns `false`) when either:
254    /// - the per-topic peer limit (`max_peers_per_topic`) would be exceeded, or
255    /// - the global topic limit (`max_topics`) would be exceeded by a *new*
256    ///   topic.
257    pub fn subscribe(&mut self, peer_id: &str, topic: &FloodTopic, now: u64) -> bool {
258        let key = Self::sub_key(peer_id, topic);
259        if self.subscriptions.contains_key(&key) {
260            return false;
261        }
262
263        // Enforce per-topic peer cap.
264        let current_peers = self.peers_for_topic(topic).len();
265        if current_peers >= self.config.max_peers_per_topic {
266            return false;
267        }
268
269        // Enforce max-topics limit for brand-new topics.
270        let topic_exists = self.subscriptions.values().any(|r| &r.topic == topic);
271        if !topic_exists {
272            let distinct_topics: HashSet<&FloodTopic> =
273                self.subscriptions.values().map(|r| &r.topic).collect();
274            if distinct_topics.len() >= self.config.max_topics {
275                return false;
276            }
277        }
278
279        let record = SubscriptionRecord {
280            peer_id: peer_id.to_owned(),
281            topic: topic.clone(),
282            subscribed_at: now,
283            message_count: 0,
284        };
285        self.subscriptions.insert(key, record);
286        self.refresh_stats();
287        true
288    }
289
290    /// Unsubscribe `peer_id` from `topic`.
291    ///
292    /// Returns `true` if the subscription existed and was removed.
293    pub fn unsubscribe(&mut self, peer_id: &str, topic: &FloodTopic) -> bool {
294        let key = Self::sub_key(peer_id, topic);
295        let removed = self.subscriptions.remove(&key).is_some();
296        if removed {
297            self.refresh_stats();
298        }
299        removed
300    }
301
302    /// Return all peer IDs subscribed to `topic`.
303    pub fn peers_for_topic(&self, topic: &FloodTopic) -> Vec<String> {
304        self.subscriptions
305            .values()
306            .filter(|r| &r.topic == topic)
307            .map(|r| r.peer_id.clone())
308            .collect()
309    }
310
311    /// Return all topics to which `peer_id` is subscribed.
312    pub fn topics_for_peer(&self, peer_id: &str) -> Vec<FloodTopic> {
313        self.subscriptions
314            .values()
315            .filter(|r| r.peer_id == peer_id)
316            .map(|r| r.topic.clone())
317            .collect()
318    }
319
320    // ── Routing ───────────────────────────────────────────────────────────────
321
322    /// Decide how to forward `msg` and update all internal state accordingly.
323    ///
324    /// # Decision logic (in order)
325    ///
326    /// 1. If `msg.id` is already in the dedup cache → `Drop` (duplicate).
327    /// 2. If `msg.ttl <= config.min_ttl` → `Drop` (TTL exhausted).
328    /// 3. If no peer is subscribed to `msg.topic` → `Drop` (no subscribers).
329    /// 4. Insert `msg.id` into the dedup cache with `now` as the timestamp.
330    /// 5. Build the candidate forward set: all subscribers except the origin
331    ///    peer and peers already recorded in `forwarded_to[msg.id]`.
332    /// 6. Record the candidate set in `forwarded_to`.
333    /// 7. If the candidate set is empty and the origin is the *only* subscriber
334    ///    → `Loopback`.
335    /// 8. If the candidate set is empty (all already forwarded) → `Drop`.
336    /// 9. Otherwise → `Forward { to_peers }`.
337    pub fn route(&mut self, msg: &FloodMessage, now: u64) -> ForwardDecision {
338        // Step 1 — dedup.
339        if self.message_cache.contains_key(&msg.id) {
340            self.stats.messages_dropped += 1;
341            return ForwardDecision::Drop {
342                reason: "duplicate: already in dedup cache".to_owned(),
343            };
344        }
345
346        // Step 2 — TTL.
347        if msg.ttl <= self.config.min_ttl {
348            self.stats.messages_dropped += 1;
349            return ForwardDecision::Drop {
350                reason: format!("ttl {} <= min_ttl {}", msg.ttl, self.config.min_ttl),
351            };
352        }
353
354        // Step 3 — subscribers.
355        let subscribers = self.peers_for_topic(&msg.topic);
356        if subscribers.is_empty() {
357            self.stats.messages_dropped += 1;
358            return ForwardDecision::Drop {
359                reason: format!("no subscribers for topic '{}'", msg.topic),
360            };
361        }
362
363        // Step 4 — register in dedup cache (bounded eviction when full).
364        if self.message_cache.len() >= self.config.max_message_cache {
365            // Evict the oldest entry to maintain the cap.
366            if let Some(oldest_key) = self
367                .message_cache
368                .iter()
369                .min_by_key(|(_, &ts)| ts)
370                .map(|(k, _)| *k)
371            {
372                self.message_cache.remove(&oldest_key);
373                self.forwarded_to.remove(&oldest_key);
374            }
375        }
376        self.message_cache.insert(msg.id, now);
377
378        // Step 5 — compute forward set.
379        let already_forwarded = self.forwarded_to.get(&msg.id).cloned().unwrap_or_default();
380
381        let to_peers: Vec<String> = subscribers
382            .iter()
383            .filter(|p| *p != &msg.origin_peer && !already_forwarded.contains(*p))
384            .cloned()
385            .collect();
386
387        // Step 6 — record forwarded peers.
388        let entry = self.forwarded_to.entry(msg.id).or_default();
389        for p in &to_peers {
390            entry.insert(p.clone());
391        }
392
393        // Step 7 / 8 — empty candidate set.
394        if to_peers.is_empty() {
395            // Loopback: origin is the only subscriber.
396            let non_origin_subs: Vec<_> = subscribers
397                .iter()
398                .filter(|p| *p != &msg.origin_peer)
399                .collect();
400            if non_origin_subs.is_empty() {
401                self.stats.messages_looped += 1;
402                return ForwardDecision::Loopback;
403            }
404            // All non-origin subscribers already received this message.
405            self.stats.messages_dropped += 1;
406            return ForwardDecision::Drop {
407                reason: "all subscribers already received this message".to_owned(),
408            };
409        }
410
411        // Step 9 — increment per-subscriber message counts.
412        for peer in &to_peers {
413            let key = Self::sub_key(peer, &msg.topic);
414            if let Some(rec) = self.subscriptions.get_mut(&key) {
415                rec.message_count += 1;
416            }
417        }
418
419        self.stats.messages_forwarded += 1;
420        self.refresh_stats();
421        ForwardDecision::Forward { to_peers }
422    }
423
424    /// Record that `msg_id` was manually forwarded to `peer_id`.
425    ///
426    /// Useful when the caller forwards a message outside of `route` and wants
427    /// to keep the loop-prevention state consistent.
428    pub fn mark_forwarded(&mut self, id: &FloodMessageId, peer_id: &str) {
429        self.forwarded_to
430            .entry(*id)
431            .or_default()
432            .insert(peer_id.to_owned());
433    }
434
435    // ── Cache maintenance ─────────────────────────────────────────────────────
436
437    /// Evict all dedup-cache entries older than `config.dedup_window_secs`.
438    ///
439    /// Should be called periodically (e.g. every 10–30 seconds) to bound memory
440    /// consumption.
441    pub fn expire_cache(&mut self, now: u64) {
442        let window = self.config.dedup_window_secs;
443        let cutoff = now.saturating_sub(window);
444
445        // Collect keys to remove first to satisfy the borrow checker.
446        let expired: Vec<FloodMessageId> = self
447            .message_cache
448            .iter()
449            .filter(|(_, &ts)| ts <= cutoff)
450            .map(|(id, _)| *id)
451            .collect();
452
453        for id in expired {
454            self.message_cache.remove(&id);
455            self.forwarded_to.remove(&id);
456        }
457
458        self.refresh_stats();
459    }
460
461    // ── Statistics ────────────────────────────────────────────────────────────
462
463    /// Return a reference to the current operational statistics.
464    pub fn stats(&self) -> &FsrRouterStats {
465        &self.stats
466    }
467
468    // ── ID computation ────────────────────────────────────────────────────────
469
470    /// Compute a `FloodMessageId` from `payload`, `topic`, `peer_id`, and a
471    /// Unix-epoch timestamp.
472    ///
473    /// The ID is produced by running FNV-1a over all inputs concatenated in a
474    /// deterministic order, then taking two sequential hashes to fill 16 bytes.
475    pub fn compute_message_id(
476        payload: &[u8],
477        topic: &FloodTopic,
478        peer_id: &str,
479        now: u64,
480    ) -> FloodMessageId {
481        // Build a contiguous buffer: payload || topic_bytes || peer_bytes ||
482        // now_le_bytes, separated by a null byte to prevent boundary ambiguity.
483        let mut buf: Vec<u8> =
484            Vec::with_capacity(payload.len() + topic.0.len() + peer_id.len() + 18);
485        buf.extend_from_slice(payload);
486        buf.push(0x00);
487        buf.extend_from_slice(topic.0.as_bytes());
488        buf.push(0x00);
489        buf.extend_from_slice(peer_id.as_bytes());
490        buf.push(0x00);
491        buf.extend_from_slice(&now.to_le_bytes());
492
493        let h1 = fnv1a_64(&buf);
494        // Second hash: feed h1's bytes back through FNV-1a for the upper half.
495        let h2 = fnv1a_64(&h1.to_le_bytes());
496
497        let mut id = [0u8; 16];
498        id[..8].copy_from_slice(&h1.to_le_bytes());
499        id[8..].copy_from_slice(&h2.to_le_bytes());
500        FloodMessageId(id)
501    }
502
503    // ── Private helpers ───────────────────────────────────────────────────────
504
505    /// Canonical subscription map key: `"peer_id\x00topic"`.
506    ///
507    /// Using `\x00` as separator prevents collisions when peer IDs or topic
508    /// names contain colons.
509    #[inline]
510    fn sub_key(peer_id: &str, topic: &FloodTopic) -> String {
511        format!("{}\x00{}", peer_id, topic.0)
512    }
513
514    /// Refresh derived counter fields in `stats`.
515    #[inline]
516    fn refresh_stats(&mut self) {
517        let distinct_topics: HashSet<&FloodTopic> =
518            self.subscriptions.values().map(|r| &r.topic).collect();
519        self.stats.subscriptions = self.subscriptions.len();
520        self.stats.topics = distinct_topics.len();
521        self.stats.cache_size = self.message_cache.len();
522    }
523
524    /// Return `true` when the dedup cache is at capacity.
525    pub fn cache_full(&self) -> bool {
526        self.message_cache.len() >= self.config.max_message_cache
527    }
528
529    /// Return the number of peers that have already been sent `id`.
530    pub fn forwarded_count(&self, id: &FloodMessageId) -> usize {
531        self.forwarded_to.get(id).map(|s| s.len()).unwrap_or(0)
532    }
533
534    /// Return `true` if `peer_id` is subscribed to `topic`.
535    pub fn is_subscribed(&self, peer_id: &str, topic: &FloodTopic) -> bool {
536        self.subscriptions
537            .contains_key(&Self::sub_key(peer_id, topic))
538    }
539
540    /// Return the `SubscriptionRecord` for `peer_id` on `topic`, if any.
541    pub fn subscription(&self, peer_id: &str, topic: &FloodTopic) -> Option<&SubscriptionRecord> {
542        self.subscriptions.get(&Self::sub_key(peer_id, topic))
543    }
544
545    /// Return the total number of active subscriptions.
546    pub fn subscription_count(&self) -> usize {
547        self.subscriptions.len()
548    }
549
550    /// Return an iterator over all active `SubscriptionRecord`s.
551    pub fn all_subscriptions(&self) -> impl Iterator<Item = &SubscriptionRecord> {
552        self.subscriptions.values()
553    }
554}
555
556// ═══════════════════════════════════════════════════════════════════════════════
557// Tests
558// ═══════════════════════════════════════════════════════════════════════════════
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563
564    // ── Helpers ────────────────────────────────────────────────────────────────
565
566    fn default_router() -> FloodSubRouter {
567        FloodSubRouter::new(RouterConfig::default())
568    }
569
570    fn topic(s: &str) -> FloodTopic {
571        FloodTopic::new(s)
572    }
573
574    /// Build a `FloodMessage` with a freshly computed ID.
575    fn make_msg(payload: &[u8], t: &FloodTopic, peer: &str, ttl: u8, now: u64) -> FloodMessage {
576        let id = FloodSubRouter::compute_message_id(payload, t, peer, now);
577        FloodMessage {
578            id,
579            topic: t.clone(),
580            payload: payload.to_vec(),
581            ttl,
582            origin_peer: peer.to_owned(),
583            created_at: now,
584        }
585    }
586
587    // ── 1. Basic construction ──────────────────────────────────────────────────
588
589    #[test]
590    fn test_new_router_is_empty() {
591        let r = default_router();
592        let s = r.stats();
593        assert_eq!(s.subscriptions, 0);
594        assert_eq!(s.topics, 0);
595        assert_eq!(s.cache_size, 0);
596        assert_eq!(s.messages_forwarded, 0);
597        assert_eq!(s.messages_dropped, 0);
598        assert_eq!(s.messages_looped, 0);
599    }
600
601    // ── 2. Subscribe ───────────────────────────────────────────────────────────
602
603    #[test]
604    fn test_subscribe_new_returns_true() {
605        let mut r = default_router();
606        assert!(r.subscribe("peer1", &topic("news"), 100));
607    }
608
609    #[test]
610    fn test_subscribe_duplicate_returns_false() {
611        let mut r = default_router();
612        r.subscribe("peer1", &topic("news"), 100);
613        assert!(!r.subscribe("peer1", &topic("news"), 200));
614    }
615
616    #[test]
617    fn test_subscribe_updates_stats() {
618        let mut r = default_router();
619        r.subscribe("p1", &topic("t1"), 1);
620        r.subscribe("p2", &topic("t1"), 2);
621        r.subscribe("p1", &topic("t2"), 3);
622        assert_eq!(r.stats().subscriptions, 3);
623        assert_eq!(r.stats().topics, 2);
624    }
625
626    #[test]
627    fn test_subscribe_respects_max_peers_per_topic() {
628        let config = RouterConfig {
629            max_peers_per_topic: 2,
630            ..Default::default()
631        };
632        let mut r = FloodSubRouter::new(config);
633        let t = topic("flood");
634        assert!(r.subscribe("p1", &t, 1));
635        assert!(r.subscribe("p2", &t, 2));
636        assert!(!r.subscribe("p3", &t, 3)); // over limit
637    }
638
639    #[test]
640    fn test_subscribe_respects_max_topics() {
641        let config = RouterConfig {
642            max_topics: 2,
643            ..Default::default()
644        };
645        let mut r = FloodSubRouter::new(config);
646        assert!(r.subscribe("p1", &topic("t1"), 1));
647        assert!(r.subscribe("p1", &topic("t2"), 2));
648        assert!(!r.subscribe("p1", &topic("t3"), 3)); // would exceed max_topics
649    }
650
651    // ── 3. Unsubscribe ─────────────────────────────────────────────────────────
652
653    #[test]
654    fn test_unsubscribe_existing_returns_true() {
655        let mut r = default_router();
656        r.subscribe("p1", &topic("x"), 1);
657        assert!(r.unsubscribe("p1", &topic("x")));
658    }
659
660    #[test]
661    fn test_unsubscribe_missing_returns_false() {
662        let mut r = default_router();
663        assert!(!r.unsubscribe("p1", &topic("x")));
664    }
665
666    #[test]
667    fn test_unsubscribe_decrements_stats() {
668        let mut r = default_router();
669        r.subscribe("p1", &topic("x"), 1);
670        r.subscribe("p2", &topic("x"), 2);
671        r.unsubscribe("p1", &topic("x"));
672        assert_eq!(r.stats().subscriptions, 1);
673        assert_eq!(r.stats().topics, 1); // topic still active via p2
674    }
675
676    #[test]
677    fn test_unsubscribe_last_peer_removes_topic_from_stats() {
678        let mut r = default_router();
679        r.subscribe("p1", &topic("x"), 1);
680        r.unsubscribe("p1", &topic("x"));
681        assert_eq!(r.stats().topics, 0);
682    }
683
684    // ── 4. peers_for_topic ────────────────────────────────────────────────────
685
686    #[test]
687    fn test_peers_for_topic_empty_when_no_subs() {
688        let r = default_router();
689        assert!(r.peers_for_topic(&topic("news")).is_empty());
690    }
691
692    #[test]
693    fn test_peers_for_topic_returns_all_subscribers() {
694        let mut r = default_router();
695        r.subscribe("p1", &topic("news"), 1);
696        r.subscribe("p2", &topic("news"), 2);
697        r.subscribe("p3", &topic("other"), 3);
698        let mut peers = r.peers_for_topic(&topic("news"));
699        peers.sort();
700        assert_eq!(peers, vec!["p1", "p2"]);
701    }
702
703    // ── 5. topics_for_peer ────────────────────────────────────────────────────
704
705    #[test]
706    fn test_topics_for_peer_empty_when_not_subscribed() {
707        let r = default_router();
708        assert!(r.topics_for_peer("p1").is_empty());
709    }
710
711    #[test]
712    fn test_topics_for_peer_returns_all_topics() {
713        let mut r = default_router();
714        r.subscribe("p1", &topic("t1"), 1);
715        r.subscribe("p1", &topic("t2"), 2);
716        r.subscribe("p2", &topic("t1"), 3);
717        let mut topics = r
718            .topics_for_peer("p1")
719            .into_iter()
720            .map(|t| t.0)
721            .collect::<Vec<_>>();
722        topics.sort();
723        assert_eq!(topics, vec!["t1", "t2"]);
724    }
725
726    // ── 6. route — duplicate drop ─────────────────────────────────────────────
727
728    #[test]
729    fn test_route_drops_duplicate_message() {
730        let mut r = default_router();
731        r.subscribe("p2", &topic("t"), 1);
732        let msg = make_msg(b"hello", &topic("t"), "p1", 5, 1000);
733        let d1 = r.route(&msg, 1000);
734        assert!(matches!(d1, ForwardDecision::Forward { .. }));
735        let d2 = r.route(&msg, 1001);
736        assert!(matches!(d2, ForwardDecision::Drop { .. }));
737        if let ForwardDecision::Drop { reason } = d2 {
738            assert!(reason.contains("duplicate"));
739        }
740    }
741
742    // ── 7. route — TTL drop ───────────────────────────────────────────────────
743
744    #[test]
745    fn test_route_drops_on_min_ttl() {
746        let mut r = default_router();
747        r.subscribe("p2", &topic("t"), 1);
748        let msg = make_msg(b"x", &topic("t"), "p1", 1, 1000); // ttl == min_ttl (1)
749        let d = r.route(&msg, 1000);
750        assert!(matches!(d, ForwardDecision::Drop { .. }));
751        if let ForwardDecision::Drop { reason } = d {
752            assert!(reason.contains("ttl"));
753        }
754    }
755
756    #[test]
757    fn test_route_drops_on_zero_ttl() {
758        let mut r = default_router();
759        r.subscribe("p2", &topic("t"), 1);
760        let msg = make_msg(b"x", &topic("t"), "p1", 0, 1000);
761        assert!(matches!(r.route(&msg, 1000), ForwardDecision::Drop { .. }));
762    }
763
764    #[test]
765    fn test_route_forwards_with_ttl_above_min() {
766        let mut r = default_router();
767        r.subscribe("p2", &topic("t"), 1);
768        let msg = make_msg(b"x", &topic("t"), "p1", 2, 1000); // ttl = 2 > min_ttl = 1
769        assert!(matches!(
770            r.route(&msg, 1000),
771            ForwardDecision::Forward { .. }
772        ));
773    }
774
775    // ── 8. route — no subscribers ─────────────────────────────────────────────
776
777    #[test]
778    fn test_route_drops_on_no_subscribers() {
779        let mut r = default_router();
780        let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
781        let d = r.route(&msg, 1000);
782        assert!(matches!(d, ForwardDecision::Drop { .. }));
783        if let ForwardDecision::Drop { reason } = d {
784            assert!(reason.contains("no subscribers"));
785        }
786    }
787
788    // ── 9. route — loopback ───────────────────────────────────────────────────
789
790    #[test]
791    fn test_route_loopback_when_origin_only_subscriber() {
792        let mut r = default_router();
793        r.subscribe("p1", &topic("t"), 1); // only p1 subscribed
794        let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000); // p1 is origin
795        let d = r.route(&msg, 1000);
796        assert_eq!(d, ForwardDecision::Loopback);
797        assert_eq!(r.stats().messages_looped, 1);
798    }
799
800    // ── 10. route — forward excludes origin ───────────────────────────────────
801
802    #[test]
803    fn test_route_excludes_origin_from_forward_set() {
804        let mut r = default_router();
805        r.subscribe("p1", &topic("t"), 1);
806        r.subscribe("p2", &topic("t"), 2);
807        r.subscribe("p3", &topic("t"), 3);
808        let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
809        if let ForwardDecision::Forward { to_peers } = r.route(&msg, 1000) {
810            assert!(!to_peers.contains(&"p1".to_owned()));
811            assert!(to_peers.contains(&"p2".to_owned()));
812            assert!(to_peers.contains(&"p3".to_owned()));
813        } else {
814            panic!("expected Forward");
815        }
816    }
817
818    // ── 11. route — loop prevention via forwarded_to ──────────────────────────
819
820    #[test]
821    fn test_route_excludes_already_forwarded_peers() {
822        let mut r = default_router();
823        r.subscribe("p2", &topic("t"), 1);
824        r.subscribe("p3", &topic("t"), 2);
825        let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
826        // First route: should forward to p2 and p3.
827        if let ForwardDecision::Forward { to_peers } = r.route(&msg, 1000) {
828            assert_eq!(to_peers.len(), 2);
829        } else {
830            panic!("expected Forward on first call");
831        }
832        // Second route with a different ID but same peers (simulate another msg).
833        let msg2 = make_msg(b"y", &topic("t"), "p1", 5, 1001);
834        // p2 and p3 have NOT been forwarded msg2 yet — should forward.
835        if let ForwardDecision::Forward { to_peers } = r.route(&msg2, 1001) {
836            assert!(to_peers.contains(&"p2".to_owned()) || to_peers.contains(&"p3".to_owned()));
837        } else {
838            panic!("expected Forward for second distinct message");
839        }
840    }
841
842    // ── 12. mark_forwarded ────────────────────────────────────────────────────
843
844    #[test]
845    fn test_mark_forwarded_prevents_re_forward() {
846        let mut r = default_router();
847        r.subscribe("p2", &topic("t"), 1);
848        r.subscribe("p3", &topic("t"), 2);
849        let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
850        // Manually mark p2 as already forwarded.
851        r.mark_forwarded(&msg.id, "p2");
852        // Insert into cache manually so route() doesn't think it's a dup.
853        // (We call route, expecting only p3.)
854        if let ForwardDecision::Forward { to_peers } = r.route(&msg, 1000) {
855            assert!(!to_peers.contains(&"p2".to_owned()));
856            assert!(to_peers.contains(&"p3".to_owned()));
857        } else {
858            panic!("expected Forward");
859        }
860    }
861
862    // ── 13. expire_cache ──────────────────────────────────────────────────────
863
864    #[test]
865    fn test_expire_cache_removes_old_entries() {
866        let mut r = default_router();
867        r.subscribe("p2", &topic("t"), 1);
868        let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
869        r.route(&msg, 1000); // inserts into cache at t=1000
870        assert_eq!(r.stats().cache_size, 1);
871        // Expire with `now` = 1000 + dedup_window + 1 = 1061.
872        r.expire_cache(1061);
873        assert_eq!(r.stats().cache_size, 0);
874    }
875
876    #[test]
877    fn test_expire_cache_keeps_recent_entries() {
878        let mut r = default_router();
879        r.subscribe("p2", &topic("t"), 1);
880        let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
881        r.route(&msg, 1000);
882        // Expire at t=1050 (only 50 s elapsed, window=60 s → not expired).
883        r.expire_cache(1050);
884        assert_eq!(r.stats().cache_size, 1);
885    }
886
887    #[test]
888    fn test_expire_cache_clears_forwarded_to_map() {
889        let mut r = default_router();
890        r.subscribe("p2", &topic("t"), 1);
891        let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
892        r.route(&msg, 1000);
893        r.expire_cache(1061);
894        // After expiry the forwarded_to entry should also be gone.
895        assert_eq!(r.forwarded_count(&msg.id), 0);
896    }
897
898    // ── 14. stats counters ────────────────────────────────────────────────────
899
900    #[test]
901    fn test_stats_messages_forwarded_increments() {
902        let mut r = default_router();
903        r.subscribe("p2", &topic("t"), 1);
904        let msg = make_msg(b"a", &topic("t"), "p1", 5, 1000);
905        r.route(&msg, 1000);
906        assert_eq!(r.stats().messages_forwarded, 1);
907    }
908
909    #[test]
910    fn test_stats_messages_dropped_increments_on_dup() {
911        let mut r = default_router();
912        r.subscribe("p2", &topic("t"), 1);
913        let msg = make_msg(b"a", &topic("t"), "p1", 5, 1000);
914        r.route(&msg, 1000);
915        r.route(&msg, 1001);
916        assert_eq!(r.stats().messages_dropped, 1);
917    }
918
919    #[test]
920    fn test_stats_messages_dropped_increments_on_ttl() {
921        let mut r = default_router();
922        r.subscribe("p2", &topic("t"), 1);
923        let msg = make_msg(b"a", &topic("t"), "p1", 1, 1000);
924        r.route(&msg, 1000);
925        assert_eq!(r.stats().messages_dropped, 1);
926    }
927
928    // ── 15. compute_message_id ────────────────────────────────────────────────
929
930    #[test]
931    fn test_compute_message_id_is_deterministic() {
932        let t = topic("x");
933        let id1 = FloodSubRouter::compute_message_id(b"hello", &t, "peer1", 42);
934        let id2 = FloodSubRouter::compute_message_id(b"hello", &t, "peer1", 42);
935        assert_eq!(id1, id2);
936    }
937
938    #[test]
939    fn test_compute_message_id_differs_on_different_payload() {
940        let t = topic("x");
941        let id1 = FloodSubRouter::compute_message_id(b"hello", &t, "peer1", 42);
942        let id2 = FloodSubRouter::compute_message_id(b"world", &t, "peer1", 42);
943        assert_ne!(id1, id2);
944    }
945
946    #[test]
947    fn test_compute_message_id_differs_on_different_topic() {
948        let id1 = FloodSubRouter::compute_message_id(b"x", &topic("t1"), "peer1", 1);
949        let id2 = FloodSubRouter::compute_message_id(b"x", &topic("t2"), "peer1", 1);
950        assert_ne!(id1, id2);
951    }
952
953    #[test]
954    fn test_compute_message_id_differs_on_different_peer() {
955        let t = topic("t");
956        let id1 = FloodSubRouter::compute_message_id(b"x", &t, "p1", 1);
957        let id2 = FloodSubRouter::compute_message_id(b"x", &t, "p2", 1);
958        assert_ne!(id1, id2);
959    }
960
961    #[test]
962    fn test_compute_message_id_differs_on_different_timestamp() {
963        let t = topic("t");
964        let id1 = FloodSubRouter::compute_message_id(b"x", &t, "p1", 1);
965        let id2 = FloodSubRouter::compute_message_id(b"x", &t, "p1", 2);
966        assert_ne!(id1, id2);
967    }
968
969    #[test]
970    fn test_compute_message_id_is_16_bytes() {
971        let id = FloodSubRouter::compute_message_id(b"test", &topic("t"), "p", 0);
972        assert_eq!(id.as_bytes().len(), 16);
973    }
974
975    // ── 16. FloodMessageId helpers ────────────────────────────────────────────
976
977    #[test]
978    fn test_flood_message_id_to_hex_is_32_chars() {
979        let id = FloodSubRouter::compute_message_id(b"test", &topic("t"), "p", 0);
980        assert_eq!(id.to_hex().len(), 32);
981    }
982
983    #[test]
984    fn test_flood_message_id_display() {
985        let id = FloodMessageId([0u8; 16]);
986        assert_eq!(id.to_string(), "0".repeat(32));
987    }
988
989    // ── 17. FloodTopic helpers ────────────────────────────────────────────────
990
991    #[test]
992    fn test_flood_topic_as_str() {
993        let t = FloodTopic::new("sports");
994        assert_eq!(t.as_str(), "sports");
995    }
996
997    #[test]
998    fn test_flood_topic_display() {
999        let t = FloodTopic::new("sports");
1000        assert_eq!(t.to_string(), "sports");
1001    }
1002
1003    #[test]
1004    fn test_flood_topic_equality() {
1005        assert_eq!(FloodTopic::new("a"), FloodTopic::new("a"));
1006        assert_ne!(FloodTopic::new("a"), FloodTopic::new("b"));
1007    }
1008
1009    // ── 18. Multi-topic routing ───────────────────────────────────────────────
1010
1011    #[test]
1012    fn test_route_only_delivers_to_topic_subscribers() {
1013        let mut r = default_router();
1014        r.subscribe("p2", &topic("sports"), 1);
1015        r.subscribe("p3", &topic("news"), 2);
1016        let msg = make_msg(b"goal!", &topic("sports"), "p1", 5, 1000);
1017        if let ForwardDecision::Forward { to_peers } = r.route(&msg, 1000) {
1018            assert!(to_peers.contains(&"p2".to_owned()));
1019            assert!(!to_peers.contains(&"p3".to_owned()));
1020        } else {
1021            panic!("expected Forward");
1022        }
1023    }
1024
1025    // ── 19. Subscription record message_count ─────────────────────────────────
1026
1027    #[test]
1028    fn test_subscription_record_message_count_increments() {
1029        let mut r = default_router();
1030        r.subscribe("p2", &topic("t"), 1);
1031        let msg1 = make_msg(b"a", &topic("t"), "p1", 5, 1000);
1032        let msg2 = make_msg(b"b", &topic("t"), "p1", 5, 1001);
1033        r.route(&msg1, 1000);
1034        r.route(&msg2, 1001);
1035        let rec = r.subscription("p2", &topic("t")).expect("must exist");
1036        assert_eq!(rec.message_count, 2);
1037    }
1038
1039    // ── 20. is_subscribed ─────────────────────────────────────────────────────
1040
1041    #[test]
1042    fn test_is_subscribed_true_after_subscribe() {
1043        let mut r = default_router();
1044        r.subscribe("p1", &topic("t"), 1);
1045        assert!(r.is_subscribed("p1", &topic("t")));
1046    }
1047
1048    #[test]
1049    fn test_is_subscribed_false_before_subscribe() {
1050        let r = default_router();
1051        assert!(!r.is_subscribed("p1", &topic("t")));
1052    }
1053
1054    #[test]
1055    fn test_is_subscribed_false_after_unsubscribe() {
1056        let mut r = default_router();
1057        r.subscribe("p1", &topic("t"), 1);
1058        r.unsubscribe("p1", &topic("t"));
1059        assert!(!r.is_subscribed("p1", &topic("t")));
1060    }
1061
1062    // ── 21. cache eviction under max_message_cache ────────────────────────────
1063
1064    #[test]
1065    fn test_cache_does_not_exceed_max_message_cache() {
1066        let config = RouterConfig {
1067            max_message_cache: 3,
1068            ..Default::default()
1069        };
1070        let mut r = FloodSubRouter::new(config);
1071        r.subscribe("p2", &topic("t"), 1);
1072        for i in 0u64..5 {
1073            let msg = make_msg(
1074                format!("payload{i}").as_bytes(),
1075                &topic("t"),
1076                "p1",
1077                5,
1078                1000 + i,
1079            );
1080            r.route(&msg, 1000 + i);
1081        }
1082        // Cache should never grow beyond max_message_cache.
1083        assert!(r.stats().cache_size <= 3);
1084    }
1085
1086    // ── 22. forwarded_count ───────────────────────────────────────────────────
1087
1088    #[test]
1089    fn test_forwarded_count_zero_before_route() {
1090        let r = default_router();
1091        let id = FloodSubRouter::compute_message_id(b"x", &topic("t"), "p", 1);
1092        assert_eq!(r.forwarded_count(&FloodMessageId(id.0)), 0);
1093    }
1094
1095    #[test]
1096    fn test_forwarded_count_after_route() {
1097        let mut r = default_router();
1098        r.subscribe("p2", &topic("t"), 1);
1099        r.subscribe("p3", &topic("t"), 2);
1100        let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
1101        r.route(&msg, 1000);
1102        assert_eq!(r.forwarded_count(&msg.id), 2);
1103    }
1104
1105    // ── 23. cache_full ────────────────────────────────────────────────────────
1106
1107    #[test]
1108    fn test_cache_full_returns_false_when_empty() {
1109        let r = default_router();
1110        assert!(!r.cache_full());
1111    }
1112
1113    #[test]
1114    fn test_cache_full_returns_true_at_capacity() {
1115        let config = RouterConfig {
1116            max_message_cache: 1,
1117            ..Default::default()
1118        };
1119        let mut r = FloodSubRouter::new(config);
1120        r.subscribe("p2", &topic("t"), 1);
1121        let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
1122        r.route(&msg, 1000);
1123        assert!(r.cache_full());
1124    }
1125
1126    // ── 24. subscription_count ────────────────────────────────────────────────
1127
1128    #[test]
1129    fn test_subscription_count_matches_stats() {
1130        let mut r = default_router();
1131        r.subscribe("p1", &topic("a"), 1);
1132        r.subscribe("p2", &topic("a"), 2);
1133        r.subscribe("p1", &topic("b"), 3);
1134        assert_eq!(r.subscription_count(), r.stats().subscriptions);
1135        assert_eq!(r.subscription_count(), 3);
1136    }
1137
1138    // ── 25. all_subscriptions iterator ───────────────────────────────────────
1139
1140    #[test]
1141    fn test_all_subscriptions_iterator_count() {
1142        let mut r = default_router();
1143        r.subscribe("p1", &topic("t1"), 1);
1144        r.subscribe("p2", &topic("t1"), 2);
1145        r.subscribe("p1", &topic("t2"), 3);
1146        assert_eq!(r.all_subscriptions().count(), 3);
1147    }
1148
1149    // ── 26. xorshift64 PRNG sanity ────────────────────────────────────────────
1150
1151    #[test]
1152    fn test_xorshift64_produces_different_values() {
1153        let mut state: u64 = 0xDEAD_BEEF_CAFE_1234;
1154        let v1 = xorshift64(&mut state);
1155        let v2 = xorshift64(&mut state);
1156        let v3 = xorshift64(&mut state);
1157        // Values must differ (practically guaranteed for any non-zero seed).
1158        assert_ne!(v1, v2);
1159        assert_ne!(v2, v3);
1160    }
1161
1162    #[test]
1163    fn test_xorshift64_is_deterministic() {
1164        let mut s1: u64 = 42;
1165        let mut s2: u64 = 42;
1166        assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
1167    }
1168
1169    // ── 27. Drop reason for all-forwarded peers ───────────────────────────────
1170
1171    #[test]
1172    fn test_route_drops_when_all_peers_already_forwarded() {
1173        let mut r = default_router();
1174        r.subscribe("p2", &topic("t"), 1);
1175        let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
1176        // Mark p2 as already forwarded before routing.
1177        r.mark_forwarded(&msg.id, "p2");
1178        // Also need msg.id to NOT be in dedup cache yet.
1179        // route() will see no new peers → Drop (all already forwarded).
1180        let d = r.route(&msg, 1000);
1181        assert!(matches!(d, ForwardDecision::Drop { .. }));
1182        if let ForwardDecision::Drop { reason } = d {
1183            assert!(reason.contains("already received") || reason.contains("already"));
1184        }
1185    }
1186
1187    // ── 28. multiple expire rounds ────────────────────────────────────────────
1188
1189    #[test]
1190    fn test_multiple_expire_rounds_clean_incrementally() {
1191        let mut r = default_router();
1192        r.subscribe("p2", &topic("t"), 1);
1193        let msg1 = make_msg(b"first", &topic("t"), "p1", 5, 1000);
1194        let msg2 = make_msg(b"second", &topic("t"), "p1", 5, 1050);
1195        r.route(&msg1, 1000);
1196        r.route(&msg2, 1050);
1197        assert_eq!(r.stats().cache_size, 2);
1198        // Expire at t=1061: only msg1 (created_at=1000, window=60, cutoff=1001) expires.
1199        r.expire_cache(1061);
1200        assert_eq!(r.stats().cache_size, 1);
1201        // Expire at t=1111: msg2 (created_at=1050, cutoff=1051) expires.
1202        r.expire_cache(1111);
1203        assert_eq!(r.stats().cache_size, 0);
1204    }
1205
1206    // ── 29. RouterConfig default values ──────────────────────────────────────
1207
1208    #[test]
1209    fn test_router_config_defaults() {
1210        let cfg = RouterConfig::default();
1211        assert_eq!(cfg.max_peers_per_topic, 100);
1212        assert_eq!(cfg.max_message_cache, 10_000);
1213        assert_eq!(cfg.min_ttl, 1);
1214        assert_eq!(cfg.dedup_window_secs, 60);
1215        assert_eq!(cfg.max_topics, 256);
1216    }
1217
1218    // ── 30. FsrRouterStats default values ────────────────────────────────────
1219
1220    #[test]
1221    fn test_fsr_router_stats_defaults() {
1222        let s = FsrRouterStats::default();
1223        assert_eq!(s.messages_forwarded, 0);
1224        assert_eq!(s.messages_dropped, 0);
1225        assert_eq!(s.messages_looped, 0);
1226        assert_eq!(s.subscriptions, 0);
1227        assert_eq!(s.topics, 0);
1228        assert_eq!(s.cache_size, 0);
1229    }
1230
1231    // ── 31. Peer with many topics respects max_topics boundary ───────────────
1232
1233    #[test]
1234    fn test_subscribe_at_exactly_max_topics() {
1235        let config = RouterConfig {
1236            max_topics: 3,
1237            ..Default::default()
1238        };
1239        let mut r = FloodSubRouter::new(config);
1240        assert!(r.subscribe("p1", &topic("t1"), 1));
1241        assert!(r.subscribe("p1", &topic("t2"), 2));
1242        assert!(r.subscribe("p1", &topic("t3"), 3));
1243        // Same peer, new topic — must be rejected because we are already at 3.
1244        assert!(!r.subscribe("p1", &topic("t4"), 4));
1245    }
1246
1247    // ── 32. Resubscribe after unsubscribe is allowed ──────────────────────────
1248
1249    #[test]
1250    fn test_resubscribe_after_unsubscribe() {
1251        let mut r = default_router();
1252        r.subscribe("p1", &topic("t"), 1);
1253        r.unsubscribe("p1", &topic("t"));
1254        assert!(r.subscribe("p1", &topic("t"), 2));
1255    }
1256
1257    // ── 33. Stats cache_size reflects expire ──────────────────────────────────
1258
1259    #[test]
1260    fn test_stats_cache_size_reflects_expire() {
1261        let mut r = default_router();
1262        r.subscribe("p2", &topic("t"), 1);
1263        for i in 0u64..5 {
1264            let msg = make_msg(format!("m{i}").as_bytes(), &topic("t"), "p1", 5, 1000 + i);
1265            r.route(&msg, 1000 + i);
1266        }
1267        assert_eq!(r.stats().cache_size, 5);
1268        r.expire_cache(1100);
1269        assert_eq!(r.stats().cache_size, 0);
1270    }
1271
1272    // ── 34. Route after cache expiry allows re-routing same message ───────────
1273
1274    #[test]
1275    fn test_can_reroute_message_after_cache_expiry() {
1276        let mut r = default_router();
1277        r.subscribe("p2", &topic("t"), 1);
1278        let msg = make_msg(b"x", &topic("t"), "p1", 5, 1000);
1279        r.route(&msg, 1000);
1280        r.expire_cache(1100); // Expire the entry.
1281                              // After expiry, the same message ID should be routable again.
1282        let d = r.route(&msg, 1100);
1283        assert!(matches!(d, ForwardDecision::Forward { .. }));
1284    }
1285
1286    // ── 35. Forward to multiple peers updates all message_counts ─────────────
1287
1288    #[test]
1289    fn test_forward_to_multiple_peers_increments_all_counts() {
1290        let mut r = default_router();
1291        for i in 1u8..=5 {
1292            r.subscribe(&format!("p{i}"), &topic("t"), i as u64);
1293        }
1294        let msg = make_msg(b"broadcast", &topic("t"), "p_origin", 5, 1000);
1295        r.route(&msg, 1000);
1296        for i in 1u8..=5 {
1297            let rec = r
1298                .subscription(&format!("p{i}"), &topic("t"))
1299                .expect("must exist");
1300            assert_eq!(rec.message_count, 1, "p{i} should have count 1");
1301        }
1302    }
1303}