Skip to main content

ipfrs_network/
bandwidth_monitor.rs

1//! Per-peer and aggregate bandwidth usage tracking with sliding window rate calculation.
2//!
3//! This module provides fine-grained bandwidth monitoring for each connected peer,
4//! including:
5//! - Recording inbound and outbound bytes per peer with timestamps
6//! - Sliding window rate calculation (bytes/sec) over a configurable window
7//! - Peak rate detection based on inter-sample intervals
8//! - Aggregate statistics across all peers
9//! - Atomic global counters for lock-free stats snapshots
10//! - Idle peer eviction to bound memory usage
11//!
12//! # Example
13//!
14//! ```rust
15//! use ipfrs_network::bandwidth_monitor::{BandwidthMonitor, Direction};
16//! use std::time::Duration;
17//!
18//! let monitor = BandwidthMonitor::with_window(Duration::from_secs(10));
19//!
20//! monitor.record("peer-1", 1024, Direction::Inbound);
21//! monitor.record("peer-1", 512, Direction::Outbound);
22//!
23//! let inbound_rate = monitor.rate_for_peer("peer-1", Direction::Inbound);
24//! println!("peer-1 inbound rate: {:.1} B/s", inbound_rate);
25//!
26//! let top = monitor.top_receivers(5);
27//! println!("top receiver: {:?}", top.first());
28//! ```
29
30use parking_lot::RwLock;
31use serde::{Deserialize, Serialize};
32use std::collections::{HashMap, VecDeque};
33use std::sync::atomic::{AtomicU64, Ordering};
34use std::sync::Arc;
35use std::time::{Duration, Instant};
36
37// ---------------------------------------------------------------------------
38// Direction
39// ---------------------------------------------------------------------------
40
41/// Traffic direction for a bandwidth sample.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43pub enum Direction {
44    /// Bytes received from a remote peer.
45    Inbound,
46    /// Bytes sent to a remote peer.
47    Outbound,
48}
49
50// ---------------------------------------------------------------------------
51// BandwidthSample
52// ---------------------------------------------------------------------------
53
54/// A single bandwidth observation recorded at a specific point in time.
55#[derive(Debug, Clone)]
56pub struct BandwidthSample {
57    /// Number of bytes transferred in this sample.
58    pub bytes: u64,
59    /// Wall-clock instant when the transfer was observed.
60    pub timestamp: Instant,
61    /// Whether the bytes were received or sent.
62    pub direction: Direction,
63}
64
65impl BandwidthSample {
66    /// Create a new sample stamped at `now`.
67    pub fn new(bytes: u64, direction: Direction, now: Instant) -> Self {
68        Self {
69            bytes,
70            timestamp: now,
71            direction,
72        }
73    }
74}
75
76// ---------------------------------------------------------------------------
77// PeerBandwidth
78// ---------------------------------------------------------------------------
79
80/// Per-peer bandwidth state keeping a sliding window of recent samples.
81#[derive(Debug)]
82pub struct PeerBandwidth {
83    /// Stable identifier for this peer (e.g. libp2p PeerId string).
84    pub peer_id: String,
85    /// Inbound samples within the current retention window.
86    pub inbound_samples: VecDeque<BandwidthSample>,
87    /// Outbound samples within the current retention window.
88    pub outbound_samples: VecDeque<BandwidthSample>,
89    /// Running total of all inbound bytes ever recorded (never decremented).
90    pub total_inbound_bytes: u64,
91    /// Running total of all outbound bytes ever recorded (never decremented).
92    pub total_outbound_bytes: u64,
93}
94
95impl PeerBandwidth {
96    /// Create a new, empty `PeerBandwidth` for the given peer.
97    pub fn new(peer_id: impl Into<String>) -> Self {
98        Self {
99            peer_id: peer_id.into(),
100            inbound_samples: VecDeque::new(),
101            outbound_samples: VecDeque::new(),
102            total_inbound_bytes: 0,
103            total_outbound_bytes: 0,
104        }
105    }
106
107    /// Record a transfer of `bytes` in `direction` at time `now`.
108    ///
109    /// Updates both the sliding-window sample queue and the all-time totals.
110    pub fn record(&mut self, bytes: u64, direction: Direction, now: Instant) {
111        let sample = BandwidthSample::new(bytes, direction, now);
112        match direction {
113            Direction::Inbound => {
114                self.inbound_samples.push_back(sample);
115                self.total_inbound_bytes = self.total_inbound_bytes.saturating_add(bytes);
116            }
117            Direction::Outbound => {
118                self.outbound_samples.push_back(sample);
119                self.total_outbound_bytes = self.total_outbound_bytes.saturating_add(bytes);
120            }
121        }
122    }
123
124    /// Remove samples older than `window` before `now` from both queues.
125    pub fn evict_old(&mut self, now: Instant, window: Duration) {
126        let cutoff = now.checked_sub(window).unwrap_or(now);
127        while let Some(front) = self.inbound_samples.front() {
128            if front.timestamp <= cutoff {
129                self.inbound_samples.pop_front();
130            } else {
131                break;
132            }
133        }
134        while let Some(front) = self.outbound_samples.front() {
135            if front.timestamp <= cutoff {
136                self.outbound_samples.pop_front();
137            } else {
138                break;
139            }
140        }
141    }
142
143    /// Compute the average rate (bytes/sec) for `direction` within the last
144    /// `window` seconds ending at `now`.
145    ///
146    /// Returns `0.0` when there are no samples in the window or the window
147    /// duration is zero.
148    pub fn rate_bps(&self, direction: Direction, now: Instant, window: Duration) -> f64 {
149        let window_secs = window.as_secs_f64();
150        if window_secs <= 0.0 {
151            return 0.0;
152        }
153        let cutoff = now.checked_sub(window).unwrap_or(now);
154        let queue = match direction {
155            Direction::Inbound => &self.inbound_samples,
156            Direction::Outbound => &self.outbound_samples,
157        };
158        let total_bytes: u64 = queue
159            .iter()
160            .filter(|s| s.timestamp > cutoff)
161            .map(|s| s.bytes)
162            .sum();
163        total_bytes as f64 / window_secs
164    }
165
166    /// Compute the peak instantaneous rate (bytes/sec) for `direction`.
167    ///
168    /// The peak is defined as the maximum single-sample byte count divided by
169    /// the elapsed time since the previous sample in the queue.  The very first
170    /// sample is excluded because there is no prior reference point.
171    ///
172    /// Returns `0.0` when fewer than two samples exist.
173    pub fn peak_rate_bps(&self, direction: Direction) -> f64 {
174        let queue = match direction {
175            Direction::Inbound => &self.inbound_samples,
176            Direction::Outbound => &self.outbound_samples,
177        };
178        if queue.len() < 2 {
179            return 0.0;
180        }
181        let mut peak: f64 = 0.0;
182        let samples: Vec<&BandwidthSample> = queue.iter().collect();
183        for i in 1..samples.len() {
184            let elapsed = samples[i]
185                .timestamp
186                .duration_since(samples[i - 1].timestamp)
187                .as_secs_f64();
188            if elapsed > 0.0 {
189                let rate = samples[i].bytes as f64 / elapsed;
190                if rate > peak {
191                    peak = rate;
192                }
193            }
194        }
195        peak
196    }
197
198    /// Return the timestamp of the most recent sample in either direction,
199    /// or `None` if no samples have been recorded yet.
200    pub fn last_activity(&self) -> Option<Instant> {
201        let inbound_ts = self.inbound_samples.back().map(|s| s.timestamp);
202        let outbound_ts = self.outbound_samples.back().map(|s| s.timestamp);
203        match (inbound_ts, outbound_ts) {
204            (Some(a), Some(b)) => Some(a.max(b)),
205            (Some(a), None) => Some(a),
206            (None, Some(b)) => Some(b),
207            (None, None) => None,
208        }
209    }
210}
211
212// ---------------------------------------------------------------------------
213// BandwidthStats
214// ---------------------------------------------------------------------------
215
216/// Atomic global counters for lock-free stats collection.
217#[derive(Debug, Default)]
218pub struct BandwidthStats {
219    /// Total inbound bytes recorded across all peers since monitor creation.
220    pub total_inbound_bytes: AtomicU64,
221    /// Total outbound bytes recorded across all peers since monitor creation.
222    pub total_outbound_bytes: AtomicU64,
223    /// Total number of individual samples recorded (both directions).
224    pub total_samples: AtomicU64,
225}
226
227/// A point-in-time snapshot of [`BandwidthStats`].
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct BandwidthStatsSnapshot {
230    /// Total inbound bytes at snapshot time.
231    pub total_inbound_bytes: u64,
232    /// Total outbound bytes at snapshot time.
233    pub total_outbound_bytes: u64,
234    /// Total sample count at snapshot time.
235    pub total_samples: u64,
236}
237
238impl BandwidthStats {
239    /// Create zeroed stats.
240    pub fn new() -> Self {
241        Self::default()
242    }
243
244    /// Take a consistent snapshot of all counters.
245    ///
246    /// Note: each field is read atomically but the three reads are not
247    /// collectively atomic; the snapshot may reflect slightly different
248    /// instants for each counter under heavy concurrent load.
249    pub fn snapshot(&self) -> BandwidthStatsSnapshot {
250        BandwidthStatsSnapshot {
251            total_inbound_bytes: self.total_inbound_bytes.load(Ordering::Relaxed),
252            total_outbound_bytes: self.total_outbound_bytes.load(Ordering::Relaxed),
253            total_samples: self.total_samples.load(Ordering::Relaxed),
254        }
255    }
256}
257
258// ---------------------------------------------------------------------------
259// BandwidthMonitor
260// ---------------------------------------------------------------------------
261
262/// Monitor that tracks per-peer and aggregate bandwidth usage.
263///
264/// `BandwidthMonitor` maintains a map of [`PeerBandwidth`] entries protected
265/// by a [`RwLock`].  All mutating operations take a write lock; read-only
266/// queries take a read lock.  Global byte totals are accumulated in
267/// [`BandwidthStats`] using lock-free atomics, so callers can take cheap
268/// snapshots without acquiring the peer map lock.
269///
270/// The sliding window used for rate calculations is configurable at
271/// construction time (defaults to 10 seconds).  Samples older than the
272/// window are lazily evicted on each call to [`record`](BandwidthMonitor::record).
273pub struct BandwidthMonitor {
274    /// Per-peer bandwidth state.
275    peers: RwLock<HashMap<String, PeerBandwidth>>,
276    /// Duration of the sliding window for rate calculations.
277    window: Duration,
278    /// Global atomic counters.
279    stats: Arc<BandwidthStats>,
280}
281
282impl BandwidthMonitor {
283    /// Create a new monitor with the default 10-second sliding window.
284    pub fn new() -> Self {
285        Self::with_window(Duration::from_secs(10))
286    }
287
288    /// Create a new monitor with a custom sliding `window` duration.
289    pub fn with_window(window: Duration) -> Self {
290        Self {
291            peers: RwLock::new(HashMap::new()),
292            window,
293            stats: Arc::new(BandwidthStats::new()),
294        }
295    }
296
297    /// Return the configured sliding window duration.
298    pub fn window(&self) -> Duration {
299        self.window
300    }
301
302    /// Return a shared reference to the global atomic stats.
303    pub fn stats(&self) -> &Arc<BandwidthStats> {
304        &self.stats
305    }
306
307    /// Record a transfer of `bytes` in `direction` for `peer_id`.
308    ///
309    /// This method:
310    /// 1. Obtains a write lock on the peer map.
311    /// 2. Creates a [`PeerBandwidth`] entry if one does not exist.
312    /// 3. Appends a new sample stamped with the current instant.
313    /// 4. Evicts samples older than the sliding window from that peer.
314    /// 5. Increments the global atomic counters.
315    pub fn record(&self, peer_id: &str, bytes: u64, direction: Direction) {
316        let now = Instant::now();
317        {
318            let mut peers = self.peers.write();
319            let entry = peers
320                .entry(peer_id.to_owned())
321                .or_insert_with(|| PeerBandwidth::new(peer_id));
322            entry.record(bytes, direction, now);
323            entry.evict_old(now, self.window);
324        }
325        // Update global atomics outside the write lock.
326        match direction {
327            Direction::Inbound => {
328                self.stats
329                    .total_inbound_bytes
330                    .fetch_add(bytes, Ordering::Relaxed);
331            }
332            Direction::Outbound => {
333                self.stats
334                    .total_outbound_bytes
335                    .fetch_add(bytes, Ordering::Relaxed);
336            }
337        }
338        self.stats.total_samples.fetch_add(1, Ordering::Relaxed);
339    }
340
341    /// Return the current rate (bytes/sec) for `peer_id` in `direction`.
342    ///
343    /// Returns `0.0` if the peer is unknown.
344    pub fn rate_for_peer(&self, peer_id: &str, direction: Direction) -> f64 {
345        let now = Instant::now();
346        let peers = self.peers.read();
347        peers
348            .get(peer_id)
349            .map(|p| p.rate_bps(direction, now, self.window))
350            .unwrap_or(0.0)
351    }
352
353    /// Return the top `n` peers by **outbound** rate, sorted descending.
354    ///
355    /// Each element is `(peer_id, rate_bps)`.  If there are fewer than `n`
356    /// peers, all known peers are returned.
357    pub fn top_senders(&self, n: usize) -> Vec<(String, f64)> {
358        let now = Instant::now();
359        let peers = self.peers.read();
360        let mut rates: Vec<(String, f64)> = peers
361            .values()
362            .map(|p| {
363                (
364                    p.peer_id.clone(),
365                    p.rate_bps(Direction::Outbound, now, self.window),
366                )
367            })
368            .collect();
369        rates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
370        rates.truncate(n);
371        rates
372    }
373
374    /// Return the top `n` peers by **inbound** rate, sorted descending.
375    ///
376    /// Each element is `(peer_id, rate_bps)`.
377    pub fn top_receivers(&self, n: usize) -> Vec<(String, f64)> {
378        let now = Instant::now();
379        let peers = self.peers.read();
380        let mut rates: Vec<(String, f64)> = peers
381            .values()
382            .map(|p| {
383                (
384                    p.peer_id.clone(),
385                    p.rate_bps(Direction::Inbound, now, self.window),
386                )
387            })
388            .collect();
389        rates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
390        rates.truncate(n);
391        rates
392    }
393
394    /// Compute the aggregate rate (bytes/sec) across all peers for `direction`.
395    pub fn total_rate_bps(&self, direction: Direction) -> f64 {
396        let now = Instant::now();
397        let peers = self.peers.read();
398        peers
399            .values()
400            .map(|p| p.rate_bps(direction, now, self.window))
401            .sum()
402    }
403
404    /// Remove peers whose most recent sample is older than `max_idle`.
405    ///
406    /// A peer is considered idle when:
407    /// - It has no samples at all, **or**
408    /// - Its most recent sample (in either direction) was recorded more than
409    ///   `max_idle` ago.
410    pub fn evict_idle_peers(&self, max_idle: Duration) {
411        let now = Instant::now();
412        let mut peers = self.peers.write();
413        peers.retain(|_, peer| {
414            match peer.last_activity() {
415                None => false, // No samples at all — remove immediately.
416                Some(ts) => now.duration_since(ts) <= max_idle,
417            }
418        });
419    }
420
421    /// Return the current number of tracked peers.
422    pub fn peer_count(&self) -> usize {
423        self.peers.read().len()
424    }
425
426    /// Take a snapshot of the global atomic stats without acquiring the peer
427    /// map lock.
428    pub fn stats_snapshot(&self) -> BandwidthStatsSnapshot {
429        self.stats.snapshot()
430    }
431}
432
433impl Default for BandwidthMonitor {
434    fn default() -> Self {
435        Self::new()
436    }
437}
438
439// ---------------------------------------------------------------------------
440// Tests
441// ---------------------------------------------------------------------------
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446    use std::thread;
447    use std::time::Duration;
448
449    // -----------------------------------------------------------------------
450    // Helper: fabricate a PeerBandwidth with injected instants
451    // -----------------------------------------------------------------------
452
453    fn make_peer_with_samples(
454        peer_id: &str,
455        direction: Direction,
456        samples: &[(u64, Instant)],
457    ) -> PeerBandwidth {
458        let mut peer = PeerBandwidth::new(peer_id);
459        for (bytes, ts) in samples {
460            let sample = BandwidthSample {
461                bytes: *bytes,
462                timestamp: *ts,
463                direction,
464            };
465            match direction {
466                Direction::Inbound => {
467                    peer.inbound_samples.push_back(sample);
468                    peer.total_inbound_bytes = peer.total_inbound_bytes.saturating_add(*bytes);
469                }
470                Direction::Outbound => {
471                    peer.outbound_samples.push_back(sample);
472                    peer.total_outbound_bytes = peer.total_outbound_bytes.saturating_add(*bytes);
473                }
474            }
475        }
476        peer
477    }
478
479    // -----------------------------------------------------------------------
480    // 1. record() increments per-peer totals
481    // -----------------------------------------------------------------------
482    #[test]
483    fn test_record_increments_totals() {
484        let monitor = BandwidthMonitor::new();
485        monitor.record("peer-a", 100, Direction::Inbound);
486        monitor.record("peer-a", 200, Direction::Inbound);
487        monitor.record("peer-a", 50, Direction::Outbound);
488
489        let peers = monitor.peers.read();
490        let peer = peers.get("peer-a").expect("peer-a should exist");
491        assert_eq!(peer.total_inbound_bytes, 300);
492        assert_eq!(peer.total_outbound_bytes, 50);
493    }
494
495    // -----------------------------------------------------------------------
496    // 2. Global stats counters accumulate across peers
497    // -----------------------------------------------------------------------
498    #[test]
499    fn test_stats_accumulation() {
500        let monitor = BandwidthMonitor::new();
501        monitor.record("p1", 1000, Direction::Inbound);
502        monitor.record("p2", 2000, Direction::Inbound);
503        monitor.record("p1", 500, Direction::Outbound);
504
505        let snap = monitor.stats_snapshot();
506        assert_eq!(snap.total_inbound_bytes, 3000);
507        assert_eq!(snap.total_outbound_bytes, 500);
508        assert_eq!(snap.total_samples, 3);
509    }
510
511    // -----------------------------------------------------------------------
512    // 3. rate_bps() — known window with time-shifted samples
513    // -----------------------------------------------------------------------
514    #[test]
515    fn test_rate_bps_correct_over_known_window() {
516        // 10-second window; inject two samples 5 s apart; together they are
517        // both within the 10-second window.  Rate = (100 + 200) / 10 = 30 B/s.
518        let window = Duration::from_secs(10);
519        let now = Instant::now();
520        let t0 = now - Duration::from_secs(8);
521        let t1 = now - Duration::from_secs(3);
522
523        let peer = make_peer_with_samples("x", Direction::Inbound, &[(100, t0), (200, t1)]);
524
525        let rate = peer.rate_bps(Direction::Inbound, now, window);
526        assert!((rate - 30.0).abs() < 1e-9, "expected 30 B/s, got {rate}");
527    }
528
529    // -----------------------------------------------------------------------
530    // 4. Old samples are evicted by evict_old()
531    // -----------------------------------------------------------------------
532    #[test]
533    fn test_evict_old_removes_stale_samples() {
534        let window = Duration::from_secs(10);
535        let now = Instant::now();
536        let old = now - Duration::from_secs(15);
537        let recent = now - Duration::from_secs(2);
538
539        let mut peer =
540            make_peer_with_samples("y", Direction::Inbound, &[(999, old), (100, recent)]);
541
542        assert_eq!(peer.inbound_samples.len(), 2);
543        peer.evict_old(now, window);
544        assert_eq!(
545            peer.inbound_samples.len(),
546            1,
547            "stale sample should be evicted"
548        );
549        assert_eq!(peer.inbound_samples.front().map(|s| s.bytes), Some(100));
550    }
551
552    // -----------------------------------------------------------------------
553    // 5. rate_bps() returns 0 when no samples are in the window
554    // -----------------------------------------------------------------------
555    #[test]
556    fn test_rate_bps_zero_when_no_samples_in_window() {
557        let window = Duration::from_secs(5);
558        let now = Instant::now();
559        let old = now - Duration::from_secs(20);
560
561        let peer = make_peer_with_samples("z", Direction::Outbound, &[(500, old)]);
562        let rate = peer.rate_bps(Direction::Outbound, now, window);
563        assert_eq!(rate, 0.0);
564    }
565
566    // -----------------------------------------------------------------------
567    // 6. peak_rate_bps() returns max inter-sample rate
568    // -----------------------------------------------------------------------
569    #[test]
570    fn test_peak_rate_bps() {
571        let now = Instant::now();
572        // Two samples 1 second apart: 1000 bytes → 1000 B/s
573        // Two samples 0.5 s apart:  2000 bytes → 4000 B/s  ← peak
574        let t0 = now - Duration::from_millis(1500);
575        let t1 = now - Duration::from_millis(500);
576        let t2 = now;
577
578        let peer = make_peer_with_samples(
579            "peak",
580            Direction::Outbound,
581            &[(1000, t0), (1000, t1), (2000, t2)],
582        );
583
584        let peak = peer.peak_rate_bps(Direction::Outbound);
585        // t1 → t2 gap is 500 ms; 2000 / 0.5 = 4000
586        assert!(peak >= 3999.0, "expected peak >= 4000 B/s, got {peak}");
587    }
588
589    // -----------------------------------------------------------------------
590    // 7. top_senders() is sorted descending by outbound rate
591    // -----------------------------------------------------------------------
592    #[test]
593    fn test_top_senders_sorted_descending() {
594        let monitor = BandwidthMonitor::with_window(Duration::from_secs(30));
595        // Inject samples in a single slice so Instant::now() is used:
596        // use small sleeps to ensure distinct instants don't matter — we just
597        // need relative ordering within the window, so record directly.
598        monitor.record("peer-low", 100, Direction::Outbound);
599        monitor.record("peer-mid", 1000, Direction::Outbound);
600        monitor.record("peer-high", 5000, Direction::Outbound);
601
602        let top = monitor.top_senders(3);
603        assert_eq!(top.len(), 3);
604        // Rates are equal over the same window duration so ordering is by
605        // absolute bytes — highest first.
606        assert!(
607            top[0].1 >= top[1].1,
608            "first element should have rate >= second"
609        );
610        assert!(
611            top[1].1 >= top[2].1,
612            "second element should have rate >= third"
613        );
614        assert_eq!(top[0].0, "peer-high");
615    }
616
617    // -----------------------------------------------------------------------
618    // 8. top_receivers() is sorted descending by inbound rate
619    // -----------------------------------------------------------------------
620    #[test]
621    fn test_top_receivers_sorted_descending() {
622        let monitor = BandwidthMonitor::with_window(Duration::from_secs(30));
623        monitor.record("recv-a", 200, Direction::Inbound);
624        monitor.record("recv-b", 800, Direction::Inbound);
625        monitor.record("recv-c", 50, Direction::Inbound);
626
627        let top = monitor.top_receivers(3);
628        assert_eq!(top.len(), 3);
629        assert!(top[0].1 >= top[1].1);
630        assert!(top[1].1 >= top[2].1);
631        assert_eq!(top[0].0, "recv-b");
632    }
633
634    // -----------------------------------------------------------------------
635    // 9. top_senders() returns at most n entries
636    // -----------------------------------------------------------------------
637    #[test]
638    fn test_top_senders_truncates_to_n() {
639        let monitor = BandwidthMonitor::new();
640        for i in 0..10_u64 {
641            monitor.record(&format!("p{i}"), i * 100, Direction::Outbound);
642        }
643        let top = monitor.top_senders(3);
644        assert_eq!(top.len(), 3);
645    }
646
647    // -----------------------------------------------------------------------
648    // 10. total_rate_bps() sums all peer rates
649    // -----------------------------------------------------------------------
650    #[test]
651    fn test_total_rate_bps_sums_all_peers() {
652        let window = Duration::from_secs(10);
653        let monitor = BandwidthMonitor::with_window(window);
654
655        // All samples recorded at approximately "now", so rate per peer ≈ bytes / 10
656        monitor.record("p1", 1000, Direction::Inbound);
657        monitor.record("p2", 2000, Direction::Inbound);
658        monitor.record("p3", 500, Direction::Inbound);
659
660        let total = monitor.total_rate_bps(Direction::Inbound);
661        // Expected: (1000 + 2000 + 500) / 10 = 350 B/s
662        assert!(
663            (total - 350.0).abs() < 1.0,
664            "expected ~350 B/s, got {total}"
665        );
666    }
667
668    // -----------------------------------------------------------------------
669    // 11. evict_idle_peers() removes peers with no recent activity
670    // -----------------------------------------------------------------------
671    #[test]
672    fn test_evict_idle_peers_removes_inactive() {
673        let monitor = BandwidthMonitor::new();
674        // Record for two peers; then sleep briefly so we can set a very short
675        // max_idle below the sleep duration.
676        monitor.record("active-peer", 100, Direction::Inbound);
677        monitor.record("idle-peer", 100, Direction::Inbound);
678
679        // Wait long enough so "idle-peer" becomes stale for a 1-ms window.
680        thread::sleep(Duration::from_millis(5));
681
682        // Record fresh activity for "active-peer" only.
683        monitor.record("active-peer", 100, Direction::Inbound);
684
685        // max_idle of 3 ms — "idle-peer" last recorded > 5 ms ago.
686        monitor.evict_idle_peers(Duration::from_millis(3));
687
688        let count = monitor.peer_count();
689        assert_eq!(count, 1, "only active-peer should remain, got {count}");
690
691        let peers = monitor.peers.read();
692        assert!(peers.contains_key("active-peer"));
693        assert!(!peers.contains_key("idle-peer"));
694    }
695
696    // -----------------------------------------------------------------------
697    // 12. peer_count() returns correct count
698    // -----------------------------------------------------------------------
699    #[test]
700    fn test_peer_count_correct() {
701        let monitor = BandwidthMonitor::new();
702        assert_eq!(monitor.peer_count(), 0);
703
704        monitor.record("a", 1, Direction::Inbound);
705        assert_eq!(monitor.peer_count(), 1);
706
707        monitor.record("b", 1, Direction::Outbound);
708        assert_eq!(monitor.peer_count(), 2);
709
710        monitor.record("a", 1, Direction::Outbound); // existing peer
711        assert_eq!(monitor.peer_count(), 2);
712    }
713
714    // -----------------------------------------------------------------------
715    // 13. evict_idle_peers() keeps peers with recent activity
716    // -----------------------------------------------------------------------
717    #[test]
718    fn test_evict_idle_peers_retains_active() {
719        let monitor = BandwidthMonitor::new();
720        monitor.record("fresh", 500, Direction::Inbound);
721        // Use a generous max_idle so no peer is evicted.
722        monitor.evict_idle_peers(Duration::from_secs(60));
723        assert_eq!(monitor.peer_count(), 1);
724    }
725
726    // -----------------------------------------------------------------------
727    // 14. Direction enum: Inbound and Outbound are distinct
728    // -----------------------------------------------------------------------
729    #[test]
730    fn test_direction_inbound_outbound_independent() {
731        let monitor = BandwidthMonitor::new();
732        monitor.record("peer-x", 1000, Direction::Inbound);
733        monitor.record("peer-x", 500, Direction::Outbound);
734
735        let in_rate = monitor.rate_for_peer("peer-x", Direction::Inbound);
736        let out_rate = monitor.rate_for_peer("peer-x", Direction::Outbound);
737
738        // inbound rate should be higher than outbound rate
739        assert!(
740            in_rate > out_rate,
741            "inbound ({in_rate}) should > outbound ({out_rate})"
742        );
743
744        // Verify totals are tracked separately
745        let peers = monitor.peers.read();
746        let p = peers.get("peer-x").expect("peer-x should exist");
747        assert_eq!(p.total_inbound_bytes, 1000);
748        assert_eq!(p.total_outbound_bytes, 500);
749    }
750
751    // -----------------------------------------------------------------------
752    // 15. BandwidthStats::snapshot() returns consistent values
753    // -----------------------------------------------------------------------
754    #[test]
755    fn test_stats_snapshot_values() {
756        let monitor = BandwidthMonitor::new();
757        monitor.record("s1", 4096, Direction::Inbound);
758        monitor.record("s1", 1024, Direction::Outbound);
759        monitor.record("s2", 8192, Direction::Inbound);
760
761        let snap = monitor.stats_snapshot();
762        assert_eq!(snap.total_inbound_bytes, 4096 + 8192);
763        assert_eq!(snap.total_outbound_bytes, 1024);
764        assert_eq!(snap.total_samples, 3);
765    }
766
767    // -----------------------------------------------------------------------
768    // 16. Unknown peer rate_for_peer() returns 0.0
769    // -----------------------------------------------------------------------
770    #[test]
771    fn test_rate_for_unknown_peer() {
772        let monitor = BandwidthMonitor::new();
773        assert_eq!(monitor.rate_for_peer("nobody", Direction::Inbound), 0.0);
774        assert_eq!(monitor.rate_for_peer("nobody", Direction::Outbound), 0.0);
775    }
776}
777
778// ---------------------------------------------------------------------------
779// PeerBandwidthMonitor — tick-based sliding window bandwidth tracking
780// ---------------------------------------------------------------------------
781
782/// A single tick-based bandwidth measurement for a peer.
783#[derive(Debug, Clone)]
784pub struct TickBandwidthSample {
785    /// The logical tick at which this sample was recorded.
786    pub tick: u64,
787    /// Number of bytes sent during this tick.
788    pub bytes_sent: u64,
789    /// Number of bytes received during this tick.
790    pub bytes_received: u64,
791}
792
793/// Sliding-window bandwidth state for a single peer, indexed by logical tick.
794#[derive(Debug, Clone)]
795pub struct PeerBandwidthWindow {
796    /// Stable identifier for this peer.
797    pub peer_id: String,
798    /// Ordered samples (oldest first); bounded by `window_size`.
799    pub samples: Vec<TickBandwidthSample>,
800    /// Maximum number of samples to retain.
801    pub window_size: usize,
802    /// Cumulative bytes sent across all samples ever recorded (not windowed).
803    pub total_sent: u64,
804    /// Cumulative bytes received across all samples ever recorded (not windowed).
805    pub total_received: u64,
806}
807
808impl PeerBandwidthWindow {
809    /// Create a new, empty window for the given peer with the given capacity.
810    pub fn new(peer_id: impl Into<String>, window_size: usize) -> Self {
811        Self {
812            peer_id: peer_id.into(),
813            samples: Vec::new(),
814            window_size,
815            total_sent: 0,
816            total_received: 0,
817        }
818    }
819
820    /// Append a new sample, evicting the oldest when over capacity.
821    ///
822    /// Updates cumulative totals before any eviction.
823    pub fn add_sample(&mut self, tick: u64, bytes_sent: u64, bytes_received: u64) {
824        self.total_sent = self.total_sent.saturating_add(bytes_sent);
825        self.total_received = self.total_received.saturating_add(bytes_received);
826        self.samples.push(TickBandwidthSample {
827            tick,
828            bytes_sent,
829            bytes_received,
830        });
831        if self.samples.len() > self.window_size {
832            self.samples.remove(0);
833        }
834    }
835
836    /// Mean `bytes_sent` across all samples currently in the window.
837    ///
838    /// Returns `0.0` when the window is empty.
839    pub fn avg_send_rate(&self) -> f64 {
840        if self.samples.is_empty() {
841            return 0.0;
842        }
843        let total: u64 = self.samples.iter().map(|s| s.bytes_sent).sum();
844        total as f64 / self.samples.len() as f64
845    }
846
847    /// Mean `bytes_received` across all samples currently in the window.
848    ///
849    /// Returns `0.0` when the window is empty.
850    pub fn avg_recv_rate(&self) -> f64 {
851        if self.samples.is_empty() {
852            return 0.0;
853        }
854        let total: u64 = self.samples.iter().map(|s| s.bytes_received).sum();
855        total as f64 / self.samples.len() as f64
856    }
857
858    /// Maximum `bytes_sent` of any sample in the current window.
859    ///
860    /// Returns `0` when the window is empty.
861    pub fn peak_send(&self) -> u64 {
862        self.samples.iter().map(|s| s.bytes_sent).max().unwrap_or(0)
863    }
864
865    /// Maximum `bytes_received` of any sample in the current window.
866    ///
867    /// Returns `0` when the window is empty.
868    pub fn peak_recv(&self) -> u64 {
869        self.samples
870            .iter()
871            .map(|s| s.bytes_received)
872            .max()
873            .unwrap_or(0)
874    }
875}
876
877// ---------------------------------------------------------------------------
878// BandwidthAnomaly
879// ---------------------------------------------------------------------------
880
881/// An anomaly detected by [`PeerBandwidthMonitor`].
882#[derive(Debug, Clone, PartialEq)]
883pub enum BandwidthAnomaly {
884    /// A single send sample is more than `spike_multiplier` times the prior
885    /// rolling average.
886    SendSpike {
887        /// The peer that triggered the spike.
888        peer_id: String,
889        /// The raw bytes_sent value that caused the spike.
890        sample_bytes: u64,
891        /// The rolling average that was exceeded.
892        avg_bytes: f64,
893    },
894    /// A single receive sample is more than `spike_multiplier` times the prior
895    /// rolling average.
896    RecvSpike {
897        /// The peer that triggered the spike.
898        peer_id: String,
899        /// The raw bytes_received value that caused the spike.
900        sample_bytes: u64,
901        /// The rolling average that was exceeded.
902        avg_bytes: f64,
903    },
904    /// A peer has produced no samples for at least `idle_threshold_ticks` ticks.
905    Idle {
906        /// The idle peer.
907        peer_id: String,
908        /// How many ticks have elapsed since the last recorded sample.
909        ticks_since_last: u64,
910    },
911}
912
913// ---------------------------------------------------------------------------
914// MonitorConfig
915// ---------------------------------------------------------------------------
916
917/// Configuration for [`PeerBandwidthMonitor`].
918#[derive(Debug, Clone)]
919pub struct MonitorConfig {
920    /// Number of ticks to keep in each peer's sliding window.
921    pub window_size: usize,
922    /// Threshold multiplier: a sample is a spike when `sample > multiplier * avg`.
923    pub spike_multiplier: f64,
924    /// Ticks without a sample before a peer is considered idle.
925    pub idle_threshold_ticks: u64,
926}
927
928impl Default for MonitorConfig {
929    fn default() -> Self {
930        Self {
931            window_size: 60,
932            spike_multiplier: 3.0,
933            idle_threshold_ticks: 120,
934        }
935    }
936}
937
938// ---------------------------------------------------------------------------
939// BandwidthMonitorStats
940// ---------------------------------------------------------------------------
941
942/// Aggregate statistics maintained by [`PeerBandwidthMonitor`].
943#[derive(Debug, Clone, Default)]
944pub struct BandwidthMonitorStats {
945    /// Number of distinct peers currently tracked.
946    pub total_peers: usize,
947    /// Total number of samples recorded across all peers.
948    pub total_samples_recorded: u64,
949    /// Total number of anomalies detected (spikes + idles).
950    pub total_anomalies_detected: u64,
951    /// Aggregate bytes sent across all peers (cumulative).
952    pub aggregate_sent_bytes: u64,
953    /// Aggregate bytes received across all peers (cumulative).
954    pub aggregate_received_bytes: u64,
955}
956
957// ---------------------------------------------------------------------------
958// PeerBandwidthMonitor
959// ---------------------------------------------------------------------------
960
961/// Tracks per-peer and aggregate bandwidth over a sliding tick window.
962///
963/// Call [`record`](PeerBandwidthMonitor::record) on each tick per peer.
964/// Spike anomalies are appended to `pending_anomalies` and can be retrieved
965/// with [`drain_anomalies`](PeerBandwidthMonitor::drain_anomalies).
966/// Idle anomalies are computed on demand via
967/// [`check_idle`](PeerBandwidthMonitor::check_idle).
968pub struct PeerBandwidthMonitor {
969    /// Per-peer sliding windows, keyed by peer_id.
970    pub windows: HashMap<String, PeerBandwidthWindow>,
971    /// Monitor configuration.
972    pub config: MonitorConfig,
973    /// Aggregate statistics.
974    pub stats: BandwidthMonitorStats,
975    /// Anomalies detected during `record` calls; cleared by `drain_anomalies`.
976    pub pending_anomalies: Vec<BandwidthAnomaly>,
977}
978
979impl PeerBandwidthMonitor {
980    /// Create a new monitor with the given configuration.
981    pub fn new(config: MonitorConfig) -> Self {
982        Self {
983            windows: HashMap::new(),
984            config,
985            stats: BandwidthMonitorStats::default(),
986            pending_anomalies: Vec::new(),
987        }
988    }
989
990    /// Record a bandwidth sample for `peer_id` at logical `tick`.
991    ///
992    /// Automatically creates a window entry for new peers.  After recording,
993    /// checks for send/receive spikes and appends any detected
994    /// [`BandwidthAnomaly`] to `pending_anomalies`.
995    pub fn record(&mut self, peer_id: &str, tick: u64, bytes_sent: u64, bytes_recv: u64) {
996        let window_size = self.config.window_size;
997        let window = self
998            .windows
999            .entry(peer_id.to_owned())
1000            .or_insert_with(|| PeerBandwidthWindow::new(peer_id, window_size));
1001
1002        // Compute prior averages before the new sample is added (needs >= 1
1003        // existing sample so we have a meaningful baseline).
1004        let (prior_avg_send, prior_avg_recv, has_prior) = if !window.samples.is_empty() {
1005            (window.avg_send_rate(), window.avg_recv_rate(), true)
1006        } else {
1007            (0.0, 0.0, false)
1008        };
1009
1010        window.add_sample(tick, bytes_sent, bytes_recv);
1011
1012        // Update aggregate stats.
1013        self.stats.total_samples_recorded = self.stats.total_samples_recorded.saturating_add(1);
1014        self.stats.aggregate_sent_bytes =
1015            self.stats.aggregate_sent_bytes.saturating_add(bytes_sent);
1016        self.stats.aggregate_received_bytes = self
1017            .stats
1018            .aggregate_received_bytes
1019            .saturating_add(bytes_recv);
1020        self.stats.total_peers = self.windows.len();
1021
1022        // Spike detection requires at least one prior sample.
1023        if !has_prior {
1024            return;
1025        }
1026
1027        let multiplier = self.config.spike_multiplier;
1028
1029        if prior_avg_send > 0.0 && bytes_sent as f64 > multiplier * prior_avg_send {
1030            self.pending_anomalies.push(BandwidthAnomaly::SendSpike {
1031                peer_id: peer_id.to_owned(),
1032                sample_bytes: bytes_sent,
1033                avg_bytes: prior_avg_send,
1034            });
1035            self.stats.total_anomalies_detected =
1036                self.stats.total_anomalies_detected.saturating_add(1);
1037        }
1038
1039        if prior_avg_recv > 0.0 && bytes_recv as f64 > multiplier * prior_avg_recv {
1040            self.pending_anomalies.push(BandwidthAnomaly::RecvSpike {
1041                peer_id: peer_id.to_owned(),
1042                sample_bytes: bytes_recv,
1043                avg_bytes: prior_avg_recv,
1044            });
1045            self.stats.total_anomalies_detected =
1046                self.stats.total_anomalies_detected.saturating_add(1);
1047        }
1048    }
1049
1050    /// Take all pending anomalies, leaving an empty list.
1051    pub fn drain_anomalies(&mut self) -> Vec<BandwidthAnomaly> {
1052        std::mem::take(&mut self.pending_anomalies)
1053    }
1054
1055    /// Scan all tracked peers for idle conditions at `current_tick`.
1056    ///
1057    /// A peer is idle when it has no samples in its window **or** its most
1058    /// recent sample tick is more than `idle_threshold_ticks` behind
1059    /// `current_tick`.
1060    ///
1061    /// Unlike spike detection, idle anomalies are **returned directly** and
1062    /// are NOT added to `pending_anomalies`.
1063    pub fn check_idle(&mut self, current_tick: u64) -> Vec<BandwidthAnomaly> {
1064        let threshold = self.config.idle_threshold_ticks;
1065        let mut anomalies = Vec::new();
1066
1067        for (peer_id, window) in &self.windows {
1068            let is_idle = match window.samples.last() {
1069                None => true,
1070                Some(last) => {
1071                    // saturating_sub avoids underflow if current_tick < last.tick
1072                    let elapsed = current_tick.saturating_sub(last.tick);
1073                    elapsed >= threshold
1074                }
1075            };
1076
1077            if is_idle {
1078                let ticks_since_last = match window.samples.last() {
1079                    None => current_tick,
1080                    Some(last) => current_tick.saturating_sub(last.tick),
1081                };
1082                anomalies.push(BandwidthAnomaly::Idle {
1083                    peer_id: peer_id.clone(),
1084                    ticks_since_last,
1085                });
1086                self.stats.total_anomalies_detected =
1087                    self.stats.total_anomalies_detected.saturating_add(1);
1088            }
1089        }
1090
1091        anomalies
1092    }
1093
1094    /// Return a shared reference to the sliding window for `peer_id`, if any.
1095    pub fn peer_window(&self, peer_id: &str) -> Option<&PeerBandwidthWindow> {
1096        self.windows.get(peer_id)
1097    }
1098
1099    /// Return a shared reference to the aggregate statistics.
1100    pub fn stats(&self) -> &BandwidthMonitorStats {
1101        &self.stats
1102    }
1103}
1104
1105// ---------------------------------------------------------------------------
1106// PeerBandwidthMonitor tests
1107// ---------------------------------------------------------------------------
1108
1109#[cfg(test)]
1110mod peer_monitor_tests {
1111    use super::{BandwidthAnomaly, MonitorConfig, PeerBandwidthMonitor, PeerBandwidthWindow};
1112
1113    // -----------------------------------------------------------------------
1114    // Helper: build a fresh monitor with default config
1115    // -----------------------------------------------------------------------
1116    fn default_monitor() -> PeerBandwidthMonitor {
1117        PeerBandwidthMonitor::new(MonitorConfig::default())
1118    }
1119
1120    // -----------------------------------------------------------------------
1121    // 1. add_sample — basic insertion
1122    // -----------------------------------------------------------------------
1123    #[test]
1124    fn test_add_sample_basic() {
1125        let mut w = PeerBandwidthWindow::new("p1", 5);
1126        w.add_sample(1, 100, 200);
1127        assert_eq!(w.samples.len(), 1);
1128        assert_eq!(w.samples[0].tick, 1);
1129        assert_eq!(w.samples[0].bytes_sent, 100);
1130        assert_eq!(w.samples[0].bytes_received, 200);
1131    }
1132
1133    // -----------------------------------------------------------------------
1134    // 2. add_sample — sliding window eviction (oldest first)
1135    // -----------------------------------------------------------------------
1136    #[test]
1137    fn test_add_sample_evicts_oldest() {
1138        let mut w = PeerBandwidthWindow::new("p1", 3);
1139        w.add_sample(1, 10, 10);
1140        w.add_sample(2, 20, 20);
1141        w.add_sample(3, 30, 30);
1142        // Window is full; adding one more must evict tick=1
1143        w.add_sample(4, 40, 40);
1144        assert_eq!(w.samples.len(), 3);
1145        assert_eq!(
1146            w.samples[0].tick, 2,
1147            "oldest (tick=1) should have been evicted"
1148        );
1149        assert_eq!(w.samples[2].tick, 4);
1150    }
1151
1152    // -----------------------------------------------------------------------
1153    // 3. add_sample — cumulative totals are not affected by eviction
1154    // -----------------------------------------------------------------------
1155    #[test]
1156    fn test_add_sample_totals_accumulate_beyond_window() {
1157        let mut w = PeerBandwidthWindow::new("p1", 2);
1158        w.add_sample(1, 100, 50);
1159        w.add_sample(2, 200, 100);
1160        w.add_sample(3, 300, 150); // evicts tick=1
1161        assert_eq!(w.total_sent, 600);
1162        assert_eq!(w.total_received, 300);
1163        // Only the last 2 samples are in the window
1164        assert_eq!(w.samples.len(), 2);
1165    }
1166
1167    // -----------------------------------------------------------------------
1168    // 4. avg_send_rate — empty window
1169    // -----------------------------------------------------------------------
1170    #[test]
1171    fn test_avg_send_rate_empty() {
1172        let w = PeerBandwidthWindow::new("p1", 10);
1173        assert_eq!(w.avg_send_rate(), 0.0);
1174    }
1175
1176    // -----------------------------------------------------------------------
1177    // 5. avg_send_rate — correct mean
1178    // -----------------------------------------------------------------------
1179    #[test]
1180    fn test_avg_send_rate_correct() {
1181        let mut w = PeerBandwidthWindow::new("p1", 10);
1182        w.add_sample(1, 100, 0);
1183        w.add_sample(2, 200, 0);
1184        w.add_sample(3, 300, 0);
1185        // mean = (100+200+300)/3 = 200
1186        assert!((w.avg_send_rate() - 200.0).abs() < 1e-9);
1187    }
1188
1189    // -----------------------------------------------------------------------
1190    // 6. avg_recv_rate — empty window
1191    // -----------------------------------------------------------------------
1192    #[test]
1193    fn test_avg_recv_rate_empty() {
1194        let w = PeerBandwidthWindow::new("p1", 10);
1195        assert_eq!(w.avg_recv_rate(), 0.0);
1196    }
1197
1198    // -----------------------------------------------------------------------
1199    // 7. avg_recv_rate — correct mean
1200    // -----------------------------------------------------------------------
1201    #[test]
1202    fn test_avg_recv_rate_correct() {
1203        let mut w = PeerBandwidthWindow::new("p1", 10);
1204        w.add_sample(1, 0, 400);
1205        w.add_sample(2, 0, 600);
1206        // mean = 500
1207        assert!((w.avg_recv_rate() - 500.0).abs() < 1e-9);
1208    }
1209
1210    // -----------------------------------------------------------------------
1211    // 8. peak_send — empty window
1212    // -----------------------------------------------------------------------
1213    #[test]
1214    fn test_peak_send_empty() {
1215        let w = PeerBandwidthWindow::new("p1", 10);
1216        assert_eq!(w.peak_send(), 0);
1217    }
1218
1219    // -----------------------------------------------------------------------
1220    // 9. peak_send — correct maximum
1221    // -----------------------------------------------------------------------
1222    #[test]
1223    fn test_peak_send_correct() {
1224        let mut w = PeerBandwidthWindow::new("p1", 10);
1225        w.add_sample(1, 50, 0);
1226        w.add_sample(2, 999, 0);
1227        w.add_sample(3, 100, 0);
1228        assert_eq!(w.peak_send(), 999);
1229    }
1230
1231    // -----------------------------------------------------------------------
1232    // 10. peak_recv — empty window
1233    // -----------------------------------------------------------------------
1234    #[test]
1235    fn test_peak_recv_empty() {
1236        let w = PeerBandwidthWindow::new("p1", 10);
1237        assert_eq!(w.peak_recv(), 0);
1238    }
1239
1240    // -----------------------------------------------------------------------
1241    // 11. peak_recv — correct maximum
1242    // -----------------------------------------------------------------------
1243    #[test]
1244    fn test_peak_recv_correct() {
1245        let mut w = PeerBandwidthWindow::new("p1", 10);
1246        w.add_sample(1, 0, 1000);
1247        w.add_sample(2, 0, 500);
1248        w.add_sample(3, 0, 2000);
1249        assert_eq!(w.peak_recv(), 2000);
1250    }
1251
1252    // -----------------------------------------------------------------------
1253    // 12. record() auto-creates window for new peer
1254    // -----------------------------------------------------------------------
1255    #[test]
1256    fn test_record_creates_window() {
1257        let mut monitor = default_monitor();
1258        assert!(monitor.peer_window("peer-a").is_none());
1259        monitor.record("peer-a", 1, 100, 200);
1260        assert!(monitor.peer_window("peer-a").is_some());
1261    }
1262
1263    // -----------------------------------------------------------------------
1264    // 13. record() updates aggregate stats
1265    // -----------------------------------------------------------------------
1266    #[test]
1267    fn test_record_updates_aggregate_stats() {
1268        let mut monitor = default_monitor();
1269        monitor.record("p1", 1, 100, 50);
1270        monitor.record("p2", 1, 200, 75);
1271
1272        let s = monitor.stats();
1273        assert_eq!(s.total_peers, 2);
1274        assert_eq!(s.total_samples_recorded, 2);
1275        assert_eq!(s.aggregate_sent_bytes, 300);
1276        assert_eq!(s.aggregate_received_bytes, 125);
1277    }
1278
1279    // -----------------------------------------------------------------------
1280    // 14. No spike on first sample (only 1 sample — no prior avg)
1281    // -----------------------------------------------------------------------
1282    #[test]
1283    fn test_no_spike_on_first_sample() {
1284        let mut monitor = default_monitor();
1285        // Enormous values that would definitely trigger a spike if checked
1286        monitor.record("p1", 1, u64::MAX / 2, u64::MAX / 2);
1287        assert!(
1288            monitor.pending_anomalies.is_empty(),
1289            "no spike should be detected on the very first sample"
1290        );
1291    }
1292
1293    // -----------------------------------------------------------------------
1294    // 15. SendSpike detected when sample > 3x prior avg
1295    // -----------------------------------------------------------------------
1296    #[test]
1297    fn test_send_spike_detected() {
1298        let config = MonitorConfig {
1299            window_size: 60,
1300            spike_multiplier: 3.0,
1301            idle_threshold_ticks: 120,
1302        };
1303        let mut monitor = PeerBandwidthMonitor::new(config);
1304        // Establish baseline of 100 bytes/tick
1305        monitor.record("peer", 1, 100, 0);
1306        monitor.record("peer", 2, 100, 0);
1307        monitor.record("peer", 3, 100, 0);
1308        // Clear any incidental anomalies from setup
1309        let _ = monitor.drain_anomalies();
1310
1311        // Now send 400 bytes — prior avg is 100, 400 > 3 * 100 → spike
1312        monitor.record("peer", 4, 400, 0);
1313        let anomalies = monitor.drain_anomalies();
1314        assert_eq!(anomalies.len(), 1);
1315        match &anomalies[0] {
1316            BandwidthAnomaly::SendSpike {
1317                peer_id,
1318                sample_bytes,
1319                ..
1320            } => {
1321                assert_eq!(peer_id, "peer");
1322                assert_eq!(*sample_bytes, 400);
1323            }
1324            other => panic!("expected SendSpike, got {:?}", other),
1325        }
1326    }
1327
1328    // -----------------------------------------------------------------------
1329    // 16. RecvSpike detected when sample > 3x prior avg
1330    // -----------------------------------------------------------------------
1331    #[test]
1332    fn test_recv_spike_detected() {
1333        let mut monitor = default_monitor();
1334        monitor.record("peer", 1, 0, 100);
1335        monitor.record("peer", 2, 0, 100);
1336        monitor.record("peer", 3, 0, 100);
1337        let _ = monitor.drain_anomalies();
1338
1339        // 400 bytes recv > 3 * 100
1340        monitor.record("peer", 4, 0, 400);
1341        let anomalies = monitor.drain_anomalies();
1342        assert_eq!(anomalies.len(), 1);
1343        match &anomalies[0] {
1344            BandwidthAnomaly::RecvSpike {
1345                peer_id,
1346                sample_bytes,
1347                ..
1348            } => {
1349                assert_eq!(peer_id, "peer");
1350                assert_eq!(*sample_bytes, 400);
1351            }
1352            other => panic!("expected RecvSpike, got {:?}", other),
1353        }
1354    }
1355
1356    // -----------------------------------------------------------------------
1357    // 17. Exactly 3x multiplier does NOT trigger a spike (> not >=)
1358    // -----------------------------------------------------------------------
1359    #[test]
1360    fn test_spike_boundary_exactly_3x_no_spike() {
1361        let config = MonitorConfig {
1362            window_size: 60,
1363            spike_multiplier: 3.0,
1364            idle_threshold_ticks: 120,
1365        };
1366        let mut monitor = PeerBandwidthMonitor::new(config);
1367        // Baseline: avg_send = 100
1368        monitor.record("peer", 1, 100, 0);
1369        monitor.record("peer", 2, 100, 0);
1370        let _ = monitor.drain_anomalies();
1371
1372        // Exactly 3 * avg = 300 → NOT a spike (must be strictly greater)
1373        monitor.record("peer", 3, 300, 0);
1374        let anomalies = monitor.drain_anomalies();
1375        assert!(
1376            anomalies.is_empty(),
1377            "exactly 3x average should NOT trigger a spike, got {:?}",
1378            anomalies
1379        );
1380    }
1381
1382    // -----------------------------------------------------------------------
1383    // 18. One tick above 3x multiplier triggers a spike
1384    // -----------------------------------------------------------------------
1385    #[test]
1386    fn test_spike_boundary_above_3x_triggers_spike() {
1387        let config = MonitorConfig {
1388            window_size: 60,
1389            spike_multiplier: 3.0,
1390            idle_threshold_ticks: 120,
1391        };
1392        let mut monitor = PeerBandwidthMonitor::new(config);
1393        monitor.record("peer", 1, 100, 0);
1394        monitor.record("peer", 2, 100, 0);
1395        let _ = monitor.drain_anomalies();
1396
1397        // 301 > 3 * 100 → spike
1398        monitor.record("peer", 3, 301, 0);
1399        let anomalies = monitor.drain_anomalies();
1400        assert_eq!(anomalies.len(), 1, "301 > 300 should trigger a spike");
1401    }
1402
1403    // -----------------------------------------------------------------------
1404    // 19. drain_anomalies clears the pending list
1405    // -----------------------------------------------------------------------
1406    #[test]
1407    fn test_drain_anomalies_clears_list() {
1408        let mut monitor = default_monitor();
1409        monitor.record("peer", 1, 100, 0);
1410        monitor.record("peer", 2, 100, 0);
1411        monitor.record("peer", 3, 100, 0);
1412        // Trigger a spike
1413        monitor.record("peer", 4, 999, 0);
1414
1415        let first = monitor.drain_anomalies();
1416        assert!(!first.is_empty(), "should have anomalies after spike");
1417
1418        let second = monitor.drain_anomalies();
1419        assert!(second.is_empty(), "drain_anomalies should clear the list");
1420    }
1421
1422    // -----------------------------------------------------------------------
1423    // 20. check_idle — detects idle peer with no samples
1424    // -----------------------------------------------------------------------
1425    #[test]
1426    fn test_check_idle_no_samples() {
1427        let config = MonitorConfig {
1428            window_size: 5,
1429            spike_multiplier: 3.0,
1430            idle_threshold_ticks: 10,
1431        };
1432        let mut monitor = PeerBandwidthMonitor::new(config);
1433        // Create a window with no samples by recording then draining via a
1434        // fresh monitor with a tiny window that evicts everything — simpler:
1435        // just insert an entry manually.
1436        monitor.windows.insert(
1437            "idle-peer".to_owned(),
1438            PeerBandwidthWindow::new("idle-peer", 5),
1439        );
1440
1441        let anomalies = monitor.check_idle(50);
1442        assert_eq!(anomalies.len(), 1);
1443        match &anomalies[0] {
1444            BandwidthAnomaly::Idle {
1445                peer_id,
1446                ticks_since_last,
1447            } => {
1448                assert_eq!(peer_id, "idle-peer");
1449                assert_eq!(*ticks_since_last, 50); // current_tick when no samples
1450            }
1451            other => panic!("expected Idle, got {:?}", other),
1452        }
1453    }
1454
1455    // -----------------------------------------------------------------------
1456    // 21. check_idle — detects peer whose last sample is old enough
1457    // -----------------------------------------------------------------------
1458    #[test]
1459    fn test_check_idle_stale_last_sample() {
1460        let config = MonitorConfig {
1461            window_size: 60,
1462            spike_multiplier: 3.0,
1463            idle_threshold_ticks: 10,
1464        };
1465        let mut monitor = PeerBandwidthMonitor::new(config);
1466        monitor.record("peer", 5, 100, 100); // last tick = 5
1467
1468        // current_tick = 16; elapsed = 11 >= threshold 10 → idle
1469        let anomalies = monitor.check_idle(16);
1470        assert_eq!(anomalies.len(), 1);
1471        match &anomalies[0] {
1472            BandwidthAnomaly::Idle {
1473                peer_id,
1474                ticks_since_last,
1475            } => {
1476                assert_eq!(peer_id, "peer");
1477                assert_eq!(*ticks_since_last, 11);
1478            }
1479            other => panic!("expected Idle, got {:?}", other),
1480        }
1481    }
1482
1483    // -----------------------------------------------------------------------
1484    // 22. check_idle — does NOT flag a recently-active peer
1485    // -----------------------------------------------------------------------
1486    #[test]
1487    fn test_check_idle_active_peer_not_flagged() {
1488        let config = MonitorConfig {
1489            window_size: 60,
1490            spike_multiplier: 3.0,
1491            idle_threshold_ticks: 10,
1492        };
1493        let mut monitor = PeerBandwidthMonitor::new(config);
1494        monitor.record("peer", 95, 100, 100); // last tick = 95
1495
1496        // current_tick = 100; elapsed = 5 < threshold 10 → not idle
1497        let anomalies = monitor.check_idle(100);
1498        assert!(
1499            anomalies.is_empty(),
1500            "active peer should not be flagged as idle"
1501        );
1502    }
1503
1504    // -----------------------------------------------------------------------
1505    // 23. check_idle — exactly at threshold IS idle (>=)
1506    // -----------------------------------------------------------------------
1507    #[test]
1508    fn test_check_idle_exactly_at_threshold() {
1509        let config = MonitorConfig {
1510            window_size: 60,
1511            spike_multiplier: 3.0,
1512            idle_threshold_ticks: 10,
1513        };
1514        let mut monitor = PeerBandwidthMonitor::new(config);
1515        monitor.record("peer", 90, 100, 100); // last tick = 90
1516
1517        // elapsed = 10 = threshold → idle
1518        let anomalies = monitor.check_idle(100);
1519        assert_eq!(anomalies.len(), 1, "elapsed == threshold should be idle");
1520    }
1521
1522    // -----------------------------------------------------------------------
1523    // 24. check_idle increments stats.total_anomalies_detected
1524    // -----------------------------------------------------------------------
1525    #[test]
1526    fn test_check_idle_increments_anomaly_count() {
1527        let config = MonitorConfig {
1528            window_size: 60,
1529            spike_multiplier: 3.0,
1530            idle_threshold_ticks: 5,
1531        };
1532        let mut monitor = PeerBandwidthMonitor::new(config);
1533        monitor
1534            .windows
1535            .insert("a".to_owned(), PeerBandwidthWindow::new("a", 60));
1536        monitor
1537            .windows
1538            .insert("b".to_owned(), PeerBandwidthWindow::new("b", 60));
1539
1540        let before = monitor.stats().total_anomalies_detected;
1541        let anomalies = monitor.check_idle(100);
1542        let after = monitor.stats().total_anomalies_detected;
1543
1544        assert_eq!(anomalies.len(), 2);
1545        assert_eq!(after - before, 2);
1546    }
1547
1548    // -----------------------------------------------------------------------
1549    // 25. check_idle does NOT add to pending_anomalies
1550    // -----------------------------------------------------------------------
1551    #[test]
1552    fn test_check_idle_does_not_populate_pending() {
1553        let config = MonitorConfig {
1554            window_size: 60,
1555            spike_multiplier: 3.0,
1556            idle_threshold_ticks: 5,
1557        };
1558        let mut monitor = PeerBandwidthMonitor::new(config);
1559        monitor
1560            .windows
1561            .insert("idle".to_owned(), PeerBandwidthWindow::new("idle", 60));
1562
1563        let _ = monitor.check_idle(100);
1564        assert!(
1565            monitor.pending_anomalies.is_empty(),
1566            "check_idle must not populate pending_anomalies"
1567        );
1568    }
1569
1570    // -----------------------------------------------------------------------
1571    // 26. Both send and recv spikes can fire in the same record() call
1572    // -----------------------------------------------------------------------
1573    #[test]
1574    fn test_both_send_and_recv_spike_same_record() {
1575        let mut monitor = default_monitor();
1576        monitor.record("peer", 1, 100, 100);
1577        monitor.record("peer", 2, 100, 100);
1578        monitor.record("peer", 3, 100, 100);
1579        let _ = monitor.drain_anomalies();
1580
1581        // 500 bytes send AND recv — both > 3 * 100
1582        monitor.record("peer", 4, 500, 500);
1583        let anomalies = monitor.drain_anomalies();
1584        assert_eq!(
1585            anomalies.len(),
1586            2,
1587            "should detect both a SendSpike and RecvSpike"
1588        );
1589        let has_send = anomalies
1590            .iter()
1591            .any(|a| matches!(a, BandwidthAnomaly::SendSpike { .. }));
1592        let has_recv = anomalies
1593            .iter()
1594            .any(|a| matches!(a, BandwidthAnomaly::RecvSpike { .. }));
1595        assert!(has_send, "expected SendSpike in anomalies");
1596        assert!(has_recv, "expected RecvSpike in anomalies");
1597    }
1598
1599    // -----------------------------------------------------------------------
1600    // 27. peer_window() returns None for unknown peer
1601    // -----------------------------------------------------------------------
1602    #[test]
1603    fn test_peer_window_unknown_peer() {
1604        let monitor = default_monitor();
1605        assert!(monitor.peer_window("nobody").is_none());
1606    }
1607
1608    // -----------------------------------------------------------------------
1609    // 28. stats() reflects the latest snapshot
1610    // -----------------------------------------------------------------------
1611    #[test]
1612    fn test_stats_reflects_current_state() {
1613        let mut monitor = default_monitor();
1614        monitor.record("a", 1, 1000, 2000);
1615        monitor.record("b", 1, 3000, 4000);
1616
1617        let s = monitor.stats();
1618        assert_eq!(s.total_peers, 2);
1619        assert_eq!(s.aggregate_sent_bytes, 4000);
1620        assert_eq!(s.aggregate_received_bytes, 6000);
1621        assert_eq!(s.total_samples_recorded, 2);
1622    }
1623
1624    // -----------------------------------------------------------------------
1625    // 29. Spike anomaly carries the correct avg_bytes field
1626    // -----------------------------------------------------------------------
1627    #[test]
1628    fn test_spike_carries_correct_avg_bytes() {
1629        let mut monitor = default_monitor();
1630        // avg_send after 2 samples of 100 = 100.0
1631        monitor.record("peer", 1, 100, 0);
1632        monitor.record("peer", 2, 100, 0);
1633        let _ = monitor.drain_anomalies();
1634
1635        monitor.record("peer", 3, 400, 0);
1636        let anomalies = monitor.drain_anomalies();
1637        match &anomalies[0] {
1638            BandwidthAnomaly::SendSpike { avg_bytes, .. } => {
1639                assert!(
1640                    (avg_bytes - 100.0).abs() < 1e-6,
1641                    "avg_bytes should be 100.0, got {}",
1642                    avg_bytes
1643                );
1644            }
1645            other => panic!("expected SendSpike, got {:?}", other),
1646        }
1647    }
1648
1649    // -----------------------------------------------------------------------
1650    // 30. MonitorConfig::default() has expected field values
1651    // -----------------------------------------------------------------------
1652    #[test]
1653    fn test_monitor_config_defaults() {
1654        let cfg = MonitorConfig::default();
1655        assert_eq!(cfg.window_size, 60);
1656        assert!((cfg.spike_multiplier - 3.0).abs() < 1e-9);
1657        assert_eq!(cfg.idle_threshold_ticks, 120);
1658    }
1659}