Skip to main content

ipfrs_network/
adaptive_bandwidth_allocator.rs

1//! Adaptive bandwidth allocator for peer-to-peer networks.
2//!
3//! Provides dynamic bandwidth management across peers using configurable allocation
4//! policies, congestion detection, rolling measurement windows, and event history.
5//!
6//! # Policies
7//! - [`AllocationPolicy::EqualShare`]: divide total capacity equally among all peers
8//! - [`AllocationPolicy::WeightedFair`]: weight by [`BandwidthClass`] (High=4, Normal=2, Low=1, Background=0.5)
9//! - [`AllocationPolicy::MinGuarantee`]: each peer gets at least `n` bps, remainder distributed equally
10//! - [`AllocationPolicy::MaxCapacity`]: each peer gets at most `n` bps
11//! - [`AllocationPolicy::PriorityQueue`]: high-priority peers allocated first until capacity exhausted
12//!
13//! # Example
14//! ```rust
15//! use ipfrs_network::adaptive_bandwidth_allocator::{
16//!     AdaptiveBandwidthAllocator, AllocatorConfig, AllocationPolicy, BandwidthClass,
17//! };
18//!
19//! let config = AllocatorConfig {
20//!     total_capacity_bps: 100_000_000,
21//!     min_allocation_bps: 1_000_000,
22//!     max_allocation_bps: 50_000_000,
23//!     policy: AllocationPolicy::WeightedFair,
24//!     rebalance_interval_secs: 30,
25//!     congestion_threshold_ppm: 10_000,
26//! };
27//! let mut allocator = AdaptiveBandwidthAllocator::new(config);
28//! allocator.add_peer("peer-1".to_string(), BandwidthClass::High, 10_000_000).unwrap();
29//! ```
30
31use std::collections::{HashMap, VecDeque};
32
33// ─── PRNG ────────────────────────────────────────────────────────────────────
34
35/// Xorshift64 PRNG — used for jitter simulation in [`BandwidthWindow`].
36/// No external rand crate required.
37#[inline]
38fn xorshift64(state: &mut u64) -> u64 {
39    let mut x = *state;
40    x ^= x << 13;
41    x ^= x >> 7;
42    x ^= x << 17;
43    *state = x;
44    x
45}
46
47// ─── BandwidthClass ──────────────────────────────────────────────────────────
48
49/// Priority classification for a peer, used to drive weighted-fair allocation.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
51pub enum BandwidthClass {
52    /// Weight multiplier 4.0 — latency-sensitive or critical peers.
53    High,
54    /// Weight multiplier 2.0 — default peer classification.
55    Normal,
56    /// Weight multiplier 1.0 — best-effort, non-critical peers.
57    Low,
58    /// Weight multiplier 0.5 — idle or background-only traffic.
59    Background,
60}
61
62impl BandwidthClass {
63    /// Returns the floating-point weight used in weighted-fair allocation.
64    pub fn weight(self) -> f64 {
65        match self {
66            BandwidthClass::High => 4.0,
67            BandwidthClass::Normal => 2.0,
68            BandwidthClass::Low => 1.0,
69            BandwidthClass::Background => 0.5,
70        }
71    }
72
73    /// Numeric priority (higher = more important) used in priority-queue allocation.
74    pub fn priority(self) -> u32 {
75        match self {
76            BandwidthClass::High => 4,
77            BandwidthClass::Normal => 3,
78            BandwidthClass::Low => 2,
79            BandwidthClass::Background => 1,
80        }
81    }
82}
83
84// ─── AllocationPolicy ────────────────────────────────────────────────────────
85
86/// Strategy used by [`AdaptiveBandwidthAllocator::reallocate`] to distribute
87/// the total available bandwidth across peers.
88#[derive(Debug, Clone, PartialEq)]
89pub enum AllocationPolicy {
90    /// Divide total capacity equally among all active peers.
91    EqualShare,
92    /// Allocate proportionally to each peer's [`BandwidthClass`] weight.
93    WeightedFair,
94    /// Guarantee every peer at least `n` bps; distribute surplus equally.
95    MinGuarantee(u64),
96    /// Cap every peer at `n` bps; leftover capacity is unused.
97    MaxCapacity(u64),
98    /// Allocate to peers in descending priority order until capacity is exhausted.
99    PriorityQueue,
100}
101
102// ─── BandwidthWindow ─────────────────────────────────────────────────────────
103
104/// Rolling 10-sample window of bandwidth measurements.
105///
106/// Uses an `xorshift64` PRNG (seeded from the first sample) to simulate
107/// measurement jitter for testing purposes without pulling in the `rand` crate.
108#[derive(Debug, Clone)]
109pub struct BandwidthWindow {
110    samples: VecDeque<u64>,
111    prng_state: u64,
112    capacity: usize,
113}
114
115impl BandwidthWindow {
116    const DEFAULT_CAPACITY: usize = 10;
117
118    /// Create a new window with default capacity (10 samples).
119    pub fn new() -> Self {
120        Self {
121            samples: VecDeque::with_capacity(Self::DEFAULT_CAPACITY),
122            prng_state: 0xcafe_babe_dead_beef,
123            capacity: Self::DEFAULT_CAPACITY,
124        }
125    }
126
127    /// Push a new measurement, evicting the oldest when the window is full.
128    /// Applies a small xorshift64-derived jitter (±0.5%) to the raw sample.
129    pub fn push(&mut self, raw_bps: u64) {
130        // Derive jitter in range [0, raw_bps/200], subtract half for ±effect.
131        let jitter_range = (raw_bps / 200).max(1);
132        let jitter = xorshift64(&mut self.prng_state) % jitter_range;
133        // Use jitter as a signed offset anchored at raw_bps.
134        let jittered = if xorshift64(&mut self.prng_state) & 1 == 0 {
135            raw_bps.saturating_add(jitter)
136        } else {
137            raw_bps.saturating_sub(jitter)
138        };
139
140        if self.samples.len() == self.capacity {
141            self.samples.pop_front();
142        }
143        self.samples.push_back(jittered);
144    }
145
146    /// Arithmetic mean of all samples; returns 0 when no samples exist.
147    pub fn average_bps(&self) -> u64 {
148        if self.samples.is_empty() {
149            return 0;
150        }
151        let sum: u64 = self.samples.iter().sum();
152        sum / self.samples.len() as u64
153    }
154
155    /// Maximum sample in the window; returns 0 when no samples exist.
156    pub fn peak_bps(&self) -> u64 {
157        self.samples.iter().copied().max().unwrap_or(0)
158    }
159
160    /// Number of samples currently held.
161    pub fn len(&self) -> usize {
162        self.samples.len()
163    }
164
165    /// Returns `true` when no samples are stored.
166    pub fn is_empty(&self) -> bool {
167        self.samples.is_empty()
168    }
169}
170
171impl Default for BandwidthWindow {
172    fn default() -> Self {
173        Self::new()
174    }
175}
176
177// ─── PeerBandwidthProfile ────────────────────────────────────────────────────
178
179/// Complete bandwidth state for a single peer.
180#[derive(Debug, Clone)]
181pub struct PeerBandwidthProfile {
182    /// Unique peer identifier.
183    pub peer_id: String,
184    /// Currently allocated bandwidth in bits-per-second.
185    pub allocated_bps: u64,
186    /// Most recently measured used bandwidth in bits-per-second.
187    pub used_bps: u64,
188    /// Most recently measured round-trip latency in milliseconds.
189    pub latency_ms: u64,
190    /// Packet loss rate in parts-per-million (0 = no loss, 1_000_000 = 100% loss).
191    pub packet_loss_ppm: u32,
192    /// Bandwidth priority class for this peer.
193    pub class: BandwidthClass,
194    /// Unix-millisecond timestamp of the last measurement update.
195    pub last_updated: u64,
196    /// Rolling measurement window.
197    pub(crate) window: BandwidthWindow,
198}
199
200impl PeerBandwidthProfile {
201    fn new(peer_id: String, class: BandwidthClass, initial_bps: u64, now: u64) -> Self {
202        Self {
203            peer_id,
204            allocated_bps: initial_bps,
205            used_bps: 0,
206            latency_ms: 0,
207            packet_loss_ppm: 0,
208            class,
209            last_updated: now,
210            window: BandwidthWindow::new(),
211        }
212    }
213}
214
215// ─── BandwidthEvent ──────────────────────────────────────────────────────────
216
217/// Events emitted by the allocator for observability and history queries.
218#[derive(Debug, Clone)]
219pub enum BandwidthEvent {
220    /// A new peer has been added to the allocator.
221    PeerAdded {
222        peer_id: String,
223        allocated_bps: u64,
224        timestamp: u64,
225    },
226    /// A peer has been removed from the allocator.
227    PeerRemoved { peer_id: String, timestamp: u64 },
228    /// A live bandwidth measurement was recorded for a peer.
229    BandwidthUpdated {
230        peer_id: String,
231        new_bps: u64,
232        timestamp: u64,
233    },
234    /// Packet-loss congestion detected for a peer.
235    CongestionDetected {
236        peer_id: String,
237        loss_ppm: u32,
238        timestamp: u64,
239    },
240    /// Allocations have been revised across all peers.
241    AllocationRevised { timestamp: u64 },
242}
243
244impl BandwidthEvent {
245    /// Returns the timestamp embedded in any variant.
246    pub fn timestamp(&self) -> u64 {
247        match self {
248            BandwidthEvent::PeerAdded { timestamp, .. } => *timestamp,
249            BandwidthEvent::PeerRemoved { timestamp, .. } => *timestamp,
250            BandwidthEvent::BandwidthUpdated { timestamp, .. } => *timestamp,
251            BandwidthEvent::CongestionDetected { timestamp, .. } => *timestamp,
252            BandwidthEvent::AllocationRevised { timestamp } => *timestamp,
253        }
254    }
255}
256
257// ─── AllocatorConfig ─────────────────────────────────────────────────────────
258
259/// Configuration for [`AdaptiveBandwidthAllocator`].
260#[derive(Debug, Clone)]
261pub struct AllocatorConfig {
262    /// Total available bandwidth in bits-per-second (hard upper bound).
263    pub total_capacity_bps: u64,
264    /// Minimum allocation any single peer can receive.
265    pub min_allocation_bps: u64,
266    /// Maximum allocation any single peer can receive.
267    pub max_allocation_bps: u64,
268    /// Allocation policy applied during [`AdaptiveBandwidthAllocator::reallocate`].
269    pub policy: AllocationPolicy,
270    /// How often to suggest a rebalance cycle (advisory; not enforced internally).
271    pub rebalance_interval_secs: u64,
272    /// Packet-loss threshold in parts-per-million above which congestion is flagged.
273    pub congestion_threshold_ppm: u32,
274}
275
276impl AllocatorConfig {
277    /// Validate the configuration, returning an error on logical inconsistencies.
278    pub fn validate(&self) -> Result<(), AllocatorError> {
279        if self.min_allocation_bps > self.max_allocation_bps {
280            return Err(AllocatorError::InvalidConfiguration(
281                "min_allocation_bps must be ≤ max_allocation_bps".to_string(),
282            ));
283        }
284        if self.total_capacity_bps == 0 {
285            return Err(AllocatorError::InvalidConfiguration(
286                "total_capacity_bps must be > 0".to_string(),
287            ));
288        }
289        if self.max_allocation_bps > self.total_capacity_bps {
290            return Err(AllocatorError::InvalidConfiguration(
291                "max_allocation_bps must be ≤ total_capacity_bps".to_string(),
292            ));
293        }
294        Ok(())
295    }
296}
297
298// ─── AbaAllocationStats (Aba-prefixed to avoid collision) ────────────────────
299
300/// Aggregate bandwidth statistics across all tracked peers.
301///
302/// Exported as `AbaBandwidthStats` at the crate root to avoid collision with
303/// `bandwidth_monitor::BandwidthStats`.
304#[derive(Debug, Clone)]
305pub struct BandwidthStats {
306    /// Sum of all peer allocations in bps.
307    pub total_allocated_bps: u64,
308    /// Sum of all measured used bandwidths in bps.
309    pub total_used_bps: u64,
310    /// Ratio of total_used_bps to total_allocated_bps, expressed as a percentage.
311    pub utilization_pct: f64,
312    /// Number of peers currently flagged as congested.
313    pub congested_peers: usize,
314    /// Total number of tracked peers.
315    pub peer_count: usize,
316    /// Jain's fairness index over current allocations (1.0 = perfectly fair).
317    pub fairness_index: f64,
318}
319
320// ─── AllocatorError ──────────────────────────────────────────────────────────
321
322/// Error variants returned by [`AdaptiveBandwidthAllocator`] methods.
323#[derive(Debug, Clone, PartialEq, thiserror::Error)]
324pub enum AllocatorError {
325    #[error("peer not found: {0}")]
326    PeerNotFound(String),
327
328    #[error("allocation of {0} bps exceeds remaining capacity")]
329    AllocationExceedsCapacity(u64),
330
331    #[error("invalid configuration: {0}")]
332    InvalidConfiguration(String),
333
334    #[error("congestion detected on peer {peer_id}: loss_ppm={loss_ppm}")]
335    CongestionDetected { peer_id: String, loss_ppm: u32 },
336}
337
338// ─── AdaptiveBandwidthAllocator ──────────────────────────────────────────────
339
340/// Adaptive bandwidth manager that dynamically allocates bandwidth across peers.
341///
342/// Tracks per-peer profiles, maintains a bounded event history, and supports
343/// five allocation policies via [`AdaptiveBandwidthAllocator::reallocate`].
344pub struct AdaptiveBandwidthAllocator {
345    config: AllocatorConfig,
346    /// Peer profiles keyed by peer_id.
347    peers: HashMap<String, PeerBandwidthProfile>,
348    /// Bounded FIFO event history (max 200 events).
349    events: VecDeque<BandwidthEvent>,
350    /// Total bps currently allocated across all peers.
351    allocated_total: u64,
352    /// Monotonic-ish timestamp for events — callers may pass real timestamps via
353    /// update_measurement; internal operations use this counter.
354    now_ms: u64,
355}
356
357impl AdaptiveBandwidthAllocator {
358    /// Maximum number of events retained in the history ring-buffer.
359    pub const MAX_EVENT_HISTORY: usize = 200;
360
361    /// Create a new allocator with the given configuration.
362    ///
363    /// Returns [`AllocatorError::InvalidConfiguration`] if the config is logically
364    /// inconsistent.
365    pub fn new(config: AllocatorConfig) -> Self {
366        // Validation is best-effort at construction; callers may call validate() first.
367        Self {
368            config,
369            peers: HashMap::new(),
370            events: VecDeque::with_capacity(Self::MAX_EVENT_HISTORY + 1),
371            allocated_total: 0,
372            now_ms: 1_000, // arbitrary non-zero start
373        }
374    }
375
376    /// Advance the internal clock by `delta_ms` milliseconds.
377    /// Useful in testing to create distinct timestamps without real-time delays.
378    pub fn advance_time(&mut self, delta_ms: u64) {
379        self.now_ms = self.now_ms.saturating_add(delta_ms);
380    }
381
382    // ── Public API ─────────────────────────────────────────────────────────
383
384    /// Add a peer with an initial bandwidth class and desired allocation.
385    ///
386    /// Returns [`AllocatorError::AllocationExceedsCapacity`] if adding this
387    /// allocation would exceed `total_capacity_bps`.
388    pub fn add_peer(
389        &mut self,
390        peer_id: String,
391        class: BandwidthClass,
392        initial_bps: u64,
393    ) -> Result<(), AllocatorError> {
394        let clamped = initial_bps.clamp(
395            self.config.min_allocation_bps,
396            self.config.max_allocation_bps,
397        );
398
399        let new_total = self.allocated_total.saturating_add(clamped);
400        if new_total > self.config.total_capacity_bps {
401            return Err(AllocatorError::AllocationExceedsCapacity(clamped));
402        }
403
404        let profile = PeerBandwidthProfile::new(peer_id.clone(), class, clamped, self.now_ms);
405        self.peers.insert(peer_id.clone(), profile);
406        self.allocated_total = new_total;
407
408        self.push_event(BandwidthEvent::PeerAdded {
409            peer_id,
410            allocated_bps: clamped,
411            timestamp: self.now_ms,
412        });
413        Ok(())
414    }
415
416    /// Remove a peer and release its bandwidth allocation.
417    ///
418    /// Returns the removed [`PeerBandwidthProfile`] or
419    /// [`AllocatorError::PeerNotFound`].
420    pub fn remove_peer(&mut self, peer_id: &str) -> Result<PeerBandwidthProfile, AllocatorError> {
421        let profile = self
422            .peers
423            .remove(peer_id)
424            .ok_or_else(|| AllocatorError::PeerNotFound(peer_id.to_string()))?;
425
426        self.allocated_total = self.allocated_total.saturating_sub(profile.allocated_bps);
427
428        self.push_event(BandwidthEvent::PeerRemoved {
429            peer_id: peer_id.to_string(),
430            timestamp: self.now_ms,
431        });
432        Ok(profile)
433    }
434
435    /// Record a live measurement for a peer.
436    ///
437    /// Updates the rolling [`BandwidthWindow`], stores latency and loss, and
438    /// emits a [`BandwidthEvent::CongestionDetected`] when loss_ppm exceeds the
439    /// configured threshold.
440    ///
441    /// Returns the most pertinent event for the caller's convenience.
442    pub fn update_measurement(
443        &mut self,
444        peer_id: &str,
445        used_bps: u64,
446        latency_ms: u64,
447        packet_loss_ppm: u32,
448    ) -> Result<BandwidthEvent, AllocatorError> {
449        let now = self.now_ms;
450        let profile = self
451            .peers
452            .get_mut(peer_id)
453            .ok_or_else(|| AllocatorError::PeerNotFound(peer_id.to_string()))?;
454
455        profile.used_bps = used_bps;
456        profile.latency_ms = latency_ms;
457        profile.packet_loss_ppm = packet_loss_ppm;
458        profile.last_updated = now;
459        profile.window.push(used_bps);
460
461        let event = if packet_loss_ppm > self.config.congestion_threshold_ppm {
462            BandwidthEvent::CongestionDetected {
463                peer_id: peer_id.to_string(),
464                loss_ppm: packet_loss_ppm,
465                timestamp: now,
466            }
467        } else {
468            BandwidthEvent::BandwidthUpdated {
469                peer_id: peer_id.to_string(),
470                new_bps: used_bps,
471                timestamp: now,
472            }
473        };
474
475        self.push_event(event.clone());
476        Ok(event)
477    }
478
479    /// Rebalance all peer allocations according to the configured policy.
480    ///
481    /// Returns the list of [`BandwidthEvent`]s produced (one
482    /// `AllocationRevised` event at minimum, plus any `CongestionDetected`
483    /// events for peers that are still over threshold after rebalancing).
484    pub fn reallocate(&mut self) -> Vec<BandwidthEvent> {
485        let mut out_events: Vec<BandwidthEvent> = Vec::new();
486        let now = self.now_ms;
487
488        if self.peers.is_empty() {
489            self.push_event(BandwidthEvent::AllocationRevised { timestamp: now });
490            out_events.push(BandwidthEvent::AllocationRevised { timestamp: now });
491            return out_events;
492        }
493
494        let new_allocs = self.compute_allocations();
495
496        // Apply new allocations and track the aggregate.
497        let mut new_total: u64 = 0;
498        for (peer_id, alloc) in &new_allocs {
499            if let Some(profile) = self.peers.get_mut(peer_id) {
500                profile.allocated_bps = *alloc;
501                new_total = new_total.saturating_add(*alloc);
502            }
503        }
504        self.allocated_total = new_total;
505
506        // Emit congestion events for peers still over threshold.
507        let threshold = self.config.congestion_threshold_ppm;
508        let congested: Vec<(String, u32)> = self
509            .peers
510            .values()
511            .filter(|p| p.packet_loss_ppm > threshold)
512            .map(|p| (p.peer_id.clone(), p.packet_loss_ppm))
513            .collect();
514
515        for (peer_id, loss_ppm) in congested {
516            let ev = BandwidthEvent::CongestionDetected {
517                peer_id,
518                loss_ppm,
519                timestamp: now,
520            };
521            self.push_event(ev.clone());
522            out_events.push(ev);
523        }
524
525        let revised = BandwidthEvent::AllocationRevised { timestamp: now };
526        self.push_event(revised.clone());
527        out_events.push(revised);
528
529        out_events
530    }
531
532    /// Return the current allocated bps for the given peer.
533    pub fn get_allocation(&self, peer_id: &str) -> Result<u64, AllocatorError> {
534        self.peers
535            .get(peer_id)
536            .map(|p| p.allocated_bps)
537            .ok_or_else(|| AllocatorError::PeerNotFound(peer_id.to_string()))
538    }
539
540    /// Return aggregate statistics including Jain's fairness index.
541    pub fn stats(&self) -> BandwidthStats {
542        let peer_count = self.peers.len();
543        let total_allocated_bps: u64 = self.peers.values().map(|p| p.allocated_bps).sum();
544        let total_used_bps: u64 = self.peers.values().map(|p| p.used_bps).sum();
545
546        let utilization_pct = if total_allocated_bps == 0 {
547            0.0
548        } else {
549            (total_used_bps as f64 / total_allocated_bps as f64) * 100.0
550        };
551
552        let threshold = self.config.congestion_threshold_ppm;
553        let congested_peers = self
554            .peers
555            .values()
556            .filter(|p| p.packet_loss_ppm > threshold)
557            .count();
558
559        let fairness_index = Self::jain_fairness_index(
560            self.peers.values().map(|p| p.allocated_bps as f64),
561            peer_count,
562        );
563
564        BandwidthStats {
565            total_allocated_bps,
566            total_used_bps,
567            utilization_pct,
568            congested_peers,
569            peer_count,
570            fairness_index,
571        }
572    }
573
574    /// Return peer IDs for all peers whose `packet_loss_ppm` exceeds the threshold.
575    pub fn congested_peers(&self) -> Vec<String> {
576        let threshold = self.config.congestion_threshold_ppm;
577        self.peers
578            .values()
579            .filter(|p| p.packet_loss_ppm > threshold)
580            .map(|p| p.peer_id.clone())
581            .collect()
582    }
583
584    /// Return all events with `timestamp >= since_ms`.
585    pub fn events_since(&self, since_ms: u64) -> Vec<BandwidthEvent> {
586        self.events
587            .iter()
588            .filter(|e| e.timestamp() >= since_ms)
589            .cloned()
590            .collect()
591    }
592
593    /// Drain and return all buffered events, leaving the history empty.
594    pub fn drain_events(&mut self) -> Vec<BandwidthEvent> {
595        self.events.drain(..).collect()
596    }
597
598    /// Return a reference to the profile for `peer_id`, if present.
599    pub fn peer_profile(&self, peer_id: &str) -> Option<&PeerBandwidthProfile> {
600        self.peers.get(peer_id)
601    }
602
603    /// Return the number of tracked peers.
604    pub fn peer_count(&self) -> usize {
605        self.peers.len()
606    }
607
608    /// Return the total capacity configured for this allocator.
609    pub fn total_capacity_bps(&self) -> u64 {
610        self.config.total_capacity_bps
611    }
612
613    /// Return a reference to the current configuration.
614    pub fn config(&self) -> &AllocatorConfig {
615        &self.config
616    }
617
618    // ── Private helpers ────────────────────────────────────────────────────
619
620    /// Push an event, evicting the oldest when history is full.
621    fn push_event(&mut self, event: BandwidthEvent) {
622        if self.events.len() == Self::MAX_EVENT_HISTORY {
623            self.events.pop_front();
624        }
625        self.events.push_back(event);
626    }
627
628    /// Compute new allocations for each peer according to the active policy.
629    /// Returns a map of peer_id → new_bps.
630    fn compute_allocations(&self) -> HashMap<String, u64> {
631        match &self.config.policy {
632            AllocationPolicy::EqualShare => self.policy_equal_share(),
633            AllocationPolicy::WeightedFair => self.policy_weighted_fair(),
634            AllocationPolicy::MinGuarantee(min_n) => self.policy_min_guarantee(*min_n),
635            AllocationPolicy::MaxCapacity(max_n) => self.policy_max_capacity(*max_n),
636            AllocationPolicy::PriorityQueue => self.policy_priority_queue(),
637        }
638    }
639
640    /// EqualShare: total_capacity / n, clamped to [min, max].
641    fn policy_equal_share(&self) -> HashMap<String, u64> {
642        let n = self.peers.len() as u64;
643        let share = self.config.total_capacity_bps.checked_div(n).unwrap_or(0);
644        let clamped = share.clamp(
645            self.config.min_allocation_bps,
646            self.config.max_allocation_bps,
647        );
648        self.peers.keys().map(|id| (id.clone(), clamped)).collect()
649    }
650
651    /// WeightedFair: proportional by class weight, clamped to [min, max].
652    fn policy_weighted_fair(&self) -> HashMap<String, u64> {
653        let total_weight: f64 = self.peers.values().map(|p| p.class.weight()).sum();
654        if total_weight == 0.0 {
655            return self.policy_equal_share();
656        }
657        self.peers
658            .values()
659            .map(|p| {
660                let raw = (self.config.total_capacity_bps as f64 * p.class.weight() / total_weight)
661                    as u64;
662                let alloc = raw.clamp(
663                    self.config.min_allocation_bps,
664                    self.config.max_allocation_bps,
665                );
666                (p.peer_id.clone(), alloc)
667            })
668            .collect()
669    }
670
671    /// MinGuarantee: each peer gets at least `n`, remainder distributed equally.
672    fn policy_min_guarantee(&self, min_n: u64) -> HashMap<String, u64> {
673        let peer_count = self.peers.len() as u64;
674        if peer_count == 0 {
675            return HashMap::new();
676        }
677        // Effective minimum is max(config.min, min_n).
678        let effective_min = min_n
679            .max(self.config.min_allocation_bps)
680            .min(self.config.max_allocation_bps);
681
682        let guaranteed_total = effective_min.saturating_mul(peer_count);
683        let remainder = self
684            .config
685            .total_capacity_bps
686            .saturating_sub(guaranteed_total);
687        let bonus_per_peer = remainder / peer_count;
688
689        self.peers
690            .keys()
691            .map(|id| {
692                let alloc = (effective_min + bonus_per_peer).min(self.config.max_allocation_bps);
693                (id.clone(), alloc)
694            })
695            .collect()
696    }
697
698    /// MaxCapacity: each peer gets at most `n` bps; otherwise equal share.
699    fn policy_max_capacity(&self, max_n: u64) -> HashMap<String, u64> {
700        let peer_count = self.peers.len() as u64;
701        if peer_count == 0 {
702            return HashMap::new();
703        }
704        let effective_max = max_n.min(self.config.max_allocation_bps);
705        let equal = (self.config.total_capacity_bps / peer_count).min(effective_max);
706        let clamped = equal.clamp(self.config.min_allocation_bps, effective_max);
707        self.peers.keys().map(|id| (id.clone(), clamped)).collect()
708    }
709
710    /// PriorityQueue: highest-priority peers fill up first; remainder gets min.
711    fn policy_priority_queue(&self) -> HashMap<String, u64> {
712        // Sort peers: descending by (class priority, peer_id).
713        let mut ordered: Vec<&PeerBandwidthProfile> = self.peers.values().collect();
714        ordered.sort_by(|a, b| {
715            b.class
716                .priority()
717                .cmp(&a.class.priority())
718                .then_with(|| a.peer_id.cmp(&b.peer_id))
719        });
720
721        let mut remaining = self.config.total_capacity_bps;
722        let mut allocs: HashMap<String, u64> = HashMap::new();
723
724        for profile in &ordered {
725            let grant = remaining.min(self.config.max_allocation_bps);
726            let grant = grant.max(self.config.min_allocation_bps);
727            // Don't overshoot.
728            let grant = if grant > remaining { remaining } else { grant };
729            allocs.insert(profile.peer_id.clone(), grant);
730            remaining = remaining.saturating_sub(grant);
731        }
732
733        // Any peer that got 0 (exhausted capacity) receives the configured minimum.
734        for profile in &ordered {
735            let entry = allocs.entry(profile.peer_id.clone()).or_insert(0);
736            if *entry == 0 {
737                *entry = self.config.min_allocation_bps;
738            }
739        }
740
741        allocs
742    }
743
744    /// Jain's fairness index: (Σxᵢ)² / (n × Σxᵢ²)
745    /// Returns 1.0 for perfect fairness, approaches 1/n for maximum unfairness.
746    fn jain_fairness_index(allocations: impl Iterator<Item = f64>, n: usize) -> f64 {
747        if n == 0 {
748            return 1.0;
749        }
750        let (sum, sum_sq) = allocations.fold((0.0_f64, 0.0_f64), |(s, sq), x| (s + x, sq + x * x));
751        if sum_sq == 0.0 {
752            return 1.0;
753        }
754        (sum * sum) / (n as f64 * sum_sq)
755    }
756}
757
758// ─── Tests ───────────────────────────────────────────────────────────────────
759
760#[cfg(test)]
761mod tests {
762    use super::*;
763
764    // ── Helpers ──────────────────────────────────────────────────────────────
765
766    fn default_config() -> AllocatorConfig {
767        AllocatorConfig {
768            total_capacity_bps: 100_000_000, // 100 Mbps
769            min_allocation_bps: 1_000_000,   // 1 Mbps
770            max_allocation_bps: 50_000_000,  // 50 Mbps
771            policy: AllocationPolicy::EqualShare,
772            rebalance_interval_secs: 30,
773            congestion_threshold_ppm: 10_000, // 1%
774        }
775    }
776
777    fn alloc_with_policy(policy: AllocationPolicy) -> AdaptiveBandwidthAllocator {
778        let mut cfg = default_config();
779        cfg.policy = policy;
780        AdaptiveBandwidthAllocator::new(cfg)
781    }
782
783    fn populated_allocator(n: usize, policy: AllocationPolicy) -> AdaptiveBandwidthAllocator {
784        let mut a = alloc_with_policy(policy);
785        for i in 0..n {
786            let class = match i % 4 {
787                0 => BandwidthClass::High,
788                1 => BandwidthClass::Normal,
789                2 => BandwidthClass::Low,
790                _ => BandwidthClass::Background,
791            };
792            a.add_peer(format!("peer-{i}"), class, 5_000_000)
793                .expect("test: failed to add peer in populated_allocator");
794        }
795        a
796    }
797
798    // ── xorshift64 ───────────────────────────────────────────────────────────
799
800    #[test]
801    fn test_xorshift64_non_zero_output() {
802        let mut state = 12345u64;
803        let v = xorshift64(&mut state);
804        assert_ne!(v, 0);
805        assert_ne!(state, 12345u64);
806    }
807
808    #[test]
809    fn test_xorshift64_deterministic() {
810        let mut s1 = 99999u64;
811        let mut s2 = 99999u64;
812        assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
813        assert_eq!(s1, s2);
814    }
815
816    #[test]
817    fn test_xorshift64_different_states_differ() {
818        let mut s1 = 1u64;
819        let mut s2 = 2u64;
820        assert_ne!(xorshift64(&mut s1), xorshift64(&mut s2));
821    }
822
823    // ── BandwidthClass ────────────────────────────────────────────────────────
824
825    #[test]
826    fn test_bandwidth_class_weights() {
827        assert_eq!(BandwidthClass::High.weight(), 4.0);
828        assert_eq!(BandwidthClass::Normal.weight(), 2.0);
829        assert_eq!(BandwidthClass::Low.weight(), 1.0);
830        assert_eq!(BandwidthClass::Background.weight(), 0.5);
831    }
832
833    #[test]
834    fn test_bandwidth_class_priority_ordering() {
835        assert!(BandwidthClass::High.priority() > BandwidthClass::Normal.priority());
836        assert!(BandwidthClass::Normal.priority() > BandwidthClass::Low.priority());
837        assert!(BandwidthClass::Low.priority() > BandwidthClass::Background.priority());
838    }
839
840    // ── BandwidthWindow ───────────────────────────────────────────────────────
841
842    #[test]
843    fn test_window_empty_returns_zero() {
844        let w = BandwidthWindow::new();
845        assert_eq!(w.average_bps(), 0);
846        assert_eq!(w.peak_bps(), 0);
847        assert!(w.is_empty());
848    }
849
850    #[test]
851    fn test_window_single_sample() {
852        let mut w = BandwidthWindow::new();
853        w.push(1_000_000);
854        assert!(!w.is_empty());
855        assert_eq!(w.len(), 1);
856        // average and peak should be within ±0.5% of the raw input
857        let avg = w.average_bps();
858        assert!((990_000..=1_010_000).contains(&avg));
859    }
860
861    #[test]
862    fn test_window_evicts_oldest_at_capacity() {
863        let mut w = BandwidthWindow::new();
864        for _ in 0..12 {
865            w.push(5_000_000);
866        }
867        assert_eq!(w.len(), 10);
868    }
869
870    #[test]
871    fn test_window_peak_is_max_sample() {
872        let mut w = BandwidthWindow::new();
873        for v in [1_000_000u64, 2_000_000, 3_000_000] {
874            w.push(v);
875        }
876        // Peak should be close to 3_000_000 (jitter ±0.5%)
877        assert!(w.peak_bps() >= 2_900_000);
878    }
879
880    #[test]
881    fn test_window_average_convergence() {
882        let mut w = BandwidthWindow::new();
883        let target = 10_000_000u64;
884        for _ in 0..10 {
885            w.push(target);
886        }
887        let avg = w.average_bps();
888        // With jitter ±0.5%, average should be within 1% of target.
889        assert!((avg as i64 - target as i64).abs() < 100_001);
890    }
891
892    // ── add_peer ──────────────────────────────────────────────────────────────
893
894    #[test]
895    fn test_add_peer_success() {
896        let mut a = AdaptiveBandwidthAllocator::new(default_config());
897        assert!(a
898            .add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
899            .is_ok());
900        assert_eq!(a.peer_count(), 1);
901    }
902
903    #[test]
904    fn test_add_peer_clamps_to_max() {
905        let mut a = AdaptiveBandwidthAllocator::new(default_config());
906        a.add_peer("p1".to_string(), BandwidthClass::High, 999_000_000)
907            .expect("test: add high-bandwidth peer");
908        assert_eq!(
909            a.get_allocation("p1").expect("test: get allocation for p1"),
910            50_000_000
911        );
912    }
913
914    #[test]
915    fn test_add_peer_clamps_to_min() {
916        let mut a = AdaptiveBandwidthAllocator::new(default_config());
917        a.add_peer("p1".to_string(), BandwidthClass::Background, 1)
918            .expect("test: add background peer");
919        assert_eq!(
920            a.get_allocation("p1")
921                .expect("test: get allocation for background peer"),
922            1_000_000
923        );
924    }
925
926    #[test]
927    fn test_add_peer_exceeds_capacity() {
928        let mut a = AdaptiveBandwidthAllocator::new(default_config());
929        // Fill 80 Mbps (max per peer = 50, so two peers at 40 each).
930        a.add_peer("p1".to_string(), BandwidthClass::High, 40_000_000)
931            .expect("test: add first high peer");
932        a.add_peer("p2".to_string(), BandwidthClass::High, 40_000_000)
933            .expect("test: add second high peer");
934        // Third peer would push beyond 100 Mbps.
935        let err = a
936            .add_peer("p3".to_string(), BandwidthClass::Normal, 40_000_000)
937            .unwrap_err();
938        assert!(matches!(err, AllocatorError::AllocationExceedsCapacity(_)));
939    }
940
941    #[test]
942    fn test_add_peer_emits_event() {
943        let mut a = AdaptiveBandwidthAllocator::new(default_config());
944        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
945            .expect("test: add normal peer for event test");
946        let events = a.drain_events();
947        assert_eq!(events.len(), 1);
948        assert!(matches!(events[0], BandwidthEvent::PeerAdded { .. }));
949    }
950
951    // ── remove_peer ───────────────────────────────────────────────────────────
952
953    #[test]
954    fn test_remove_peer_success() {
955        let mut a = AdaptiveBandwidthAllocator::new(default_config());
956        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
957            .expect("test: add peer before remove");
958        let profile = a.remove_peer("p1").expect("test: remove existing peer");
959        assert_eq!(profile.peer_id, "p1");
960        assert_eq!(a.peer_count(), 0);
961    }
962
963    #[test]
964    fn test_remove_peer_releases_capacity() {
965        let mut a = AdaptiveBandwidthAllocator::new(default_config());
966        a.add_peer("p1".to_string(), BandwidthClass::High, 40_000_000)
967            .expect("test: add p1 before release test");
968        a.add_peer("p2".to_string(), BandwidthClass::High, 40_000_000)
969            .expect("test: add p2 before release test");
970        a.remove_peer("p1")
971            .expect("test: remove p1 to release capacity");
972        // After removing p1, enough capacity is free for a 40 Mbps peer.
973        assert!(a
974            .add_peer("p3".to_string(), BandwidthClass::High, 40_000_000)
975            .is_ok());
976    }
977
978    #[test]
979    fn test_remove_peer_not_found() {
980        let mut a = AdaptiveBandwidthAllocator::new(default_config());
981        let err = a.remove_peer("ghost").unwrap_err();
982        assert!(matches!(err, AllocatorError::PeerNotFound(_)));
983    }
984
985    #[test]
986    fn test_remove_peer_emits_event() {
987        let mut a = AdaptiveBandwidthAllocator::new(default_config());
988        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
989            .expect("test: add peer before remove event test");
990        a.drain_events();
991        a.remove_peer("p1")
992            .expect("test: remove peer to trigger event");
993        let events = a.drain_events();
994        assert!(matches!(events[0], BandwidthEvent::PeerRemoved { .. }));
995    }
996
997    // ── update_measurement ────────────────────────────────────────────────────
998
999    #[test]
1000    fn test_update_measurement_normal() {
1001        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1002        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1003            .expect("test: add peer before measurement update");
1004        let ev = a
1005            .update_measurement("p1", 4_000_000, 20, 100)
1006            .expect("test: update measurement for p1");
1007        assert!(matches!(ev, BandwidthEvent::BandwidthUpdated { .. }));
1008    }
1009
1010    #[test]
1011    fn test_update_measurement_congestion_detected() {
1012        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1013        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1014            .expect("test: add peer before congestion test");
1015        // loss_ppm > threshold (10_000)
1016        let ev = a
1017            .update_measurement("p1", 2_000_000, 100, 50_000)
1018            .expect("test: update measurement with high loss");
1019        assert!(matches!(ev, BandwidthEvent::CongestionDetected { .. }));
1020    }
1021
1022    #[test]
1023    fn test_update_measurement_not_found() {
1024        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1025        let err = a.update_measurement("ghost", 1_000, 5, 0).unwrap_err();
1026        assert!(matches!(err, AllocatorError::PeerNotFound(_)));
1027    }
1028
1029    #[test]
1030    fn test_update_measurement_updates_window() {
1031        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1032        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1033            .expect("test: add peer before window test");
1034        for _ in 0..5 {
1035            a.update_measurement("p1", 4_000_000, 10, 0)
1036                .expect("test: update measurement in loop");
1037        }
1038        let profile = a
1039            .peer_profile("p1")
1040            .expect("test: get peer profile after window updates");
1041        assert_eq!(profile.window.len(), 5);
1042    }
1043
1044    #[test]
1045    fn test_update_measurement_updates_latency_and_loss() {
1046        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1047        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1048            .expect("test: add peer before latency/loss test");
1049        a.update_measurement("p1", 3_000_000, 42, 500)
1050            .expect("test: update measurement with latency and loss");
1051        let p = a
1052            .peer_profile("p1")
1053            .expect("test: get peer profile for latency/loss check");
1054        assert_eq!(p.latency_ms, 42);
1055        assert_eq!(p.packet_loss_ppm, 500);
1056    }
1057
1058    // ── reallocate — EqualShare ───────────────────────────────────────────────
1059
1060    #[test]
1061    fn test_reallocate_equal_share_basic() {
1062        let mut a = alloc_with_policy(AllocationPolicy::EqualShare);
1063        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1064            .expect("test: add p1 for equal share test");
1065        a.add_peer("p2".to_string(), BandwidthClass::Normal, 5_000_000)
1066            .expect("test: add p2 for equal share test");
1067        a.reallocate();
1068        // 100 Mbps / 2 = 50 Mbps (matches max_allocation_bps)
1069        assert_eq!(
1070            a.get_allocation("p1")
1071                .expect("test: get p1 allocation after equal share reallocate"),
1072            50_000_000
1073        );
1074        assert_eq!(
1075            a.get_allocation("p2")
1076                .expect("test: get p2 allocation after equal share reallocate"),
1077            50_000_000
1078        );
1079    }
1080
1081    #[test]
1082    fn test_reallocate_equal_share_zero_peers_is_safe() {
1083        let mut a = alloc_with_policy(AllocationPolicy::EqualShare);
1084        let events = a.reallocate();
1085        assert!(!events.is_empty());
1086        assert!(matches!(
1087            events.last().expect("test: events list must not be empty"),
1088            BandwidthEvent::AllocationRevised { .. }
1089        ));
1090    }
1091
1092    #[test]
1093    fn test_reallocate_equal_share_clamped_to_min() {
1094        let mut cfg = default_config();
1095        cfg.total_capacity_bps = 5_000_000; // 5 Mbps total
1096        cfg.min_allocation_bps = 1_000_000;
1097        cfg.max_allocation_bps = 5_000_000;
1098        let mut a = AdaptiveBandwidthAllocator::new(cfg);
1099        // Add 10 peers — share would be 500k, clamped to min 1 Mbps.
1100        for i in 0..10 {
1101            // Some will exceed capacity but we want to see clamping in allocation
1102            let _ = a.add_peer(format!("p{i}"), BandwidthClass::Low, 1_000_000);
1103        }
1104        a.reallocate();
1105        // Verify clamping: allocation must be >= min
1106        for i in 0..a.peer_count() {
1107            if let Ok(alloc) = a.get_allocation(&format!("p{i}")) {
1108                assert!(alloc >= 1_000_000);
1109            }
1110        }
1111    }
1112
1113    // ── reallocate — WeightedFair ─────────────────────────────────────────────
1114
1115    #[test]
1116    fn test_reallocate_weighted_fair_respects_weight_order() {
1117        let mut a = alloc_with_policy(AllocationPolicy::WeightedFair);
1118        a.add_peer("high".to_string(), BandwidthClass::High, 5_000_000)
1119            .expect("test: add high-priority peer for weight order test");
1120        a.add_peer("low".to_string(), BandwidthClass::Low, 5_000_000)
1121            .expect("test: add low-priority peer for weight order test");
1122        a.reallocate();
1123        let high_alloc = a
1124            .get_allocation("high")
1125            .expect("test: get allocation for high priority peer");
1126        let low_alloc = a
1127            .get_allocation("low")
1128            .expect("test: get allocation for low priority peer");
1129        assert!(
1130            high_alloc > low_alloc,
1131            "high priority must get more than low"
1132        );
1133    }
1134
1135    #[test]
1136    fn test_reallocate_weighted_fair_sum_bounded_by_capacity() {
1137        let mut a = populated_allocator(5, AllocationPolicy::WeightedFair);
1138        a.reallocate();
1139        let total: u64 = (0..5)
1140            .map(|i| {
1141                a.get_allocation(&format!("peer-{i}"))
1142                    .expect("test: get allocation for peer in weighted fair sum")
1143            })
1144            .sum();
1145        assert!(total <= 100_000_000);
1146    }
1147
1148    #[test]
1149    fn test_reallocate_weighted_fair_single_peer() {
1150        let mut a = alloc_with_policy(AllocationPolicy::WeightedFair);
1151        a.add_peer("only".to_string(), BandwidthClass::Normal, 5_000_000)
1152            .expect("test: add single peer for weighted fair test");
1153        a.reallocate();
1154        // 100% weight → 100 Mbps, but capped at max_allocation_bps = 50 Mbps
1155        assert_eq!(
1156            a.get_allocation("only")
1157                .expect("test: get allocation for sole weighted fair peer"),
1158            50_000_000
1159        );
1160    }
1161
1162    // ── reallocate — MinGuarantee ─────────────────────────────────────────────
1163
1164    #[test]
1165    fn test_reallocate_min_guarantee_basic() {
1166        let mut a = alloc_with_policy(AllocationPolicy::MinGuarantee(5_000_000));
1167        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1168            .expect("test: add p1 for min guarantee basic test");
1169        a.add_peer("p2".to_string(), BandwidthClass::Normal, 5_000_000)
1170            .expect("test: add p2 for min guarantee basic test");
1171        a.reallocate();
1172        assert!(
1173            a.get_allocation("p1")
1174                .expect("test: get allocation for p1 in min guarantee basic")
1175                >= 5_000_000
1176        );
1177        assert!(
1178            a.get_allocation("p2")
1179                .expect("test: get allocation for p2 in min guarantee basic")
1180                >= 5_000_000
1181        );
1182    }
1183
1184    #[test]
1185    fn test_reallocate_min_guarantee_distributes_surplus() {
1186        let mut a = alloc_with_policy(AllocationPolicy::MinGuarantee(2_000_000));
1187        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1188            .expect("test: add p1 for min guarantee surplus test");
1189        a.add_peer("p2".to_string(), BandwidthClass::Normal, 5_000_000)
1190            .expect("test: add p2 for min guarantee surplus test");
1191        a.reallocate();
1192        // 2 peers × 2 Mbps guarantee = 4 Mbps, 96 Mbps surplus → bonus 48 per peer.
1193        // Total per peer = 50 Mbps (capped at max).
1194        assert_eq!(
1195            a.get_allocation("p1")
1196                .expect("test: get allocation for p1 after surplus distribution"),
1197            50_000_000
1198        );
1199    }
1200
1201    #[test]
1202    fn test_reallocate_min_guarantee_honors_config_min() {
1203        let mut a = alloc_with_policy(AllocationPolicy::MinGuarantee(500_000));
1204        // Policy min of 500k < config min of 1 Mbps → effective min is 1 Mbps.
1205        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1206            .expect("test: add p1 for min guarantee honors config min test");
1207        a.reallocate();
1208        assert!(
1209            a.get_allocation("p1")
1210                .expect("test: get allocation for p1 with config min enforcement")
1211                >= 1_000_000
1212        );
1213    }
1214
1215    // ── reallocate — MaxCapacity ──────────────────────────────────────────────
1216
1217    #[test]
1218    fn test_reallocate_max_capacity_caps_peers() {
1219        let mut a = alloc_with_policy(AllocationPolicy::MaxCapacity(10_000_000));
1220        a.add_peer("p1".to_string(), BandwidthClass::High, 5_000_000)
1221            .expect("test: add p1 for max capacity caps test");
1222        a.add_peer("p2".to_string(), BandwidthClass::High, 5_000_000)
1223            .expect("test: add p2 for max capacity caps test");
1224        a.reallocate();
1225        assert!(
1226            a.get_allocation("p1")
1227                .expect("test: get allocation for p1 under max capacity cap")
1228                <= 10_000_000
1229        );
1230        assert!(
1231            a.get_allocation("p2")
1232                .expect("test: get allocation for p2 under max capacity cap")
1233                <= 10_000_000
1234        );
1235    }
1236
1237    #[test]
1238    fn test_reallocate_max_capacity_zero_peers_safe() {
1239        let mut a = alloc_with_policy(AllocationPolicy::MaxCapacity(10_000_000));
1240        a.reallocate();
1241        assert_eq!(a.peer_count(), 0);
1242    }
1243
1244    // ── reallocate — PriorityQueue ────────────────────────────────────────────
1245
1246    #[test]
1247    fn test_reallocate_priority_queue_high_gets_more() {
1248        let mut a = alloc_with_policy(AllocationPolicy::PriorityQueue);
1249        a.add_peer("bg".to_string(), BandwidthClass::Background, 5_000_000)
1250            .expect("test: add background peer for priority queue high-gets-more test");
1251        a.add_peer("hi".to_string(), BandwidthClass::High, 5_000_000)
1252            .expect("test: add high priority peer for priority queue high-gets-more test");
1253        a.reallocate();
1254        let hi = a
1255            .get_allocation("hi")
1256            .expect("test: get allocation for high priority peer in priority queue");
1257        let bg = a
1258            .get_allocation("bg")
1259            .expect("test: get allocation for background peer in priority queue");
1260        assert!(hi >= bg, "high priority should get >= background");
1261    }
1262
1263    #[test]
1264    fn test_reallocate_priority_queue_no_negative_allocations() {
1265        let mut a = alloc_with_policy(AllocationPolicy::PriorityQueue);
1266        for i in 0..8 {
1267            a.add_peer(format!("p{i}"), BandwidthClass::High, 5_000_000)
1268                .expect("test: add high-priority peer in no-negative-allocations test");
1269        }
1270        a.reallocate();
1271        for i in 0..8 {
1272            assert!(
1273                a.get_allocation(&format!("p{i}"))
1274                    .expect("test: get allocation for peer in no-negative-allocations check")
1275                    >= 1_000_000
1276            );
1277        }
1278    }
1279
1280    #[test]
1281    fn test_reallocate_priority_queue_emits_revised() {
1282        let mut a = populated_allocator(3, AllocationPolicy::PriorityQueue);
1283        a.drain_events();
1284        let events = a.reallocate();
1285        assert!(events
1286            .iter()
1287            .any(|e| matches!(e, BandwidthEvent::AllocationRevised { .. })));
1288    }
1289
1290    // ── get_allocation ────────────────────────────────────────────────────────
1291
1292    #[test]
1293    fn test_get_allocation_not_found() {
1294        let a = AdaptiveBandwidthAllocator::new(default_config());
1295        let err = a.get_allocation("nobody").unwrap_err();
1296        assert!(matches!(err, AllocatorError::PeerNotFound(_)));
1297    }
1298
1299    // ── stats ─────────────────────────────────────────────────────────────────
1300
1301    #[test]
1302    fn test_stats_empty_allocator() {
1303        let a = AdaptiveBandwidthAllocator::new(default_config());
1304        let s = a.stats();
1305        assert_eq!(s.peer_count, 0);
1306        assert_eq!(s.total_allocated_bps, 0);
1307        assert_eq!(s.fairness_index, 1.0);
1308    }
1309
1310    #[test]
1311    fn test_stats_utilization_pct() {
1312        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1313        a.add_peer("p1".to_string(), BandwidthClass::Normal, 10_000_000)
1314            .expect("test: add p1 for stats utilization test");
1315        a.update_measurement("p1", 5_000_000, 10, 0)
1316            .expect("test: record measurement for p1 in utilization test");
1317        let s = a.stats();
1318        // 5 Mbps used / 10 Mbps allocated = 50%
1319        assert!((s.utilization_pct - 50.0).abs() < 1.0);
1320    }
1321
1322    #[test]
1323    fn test_stats_congested_count() {
1324        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1325        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1326            .expect("test: add p1 for stats congested count test");
1327        a.add_peer("p2".to_string(), BandwidthClass::Normal, 5_000_000)
1328            .expect("test: add p2 for stats congested count test");
1329        a.update_measurement("p1", 3_000_000, 100, 50_000)
1330            .expect("test: record congested measurement for p1"); // congested
1331        a.update_measurement("p2", 4_000_000, 10, 0)
1332            .expect("test: record healthy measurement for p2"); // healthy
1333        let s = a.stats();
1334        assert_eq!(s.congested_peers, 1);
1335    }
1336
1337    #[test]
1338    fn test_stats_fairness_perfect() {
1339        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1340        a.add_peer("p1".to_string(), BandwidthClass::Normal, 10_000_000)
1341            .expect("test: add p1 for stats fairness perfect test");
1342        a.add_peer("p2".to_string(), BandwidthClass::Normal, 10_000_000)
1343            .expect("test: add p2 for stats fairness perfect test");
1344        let s = a.stats();
1345        // Both peers have identical allocation → Jain's index = 1.0
1346        assert!((s.fairness_index - 1.0).abs() < 1e-9);
1347    }
1348
1349    #[test]
1350    fn test_stats_fairness_unequal() {
1351        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1352        a.add_peer("p1".to_string(), BandwidthClass::High, 10_000_000)
1353            .expect("test: add p1 High class for stats fairness unequal test");
1354        a.add_peer("p2".to_string(), BandwidthClass::High, 10_000_000)
1355            .expect("test: add p2 High class for stats fairness unequal test");
1356        // Manually manipulate via WeightedFair to get unequal allocations.
1357        a.reallocate();
1358        let s = a.stats();
1359        assert!(s.fairness_index > 0.0 && s.fairness_index <= 1.0);
1360    }
1361
1362    // ── congested_peers ───────────────────────────────────────────────────────
1363
1364    #[test]
1365    fn test_congested_peers_empty() {
1366        let a = AdaptiveBandwidthAllocator::new(default_config());
1367        assert!(a.congested_peers().is_empty());
1368    }
1369
1370    #[test]
1371    fn test_congested_peers_detection() {
1372        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1373        a.add_peer("good".to_string(), BandwidthClass::Normal, 5_000_000)
1374            .expect("test: add good peer for congested peers detection test");
1375        a.add_peer("bad".to_string(), BandwidthClass::Normal, 5_000_000)
1376            .expect("test: add bad peer for congested peers detection test");
1377        a.update_measurement("bad", 1_000_000, 200, 100_000)
1378            .expect("test: record high-latency measurement for bad peer");
1379        let congested = a.congested_peers();
1380        assert_eq!(congested.len(), 1);
1381        assert_eq!(congested[0], "bad");
1382    }
1383
1384    #[test]
1385    fn test_congested_peers_at_threshold_not_flagged() {
1386        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1387        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1388            .expect("test: add p1 for congested peers at threshold test");
1389        // Exactly at threshold (10_000) — not above, so not congested.
1390        a.update_measurement("p1", 3_000_000, 50, 10_000)
1391            .expect("test: record measurement at congestion threshold for p1");
1392        assert!(a.congested_peers().is_empty());
1393    }
1394
1395    // ── events_since / drain_events ───────────────────────────────────────────
1396
1397    #[test]
1398    fn test_events_since_filters_by_timestamp() {
1399        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1400        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1401            .expect("test: add p1 for events since timestamp filter test");
1402        a.advance_time(100);
1403        let mid_ts = a.now_ms;
1404        a.add_peer("p2".to_string(), BandwidthClass::Normal, 5_000_000)
1405            .expect("test: add p2 after time advance for events since timestamp filter test");
1406        let recent = a.events_since(mid_ts);
1407        // Only p2's PeerAdded event should be >= mid_ts.
1408        assert_eq!(recent.len(), 1);
1409    }
1410
1411    #[test]
1412    fn test_drain_events_clears_history() {
1413        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1414        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1415            .expect("test: add p1 for drain events clears history test");
1416        assert!(!a.drain_events().is_empty());
1417        assert!(a.drain_events().is_empty());
1418    }
1419
1420    #[test]
1421    fn test_event_history_bounded_at_200() {
1422        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1423        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1424            .expect("test: add p1 for event history bounded test");
1425        // Push 300 update_measurement events.
1426        for _ in 0..300 {
1427            a.update_measurement("p1", 4_000_000, 10, 0)
1428                .expect("test: record repeated measurement for event history bounded test");
1429        }
1430        // History should be capped at MAX_EVENT_HISTORY.
1431        let events = a.events_since(0);
1432        assert!(events.len() <= AdaptiveBandwidthAllocator::MAX_EVENT_HISTORY);
1433    }
1434
1435    #[test]
1436    fn test_events_since_zero_returns_all() {
1437        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1438        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1439            .expect("test: add p1 for events since zero returns all test");
1440        a.add_peer("p2".to_string(), BandwidthClass::Normal, 5_000_000)
1441            .expect("test: add p2 for events since zero returns all test");
1442        let all = a.events_since(0);
1443        assert_eq!(all.len(), 2);
1444    }
1445
1446    // ── AllocatorConfig::validate ─────────────────────────────────────────────
1447
1448    #[test]
1449    fn test_config_validate_ok() {
1450        assert!(default_config().validate().is_ok());
1451    }
1452
1453    #[test]
1454    fn test_config_validate_min_gt_max_fails() {
1455        let mut cfg = default_config();
1456        cfg.min_allocation_bps = 60_000_000;
1457        cfg.max_allocation_bps = 10_000_000;
1458        assert!(cfg.validate().is_err());
1459    }
1460
1461    #[test]
1462    fn test_config_validate_zero_capacity_fails() {
1463        let mut cfg = default_config();
1464        cfg.total_capacity_bps = 0;
1465        assert!(cfg.validate().is_err());
1466    }
1467
1468    #[test]
1469    fn test_config_validate_max_gt_capacity_fails() {
1470        let mut cfg = default_config();
1471        cfg.max_allocation_bps = cfg.total_capacity_bps + 1;
1472        assert!(cfg.validate().is_err());
1473    }
1474
1475    // ── Jain's fairness index ─────────────────────────────────────────────────
1476
1477    #[test]
1478    fn test_jain_fairness_all_equal() {
1479        let fi =
1480            AdaptiveBandwidthAllocator::jain_fairness_index([10.0, 10.0, 10.0].iter().copied(), 3);
1481        assert!((fi - 1.0).abs() < 1e-9);
1482    }
1483
1484    #[test]
1485    fn test_jain_fairness_one_takes_all() {
1486        // One peer with all bandwidth, n-1 with 0.
1487        let fi =
1488            AdaptiveBandwidthAllocator::jain_fairness_index([100.0, 0.0, 0.0].iter().copied(), 3);
1489        // (100)² / (3 × 100²) = 1/3
1490        assert!((fi - 1.0 / 3.0).abs() < 1e-9);
1491    }
1492
1493    #[test]
1494    fn test_jain_fairness_zero_peers() {
1495        let fi = AdaptiveBandwidthAllocator::jain_fairness_index(std::iter::empty(), 0);
1496        assert_eq!(fi, 1.0);
1497    }
1498
1499    #[test]
1500    fn test_jain_fairness_single_peer() {
1501        let fi = AdaptiveBandwidthAllocator::jain_fairness_index([42.0].iter().copied(), 1);
1502        assert!((fi - 1.0).abs() < 1e-9);
1503    }
1504
1505    // ── Edge cases ────────────────────────────────────────────────────────────
1506
1507    #[test]
1508    fn test_reallocate_emits_allocation_revised() {
1509        let mut a = populated_allocator(3, AllocationPolicy::EqualShare);
1510        a.drain_events();
1511        let events = a.reallocate();
1512        assert!(events
1513            .iter()
1514            .any(|e| matches!(e, BandwidthEvent::AllocationRevised { .. })));
1515    }
1516
1517    #[test]
1518    fn test_single_peer_equal_share() {
1519        let mut a = alloc_with_policy(AllocationPolicy::EqualShare);
1520        a.add_peer("only".to_string(), BandwidthClass::Normal, 5_000_000)
1521            .expect("test: add single peer for equal share test");
1522        a.reallocate();
1523        // 100 Mbps / 1 peer = 100 Mbps, capped at max 50 Mbps.
1524        assert_eq!(
1525            a.get_allocation("only")
1526                .expect("test: get allocation for single peer"),
1527            50_000_000
1528        );
1529    }
1530
1531    #[test]
1532    fn test_over_capacity_add_rejected() {
1533        let mut cfg = default_config();
1534        cfg.total_capacity_bps = 10_000_000; // 10 Mbps
1535        cfg.max_allocation_bps = 10_000_000;
1536        let mut a = AdaptiveBandwidthAllocator::new(cfg);
1537        a.add_peer("p1".to_string(), BandwidthClass::Normal, 10_000_000)
1538            .expect("test: add p1 within capacity for over-capacity rejection test");
1539        // Adding any more exceeds capacity.
1540        let err = a
1541            .add_peer("p2".to_string(), BandwidthClass::Normal, 1_000_000)
1542            .unwrap_err();
1543        assert!(matches!(err, AllocatorError::AllocationExceedsCapacity(_)));
1544    }
1545
1546    #[test]
1547    fn test_reallocate_after_remove_rebalances_correctly() {
1548        let mut a = alloc_with_policy(AllocationPolicy::EqualShare);
1549        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1550            .expect("test: add p1 for rebalance after remove test");
1551        a.add_peer("p2".to_string(), BandwidthClass::Normal, 5_000_000)
1552            .expect("test: add p2 for rebalance after remove test");
1553        a.add_peer("p3".to_string(), BandwidthClass::Normal, 5_000_000)
1554            .expect("test: add p3 to be removed for rebalance test");
1555        a.remove_peer("p3")
1556            .expect("test: remove p3 to trigger rebalance");
1557        a.reallocate();
1558        // 100 Mbps / 2 = 50 Mbps (at max cap)
1559        assert_eq!(
1560            a.get_allocation("p1")
1561                .expect("test: get allocation for p1 after rebalance"),
1562            50_000_000
1563        );
1564        assert_eq!(
1565            a.get_allocation("p2")
1566                .expect("test: get allocation for p2 after rebalance"),
1567            50_000_000
1568        );
1569    }
1570
1571    #[test]
1572    fn test_congestion_clears_after_update() {
1573        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1574        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1575            .expect("test: add p1 for congestion clear test");
1576        a.update_measurement("p1", 1_000_000, 200, 100_000)
1577            .expect("test: update measurement to mark p1 as congested"); // congested
1578        assert_eq!(a.congested_peers().len(), 1);
1579        a.update_measurement("p1", 4_000_000, 10, 0)
1580            .expect("test: update measurement to clear p1 congestion"); // recovered
1581        assert_eq!(a.congested_peers().len(), 0);
1582    }
1583
1584    #[test]
1585    fn test_advance_time_affects_events() {
1586        let mut a = AdaptiveBandwidthAllocator::new(default_config());
1587        a.add_peer("p1".to_string(), BandwidthClass::Normal, 5_000_000)
1588            .expect("test: add p1 before time advance");
1589        a.advance_time(500);
1590        a.add_peer("p2".to_string(), BandwidthClass::Normal, 5_000_000)
1591            .expect("test: add p2 after time advance");
1592        let after_advance = a.events_since(a.now_ms);
1593        // Only p2's event is at the advanced timestamp.
1594        assert_eq!(after_advance.len(), 1);
1595    }
1596
1597    #[test]
1598    fn test_multiple_reallocate_calls_stable() {
1599        let mut a = populated_allocator(4, AllocationPolicy::WeightedFair);
1600        for _ in 0..5 {
1601            a.reallocate();
1602        }
1603        // All allocations must remain within bounds.
1604        for i in 0..4 {
1605            let alloc = a
1606                .get_allocation(&format!("peer-{i}"))
1607                .expect("test: get allocation for peer in stability test");
1608            let cfg = a.config();
1609            assert!(alloc >= cfg.min_allocation_bps);
1610            assert!(alloc <= cfg.max_allocation_bps);
1611        }
1612    }
1613}