Skip to main content

ipfrs_network/
peer_bandwidth_manager.rs

1//! Per-peer bandwidth management with token-bucket rate limiting and fairness scheduling.
2//!
3//! This module provides production-grade per-peer bandwidth tracking and enforcement:
4//! - Token-bucket based rate limiting for upload and download directions
5//! - Configurable burst sizes for bursty traffic tolerance
6//! - Sliding-window usage snapshots for real-time rate computation
7//! - Fairness scheduling policies (Max-Min, Weighted Fair, Unrestricted)
8//! - Top-K upload/download peer ranking
9//! - Idle peer eviction to bound memory
10//! - Global aggregate statistics
11//!
12//! # Example
13//!
14//! ```rust
15//! use ipfrs_network::peer_bandwidth_manager::{
16//!     BandwidthManagerConfig, BandwidthLimit, BandwidthDirection, PeerBandwidthManager,
17//! };
18//!
19//! let config = BandwidthManagerConfig::default();
20//! let mut mgr = PeerBandwidthManager::new(config);
21//! mgr.register_peer("peer-1".to_string(), 0);
22//!
23//! // Try to consume 512 bytes for upload
24//! let allowed = mgr.try_consume("peer-1", 512, BandwidthDirection::Upload, 0);
25//! assert!(allowed);
26//!
27//! // Record an unconditional transfer
28//! mgr.record_transfer("peer-1", 512, 256, 1_000);
29//!
30//! // Query usage
31//! if let Some(usage) = mgr.peer_usage("peer-1", 5_000) {
32//!     println!("upload rate: {:.1} bps", usage.upload_rate_bps);
33//! }
34//! ```
35
36use serde::{Deserialize, Serialize};
37use std::collections::{HashMap, VecDeque};
38
39// ---------------------------------------------------------------------------
40// BandwidthDirection
41// ---------------------------------------------------------------------------
42
43/// Direction of bandwidth consumption for rate-limiting purposes.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
45pub enum BandwidthDirection {
46    /// Bytes being sent to remote peers.
47    Upload,
48    /// Bytes being received from remote peers.
49    Download,
50    /// Both upload and download simultaneously.
51    Both,
52}
53
54// ---------------------------------------------------------------------------
55// BandwidthLimit
56// ---------------------------------------------------------------------------
57
58/// Token-bucket parameters for a single bandwidth direction.
59///
60/// A token bucket refills at `bytes_per_second` tokens per second and can
61/// hold at most `burst_bytes` tokens (the bucket capacity).  A transfer of N
62/// bytes is allowed when at least N tokens are available.
63#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
64pub struct BandwidthLimit {
65    /// Sustained rate in bytes per second.
66    pub bytes_per_second: u64,
67    /// Maximum burst size in bytes.  Tokens accumulate up to this cap.
68    pub burst_bytes: u64,
69}
70
71impl BandwidthLimit {
72    /// Construct a new [`BandwidthLimit`].
73    pub fn new(bytes_per_second: u64, burst_bytes: u64) -> Self {
74        Self {
75            bytes_per_second,
76            burst_bytes,
77        }
78    }
79
80    /// Returns `true` when the limit is effectively "unlimited" (rate == 0).
81    pub fn is_unlimited(&self) -> bool {
82        self.bytes_per_second == 0
83    }
84}
85
86impl Default for BandwidthLimit {
87    fn default() -> Self {
88        // 1 MB/s sustained, 2 MB burst.
89        Self {
90            bytes_per_second: 1_048_576,
91            burst_bytes: 2_097_152,
92        }
93    }
94}
95
96// ---------------------------------------------------------------------------
97// PeerBandwidthState
98// ---------------------------------------------------------------------------
99
100/// Token-bucket state for a single peer.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct PeerBandwidthState {
103    /// Stable identifier for the peer.
104    pub peer_id: String,
105    /// Current upload token count (fractional tokens are kept for precision).
106    pub upload_tokens: f64,
107    /// Current download token count.
108    pub download_tokens: f64,
109    /// Rate/burst configuration for the upload direction.
110    pub upload_limit: BandwidthLimit,
111    /// Rate/burst configuration for the download direction.
112    pub download_limit: BandwidthLimit,
113    /// Cumulative bytes sent to this peer (unconditional accounting).
114    pub total_bytes_sent: u64,
115    /// Cumulative bytes received from this peer.
116    pub total_bytes_recv: u64,
117    /// Millisecond timestamp of the last token refill.
118    pub last_refill_ms: u64,
119}
120
121impl PeerBandwidthState {
122    /// Create a new state initialised with full token buckets.
123    pub fn new(
124        peer_id: String,
125        upload_limit: BandwidthLimit,
126        download_limit: BandwidthLimit,
127        now_ms: u64,
128    ) -> Self {
129        Self {
130            peer_id,
131            upload_tokens: upload_limit.burst_bytes as f64,
132            download_tokens: download_limit.burst_bytes as f64,
133            upload_limit,
134            download_limit,
135            total_bytes_sent: 0,
136            total_bytes_recv: 0,
137            last_refill_ms: now_ms,
138        }
139    }
140}
141
142// ---------------------------------------------------------------------------
143// BandwidthUsage
144// ---------------------------------------------------------------------------
145
146/// Computed bandwidth usage for a peer within the configured time window.
147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
148pub struct BandwidthUsage {
149    /// Peer identifier.
150    pub peer_id: String,
151    /// Total bytes sent within the window.
152    pub bytes_sent: u64,
153    /// Total bytes received within the window.
154    pub bytes_recv: u64,
155    /// Upload rate in bytes per second derived from window samples.
156    pub upload_rate_bps: f64,
157    /// Download rate in bytes per second.
158    pub download_rate_bps: f64,
159    /// Window duration in milliseconds over which rates were computed.
160    pub window_ms: u64,
161}
162
163// ---------------------------------------------------------------------------
164// FairnessPolicy
165// ---------------------------------------------------------------------------
166
167/// Scheduling policy used by [`PeerBandwidthManager::apply_fairness`].
168#[derive(Debug, Clone, Default, Serialize, Deserialize)]
169pub enum FairnessPolicy {
170    /// Max-Min fairness: every peer receives an equal share of the total
171    /// available tokens.  Each peer's current token count is clamped to
172    /// `total_tokens / num_peers`.
173    MaxMinFairness,
174    /// Weighted fair queuing: each peer's tokens are scaled by its normalised
175    /// weight relative to all registered peers.
176    WeightedFair {
177        /// Per-peer weights.  Peers absent from the map are assigned weight 1.0.
178        weights: HashMap<String, f64>,
179    },
180    /// No fairness enforcement; tokens accumulate freely up to the burst cap.
181    #[default]
182    Unrestricted,
183}
184
185// ---------------------------------------------------------------------------
186// BandwidthManagerConfig
187// ---------------------------------------------------------------------------
188
189/// Configuration for [`PeerBandwidthManager`].
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct BandwidthManagerConfig {
192    /// Default upload limit applied when a peer is first registered.
193    pub default_upload_limit: BandwidthLimit,
194    /// Default download limit applied when a peer is first registered.
195    pub default_download_limit: BandwidthLimit,
196    /// Maximum number of peers tracked simultaneously.  When the limit is
197    /// reached the oldest registered peer is evicted to make room.
198    pub max_peers: usize,
199    /// Policy used by [`PeerBandwidthManager::apply_fairness`].
200    pub fairness_policy: FairnessPolicy,
201    /// Rolling window in milliseconds used for rate computation.
202    pub usage_window_ms: u64,
203}
204
205impl Default for BandwidthManagerConfig {
206    fn default() -> Self {
207        Self {
208            default_upload_limit: BandwidthLimit {
209                bytes_per_second: 1_048_576, // 1 MB/s
210                burst_bytes: 2_097_152,      // 2 MB
211            },
212            default_download_limit: BandwidthLimit {
213                bytes_per_second: 2_097_152, // 2 MB/s
214                burst_bytes: 4_194_304,      // 4 MB
215            },
216            max_peers: 1_000,
217            fairness_policy: FairnessPolicy::Unrestricted,
218            usage_window_ms: 5_000,
219        }
220    }
221}
222
223// ---------------------------------------------------------------------------
224// BandwidthManagerStats
225// ---------------------------------------------------------------------------
226
227/// Aggregate statistics for the entire [`PeerBandwidthManager`].
228#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
229pub struct BandwidthManagerStats {
230    /// Number of currently registered peers.
231    pub registered_peers: usize,
232    /// Total bytes sent across all peers since manager creation.
233    pub total_bytes_sent: u64,
234    /// Total bytes received across all peers since manager creation.
235    pub total_bytes_recv: u64,
236    /// Average upload rate across all peers (bytes per second).
237    pub avg_upload_rate_bps: f64,
238    /// Average download rate across all peers (bytes per second).
239    pub avg_download_rate_bps: f64,
240}
241
242// ---------------------------------------------------------------------------
243// PeerBandwidthManager
244// ---------------------------------------------------------------------------
245
246/// Manages per-peer bandwidth state, rate limiting, and fairness scheduling.
247///
248/// All timestamps are provided by the caller as milliseconds since an
249/// arbitrary epoch (e.g. `SystemTime::UNIX_EPOCH` or `Instant`-derived).
250/// This makes the manager fully testable without real-time dependencies.
251pub struct PeerBandwidthManager {
252    /// Manager configuration.
253    pub config: BandwidthManagerConfig,
254    /// Per-peer token-bucket state keyed by peer ID.
255    pub peers: HashMap<String, PeerBandwidthState>,
256    /// Sliding-window snapshots per peer: `(timestamp_ms, bytes_sent, bytes_recv)`.
257    pub usage_snapshots: HashMap<String, VecDeque<(u64, u64, u64)>>,
258    /// Total bytes sent across all peers since manager creation.
259    pub total_bytes_sent: u64,
260    /// Total bytes received across all peers since manager creation.
261    pub total_bytes_recv: u64,
262    /// Registration order (FIFO) used for eviction when `max_peers` is reached.
263    registration_order: VecDeque<String>,
264}
265
266impl PeerBandwidthManager {
267    // -----------------------------------------------------------------------
268    // Construction
269    // -----------------------------------------------------------------------
270
271    /// Create a new manager with the given configuration.
272    pub fn new(config: BandwidthManagerConfig) -> Self {
273        Self {
274            peers: HashMap::new(),
275            usage_snapshots: HashMap::new(),
276            total_bytes_sent: 0,
277            total_bytes_recv: 0,
278            registration_order: VecDeque::new(),
279            config,
280        }
281    }
282
283    // -----------------------------------------------------------------------
284    // Peer registration
285    // -----------------------------------------------------------------------
286
287    /// Register a peer with the default limits from the manager configuration.
288    ///
289    /// If the peer is already registered, the call is a no-op.
290    /// If `max_peers` would be exceeded, the oldest registered peer is evicted.
291    pub fn register_peer(&mut self, peer_id: String, now: u64) {
292        if self.peers.contains_key(&peer_id) {
293            return;
294        }
295
296        // Evict oldest peer if we're at capacity.
297        if self.peers.len() >= self.config.max_peers {
298            if let Some(oldest) = self.registration_order.pop_front() {
299                self.peers.remove(&oldest);
300                self.usage_snapshots.remove(&oldest);
301            }
302        }
303
304        let state = PeerBandwidthState::new(
305            peer_id.clone(),
306            self.config.default_upload_limit,
307            self.config.default_download_limit,
308            now,
309        );
310        self.peers.insert(peer_id.clone(), state);
311        self.usage_snapshots
312            .insert(peer_id.clone(), VecDeque::new());
313        self.registration_order.push_back(peer_id);
314    }
315
316    /// Override the upload and download limits for an already-registered peer.
317    ///
318    /// Returns `true` on success, `false` if the peer is not registered.
319    pub fn set_peer_limits(
320        &mut self,
321        peer_id: &str,
322        upload: BandwidthLimit,
323        download: BandwidthLimit,
324    ) -> bool {
325        if let Some(state) = self.peers.get_mut(peer_id) {
326            state.upload_limit = upload;
327            state.download_limit = download;
328            // Re-clamp existing tokens so they don't exceed the new burst caps.
329            state.upload_tokens = state.upload_tokens.min(upload.burst_bytes as f64);
330            state.download_tokens = state.download_tokens.min(download.burst_bytes as f64);
331            true
332        } else {
333            false
334        }
335    }
336
337    /// Remove a peer from the manager.
338    ///
339    /// Returns `true` if the peer existed and was removed.
340    pub fn remove_peer(&mut self, peer_id: &str) -> bool {
341        if self.peers.remove(peer_id).is_some() {
342            self.usage_snapshots.remove(peer_id);
343            self.registration_order.retain(|id| id != peer_id);
344            true
345        } else {
346            false
347        }
348    }
349
350    // -----------------------------------------------------------------------
351    // Token-bucket helpers
352    // -----------------------------------------------------------------------
353
354    /// Refill the token buckets for `state` based on elapsed time since the
355    /// last refill.
356    ///
357    /// `elapsed_s = (now_ms - last_refill_ms) / 1000.0`
358    ///
359    /// New tokens are calculated as `elapsed_s * bytes_per_second` and added
360    /// to the current token count, clamped to `burst_bytes`.
361    pub fn refill_tokens(state: &mut PeerBandwidthState, now: u64) {
362        if now <= state.last_refill_ms {
363            return;
364        }
365        let elapsed_ms = now - state.last_refill_ms;
366        let elapsed_s = elapsed_ms as f64 / 1_000.0;
367
368        if !state.upload_limit.is_unlimited() {
369            let new_up = state.upload_limit.bytes_per_second as f64 * elapsed_s;
370            state.upload_tokens =
371                (state.upload_tokens + new_up).min(state.upload_limit.burst_bytes as f64);
372        }
373
374        if !state.download_limit.is_unlimited() {
375            let new_dn = state.download_limit.bytes_per_second as f64 * elapsed_s;
376            state.download_tokens =
377                (state.download_tokens + new_dn).min(state.download_limit.burst_bytes as f64);
378        }
379
380        state.last_refill_ms = now;
381    }
382
383    /// Attempt to consume `bytes` from the appropriate token bucket(s).
384    ///
385    /// Tokens are refilled before checking.
386    /// Returns `true` if the transfer is permitted, `false` if there are
387    /// insufficient tokens.
388    pub fn try_consume(
389        &mut self,
390        peer_id: &str,
391        bytes: u64,
392        direction: BandwidthDirection,
393        now: u64,
394    ) -> bool {
395        let state = match self.peers.get_mut(peer_id) {
396            Some(s) => s,
397            None => return false,
398        };
399
400        Self::refill_tokens(state, now);
401
402        let bytes_f = bytes as f64;
403
404        match direction {
405            BandwidthDirection::Upload => {
406                if state.upload_limit.is_unlimited() || state.upload_tokens >= bytes_f {
407                    if !state.upload_limit.is_unlimited() {
408                        state.upload_tokens -= bytes_f;
409                    }
410                    true
411                } else {
412                    false
413                }
414            }
415            BandwidthDirection::Download => {
416                if state.download_limit.is_unlimited() || state.download_tokens >= bytes_f {
417                    if !state.download_limit.is_unlimited() {
418                        state.download_tokens -= bytes_f;
419                    }
420                    true
421                } else {
422                    false
423                }
424            }
425            BandwidthDirection::Both => {
426                let up_ok = state.upload_limit.is_unlimited() || state.upload_tokens >= bytes_f;
427                let dn_ok = state.download_limit.is_unlimited() || state.download_tokens >= bytes_f;
428                if up_ok && dn_ok {
429                    if !state.upload_limit.is_unlimited() {
430                        state.upload_tokens -= bytes_f;
431                    }
432                    if !state.download_limit.is_unlimited() {
433                        state.download_tokens -= bytes_f;
434                    }
435                    true
436                } else {
437                    false
438                }
439            }
440        }
441    }
442
443    // -----------------------------------------------------------------------
444    // Accounting
445    // -----------------------------------------------------------------------
446
447    /// Record an unconditional transfer for a peer (no token consumption).
448    ///
449    /// The snapshot is pushed to the sliding window and the manager-wide
450    /// totals are updated.  Snapshots older than `usage_window_ms` are
451    /// dropped.
452    pub fn record_transfer(&mut self, peer_id: &str, bytes_sent: u64, bytes_recv: u64, now: u64) {
453        // Update per-peer cumulative totals.
454        if let Some(state) = self.peers.get_mut(peer_id) {
455            state.total_bytes_sent = state.total_bytes_sent.saturating_add(bytes_sent);
456            state.total_bytes_recv = state.total_bytes_recv.saturating_add(bytes_recv);
457        }
458
459        // Push snapshot and prune stale entries.
460        if let Some(snapshots) = self.usage_snapshots.get_mut(peer_id) {
461            snapshots.push_back((now, bytes_sent, bytes_recv));
462
463            let window = self.config.usage_window_ms;
464            let cutoff = now.saturating_sub(window);
465            while snapshots
466                .front()
467                .map(|(ts, _, _)| *ts < cutoff)
468                .unwrap_or(false)
469            {
470                snapshots.pop_front();
471            }
472        }
473
474        // Update global totals.
475        self.total_bytes_sent = self.total_bytes_sent.saturating_add(bytes_sent);
476        self.total_bytes_recv = self.total_bytes_recv.saturating_add(bytes_recv);
477    }
478
479    // -----------------------------------------------------------------------
480    // Usage queries
481    // -----------------------------------------------------------------------
482
483    /// Compute [`BandwidthUsage`] for a peer by summing all snapshots within
484    /// the configured window ending at `now`.
485    ///
486    /// Returns `None` if the peer is not registered.
487    pub fn peer_usage(&self, peer_id: &str, now: u64) -> Option<BandwidthUsage> {
488        if !self.peers.contains_key(peer_id) {
489            return None;
490        }
491
492        let snapshots = self.usage_snapshots.get(peer_id)?;
493        let window = self.config.usage_window_ms;
494        let cutoff = now.saturating_sub(window);
495
496        let mut bytes_sent: u64 = 0;
497        let mut bytes_recv: u64 = 0;
498        for (ts, sent, recv) in snapshots.iter() {
499            if *ts >= cutoff {
500                bytes_sent = bytes_sent.saturating_add(*sent);
501                bytes_recv = bytes_recv.saturating_add(*recv);
502            }
503        }
504
505        let window_s = window as f64 / 1_000.0;
506        let upload_rate_bps = if window_s > 0.0 {
507            bytes_sent as f64 / window_s
508        } else {
509            0.0
510        };
511        let download_rate_bps = if window_s > 0.0 {
512            bytes_recv as f64 / window_s
513        } else {
514            0.0
515        };
516
517        Some(BandwidthUsage {
518            peer_id: peer_id.to_string(),
519            bytes_sent,
520            bytes_recv,
521            upload_rate_bps,
522            download_rate_bps,
523            window_ms: window,
524        })
525    }
526
527    /// Return the top-`k` peers by upload rate, descending.
528    pub fn top_uploaders(&self, k: usize, now: u64) -> Vec<BandwidthUsage> {
529        let mut usages: Vec<BandwidthUsage> = self
530            .peers
531            .keys()
532            .filter_map(|id| self.peer_usage(id, now))
533            .collect();
534        usages.sort_by(|a, b| {
535            b.upload_rate_bps
536                .partial_cmp(&a.upload_rate_bps)
537                .unwrap_or(std::cmp::Ordering::Equal)
538        });
539        usages.truncate(k);
540        usages
541    }
542
543    /// Return the top-`k` peers by download rate, descending.
544    pub fn top_downloaders(&self, k: usize, now: u64) -> Vec<BandwidthUsage> {
545        let mut usages: Vec<BandwidthUsage> = self
546            .peers
547            .keys()
548            .filter_map(|id| self.peer_usage(id, now))
549            .collect();
550        usages.sort_by(|a, b| {
551            b.download_rate_bps
552                .partial_cmp(&a.download_rate_bps)
553                .unwrap_or(std::cmp::Ordering::Equal)
554        });
555        usages.truncate(k);
556        usages
557    }
558
559    // -----------------------------------------------------------------------
560    // Fairness scheduling
561    // -----------------------------------------------------------------------
562
563    /// Apply the configured fairness policy to all registered peers.
564    ///
565    /// - [`FairnessPolicy::MaxMinFairness`]: Compute the sum of all peers'
566    ///   upload tokens and divide evenly; clamp each peer's token count to
567    ///   that fair share.  The same applies to download tokens.
568    /// - [`FairnessPolicy::WeightedFair`]: Scale each peer's tokens by its
569    ///   normalised weight (peer_weight / sum_weights), then clamp to burst.
570    /// - [`FairnessPolicy::Unrestricted`][]: No-op.
571    pub fn apply_fairness(&mut self, now: u64) {
572        // Refill all peers first so fairness acts on up-to-date token counts.
573        let peer_ids: Vec<String> = self.peers.keys().cloned().collect();
574        for id in &peer_ids {
575            if let Some(state) = self.peers.get_mut(id) {
576                Self::refill_tokens(state, now);
577            }
578        }
579
580        let n = self.peers.len();
581        if n == 0 {
582            return;
583        }
584
585        match &self.config.fairness_policy.clone() {
586            FairnessPolicy::Unrestricted => {}
587
588            FairnessPolicy::MaxMinFairness => {
589                let total_upload: f64 = self.peers.values().map(|s| s.upload_tokens).sum();
590                let total_download: f64 = self.peers.values().map(|s| s.download_tokens).sum();
591                let fair_up = total_upload / n as f64;
592                let fair_dn = total_download / n as f64;
593
594                for state in self.peers.values_mut() {
595                    state.upload_tokens = state
596                        .upload_tokens
597                        .min(fair_up)
598                        .min(state.upload_limit.burst_bytes as f64);
599                    state.download_tokens = state
600                        .download_tokens
601                        .min(fair_dn)
602                        .min(state.download_limit.burst_bytes as f64);
603                }
604            }
605
606            FairnessPolicy::WeightedFair { weights } => {
607                // Collect (id, weight) pairs; default weight is 1.0 for absent entries.
608                let weighted: Vec<(String, f64)> = peer_ids
609                    .iter()
610                    .map(|id| {
611                        let w = weights.get(id).copied().unwrap_or(1.0).max(0.0);
612                        (id.clone(), w)
613                    })
614                    .collect();
615
616                let total_weight: f64 = weighted.iter().map(|(_, w)| w).sum();
617                if total_weight <= 0.0 {
618                    return;
619                }
620
621                for (id, weight) in &weighted {
622                    let norm = weight / total_weight;
623                    if let Some(state) = self.peers.get_mut(id) {
624                        state.upload_tokens =
625                            (state.upload_tokens * norm).min(state.upload_limit.burst_bytes as f64);
626                        state.download_tokens = (state.download_tokens * norm)
627                            .min(state.download_limit.burst_bytes as f64);
628                    }
629                }
630            }
631        }
632    }
633
634    // -----------------------------------------------------------------------
635    // Maintenance
636    // -----------------------------------------------------------------------
637
638    /// Remove peers that have no snapshot newer than `now - idle_threshold_ms`.
639    ///
640    /// Returns the number of peers evicted.
641    pub fn evict_idle_peers(&mut self, now: u64, idle_threshold_ms: u64) -> usize {
642        let cutoff = now.saturating_sub(idle_threshold_ms);
643        let idle: Vec<String> = self
644            .usage_snapshots
645            .iter()
646            .filter(|(_, snaps)| {
647                // A peer is idle if there are no snapshots at or after the cutoff.
648                !snaps.iter().any(|(ts, _, _)| *ts >= cutoff)
649            })
650            .map(|(id, _)| id.clone())
651            .collect();
652
653        let count = idle.len();
654        for id in &idle {
655            self.peers.remove(id);
656            self.usage_snapshots.remove(id);
657            self.registration_order.retain(|r| r != id);
658        }
659        count
660    }
661
662    // -----------------------------------------------------------------------
663    // Statistics
664    // -----------------------------------------------------------------------
665
666    /// Collect aggregate statistics for the manager.
667    ///
668    /// Average rates are computed from the usage window of each registered
669    /// peer at the current moment (passing `now = 0` gives a snapshot of
670    /// accumulated counters without rate computation).
671    pub fn manager_stats(&self) -> BandwidthManagerStats {
672        // Use a synthetic "now" far in the future so all snapshots fall within
673        // the window when called without a real timestamp.
674        let n = self.peers.len();
675        let (sum_up, sum_dn) = if n > 0 {
676            // We cannot call peer_usage here without a sensible `now`, so we
677            // compute rates from the raw snapshot totals using the window.
678            let window_s = self.config.usage_window_ms as f64 / 1_000.0;
679            let mut up = 0.0f64;
680            let mut dn = 0.0f64;
681            for snaps in self.usage_snapshots.values() {
682                let sent: u64 = snaps.iter().map(|(_, s, _)| s).sum();
683                let recv: u64 = snaps.iter().map(|(_, _, r)| r).sum();
684                if window_s > 0.0 {
685                    up += sent as f64 / window_s;
686                    dn += recv as f64 / window_s;
687                }
688            }
689            (up, dn)
690        } else {
691            (0.0, 0.0)
692        };
693
694        let avg_up = if n > 0 { sum_up / n as f64 } else { 0.0 };
695        let avg_dn = if n > 0 { sum_dn / n as f64 } else { 0.0 };
696
697        BandwidthManagerStats {
698            registered_peers: n,
699            total_bytes_sent: self.total_bytes_sent,
700            total_bytes_recv: self.total_bytes_recv,
701            avg_upload_rate_bps: avg_up,
702            avg_download_rate_bps: avg_dn,
703        }
704    }
705}
706
707// ---------------------------------------------------------------------------
708// Tests
709// ---------------------------------------------------------------------------
710
711#[cfg(test)]
712mod tests {
713    use super::{
714        BandwidthDirection, BandwidthLimit, BandwidthManagerConfig, BandwidthUsage, FairnessPolicy,
715        PeerBandwidthManager, PeerBandwidthState,
716    };
717    use std::collections::HashMap;
718
719    // -----------------------------------------------------------------------
720    // Helpers
721    // -----------------------------------------------------------------------
722
723    /// Build a manager with a 5-second window.
724    fn make_manager() -> PeerBandwidthManager {
725        PeerBandwidthManager::new(BandwidthManagerConfig::default())
726    }
727
728    /// Build a manager with a very low default upload/download limit (1 KB/s, 2 KB burst).
729    fn make_tight_manager() -> PeerBandwidthManager {
730        let config = BandwidthManagerConfig {
731            default_upload_limit: BandwidthLimit {
732                bytes_per_second: 1_024,
733                burst_bytes: 2_048,
734            },
735            default_download_limit: BandwidthLimit {
736                bytes_per_second: 1_024,
737                burst_bytes: 2_048,
738            },
739            ..Default::default()
740        };
741        PeerBandwidthManager::new(config)
742    }
743
744    // -----------------------------------------------------------------------
745    // register_peer
746    // -----------------------------------------------------------------------
747
748    #[test]
749    fn test_register_peer_basic() {
750        let mut mgr = make_manager();
751        mgr.register_peer("alice".to_string(), 0);
752        assert!(mgr.peers.contains_key("alice"));
753    }
754
755    #[test]
756    fn test_register_peer_idempotent() {
757        let mut mgr = make_manager();
758        mgr.register_peer("alice".to_string(), 0);
759        mgr.register_peer("alice".to_string(), 1_000);
760        // Should still be exactly one entry.
761        assert_eq!(mgr.peers.len(), 1);
762    }
763
764    #[test]
765    fn test_register_peer_initial_tokens_full() {
766        let mut mgr = make_tight_manager();
767        mgr.register_peer("peer-a".to_string(), 0);
768        let state = mgr.peers.get("peer-a").expect("peer not found");
769        assert_eq!(state.upload_tokens, 2_048.0);
770        assert_eq!(state.download_tokens, 2_048.0);
771    }
772
773    #[test]
774    fn test_register_peer_evicts_oldest_when_full() {
775        let config = BandwidthManagerConfig {
776            max_peers: 2,
777            ..Default::default()
778        };
779        let mut mgr = PeerBandwidthManager::new(config);
780        mgr.register_peer("p1".to_string(), 0);
781        mgr.register_peer("p2".to_string(), 0);
782        mgr.register_peer("p3".to_string(), 0);
783        // p1 should have been evicted.
784        assert!(!mgr.peers.contains_key("p1"));
785        assert!(mgr.peers.contains_key("p2"));
786        assert!(mgr.peers.contains_key("p3"));
787    }
788
789    // -----------------------------------------------------------------------
790    // remove_peer
791    // -----------------------------------------------------------------------
792
793    #[test]
794    fn test_remove_peer_existing() {
795        let mut mgr = make_manager();
796        mgr.register_peer("bob".to_string(), 0);
797        assert!(mgr.remove_peer("bob"));
798        assert!(!mgr.peers.contains_key("bob"));
799    }
800
801    #[test]
802    fn test_remove_peer_nonexistent() {
803        let mut mgr = make_manager();
804        assert!(!mgr.remove_peer("ghost"));
805    }
806
807    #[test]
808    fn test_remove_peer_clears_snapshots() {
809        let mut mgr = make_manager();
810        mgr.register_peer("carol".to_string(), 0);
811        mgr.record_transfer("carol", 100, 200, 1_000);
812        mgr.remove_peer("carol");
813        assert!(!mgr.usage_snapshots.contains_key("carol"));
814    }
815
816    // -----------------------------------------------------------------------
817    // set_peer_limits
818    // -----------------------------------------------------------------------
819
820    #[test]
821    fn test_set_peer_limits_success() {
822        let mut mgr = make_tight_manager();
823        mgr.register_peer("dave".to_string(), 0);
824        let new_up = BandwidthLimit::new(4_096, 8_192);
825        let new_dn = BandwidthLimit::new(4_096, 8_192);
826        assert!(mgr.set_peer_limits("dave", new_up, new_dn));
827        let state = mgr.peers.get("dave").expect("peer not found");
828        assert_eq!(state.upload_limit.bytes_per_second, 4_096);
829    }
830
831    #[test]
832    fn test_set_peer_limits_nonexistent() {
833        let mut mgr = make_manager();
834        let limit = BandwidthLimit::default();
835        assert!(!mgr.set_peer_limits("nobody", limit, limit));
836    }
837
838    #[test]
839    fn test_set_peer_limits_clamps_tokens() {
840        let mut mgr = make_tight_manager();
841        mgr.register_peer("eve".to_string(), 0);
842        // Reduce burst to 512 bytes; existing tokens (2048) must be clamped.
843        let tight = BandwidthLimit::new(1_024, 512);
844        mgr.set_peer_limits("eve", tight, tight);
845        let state = mgr.peers.get("eve").expect("peer not found");
846        assert!(state.upload_tokens <= 512.0);
847        assert!(state.download_tokens <= 512.0);
848    }
849
850    // -----------------------------------------------------------------------
851    // refill_tokens
852    // -----------------------------------------------------------------------
853
854    #[test]
855    fn test_refill_tokens_adds_correct_amount() {
856        let limit = BandwidthLimit::new(1_000, 10_000);
857        let mut state = PeerBandwidthState::new("p".to_string(), limit, limit, 0);
858        // Drain tokens completely.
859        state.upload_tokens = 0.0;
860        state.download_tokens = 0.0;
861        // Refill after 1 second.
862        PeerBandwidthManager::refill_tokens(&mut state, 1_000);
863        assert!((state.upload_tokens - 1_000.0).abs() < 1.0);
864        assert!((state.download_tokens - 1_000.0).abs() < 1.0);
865    }
866
867    #[test]
868    fn test_refill_tokens_clamps_to_burst() {
869        let limit = BandwidthLimit::new(10_000, 5_000);
870        let mut state = PeerBandwidthState::new("p".to_string(), limit, limit, 0);
871        state.upload_tokens = 0.0;
872        state.download_tokens = 0.0;
873        // 1 second refill would add 10_000 tokens but burst is 5_000.
874        PeerBandwidthManager::refill_tokens(&mut state, 1_000);
875        assert_eq!(state.upload_tokens, 5_000.0);
876        assert_eq!(state.download_tokens, 5_000.0);
877    }
878
879    #[test]
880    fn test_refill_tokens_noop_when_time_not_advanced() {
881        let limit = BandwidthLimit::new(1_000, 10_000);
882        let mut state = PeerBandwidthState::new("p".to_string(), limit, limit, 1_000);
883        state.upload_tokens = 500.0;
884        PeerBandwidthManager::refill_tokens(&mut state, 1_000); // same timestamp
885        assert_eq!(state.upload_tokens, 500.0);
886    }
887
888    // -----------------------------------------------------------------------
889    // try_consume
890    // -----------------------------------------------------------------------
891
892    #[test]
893    fn test_try_consume_upload_allowed() {
894        let mut mgr = make_tight_manager();
895        mgr.register_peer("frank".to_string(), 0);
896        assert!(mgr.try_consume("frank", 1_000, BandwidthDirection::Upload, 0));
897    }
898
899    #[test]
900    fn test_try_consume_upload_denied_insufficient_tokens() {
901        let mut mgr = make_tight_manager();
902        mgr.register_peer("grace".to_string(), 0);
903        // Drain entire burst.
904        assert!(mgr.try_consume("grace", 2_048, BandwidthDirection::Upload, 0));
905        // Next request should be denied.
906        assert!(!mgr.try_consume("grace", 1, BandwidthDirection::Upload, 0));
907    }
908
909    #[test]
910    fn test_try_consume_download_allowed() {
911        let mut mgr = make_tight_manager();
912        mgr.register_peer("hank".to_string(), 0);
913        assert!(mgr.try_consume("hank", 1_024, BandwidthDirection::Download, 0));
914    }
915
916    #[test]
917    fn test_try_consume_both_directions() {
918        let mut mgr = make_tight_manager();
919        mgr.register_peer("ivan".to_string(), 0);
920        assert!(mgr.try_consume("ivan", 1_000, BandwidthDirection::Both, 0));
921        let state = mgr.peers.get("ivan").expect("peer not found");
922        assert!((state.upload_tokens - 1_048.0).abs() < 2.0);
923        assert!((state.download_tokens - 1_048.0).abs() < 2.0);
924    }
925
926    #[test]
927    fn test_try_consume_both_denied_if_one_direction_insufficient() {
928        let mut mgr = make_tight_manager();
929        mgr.register_peer("judy".to_string(), 0);
930        // Drain upload only.
931        assert!(mgr.try_consume("judy", 2_048, BandwidthDirection::Upload, 0));
932        // Both should now fail because upload is empty.
933        assert!(!mgr.try_consume("judy", 100, BandwidthDirection::Both, 0));
934    }
935
936    #[test]
937    fn test_try_consume_unknown_peer_returns_false() {
938        let mut mgr = make_manager();
939        assert!(!mgr.try_consume("unknown", 1, BandwidthDirection::Upload, 0));
940    }
941
942    #[test]
943    fn test_try_consume_refills_before_check() {
944        let mut mgr = make_tight_manager();
945        mgr.register_peer("karl".to_string(), 0);
946        // Drain all upload tokens.
947        assert!(mgr.try_consume("karl", 2_048, BandwidthDirection::Upload, 0));
948        // Advance 2 seconds — should gain 2_048 tokens (2 * 1024), capped at burst.
949        assert!(mgr.try_consume("karl", 2_048, BandwidthDirection::Upload, 2_000));
950    }
951
952    // -----------------------------------------------------------------------
953    // record_transfer
954    // -----------------------------------------------------------------------
955
956    #[test]
957    fn test_record_transfer_updates_global_totals() {
958        let mut mgr = make_manager();
959        mgr.register_peer("liam".to_string(), 0);
960        mgr.record_transfer("liam", 1_000, 500, 1_000);
961        assert_eq!(mgr.total_bytes_sent, 1_000);
962        assert_eq!(mgr.total_bytes_recv, 500);
963    }
964
965    #[test]
966    fn test_record_transfer_updates_peer_totals() {
967        let mut mgr = make_manager();
968        mgr.register_peer("mia".to_string(), 0);
969        mgr.record_transfer("mia", 300, 150, 1_000);
970        mgr.record_transfer("mia", 200, 100, 2_000);
971        let state = mgr.peers.get("mia").expect("peer not found");
972        assert_eq!(state.total_bytes_sent, 500);
973        assert_eq!(state.total_bytes_recv, 250);
974    }
975
976    #[test]
977    fn test_record_transfer_pushes_snapshot() {
978        let mut mgr = make_manager();
979        mgr.register_peer("noah".to_string(), 0);
980        mgr.record_transfer("noah", 100, 50, 1_000);
981        let snaps = mgr.usage_snapshots.get("noah").expect("no snapshots");
982        assert_eq!(snaps.len(), 1);
983    }
984
985    #[test]
986    fn test_record_transfer_prunes_old_snapshots() {
987        let mut mgr = make_manager(); // 5_000 ms window
988        mgr.register_peer("olivia".to_string(), 0);
989        mgr.record_transfer("olivia", 100, 50, 0);
990        mgr.record_transfer("olivia", 100, 50, 4_999);
991        // Snapshot at t=0 should be pruned when we record at t=5001 (window cutoff = 1).
992        mgr.record_transfer("olivia", 100, 50, 5_001);
993        let snaps = mgr.usage_snapshots.get("olivia").expect("no snapshots");
994        // t=0 is before cutoff (5001-5000=1), t=4999 is before cutoff too, t=5001 is ok.
995        for (ts, _, _) in snaps.iter() {
996            assert!(*ts >= 5_001u64.saturating_sub(5_000));
997        }
998    }
999
1000    #[test]
1001    fn test_record_transfer_noop_for_unknown_peer_global_counts_still_updated() {
1002        let mut mgr = make_manager();
1003        // No peer registered — global counts should still go up.
1004        mgr.record_transfer("unknown", 999, 111, 1_000);
1005        assert_eq!(mgr.total_bytes_sent, 999);
1006        assert_eq!(mgr.total_bytes_recv, 111);
1007    }
1008
1009    // -----------------------------------------------------------------------
1010    // peer_usage
1011    // -----------------------------------------------------------------------
1012
1013    #[test]
1014    fn test_peer_usage_returns_none_for_unregistered() {
1015        let mgr = make_manager();
1016        assert!(mgr.peer_usage("ghost", 5_000).is_none());
1017    }
1018
1019    #[test]
1020    fn test_peer_usage_empty_snapshots() {
1021        let mut mgr = make_manager();
1022        mgr.register_peer("petra".to_string(), 0);
1023        let usage = mgr.peer_usage("petra", 5_000).expect("should return usage");
1024        assert_eq!(usage.bytes_sent, 0);
1025        assert_eq!(usage.bytes_recv, 0);
1026        assert_eq!(usage.upload_rate_bps, 0.0);
1027    }
1028
1029    #[test]
1030    fn test_peer_usage_rate_calculation() {
1031        let mut mgr = make_manager(); // 5_000 ms window
1032        mgr.register_peer("quinn".to_string(), 0);
1033        // 5_000 bytes sent within the 5-second window → 1_000 B/s
1034        mgr.record_transfer("quinn", 5_000, 0, 1_000);
1035        let usage = mgr.peer_usage("quinn", 6_000).expect("should return usage");
1036        // All snapshots within the 5 s window → rate ≈ 5000/5 = 1000 B/s.
1037        assert!((usage.upload_rate_bps - 1_000.0).abs() < 1.0);
1038    }
1039
1040    #[test]
1041    fn test_peer_usage_excludes_old_snapshots() {
1042        let mut mgr = make_manager(); // 5_000 ms window
1043        mgr.register_peer("rachel".to_string(), 0);
1044        // Record at t=0 (old) and t=5_001 (current).
1045        mgr.record_transfer("rachel", 10_000, 0, 0);
1046        mgr.record_transfer("rachel", 1_000, 0, 5_001);
1047        let usage = mgr
1048            .peer_usage("rachel", 10_001)
1049            .expect("should return usage");
1050        // Only the snapshot at t=5_001 is within window [10_001-5000, 10_001] = [5_001, 10_001].
1051        assert_eq!(usage.bytes_sent, 1_000);
1052    }
1053
1054    #[test]
1055    fn test_peer_usage_window_ms_matches_config() {
1056        let mut mgr = make_manager();
1057        mgr.register_peer("sam".to_string(), 0);
1058        let usage = mgr.peer_usage("sam", 0).expect("should return usage");
1059        assert_eq!(usage.window_ms, 5_000);
1060    }
1061
1062    // -----------------------------------------------------------------------
1063    // top_uploaders / top_downloaders
1064    // -----------------------------------------------------------------------
1065
1066    #[test]
1067    fn test_top_uploaders_ordering() {
1068        let mut mgr = make_manager();
1069        for id in &["a", "b", "c"] {
1070            mgr.register_peer(id.to_string(), 0);
1071        }
1072        mgr.record_transfer("a", 300, 0, 1_000);
1073        mgr.record_transfer("b", 100, 0, 1_000);
1074        mgr.record_transfer("c", 200, 0, 1_000);
1075        let top = mgr.top_uploaders(3, 5_000);
1076        assert_eq!(top[0].peer_id, "a");
1077        assert_eq!(top[1].peer_id, "c");
1078        assert_eq!(top[2].peer_id, "b");
1079    }
1080
1081    #[test]
1082    fn test_top_uploaders_k_limits_results() {
1083        let mut mgr = make_manager();
1084        for i in 0..5u32 {
1085            let id = format!("peer-{i}");
1086            mgr.register_peer(id.clone(), 0);
1087            mgr.record_transfer(&id, (i + 1) as u64 * 100, 0, 1_000);
1088        }
1089        let top = mgr.top_uploaders(2, 5_000);
1090        assert_eq!(top.len(), 2);
1091    }
1092
1093    #[test]
1094    fn test_top_downloaders_ordering() {
1095        let mut mgr = make_manager();
1096        for id in &["x", "y", "z"] {
1097            mgr.register_peer(id.to_string(), 0);
1098        }
1099        mgr.record_transfer("x", 0, 500, 1_000);
1100        mgr.record_transfer("y", 0, 200, 1_000);
1101        mgr.record_transfer("z", 0, 800, 1_000);
1102        let top = mgr.top_downloaders(3, 5_000);
1103        assert_eq!(top[0].peer_id, "z");
1104        assert_eq!(top[1].peer_id, "x");
1105        assert_eq!(top[2].peer_id, "y");
1106    }
1107
1108    #[test]
1109    fn test_top_downloaders_returns_all_if_k_exceeds_peers() {
1110        let mut mgr = make_manager();
1111        mgr.register_peer("only".to_string(), 0);
1112        mgr.record_transfer("only", 0, 100, 1_000);
1113        let top = mgr.top_downloaders(10, 5_000);
1114        assert_eq!(top.len(), 1);
1115    }
1116
1117    // -----------------------------------------------------------------------
1118    // apply_fairness — MaxMinFairness
1119    // -----------------------------------------------------------------------
1120
1121    #[test]
1122    fn test_fairness_maxmin_equalises_tokens() {
1123        let config = BandwidthManagerConfig {
1124            fairness_policy: FairnessPolicy::MaxMinFairness,
1125            ..Default::default()
1126        };
1127        let mut mgr = PeerBandwidthManager::new(config);
1128        mgr.register_peer("p1".to_string(), 0);
1129        mgr.register_peer("p2".to_string(), 0);
1130        // Give p1 twice the tokens of p2.
1131        mgr.peers.get_mut("p1").expect("p1").upload_tokens = 2_000.0;
1132        mgr.peers.get_mut("p2").expect("p2").upload_tokens = 1_000.0;
1133        mgr.apply_fairness(0);
1134        let fair_share = 3_000.0 / 2.0; // total / n = 1500
1135        let t1 = mgr.peers.get("p1").expect("p1").upload_tokens;
1136        let t2 = mgr.peers.get("p2").expect("p2").upload_tokens;
1137        // Both should be clamped to at most fair_share.
1138        assert!(t1 <= fair_share + 0.01);
1139        assert!(t2 <= fair_share + 0.01);
1140    }
1141
1142    #[test]
1143    fn test_fairness_maxmin_noop_when_no_peers() {
1144        let config = BandwidthManagerConfig {
1145            fairness_policy: FairnessPolicy::MaxMinFairness,
1146            ..Default::default()
1147        };
1148        let mut mgr = PeerBandwidthManager::new(config);
1149        // Should not panic.
1150        mgr.apply_fairness(0);
1151    }
1152
1153    // -----------------------------------------------------------------------
1154    // apply_fairness — WeightedFair
1155    // -----------------------------------------------------------------------
1156
1157    #[test]
1158    fn test_fairness_weighted_scales_tokens() {
1159        let mut weights = HashMap::new();
1160        weights.insert("heavy".to_string(), 3.0);
1161        weights.insert("light".to_string(), 1.0);
1162        let config = BandwidthManagerConfig {
1163            fairness_policy: FairnessPolicy::WeightedFair { weights },
1164            default_upload_limit: BandwidthLimit {
1165                bytes_per_second: 100_000,
1166                burst_bytes: 1_000_000,
1167            },
1168            default_download_limit: BandwidthLimit {
1169                bytes_per_second: 100_000,
1170                burst_bytes: 1_000_000,
1171            },
1172            ..Default::default()
1173        };
1174        let mut mgr = PeerBandwidthManager::new(config);
1175        mgr.register_peer("heavy".to_string(), 0);
1176        mgr.register_peer("light".to_string(), 0);
1177        // Set equal starting tokens.
1178        mgr.peers.get_mut("heavy").expect("heavy").upload_tokens = 1_000_000.0;
1179        mgr.peers.get_mut("light").expect("light").upload_tokens = 1_000_000.0;
1180        mgr.apply_fairness(0);
1181        let heavy_tokens = mgr.peers.get("heavy").expect("heavy").upload_tokens;
1182        let light_tokens = mgr.peers.get("light").expect("light").upload_tokens;
1183        // heavy has 3/4 weight, light has 1/4 weight.
1184        // heavy_tokens should be greater than light_tokens.
1185        assert!(
1186            heavy_tokens > light_tokens,
1187            "heavy={heavy_tokens} light={light_tokens}"
1188        );
1189    }
1190
1191    #[test]
1192    fn test_fairness_unrestricted_noop() {
1193        let mut mgr = make_manager(); // Unrestricted policy
1194        mgr.register_peer("q1".to_string(), 0);
1195        mgr.peers.get_mut("q1").expect("q1").upload_tokens = 777.0;
1196        mgr.apply_fairness(0);
1197        // Tokens should not change (only possible refill from elapsed=0).
1198        let tokens = mgr.peers.get("q1").expect("q1").upload_tokens;
1199        // After refill with elapsed=0 tokens remain unchanged.
1200        assert!(tokens >= 777.0);
1201    }
1202
1203    // -----------------------------------------------------------------------
1204    // evict_idle_peers
1205    // -----------------------------------------------------------------------
1206
1207    #[test]
1208    fn test_evict_idle_peers_removes_stale() {
1209        let mut mgr = make_manager();
1210        mgr.register_peer("active".to_string(), 0);
1211        mgr.register_peer("idle".to_string(), 0);
1212        mgr.record_transfer("active", 100, 0, 10_000);
1213        // "idle" has no snapshots.
1214        let evicted = mgr.evict_idle_peers(10_000, 5_000);
1215        assert_eq!(evicted, 1);
1216        assert!(!mgr.peers.contains_key("idle"));
1217        assert!(mgr.peers.contains_key("active"));
1218    }
1219
1220    #[test]
1221    fn test_evict_idle_peers_returns_zero_when_all_active() {
1222        let mut mgr = make_manager();
1223        mgr.register_peer("p1".to_string(), 0);
1224        mgr.record_transfer("p1", 1, 0, 5_000);
1225        let evicted = mgr.evict_idle_peers(5_000, 5_000);
1226        assert_eq!(evicted, 0);
1227    }
1228
1229    #[test]
1230    fn test_evict_idle_peers_clears_snapshots() {
1231        let mut mgr = make_manager();
1232        mgr.register_peer("stale".to_string(), 0);
1233        // Record a very old snapshot.
1234        mgr.record_transfer("stale", 100, 0, 0);
1235        // Evict with threshold 5000 at now=10_000 — cutoff = 5000; snapshot at 0 < 5000.
1236        let evicted = mgr.evict_idle_peers(10_000, 5_000);
1237        assert_eq!(evicted, 1);
1238        assert!(!mgr.usage_snapshots.contains_key("stale"));
1239    }
1240
1241    // -----------------------------------------------------------------------
1242    // manager_stats
1243    // -----------------------------------------------------------------------
1244
1245    #[test]
1246    fn test_manager_stats_registered_peers() {
1247        let mut mgr = make_manager();
1248        mgr.register_peer("s1".to_string(), 0);
1249        mgr.register_peer("s2".to_string(), 0);
1250        let stats = mgr.manager_stats();
1251        assert_eq!(stats.registered_peers, 2);
1252    }
1253
1254    #[test]
1255    fn test_manager_stats_global_byte_counts() {
1256        let mut mgr = make_manager();
1257        mgr.register_peer("t1".to_string(), 0);
1258        mgr.record_transfer("t1", 1_000, 500, 1_000);
1259        let stats = mgr.manager_stats();
1260        assert_eq!(stats.total_bytes_sent, 1_000);
1261        assert_eq!(stats.total_bytes_recv, 500);
1262    }
1263
1264    #[test]
1265    fn test_manager_stats_avg_rates() {
1266        let mut mgr = make_manager(); // 5_000 ms window
1267        mgr.register_peer("u1".to_string(), 0);
1268        mgr.register_peer("u2".to_string(), 0);
1269        mgr.record_transfer("u1", 5_000, 0, 1_000);
1270        mgr.record_transfer("u2", 10_000, 0, 1_000);
1271        let stats = mgr.manager_stats();
1272        // avg = (5000/5 + 10000/5) / 2 = (1000 + 2000) / 2 = 1500 B/s
1273        assert!((stats.avg_upload_rate_bps - 1_500.0).abs() < 1.0);
1274    }
1275
1276    #[test]
1277    fn test_manager_stats_empty() {
1278        let mgr = make_manager();
1279        let stats = mgr.manager_stats();
1280        assert_eq!(stats.registered_peers, 0);
1281        assert_eq!(stats.total_bytes_sent, 0);
1282        assert_eq!(stats.total_bytes_recv, 0);
1283        assert_eq!(stats.avg_upload_rate_bps, 0.0);
1284    }
1285
1286    // -----------------------------------------------------------------------
1287    // BandwidthLimit helpers
1288    // -----------------------------------------------------------------------
1289
1290    #[test]
1291    fn test_bandwidth_limit_is_unlimited() {
1292        let unlimited = BandwidthLimit::new(0, 0);
1293        assert!(unlimited.is_unlimited());
1294    }
1295
1296    #[test]
1297    fn test_bandwidth_limit_is_not_unlimited() {
1298        let limited = BandwidthLimit::new(1_000, 2_000);
1299        assert!(!limited.is_unlimited());
1300    }
1301
1302    #[test]
1303    fn test_bandwidth_limit_default_values() {
1304        let limit = BandwidthLimit::default();
1305        assert_eq!(limit.bytes_per_second, 1_048_576);
1306        assert_eq!(limit.burst_bytes, 2_097_152);
1307    }
1308
1309    // -----------------------------------------------------------------------
1310    // BandwidthManagerConfig default
1311    // -----------------------------------------------------------------------
1312
1313    #[test]
1314    fn test_config_default_values() {
1315        let config = BandwidthManagerConfig::default();
1316        assert_eq!(config.max_peers, 1_000);
1317        assert_eq!(config.usage_window_ms, 5_000);
1318        assert_eq!(config.default_upload_limit.bytes_per_second, 1_048_576);
1319        assert_eq!(config.default_download_limit.bytes_per_second, 2_097_152);
1320    }
1321
1322    // -----------------------------------------------------------------------
1323    // BandwidthUsage struct fields
1324    // -----------------------------------------------------------------------
1325
1326    #[test]
1327    fn test_bandwidth_usage_fields() {
1328        let usage = BandwidthUsage {
1329            peer_id: "test".to_string(),
1330            bytes_sent: 1_000,
1331            bytes_recv: 500,
1332            upload_rate_bps: 200.0,
1333            download_rate_bps: 100.0,
1334            window_ms: 5_000,
1335        };
1336        assert_eq!(usage.peer_id, "test");
1337        assert_eq!(usage.bytes_sent, 1_000);
1338        assert_eq!(usage.bytes_recv, 500);
1339        assert!((usage.upload_rate_bps - 200.0).abs() < f64::EPSILON);
1340    }
1341
1342    // -----------------------------------------------------------------------
1343    // Edge cases
1344    // -----------------------------------------------------------------------
1345
1346    #[test]
1347    fn test_unlimited_upload_always_allowed() {
1348        let config = BandwidthManagerConfig {
1349            default_upload_limit: BandwidthLimit::new(0, 0), // unlimited
1350            default_download_limit: BandwidthLimit::new(1_024, 2_048),
1351            ..Default::default()
1352        };
1353        let mut mgr = PeerBandwidthManager::new(config);
1354        mgr.register_peer("unlimited".to_string(), 0);
1355        // Should always pass, even for huge transfers.
1356        assert!(mgr.try_consume("unlimited", u64::MAX / 2, BandwidthDirection::Upload, 0));
1357    }
1358
1359    #[test]
1360    fn test_saturating_add_prevents_overflow() {
1361        let mut mgr = make_manager();
1362        mgr.register_peer("big".to_string(), 0);
1363        mgr.record_transfer("big", u64::MAX - 10, 0, 1_000);
1364        mgr.record_transfer("big", 100, 0, 2_000);
1365        // Should not panic due to overflow.
1366        assert_eq!(mgr.total_bytes_sent, u64::MAX);
1367    }
1368
1369    #[test]
1370    fn test_multiple_peers_independent_token_buckets() {
1371        let mut mgr = make_tight_manager();
1372        mgr.register_peer("p1".to_string(), 0);
1373        mgr.register_peer("p2".to_string(), 0);
1374        // Drain p1 upload entirely.
1375        assert!(mgr.try_consume("p1", 2_048, BandwidthDirection::Upload, 0));
1376        // p2 should still have full tokens.
1377        assert!(mgr.try_consume("p2", 2_048, BandwidthDirection::Upload, 0));
1378    }
1379
1380    #[test]
1381    fn test_top_uploaders_empty_manager() {
1382        let mgr = make_manager();
1383        let top = mgr.top_uploaders(5, 0);
1384        assert!(top.is_empty());
1385    }
1386
1387    #[test]
1388    fn test_top_downloaders_empty_manager() {
1389        let mgr = make_manager();
1390        let top = mgr.top_downloaders(5, 0);
1391        assert!(top.is_empty());
1392    }
1393
1394    #[test]
1395    fn test_registration_order_maintained_for_eviction() {
1396        let config = BandwidthManagerConfig {
1397            max_peers: 3,
1398            ..Default::default()
1399        };
1400        let mut mgr = PeerBandwidthManager::new(config);
1401        for id in &["r1", "r2", "r3", "r4", "r5"] {
1402            mgr.register_peer(id.to_string(), 0);
1403        }
1404        // r1, r2, r3 should have been evicted in order as r4, r5 were added.
1405        assert!(!mgr.peers.contains_key("r1"));
1406        assert!(!mgr.peers.contains_key("r2"));
1407        assert!(mgr.peers.contains_key("r3"));
1408        assert!(mgr.peers.contains_key("r4"));
1409        assert!(mgr.peers.contains_key("r5"));
1410    }
1411
1412    #[test]
1413    fn test_try_consume_after_refill_succeeds() {
1414        let mut mgr = make_tight_manager();
1415        mgr.register_peer("walter".to_string(), 0);
1416        // Exhaust all upload tokens at t=0.
1417        assert!(mgr.try_consume("walter", 2_048, BandwidthDirection::Upload, 0));
1418        // At t=1 the bucket refills by exactly 1_024 bytes (1 second at 1_024 B/s).
1419        // Try consuming 1_024 bytes — should succeed.
1420        assert!(mgr.try_consume("walter", 1_024, BandwidthDirection::Upload, 1_000));
1421    }
1422}