Skip to main content

ipfrs_network/
subscription_router.rs

1//! SubscriptionRouter — topic/type-based message routing with subscription
2//! management, filter evaluation, and delivery tracking.
3//!
4//! Routes incoming [`RoutingMessage`]s to registered [`Subscription`]s using
5//! topic matching and filter evaluation.  Deliveries are tracked in a bounded
6//! [`VecDeque`]-backed log.
7//!
8//! ## Design highlights
9//!
10//! - **Topic matching**: exact match or wildcard (`*`).
11//! - **Filter evaluation**: composable [`SubscriptionFilter`] predicates.
12//! - **Delivery log**: bounded at a configurable capacity; oldest entries are
13//!   evicted when full.
14//! - **No `unwrap()`**: all fallible operations surface errors explicitly.
15
16use std::collections::{HashMap, VecDeque};
17
18// ═══════════════════════════════════════════════════════════════════════════════
19// SubscriptionRouter — topic/type-based message routing with subscription
20// management, filter evaluation, and delivery tracking.
21// ═══════════════════════════════════════════════════════════════════════════════
22
23// ─── FNV-1a helpers ───────────────────────────────────────────────────────────
24
25/// Compute an FNV-1a 64-bit hash over an arbitrary byte slice.
26pub fn fnv1a_64(data: &[u8]) -> u64 {
27    const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
28    const FNV_PRIME: u64 = 1_099_511_628_211;
29    let mut hash = FNV_OFFSET;
30    for &byte in data {
31        hash ^= u64::from(byte);
32        hash = hash.wrapping_mul(FNV_PRIME);
33    }
34    hash
35}
36
37/// Hash multiple string slices concatenated together.
38fn fnv1a_strings(parts: &[&str]) -> u64 {
39    const FNV_OFFSET: u64 = 14_695_981_039_346_656_037;
40    const FNV_PRIME: u64 = 1_099_511_628_211;
41    let mut hash = FNV_OFFSET;
42    for part in parts {
43        for &byte in part.as_bytes() {
44            hash ^= u64::from(byte);
45            hash = hash.wrapping_mul(FNV_PRIME);
46        }
47    }
48    hash
49}
50
51// ─── MessageTopic ─────────────────────────────────────────────────────────────
52
53/// Newtype wrapper for topic identifiers used by [`SubscriptionRouter`].
54#[derive(Debug, Clone, PartialEq, Eq, Hash)]
55pub struct MessageTopic(pub String);
56
57impl MessageTopic {
58    /// Create a new [`MessageTopic`] from any string-like value.
59    pub fn new(topic: impl Into<String>) -> Self {
60        Self(topic.into())
61    }
62
63    /// Return the inner string as a `&str`.
64    pub fn as_str(&self) -> &str {
65        &self.0
66    }
67}
68
69impl std::fmt::Display for MessageTopic {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.write_str(&self.0)
72    }
73}
74
75// ─── RoutingMessage ───────────────────────────────────────────────────────────
76
77/// A message travelling through [`SubscriptionRouter`].
78///
79/// The `id` field is computed as the FNV-1a hash of `sender + topic.0 +
80/// timestamp.to_string()` at construction time.
81#[derive(Debug, Clone)]
82pub struct RoutingMessage {
83    /// FNV-1a hash of `(sender + topic + timestamp)`.
84    pub id: u64,
85    /// Topic this message belongs to.
86    pub topic: MessageTopic,
87    /// Number of bytes in the logical payload.
88    pub payload_size: usize,
89    /// Originating peer identifier.
90    pub sender: String,
91    /// Wall-clock millisecond timestamp at which the message was created.
92    pub timestamp: u64,
93    /// Remaining hops; the router drops messages where `ttl == 0` before
94    /// delivering them.
95    pub ttl: u8,
96    /// Priority: 0 = lowest, 255 = highest.
97    pub priority: u8,
98}
99
100impl RoutingMessage {
101    /// Create a new [`RoutingMessage`], computing its `id` automatically.
102    pub fn new(
103        topic: MessageTopic,
104        payload_size: usize,
105        sender: impl Into<String>,
106        timestamp: u64,
107        ttl: u8,
108        priority: u8,
109    ) -> Self {
110        let sender = sender.into();
111        let ts_str = timestamp.to_string();
112        let id = fnv1a_strings(&[sender.as_str(), topic.as_str(), ts_str.as_str()]);
113        Self {
114            id,
115            topic,
116            payload_size,
117            sender,
118            timestamp,
119            ttl,
120            priority,
121        }
122    }
123}
124
125// ─── SubscriptionFilter ───────────────────────────────────────────────────────
126
127/// A composable predicate for filtering [`RoutingMessage`]s.
128///
129/// Filters are evaluated recursively; compound variants (`And`, `Or`) enable
130/// arbitrary boolean combinations.
131#[derive(Debug, Clone)]
132pub enum SubscriptionFilter {
133    /// Accept every message regardless of content.
134    All,
135    /// Accept only messages from the specified sender.
136    BySender(String),
137    /// Accept messages whose `priority` is ≥ `min`.
138    ByPriority { min: u8 },
139    /// Accept messages whose `payload_size` is ≤ `max_bytes`.
140    BySize { max_bytes: usize },
141    /// Accept messages where **both** sub-filters accept.
142    And(Box<SubscriptionFilter>, Box<SubscriptionFilter>),
143    /// Accept messages where **either** sub-filter accepts.
144    Or(Box<SubscriptionFilter>, Box<SubscriptionFilter>),
145}
146
147impl SubscriptionFilter {
148    /// Convenience constructor for [`SubscriptionFilter::And`].
149    pub fn and(left: SubscriptionFilter, right: SubscriptionFilter) -> Self {
150        Self::And(Box::new(left), Box::new(right))
151    }
152
153    /// Convenience constructor for [`SubscriptionFilter::Or`].
154    pub fn or(left: SubscriptionFilter, right: SubscriptionFilter) -> Self {
155        Self::Or(Box::new(left), Box::new(right))
156    }
157}
158
159// ─── Subscription ─────────────────────────────────────────────────────────────
160
161/// A subscriber registration in [`SubscriptionRouter`].
162///
163/// The `id` is the FNV-1a hash of `(peer_id + topic.0 + created_at.to_string())`.
164#[derive(Debug, Clone)]
165pub struct Subscription {
166    /// Stable identifier for this subscription.
167    pub id: u64,
168    /// Peer that owns this subscription.
169    pub peer_id: String,
170    /// Topic this subscription is interested in.
171    pub topic: MessageTopic,
172    /// Additional predicate applied after topic matching.
173    pub filter: SubscriptionFilter,
174    /// Millisecond timestamp at which this subscription was created.
175    pub created_at: u64,
176    /// Number of messages successfully delivered to this subscription.
177    pub message_count: u64,
178}
179
180impl Subscription {
181    /// Create a new [`Subscription`], computing its `id` automatically.
182    pub fn new(
183        peer_id: impl Into<String>,
184        topic: MessageTopic,
185        filter: SubscriptionFilter,
186        created_at: u64,
187    ) -> Self {
188        let peer_id = peer_id.into();
189        let ts_str = created_at.to_string();
190        let id = fnv1a_strings(&[peer_id.as_str(), topic.as_str(), ts_str.as_str()]);
191        Self {
192            id,
193            peer_id,
194            topic,
195            filter,
196            created_at,
197            message_count: 0,
198        }
199    }
200}
201
202// ─── DeliveryRecord ───────────────────────────────────────────────────────────
203
204/// Audit record written whenever the router attempts delivery.
205#[derive(Debug, Clone)]
206pub struct DeliveryRecord {
207    /// ID of the [`RoutingMessage`] that was delivered (or dropped).
208    pub message_id: u64,
209    /// ID of the [`Subscription`] the router attempted to deliver to.
210    pub subscription_id: u64,
211    /// Millisecond timestamp of the delivery attempt.
212    pub delivered_at: u64,
213    /// `true` if the delivery was counted as successful, `false` otherwise.
214    pub success: bool,
215}
216
217// ─── SubRouterStats ───────────────────────────────────────────────────────────
218
219/// Point-in-time statistics snapshot for [`SubscriptionRouter`].
220///
221/// Named `SubRouterStats` to avoid collision with the existing `RouterStats`
222/// type in this module.
223#[derive(Debug, Clone, PartialEq)]
224pub struct SubRouterStats {
225    /// Number of currently active subscriptions.
226    pub total_subscriptions: usize,
227    /// Total messages passed to [`SubscriptionRouter::route`].
228    pub total_routed: u64,
229    /// Total successful deliveries across all subscriptions.
230    pub total_delivered: u64,
231    /// Total messages dropped (TTL == 0 on arrival or no matching subscription).
232    pub total_dropped: u64,
233    /// Ratio of delivered to (delivered + dropped); `1.0` when both are zero.
234    pub delivery_rate: f64,
235    /// Number of distinct topics with at least one active subscription.
236    pub topics: usize,
237}
238
239// ─── SubscriptionRouter ───────────────────────────────────────────────────────
240
241/// Topic/type-based message routing engine.
242///
243/// Manages subscriber registrations, evaluates [`SubscriptionFilter`] predicates,
244/// logs delivery attempts to a bounded ring-buffer, and exposes rich query and
245/// eviction helpers.
246///
247/// # Design
248///
249/// - Messages with `ttl == 0` on arrival are immediately dropped (counted in
250///   `total_dropped`); their TTL is *not* decremented because the router does
251///   not modify the caller-owned message.
252/// - The delivery log is a [`VecDeque`] capped at `max_log_size`.  Oldest
253///   entries are evicted when the cap is reached.
254/// - All statistics counters are plain `u64` fields (not atomic) because the
255///   router is designed for single-threaded or externally-synchronized use.
256///   Wrap in `Arc<Mutex<_>>` if concurrent access is required.
257pub struct SubscriptionRouter {
258    subscriptions: HashMap<u64, Subscription>,
259    delivery_log: VecDeque<DeliveryRecord>,
260    max_log_size: usize,
261    /// Total messages passed to [`route`](Self::route).
262    pub total_routed: u64,
263    /// Total successful deliveries.
264    pub total_delivered: u64,
265    /// Total dropped messages.
266    pub total_dropped: u64,
267}
268
269impl SubscriptionRouter {
270    /// Create a new [`SubscriptionRouter`] with a delivery log bounded at
271    /// `max_log_size` entries.
272    pub fn new(max_log_size: usize) -> Self {
273        Self {
274            subscriptions: HashMap::new(),
275            delivery_log: VecDeque::new(),
276            max_log_size,
277            total_routed: 0,
278            total_delivered: 0,
279            total_dropped: 0,
280        }
281    }
282
283    // ── Subscription management ───────────────────────────────────────────────
284
285    /// Register a subscription and return its stable ID.
286    ///
287    /// If a subscription with the same derived ID already exists it is
288    /// silently replaced.
289    pub fn subscribe(
290        &mut self,
291        peer_id: String,
292        topic: MessageTopic,
293        filter: SubscriptionFilter,
294        now: u64,
295    ) -> u64 {
296        let sub = Subscription::new(peer_id, topic, filter, now);
297        let id = sub.id;
298        self.subscriptions.insert(id, sub);
299        id
300    }
301
302    /// Remove the subscription with the given `subscription_id`.
303    ///
304    /// Returns `true` if a subscription was found and removed, `false` if none
305    /// existed with that ID.
306    pub fn unsubscribe(&mut self, subscription_id: u64) -> bool {
307        self.subscriptions.remove(&subscription_id).is_some()
308    }
309
310    // ── Routing ───────────────────────────────────────────────────────────────
311
312    /// Route `message` to all matching subscriptions.
313    ///
314    /// Rules applied in order:
315    /// 1. If `message.ttl == 0` the message is immediately dropped (counted in
316    ///    `total_dropped`); an empty `Vec` is returned.
317    /// 2. For every subscription, [`Self::matches`] is called.  Matching
318    ///    subscriptions have their `message_count` incremented and receive a
319    ///    successful [`DeliveryRecord`].
320    /// 3. If no subscriptions match, `total_dropped` is incremented and a
321    ///    failing [`DeliveryRecord`] with `subscription_id = 0` is appended.
322    ///
323    /// Returns the list of subscription IDs that were delivered to.
324    pub fn route(&mut self, message: &RoutingMessage, now: u64) -> Vec<u64> {
325        self.total_routed = self.total_routed.saturating_add(1);
326
327        // Drop expired messages (TTL == 0).
328        if message.ttl == 0 {
329            self.total_dropped = self.total_dropped.saturating_add(1);
330            self.push_delivery_record(DeliveryRecord {
331                message_id: message.id,
332                subscription_id: 0,
333                delivered_at: now,
334                success: false,
335            });
336            return Vec::new();
337        }
338
339        // Collect matching subscription IDs without borrowing mutably yet.
340        let matching: Vec<u64> = self
341            .subscriptions
342            .values()
343            .filter(|sub| Self::matches_static(sub, message))
344            .map(|sub| sub.id)
345            .collect();
346
347        if matching.is_empty() {
348            self.total_dropped = self.total_dropped.saturating_add(1);
349            self.push_delivery_record(DeliveryRecord {
350                message_id: message.id,
351                subscription_id: 0,
352                delivered_at: now,
353                success: false,
354            });
355            return Vec::new();
356        }
357
358        // Deliver to each matching subscription.
359        for &sub_id in &matching {
360            if let Some(sub) = self.subscriptions.get_mut(&sub_id) {
361                sub.message_count = sub.message_count.saturating_add(1);
362            }
363            self.total_delivered = self.total_delivered.saturating_add(1);
364            self.push_delivery_record(DeliveryRecord {
365                message_id: message.id,
366                subscription_id: sub_id,
367                delivered_at: now,
368                success: true,
369            });
370        }
371
372        matching
373    }
374
375    /// Append a [`DeliveryRecord`] to the ring-buffer, evicting the oldest
376    /// entry if the buffer is at capacity.
377    fn push_delivery_record(&mut self, record: DeliveryRecord) {
378        if self.max_log_size == 0 {
379            return;
380        }
381        if self.delivery_log.len() >= self.max_log_size {
382            self.delivery_log.pop_front();
383        }
384        self.delivery_log.push_back(record);
385    }
386
387    // ── Filter evaluation ────────────────────────────────────────────────────
388
389    /// Return `true` when `sub`'s topic matches `msg`'s topic **and** the
390    /// subscription's filter accepts the message.
391    ///
392    /// This is a `pub` method as required by the specification.
393    pub fn matches(sub: &Subscription, msg: &RoutingMessage) -> bool {
394        Self::matches_static(sub, msg)
395    }
396
397    /// Internal implementation used in both `matches` and `route` (avoids
398    /// borrow-checker issues when calling from `&mut self` contexts).
399    fn matches_static(sub: &Subscription, msg: &RoutingMessage) -> bool {
400        if sub.topic != msg.topic {
401            return false;
402        }
403        Self::filter_matches(&sub.filter, msg)
404    }
405
406    /// Recursively evaluate `filter` against `msg`.
407    ///
408    /// This is a `pub` method as required by the specification.
409    pub fn filter_matches(filter: &SubscriptionFilter, msg: &RoutingMessage) -> bool {
410        match filter {
411            SubscriptionFilter::All => true,
412            SubscriptionFilter::BySender(sender) => &msg.sender == sender,
413            SubscriptionFilter::ByPriority { min } => msg.priority >= *min,
414            SubscriptionFilter::BySize { max_bytes } => msg.payload_size <= *max_bytes,
415            SubscriptionFilter::And(left, right) => {
416                Self::filter_matches(left, msg) && Self::filter_matches(right, msg)
417            }
418            SubscriptionFilter::Or(left, right) => {
419                Self::filter_matches(left, msg) || Self::filter_matches(right, msg)
420            }
421        }
422    }
423
424    // ── Query helpers ────────────────────────────────────────────────────────
425
426    /// Return references to all active subscriptions for `topic`.
427    pub fn subscriptions_for_topic(&self, topic: &MessageTopic) -> Vec<&Subscription> {
428        self.subscriptions
429            .values()
430            .filter(|sub| &sub.topic == topic)
431            .collect()
432    }
433
434    /// Return references to all subscriptions owned by `peer_id`.
435    pub fn subscriptions_for_peer(&self, peer_id: &str) -> Vec<&Subscription> {
436        self.subscriptions
437            .values()
438            .filter(|sub| sub.peer_id == peer_id)
439            .collect()
440    }
441
442    /// Compute the delivery rate over records whose `delivered_at >= since`.
443    ///
444    /// Returns `1.0` when there are no matching records.
445    pub fn delivery_rate(&self, since: u64) -> f64 {
446        let relevant: Vec<&DeliveryRecord> = self
447            .delivery_log
448            .iter()
449            .filter(|r| r.delivered_at >= since)
450            .collect();
451
452        if relevant.is_empty() {
453            return 1.0;
454        }
455
456        let delivered = relevant.iter().filter(|r| r.success).count() as f64;
457        let total = relevant.len() as f64;
458        delivered / total
459    }
460
461    /// Return references to the `n` most-recent delivery records.
462    ///
463    /// If fewer than `n` records exist, all are returned.
464    pub fn recent_deliveries(&self, n: usize) -> Vec<&DeliveryRecord> {
465        let len = self.delivery_log.len();
466        let skip = len.saturating_sub(n);
467        self.delivery_log.iter().skip(skip).collect()
468    }
469
470    /// Remove subscriptions that have never delivered a message and were
471    /// created more than `max_age_ms` milliseconds before `now`.
472    ///
473    /// Returns the number of subscriptions evicted.
474    pub fn evict_stale_subscriptions(&mut self, max_age_ms: u64, now: u64) -> usize {
475        let before = self.subscriptions.len();
476        self.subscriptions.retain(|_, sub| {
477            // Keep if it has delivered at least one message OR is still young.
478            sub.message_count > 0 || now.saturating_sub(sub.created_at) <= max_age_ms
479        });
480        before - self.subscriptions.len()
481    }
482
483    /// Return a [`SubRouterStats`] snapshot computed at the given logical
484    /// `now` timestamp (used only for the `delivery_rate` calculation).
485    pub fn stats(&self, now: u64) -> SubRouterStats {
486        let topics: std::collections::HashSet<&MessageTopic> =
487            self.subscriptions.values().map(|s| &s.topic).collect();
488
489        SubRouterStats {
490            total_subscriptions: self.subscriptions.len(),
491            total_routed: self.total_routed,
492            total_delivered: self.total_delivered,
493            total_dropped: self.total_dropped,
494            delivery_rate: self.delivery_rate(now),
495            topics: topics.len(),
496        }
497    }
498}
499
500// ─── Tests (SubscriptionRouter) ───────────────────────────────────────────────
501
502#[cfg(test)]
503mod subscription_router_tests {
504    use crate::subscription_router::{
505        MessageTopic, RoutingMessage, Subscription, SubscriptionFilter, SubscriptionRouter,
506    };
507
508    // ── Helpers ───────────────────────────────────────────────────────────────
509
510    fn topic(s: &str) -> MessageTopic {
511        MessageTopic::new(s)
512    }
513
514    fn msg(
515        topic: &str,
516        sender: &str,
517        payload_size: usize,
518        priority: u8,
519        ttl: u8,
520        timestamp: u64,
521    ) -> RoutingMessage {
522        RoutingMessage::new(
523            MessageTopic::new(topic),
524            payload_size,
525            sender,
526            timestamp,
527            ttl,
528            priority,
529        )
530    }
531
532    fn all_filter() -> SubscriptionFilter {
533        SubscriptionFilter::All
534    }
535
536    // ── Construction ──────────────────────────────────────────────────────────
537
538    // 1. new() starts empty
539    #[test]
540    fn test_new_starts_empty() {
541        let router = SubscriptionRouter::new(100);
542        assert_eq!(router.total_routed, 0);
543        assert_eq!(router.total_delivered, 0);
544        assert_eq!(router.total_dropped, 0);
545        let s = router.stats(0);
546        assert_eq!(s.total_subscriptions, 0);
547        assert_eq!(s.topics, 0);
548    }
549
550    // 2. new() with zero log size does not panic
551    #[test]
552    fn test_new_zero_log_size() {
553        let mut router = SubscriptionRouter::new(0);
554        let m = msg("t", "alice", 10, 5, 1, 100);
555        router.subscribe("peer".to_string(), topic("t"), all_filter(), 0);
556        router.route(&m, 0);
557        // No delivery records stored, no panic.
558        assert!(router.recent_deliveries(10).is_empty());
559    }
560
561    // ── Subscribe / Unsubscribe ────────────────────────────────────────────────
562
563    // 3. subscribe returns a non-zero ID
564    #[test]
565    fn test_subscribe_returns_nonzero_id() {
566        let mut router = SubscriptionRouter::new(100);
567        let id = router.subscribe("alice".to_string(), topic("news"), all_filter(), 1000);
568        assert_ne!(id, 0);
569    }
570
571    // 4. subscribe with different args yields distinct IDs
572    #[test]
573    fn test_subscribe_distinct_ids() {
574        let mut router = SubscriptionRouter::new(100);
575        let id1 = router.subscribe("alice".to_string(), topic("news"), all_filter(), 1000);
576        let id2 = router.subscribe("bob".to_string(), topic("news"), all_filter(), 1000);
577        assert_ne!(id1, id2);
578    }
579
580    // 5. unsubscribe returns true when found
581    #[test]
582    fn test_unsubscribe_returns_true() {
583        let mut router = SubscriptionRouter::new(100);
584        let id = router.subscribe("alice".to_string(), topic("news"), all_filter(), 1000);
585        assert!(router.unsubscribe(id));
586    }
587
588    // 6. unsubscribe returns false when not found
589    #[test]
590    fn test_unsubscribe_returns_false() {
591        let mut router = SubscriptionRouter::new(100);
592        assert!(!router.unsubscribe(999_999));
593    }
594
595    // 7. subscription count decrements after unsubscribe
596    #[test]
597    fn test_subscription_count_after_unsubscribe() {
598        let mut router = SubscriptionRouter::new(100);
599        let id = router.subscribe("alice".to_string(), topic("news"), all_filter(), 1000);
600        assert_eq!(router.stats(0).total_subscriptions, 1);
601        router.unsubscribe(id);
602        assert_eq!(router.stats(0).total_subscriptions, 0);
603    }
604
605    // ── Route: TTL == 0 ───────────────────────────────────────────────────────
606
607    // 8. TTL == 0 message is dropped immediately
608    #[test]
609    fn test_ttl_zero_dropped() {
610        let mut router = SubscriptionRouter::new(100);
611        router.subscribe("alice".to_string(), topic("t"), all_filter(), 0);
612        let m = msg("t", "sender", 10, 5, 0, 1000);
613        let delivered = router.route(&m, 1000);
614        assert!(delivered.is_empty());
615        assert_eq!(router.total_dropped, 1);
616        assert_eq!(router.total_delivered, 0);
617    }
618
619    // 9. TTL == 0 message leaves a failed delivery record
620    #[test]
621    fn test_ttl_zero_leaves_failed_record() {
622        let mut router = SubscriptionRouter::new(100);
623        let m = msg("t", "sender", 10, 5, 0, 1000);
624        router.route(&m, 2000);
625        let records = router.recent_deliveries(10);
626        assert_eq!(records.len(), 1);
627        assert!(!records[0].success);
628    }
629
630    // ── Route: matching ───────────────────────────────────────────────────────
631
632    // 10. Message with TTL > 0 delivered to matching subscriber
633    #[test]
634    fn test_basic_delivery() {
635        let mut router = SubscriptionRouter::new(100);
636        let id = router.subscribe("alice".to_string(), topic("news"), all_filter(), 0);
637        let m = msg("news", "bob", 100, 10, 3, 1000);
638        let delivered = router.route(&m, 1000);
639        assert_eq!(delivered, vec![id]);
640        assert_eq!(router.total_delivered, 1);
641    }
642
643    // 11. Message delivered to multiple matching subscribers
644    #[test]
645    fn test_multiple_subscribers_delivery() {
646        let mut router = SubscriptionRouter::new(100);
647        let id1 = router.subscribe("alice".to_string(), topic("news"), all_filter(), 0);
648        let id2 = router.subscribe("bob".to_string(), topic("news"), all_filter(), 0);
649        let m = msg("news", "carol", 50, 5, 1, 1000);
650        let mut delivered = router.route(&m, 1000);
651        delivered.sort_unstable();
652        let mut expected = vec![id1, id2];
653        expected.sort_unstable();
654        assert_eq!(delivered, expected);
655        assert_eq!(router.total_delivered, 2);
656    }
657
658    // 12. Message on wrong topic is not delivered
659    #[test]
660    fn test_wrong_topic_not_delivered() {
661        let mut router = SubscriptionRouter::new(100);
662        router.subscribe("alice".to_string(), topic("sports"), all_filter(), 0);
663        let m = msg("news", "bob", 50, 5, 1, 1000);
664        let delivered = router.route(&m, 1000);
665        assert!(delivered.is_empty());
666        assert_eq!(router.total_dropped, 1);
667    }
668
669    // 13. message_count increments on delivery
670    #[test]
671    fn test_message_count_increments() {
672        let mut router = SubscriptionRouter::new(100);
673        let id = router.subscribe("alice".to_string(), topic("t"), all_filter(), 0);
674        let m = msg("t", "bob", 10, 1, 1, 100);
675        router.route(&m, 100);
676        router.route(&m, 200);
677        let sub = router.subscriptions.get(&id).expect("sub exists");
678        assert_eq!(sub.message_count, 2);
679    }
680
681    // ── Filter: BySender ──────────────────────────────────────────────────────
682
683    // 14. BySender filter accepts matching sender
684    #[test]
685    fn test_by_sender_accepts() {
686        let mut router = SubscriptionRouter::new(100);
687        let filter = SubscriptionFilter::BySender("alice".to_string());
688        router.subscribe("peer".to_string(), topic("t"), filter, 0);
689        let m = msg("t", "alice", 10, 5, 1, 100);
690        let delivered = router.route(&m, 100);
691        assert_eq!(delivered.len(), 1);
692    }
693
694    // 15. BySender filter rejects non-matching sender
695    #[test]
696    fn test_by_sender_rejects() {
697        let mut router = SubscriptionRouter::new(100);
698        let filter = SubscriptionFilter::BySender("alice".to_string());
699        router.subscribe("peer".to_string(), topic("t"), filter, 0);
700        let m = msg("t", "bob", 10, 5, 1, 100);
701        let delivered = router.route(&m, 100);
702        assert!(delivered.is_empty());
703    }
704
705    // ── Filter: ByPriority ────────────────────────────────────────────────────
706
707    // 16. ByPriority accepts message with priority >= min
708    #[test]
709    fn test_by_priority_accepts() {
710        let mut router = SubscriptionRouter::new(100);
711        router.subscribe(
712            "p".to_string(),
713            topic("t"),
714            SubscriptionFilter::ByPriority { min: 5 },
715            0,
716        );
717        let m = msg("t", "s", 10, 5, 1, 100);
718        assert_eq!(router.route(&m, 100).len(), 1);
719    }
720
721    // 17. ByPriority rejects message with priority < min
722    #[test]
723    fn test_by_priority_rejects() {
724        let mut router = SubscriptionRouter::new(100);
725        router.subscribe(
726            "p".to_string(),
727            topic("t"),
728            SubscriptionFilter::ByPriority { min: 10 },
729            0,
730        );
731        let m = msg("t", "s", 10, 5, 1, 100);
732        assert!(router.route(&m, 100).is_empty());
733    }
734
735    // ── Filter: BySize ────────────────────────────────────────────────────────
736
737    // 18. BySize accepts message with payload_size <= max_bytes
738    #[test]
739    fn test_by_size_accepts() {
740        let mut router = SubscriptionRouter::new(100);
741        router.subscribe(
742            "p".to_string(),
743            topic("t"),
744            SubscriptionFilter::BySize { max_bytes: 100 },
745            0,
746        );
747        let m = msg("t", "s", 100, 5, 1, 100);
748        assert_eq!(router.route(&m, 100).len(), 1);
749    }
750
751    // 19. BySize rejects message with payload_size > max_bytes
752    #[test]
753    fn test_by_size_rejects() {
754        let mut router = SubscriptionRouter::new(100);
755        router.subscribe(
756            "p".to_string(),
757            topic("t"),
758            SubscriptionFilter::BySize { max_bytes: 99 },
759            0,
760        );
761        let m = msg("t", "s", 100, 5, 1, 100);
762        assert!(router.route(&m, 100).is_empty());
763    }
764
765    // ── Filter: And / Or ──────────────────────────────────────────────────────
766
767    // 20. And filter: both must pass
768    #[test]
769    fn test_and_filter_both_must_pass() {
770        let filter = SubscriptionFilter::and(
771            SubscriptionFilter::BySender("alice".to_string()),
772            SubscriptionFilter::ByPriority { min: 5 },
773        );
774        let mut router = SubscriptionRouter::new(100);
775        router.subscribe("p".to_string(), topic("t"), filter, 0);
776
777        // Both pass
778        let m_ok = msg("t", "alice", 10, 7, 1, 100);
779        assert_eq!(router.route(&m_ok, 100).len(), 1);
780
781        // Priority fails
782        let m_bad_pri = msg("t", "alice", 10, 3, 1, 200);
783        assert!(router.route(&m_bad_pri, 200).is_empty());
784
785        // Sender fails
786        let m_bad_sender = msg("t", "bob", 10, 7, 1, 300);
787        assert!(router.route(&m_bad_sender, 300).is_empty());
788    }
789
790    // 21. Or filter: either can pass
791    #[test]
792    fn test_or_filter_either_can_pass() {
793        let filter = SubscriptionFilter::or(
794            SubscriptionFilter::BySender("alice".to_string()),
795            SubscriptionFilter::ByPriority { min: 200 },
796        );
797        let mut router = SubscriptionRouter::new(100);
798        router.subscribe("p".to_string(), topic("t"), filter, 0);
799
800        // Sender matches (priority doesn't need to)
801        let m_sender = msg("t", "alice", 10, 5, 1, 100);
802        assert_eq!(router.route(&m_sender, 100).len(), 1);
803
804        // Priority matches (sender doesn't need to)
805        let m_prio = msg("t", "carol", 10, 200, 1, 200);
806        assert_eq!(router.route(&m_prio, 200).len(), 1);
807
808        // Neither matches
809        let m_none = msg("t", "dave", 10, 50, 1, 300);
810        assert!(router.route(&m_none, 300).is_empty());
811    }
812
813    // ── subscriptions_for_topic ───────────────────────────────────────────────
814
815    // 22. subscriptions_for_topic returns correct entries
816    #[test]
817    fn test_subscriptions_for_topic() {
818        let mut router = SubscriptionRouter::new(100);
819        router.subscribe("alice".to_string(), topic("news"), all_filter(), 0);
820        router.subscribe("bob".to_string(), topic("news"), all_filter(), 1);
821        router.subscribe("carol".to_string(), topic("sports"), all_filter(), 2);
822
823        let news_subs = router.subscriptions_for_topic(&topic("news"));
824        assert_eq!(news_subs.len(), 2);
825
826        let sports_subs = router.subscriptions_for_topic(&topic("sports"));
827        assert_eq!(sports_subs.len(), 1);
828    }
829
830    // 23. subscriptions_for_topic returns empty for unknown topic
831    #[test]
832    fn test_subscriptions_for_topic_empty() {
833        let router = SubscriptionRouter::new(100);
834        assert!(router.subscriptions_for_topic(&topic("unknown")).is_empty());
835    }
836
837    // ── subscriptions_for_peer ────────────────────────────────────────────────
838
839    // 24. subscriptions_for_peer returns correct entries
840    #[test]
841    fn test_subscriptions_for_peer() {
842        let mut router = SubscriptionRouter::new(100);
843        router.subscribe("alice".to_string(), topic("news"), all_filter(), 0);
844        router.subscribe("alice".to_string(), topic("sports"), all_filter(), 1);
845        router.subscribe("bob".to_string(), topic("news"), all_filter(), 2);
846
847        let alice_subs = router.subscriptions_for_peer("alice");
848        assert_eq!(alice_subs.len(), 2);
849
850        let bob_subs = router.subscriptions_for_peer("bob");
851        assert_eq!(bob_subs.len(), 1);
852    }
853
854    // ── delivery_rate ─────────────────────────────────────────────────────────
855
856    // 25. delivery_rate returns 1.0 with no records
857    #[test]
858    fn test_delivery_rate_no_records() {
859        let router = SubscriptionRouter::new(100);
860        assert!((router.delivery_rate(0) - 1.0).abs() < f64::EPSILON);
861    }
862
863    // 26. delivery_rate returns 1.0 for all successful deliveries
864    #[test]
865    fn test_delivery_rate_all_success() {
866        let mut router = SubscriptionRouter::new(100);
867        router.subscribe("p".to_string(), topic("t"), all_filter(), 0);
868        let m = msg("t", "s", 10, 1, 1, 100);
869        router.route(&m, 100);
870        router.route(&m, 200);
871        // All records are successes (one subscription, two deliveries)
872        let rate = router.delivery_rate(0);
873        assert!((rate - 1.0).abs() < f64::EPSILON);
874    }
875
876    // 27. delivery_rate returns 0.0 when all records are failures
877    #[test]
878    fn test_delivery_rate_all_failures() {
879        let mut router = SubscriptionRouter::new(100);
880        // No subscriptions — every message drops
881        let m = msg("t", "s", 10, 1, 1, 100);
882        router.route(&m, 100);
883        router.route(&m, 200);
884        let rate = router.delivery_rate(0);
885        assert!((rate - 0.0).abs() < f64::EPSILON);
886    }
887
888    // 28. delivery_rate respects the `since` filter
889    #[test]
890    fn test_delivery_rate_since_filter() {
891        let mut router = SubscriptionRouter::new(100);
892        // No subscription → all drops
893        let m = msg("t", "s", 10, 1, 1, 100);
894        router.route(&m, 50); // before 'since'
895                              // Now add a subscription for future successes
896        router.subscribe("p".to_string(), topic("t"), all_filter(), 0);
897        let m2 = msg("t", "s", 10, 1, 1, 200);
898        router.route(&m2, 200); // after 'since'
899                                // Records since ts=100 should include only the second (success)
900        let rate = router.delivery_rate(100);
901        assert!((rate - 1.0).abs() < f64::EPSILON);
902    }
903
904    // ── recent_deliveries ────────────────────────────────────────────────────
905
906    // 29. recent_deliveries returns most-recent n records
907    #[test]
908    fn test_recent_deliveries() {
909        let mut router = SubscriptionRouter::new(100);
910        // No subscription — all drops (failures)
911        for ts in 0..5u64 {
912            let m = msg("t", "s", 10, 1, 1, ts * 100);
913            router.route(&m, ts * 100);
914        }
915        let recent = router.recent_deliveries(3);
916        assert_eq!(recent.len(), 3);
917        // The most recent three should have delivered_at 200, 300, 400.
918        assert_eq!(recent[2].delivered_at, 400);
919    }
920
921    // 30. recent_deliveries returns all when n > log length
922    #[test]
923    fn test_recent_deliveries_all() {
924        let mut router = SubscriptionRouter::new(100);
925        let m = msg("t", "s", 10, 1, 1, 100);
926        router.route(&m, 100);
927        let recent = router.recent_deliveries(1000);
928        assert_eq!(recent.len(), 1);
929    }
930
931    // ── evict_stale_subscriptions ─────────────────────────────────────────────
932
933    // 31. evict_stale_subscriptions removes old unused subscriptions
934    #[test]
935    fn test_evict_stale_removes_old_unused() {
936        let mut router = SubscriptionRouter::new(100);
937        // Created at t=0, no messages delivered.
938        router.subscribe("alice".to_string(), topic("t"), all_filter(), 0);
939        // Evict subs older than 1000ms at t=2000.
940        let evicted = router.evict_stale_subscriptions(1000, 2000);
941        assert_eq!(evicted, 1);
942        assert_eq!(router.stats(2000).total_subscriptions, 0);
943    }
944
945    // 32. evict_stale_subscriptions keeps young subscriptions
946    #[test]
947    fn test_evict_stale_keeps_young() {
948        let mut router = SubscriptionRouter::new(100);
949        router.subscribe("alice".to_string(), topic("t"), all_filter(), 1500);
950        // Evict subs older than 1000ms at t=2000.  Sub is only 500ms old.
951        let evicted = router.evict_stale_subscriptions(1000, 2000);
952        assert_eq!(evicted, 0);
953        assert_eq!(router.stats(2000).total_subscriptions, 1);
954    }
955
956    // 33. evict_stale_subscriptions keeps old subscriptions with messages
957    #[test]
958    fn test_evict_stale_keeps_active() {
959        let mut router = SubscriptionRouter::new(100);
960        let _id = router.subscribe("alice".to_string(), topic("t"), all_filter(), 0);
961        // Deliver a message so message_count > 0.
962        let m = msg("t", "s", 10, 1, 1, 100);
963        router.route(&m, 100);
964        // Evict subs older than 1000ms at t=5000.  Sub is old but has messages.
965        let evicted = router.evict_stale_subscriptions(1000, 5000);
966        assert_eq!(evicted, 0);
967    }
968
969    // ── stats() ───────────────────────────────────────────────────────────────
970
971    // 34. stats() reflects correct topic count
972    #[test]
973    fn test_stats_topic_count() {
974        let mut router = SubscriptionRouter::new(100);
975        router.subscribe("alice".to_string(), topic("news"), all_filter(), 0);
976        router.subscribe("bob".to_string(), topic("sports"), all_filter(), 0);
977        router.subscribe("carol".to_string(), topic("news"), all_filter(), 0);
978        let s = router.stats(0);
979        assert_eq!(s.topics, 2);
980        assert_eq!(s.total_subscriptions, 3);
981    }
982
983    // 35. stats() total_routed is accurate
984    #[test]
985    fn test_stats_total_routed() {
986        let mut router = SubscriptionRouter::new(100);
987        let m = msg("t", "s", 10, 1, 1, 100);
988        router.route(&m, 100);
989        router.route(&m, 200);
990        assert_eq!(router.stats(0).total_routed, 2);
991    }
992
993    // ── delivery log cap ─────────────────────────────────────────────────────
994
995    // 36. delivery log is capped at max_log_size
996    #[test]
997    fn test_delivery_log_capped() {
998        let cap = 5;
999        let mut router = SubscriptionRouter::new(cap);
1000        let m = msg("t", "s", 10, 1, 1, 100);
1001        for _ in 0..20 {
1002            router.route(&m, 100);
1003        }
1004        assert_eq!(router.recent_deliveries(100).len(), cap);
1005    }
1006
1007    // ── RoutingMessage helpers ────────────────────────────────────────────────
1008
1009    // 37. RoutingMessage id is derived from sender+topic+timestamp
1010    #[test]
1011    fn test_routing_message_id_derived() {
1012        let m1 = RoutingMessage::new(topic("t"), 10, "alice", 100, 1, 5);
1013        let m2 = RoutingMessage::new(topic("t"), 10, "alice", 100, 1, 5);
1014        assert_eq!(m1.id, m2.id);
1015
1016        let m3 = RoutingMessage::new(topic("t"), 10, "bob", 100, 1, 5);
1017        assert_ne!(m1.id, m3.id);
1018    }
1019
1020    // 38. Subscription id is derived from peer_id+topic+created_at
1021    #[test]
1022    fn test_subscription_id_derived() {
1023        let s1 = Subscription::new("alice", topic("t"), SubscriptionFilter::All, 1000);
1024        let s2 = Subscription::new("alice", topic("t"), SubscriptionFilter::All, 1000);
1025        assert_eq!(s1.id, s2.id);
1026
1027        let s3 = Subscription::new("bob", topic("t"), SubscriptionFilter::All, 1000);
1028        assert_ne!(s1.id, s3.id);
1029    }
1030
1031    // 39. MessageTopic Display and as_str
1032    #[test]
1033    fn test_message_topic_display() {
1034        let t = MessageTopic::new("hello");
1035        assert_eq!(t.as_str(), "hello");
1036        assert_eq!(t.to_string(), "hello");
1037    }
1038
1039    // 40. DeliveryRecord fields are correctly populated on successful delivery
1040    #[test]
1041    fn test_delivery_record_fields_on_success() {
1042        let mut router = SubscriptionRouter::new(100);
1043        router.subscribe("alice".to_string(), topic("t"), all_filter(), 0);
1044        let m = msg("t", "bob", 10, 5, 1, 999);
1045        router.route(&m, 999);
1046        let records = router.recent_deliveries(10);
1047        assert_eq!(records.len(), 1);
1048        let r = records[0];
1049        assert_eq!(r.message_id, m.id);
1050        assert!(r.success);
1051        assert_eq!(r.delivered_at, 999);
1052    }
1053
1054    // ── fnv1a helper ──────────────────────────────────────────────────────────
1055
1056    // 41. fnv1a_64 produces consistent output
1057    #[test]
1058    fn test_fnv1a_consistent() {
1059        use crate::subscription_router::fnv1a_64;
1060        let h1 = fnv1a_64(b"hello");
1061        let h2 = fnv1a_64(b"hello");
1062        assert_eq!(h1, h2);
1063        let h3 = fnv1a_64(b"world");
1064        assert_ne!(h1, h3);
1065    }
1066
1067    // 42. And filter: deeply nested
1068    #[test]
1069    fn test_and_filter_deeply_nested() {
1070        let filter = SubscriptionFilter::and(
1071            SubscriptionFilter::And(
1072                Box::new(SubscriptionFilter::ByPriority { min: 1 }),
1073                Box::new(SubscriptionFilter::BySize { max_bytes: 500 }),
1074            ),
1075            SubscriptionFilter::BySender("trusted".to_string()),
1076        );
1077        let m = RoutingMessage::new(topic("t"), 100, "trusted", 100, 1, 10);
1078        assert!(SubscriptionRouter::filter_matches(&filter, &m));
1079
1080        let m_bad = RoutingMessage::new(topic("t"), 1000, "trusted", 100, 1, 10);
1081        assert!(!SubscriptionRouter::filter_matches(&filter, &m_bad));
1082    }
1083
1084    // 43. Or filter: both fail → rejected
1085    #[test]
1086    fn test_or_filter_both_fail() {
1087        let filter = SubscriptionFilter::or(
1088            SubscriptionFilter::BySender("alice".to_string()),
1089            SubscriptionFilter::ByPriority { min: 200 },
1090        );
1091        let m = RoutingMessage::new(topic("t"), 10, "dave", 100, 1, 50);
1092        assert!(!SubscriptionRouter::filter_matches(&filter, &m));
1093    }
1094}