Skip to main content

ipfrs_network/
congestion_controller.rs

1//! TCP-inspired multi-algorithm congestion controller for IPFRS peer data streams.
2//!
3//! Implements SlowStart, CongestionAvoidance, FastRecovery, and Idle phases with
4//! five distinct algorithms: Reno, Cubic, BBR, Vegas, and Westwood.
5//!
6//! # Legacy API
7//!
8//! The original per-peer `PeerCongestionController` and `MultiPeerCongestionManager`
9//! are retained for backwards compatibility.
10//!
11//! # New API
12//!
13//! Use [`CongestionController`] for full multi-algorithm support.
14
15use std::collections::{HashMap, VecDeque};
16
17// ─── type aliases ────────────────────────────────────────────────────────────
18
19/// Connection identifier type alias.
20pub type CccConnId = u64;
21/// Main congestion controller type alias.
22pub type CccCongestionController = CongestionController;
23/// Decision returned from ack/loss/timeout handlers.
24pub type CccDecision = Decision;
25
26// ─── inline PRNG for deterministic jitter ────────────────────────────────────
27
28#[inline]
29fn xorshift64(state: &mut u64) -> u64 {
30    let mut x = *state;
31    x ^= x << 13;
32    x ^= x >> 7;
33    x ^= x << 17;
34    *state = x;
35    x
36}
37
38// ─── Algorithm ───────────────────────────────────────────────────────────────
39
40/// Congestion control algorithm selection.
41#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
42pub enum CccAlgorithm {
43    /// TCP Reno: AIMD additive-increase/multiplicative-decrease.
44    #[default]
45    Reno,
46    /// TCP CUBIC: cubic growth function for high-BDP networks.
47    Cubic,
48    /// BBR: Bottleneck Bandwidth and RTT model-based control.
49    Bbr,
50    /// TCP Vegas: RTT-based proactive congestion avoidance.
51    Vegas,
52    /// Westwood+: bandwidth estimation on loss for fast recovery.
53    Westwood,
54}
55
56// ─── State ───────────────────────────────────────────────────────────────────
57
58/// Congestion controller FSM state.
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
60pub enum CccState {
61    /// Exponential window growth until ssthresh is reached.
62    SlowStart,
63    /// Linear (or algorithm-defined) window growth.
64    #[default]
65    CongestionAvoidance,
66    /// Recovering from packet loss via limited retransmit.
67    FastRecovery,
68    /// Connection is quiescent (no in-flight data).
69    Idle,
70}
71
72// ─── EventType ───────────────────────────────────────────────────────────────
73
74/// Kinds of congestion events logged in the event ring.
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub enum CccEventType {
77    /// An ACK arrived.
78    AckReceived,
79    /// A packet was declared lost.
80    PacketLost,
81    /// Retransmission timeout triggered.
82    Timeout,
83    /// Fast retransmit triggered (3 DUP-ACKs).
84    FastRetransmit,
85    /// Controller entered slow-start.
86    SlowStartEnter,
87    /// Controller entered congestion avoidance.
88    CaEnter,
89}
90
91// ─── Event ───────────────────────────────────────────────────────────────────
92
93/// A single congestion event record.
94#[derive(Clone, Debug)]
95pub struct CccEvent {
96    /// Monotonic timestamp (ms since controller creation, from connection `last_ts`).
97    pub ts: u64,
98    /// Connection this event belongs to.
99    pub conn_id: CccConnId,
100    /// What happened.
101    pub event_type: CccEventType,
102    /// Congestion window before the event.
103    pub cwnd_before: u64,
104    /// Congestion window after the event.
105    pub cwnd_after: u64,
106}
107
108// ─── Decision ────────────────────────────────────────────────────────────────
109
110/// Result returned by `on_ack`, `on_loss`, and `on_timeout`.
111#[derive(Clone, Debug, PartialEq)]
112pub struct Decision {
113    /// New congestion window (bytes).
114    pub new_cwnd: u64,
115    /// New slow-start threshold (bytes).
116    pub new_ssthresh: u64,
117    /// New FSM state.
118    pub new_state: CccState,
119    /// Recommended sending rate (bytes/sec), if RTT is known.
120    pub sending_rate: Option<f64>,
121}
122
123// ─── Connection ──────────────────────────────────────────────────────────────
124
125/// Per-connection congestion state.
126#[derive(Clone, Debug)]
127pub struct CccConnection {
128    /// Unique connection identifier.
129    pub id: CccConnId,
130    /// Current congestion window (bytes).
131    pub cwnd: u64,
132    /// Slow-start threshold (bytes).
133    pub ssthresh: u64,
134    /// Current FSM state.
135    pub state: CccState,
136    /// Smoothed RTT (ms), exponential weighted moving average.
137    pub rtt_ms: f64,
138    /// RTT variance (ms).
139    pub rtt_var: f64,
140    /// Bytes currently in flight.
141    pub in_flight: u64,
142    /// Total bytes acknowledged.
143    pub bytes_acked: u64,
144    /// Total bytes lost.
145    pub bytes_lost: u64,
146    /// Timestamp of the last event (monotonic ms counter, internal).
147    pub last_ts: u64,
148    // ── Cubic-specific ──────────────────────────────────────────────────────
149    /// Time (in RTTs) since last congestion event (for Cubic).
150    cubic_k: f64,
151    /// Window size at last congestion event (for Cubic).
152    cubic_w_max: f64,
153    // ── BBR-specific ─────────────────────────────────────────────────────────
154    /// Estimated bottleneck bandwidth (bytes/sec).
155    bbr_bw: f64,
156    /// Minimum observed RTT for BBR (ms).
157    bbr_min_rtt: f64,
158    // ── Vegas-specific ────────────────────────────────────────────────────────
159    /// Minimum observed RTT for Vegas (ms).
160    vegas_base_rtt: f64,
161    // ── Westwood-specific ────────────────────────────────────────────────────
162    /// Bandwidth estimate (bytes/sec) for Westwood.
163    westwood_bw: f64,
164    /// Internal PRNG state for jitter.
165    prng_state: u64,
166}
167
168impl CccConnection {
169    fn new(id: CccConnId, config: &CccControllerConfig) -> Self {
170        Self {
171            id,
172            cwnd: config.initial_cwnd,
173            ssthresh: config.ssthresh,
174            state: CccState::SlowStart,
175            rtt_ms: 0.0,
176            rtt_var: 0.0,
177            in_flight: 0,
178            bytes_acked: 0,
179            bytes_lost: 0,
180            last_ts: 0,
181            cubic_k: 0.0,
182            cubic_w_max: config.initial_cwnd as f64,
183            bbr_bw: 0.0,
184            bbr_min_rtt: f64::MAX,
185            vegas_base_rtt: 0.0,
186            westwood_bw: 0.0,
187            prng_state: id.wrapping_add(1).max(1),
188        }
189    }
190
191    /// Update the EWMA-smoothed RTT.
192    fn update_rtt(&mut self, new_rtt_ms: f64, alpha: f64) {
193        if self.rtt_ms == 0.0 {
194            self.rtt_ms = new_rtt_ms;
195            self.rtt_var = new_rtt_ms / 2.0;
196        } else {
197            let diff = (new_rtt_ms - self.rtt_ms).abs();
198            self.rtt_var = (1.0 - 0.25) * self.rtt_var + 0.25 * diff;
199            self.rtt_ms = (1.0 - alpha) * self.rtt_ms + alpha * new_rtt_ms;
200        }
201        if new_rtt_ms < self.bbr_min_rtt {
202            self.bbr_min_rtt = new_rtt_ms;
203        }
204        if self.vegas_base_rtt == 0.0 || new_rtt_ms < self.vegas_base_rtt {
205            self.vegas_base_rtt = new_rtt_ms;
206        }
207    }
208
209    /// Sending rate in bytes/sec given current cwnd and RTT.
210    fn rate(&self) -> Option<f64> {
211        if self.rtt_ms > 0.0 {
212            Some(self.cwnd as f64 / (self.rtt_ms / 1000.0))
213        } else {
214            None
215        }
216    }
217
218    /// Apply a small random jitter (±1 %) to the congestion window.
219    ///
220    /// Used by algorithms that need to break synchronisation among multiple
221    /// flows.  The magnitude is bounded so cwnd never leaves `[min, max]`.
222    pub fn apply_cwnd_jitter(&mut self, min_cwnd: u64, max_cwnd: u64) {
223        let r = xorshift64(&mut self.prng_state);
224        // Map to ±1 % of cwnd.
225        let pct = (r % 201) as i64 - 100; // -100 … +100
226        let delta = (self.cwnd as i64 * pct / 10_000).unsigned_abs();
227        if pct >= 0 {
228            self.cwnd = self.cwnd.saturating_add(delta).min(max_cwnd);
229        } else {
230            self.cwnd = self.cwnd.saturating_sub(delta).max(min_cwnd);
231        }
232    }
233}
234
235// ─── Config ──────────────────────────────────────────────────────────────────
236
237/// Configuration for the multi-connection [`CongestionController`].
238#[derive(Clone, Debug)]
239pub struct CccControllerConfig {
240    /// Congestion control algorithm to apply.
241    pub algorithm: CccAlgorithm,
242    /// Initial congestion window (bytes). Default: 64 KB.
243    pub initial_cwnd: u64,
244    /// Minimum congestion window (bytes). Default: 1 MTU = 1 448 B.
245    pub min_cwnd: u64,
246    /// Maximum congestion window (bytes). Default: 16 MB.
247    pub max_cwnd: u64,
248    /// Initial slow-start threshold (bytes). Default: 1 MB.
249    pub ssthresh: u64,
250    /// EWMA smoothing factor for RTT. Typical: 0.125.
251    pub rtt_alpha: f64,
252}
253
254impl Default for CccControllerConfig {
255    fn default() -> Self {
256        Self {
257            algorithm: CccAlgorithm::Reno,
258            initial_cwnd: 65_536,
259            min_cwnd: 1_448,
260            max_cwnd: 16_777_216,
261            ssthresh: 1_048_576,
262            rtt_alpha: 0.125,
263        }
264    }
265}
266
267// ─── Stats ───────────────────────────────────────────────────────────────────
268
269/// Aggregate statistics for the entire controller.
270#[derive(Clone, Debug, Default)]
271pub struct CccControllerStats {
272    /// Total ACK events processed across all connections.
273    pub total_acks: u64,
274    /// Total loss events processed across all connections.
275    pub total_losses: u64,
276    /// Average congestion window across active connections (bytes).
277    pub avg_cwnd: f64,
278    /// Average smoothed RTT across active connections (ms).
279    pub avg_rtt: f64,
280    /// Number of active connections.
281    pub active_connections: usize,
282}
283
284// ─── CongestionController ────────────────────────────────────────────────────
285
286/// Multi-connection, multi-algorithm TCP-inspired congestion controller.
287///
288/// Manages per-connection congestion state and emits [`Decision`] structs
289/// that callers use to pace their sends.
290pub struct CongestionController {
291    /// Per-connection state.
292    connections: HashMap<CccConnId, CccConnection>,
293    /// Bounded ring buffer of recent events (max 1 000).
294    events: VecDeque<CccEvent>,
295    /// Controller-wide configuration.
296    config: CccControllerConfig,
297    /// Aggregate ACK counter.
298    total_acks: u64,
299    /// Aggregate loss counter.
300    total_losses: u64,
301    /// Monotonic internal tick (incremented on every mutating call).
302    tick: u64,
303}
304
305impl CongestionController {
306    /// Create a new controller with the given configuration.
307    pub fn new(config: CccControllerConfig) -> Self {
308        Self {
309            connections: HashMap::new(),
310            events: VecDeque::with_capacity(1_000),
311            config,
312            total_acks: 0,
313            total_losses: 0,
314            tick: 0,
315        }
316    }
317
318    /// Create a new controller with default configuration.
319    pub fn with_defaults() -> Self {
320        Self::new(CccControllerConfig::default())
321    }
322
323    // ── Connection management ────────────────────────────────────────────────
324
325    /// Register a new connection.  If the connection already exists this is a no-op.
326    pub fn add_connection(&mut self, id: CccConnId) {
327        self.connections
328            .entry(id)
329            .or_insert_with(|| CccConnection::new(id, &self.config));
330    }
331
332    /// Remove a tracked connection.  Returns `true` if it existed.
333    pub fn remove_connection(&mut self, id: CccConnId) -> bool {
334        self.connections.remove(&id).is_some()
335    }
336
337    /// Reset a connection to its initial state (keeps the entry).
338    ///
339    /// Returns an error if the connection does not exist.
340    pub fn reset_connection(&mut self, id: CccConnId) -> Result<(), &'static str> {
341        let config = &self.config;
342        let conn = self
343            .connections
344            .get_mut(&id)
345            .ok_or("connection not found")?;
346        *conn = CccConnection::new(id, config);
347        Ok(())
348    }
349
350    // ── Core operations ──────────────────────────────────────────────────────
351
352    /// Process an ACK for `bytes_acked` bytes on `conn_id` with the measured RTT.
353    ///
354    /// Returns the new [`Decision`], or an error if `conn_id` is unknown.
355    pub fn on_ack(
356        &mut self,
357        conn_id: CccConnId,
358        bytes_acked: u64,
359        rtt_ms: f64,
360    ) -> Result<CccDecision, &'static str> {
361        self.tick = self.tick.wrapping_add(1);
362        let ts = self.tick;
363
364        let conn = self
365            .connections
366            .get_mut(&conn_id)
367            .ok_or("connection not found")?;
368
369        let cwnd_before = conn.cwnd;
370        conn.update_rtt(rtt_ms, self.config.rtt_alpha);
371        conn.bytes_acked = conn.bytes_acked.saturating_add(bytes_acked);
372        conn.last_ts = ts;
373
374        // Update bandwidth estimate (Westwood / BBR).
375        if rtt_ms > 0.0 {
376            let sample_bw = bytes_acked as f64 / (rtt_ms / 1_000.0);
377            if conn.westwood_bw == 0.0 {
378                conn.westwood_bw = sample_bw;
379            } else {
380                conn.westwood_bw = 0.875 * conn.westwood_bw + 0.125 * sample_bw;
381            }
382            if conn.bbr_bw == 0.0 {
383                conn.bbr_bw = sample_bw;
384            } else {
385                conn.bbr_bw = conn.bbr_bw.max(sample_bw);
386            }
387        }
388
389        let min_cwnd = self.config.min_cwnd;
390        let max_cwnd = self.config.max_cwnd;
391
392        let new_cwnd = match self.config.algorithm {
393            CccAlgorithm::Reno => reno_on_ack(conn, bytes_acked, min_cwnd, max_cwnd),
394            CccAlgorithm::Cubic => cubic_on_ack(conn, bytes_acked, rtt_ms, min_cwnd, max_cwnd),
395            CccAlgorithm::Bbr => bbr_on_ack(conn, min_cwnd, max_cwnd),
396            CccAlgorithm::Vegas => vegas_on_ack(conn, min_cwnd, max_cwnd),
397            CccAlgorithm::Westwood => westwood_on_ack(conn, bytes_acked, min_cwnd, max_cwnd),
398        };
399
400        conn.cwnd = new_cwnd.clamp(min_cwnd, max_cwnd);
401        self.total_acks += 1;
402
403        // Extract values before releasing the borrow.
404        let cwnd_after = conn.cwnd;
405        let new_ssthresh = conn.ssthresh;
406        let new_state = conn.state;
407        let sending_rate = conn.rate();
408
409        self.push_event(CccEvent {
410            ts,
411            conn_id,
412            event_type: CccEventType::AckReceived,
413            cwnd_before,
414            cwnd_after,
415        });
416
417        Ok(Decision {
418            new_cwnd: cwnd_after,
419            new_ssthresh,
420            new_state,
421            sending_rate,
422        })
423    }
424
425    /// Process a packet loss event on `conn_id`.
426    ///
427    /// Applies multiplicative decrease and enters fast recovery.
428    pub fn on_loss(
429        &mut self,
430        conn_id: CccConnId,
431        lost_bytes: u64,
432    ) -> Result<CccDecision, &'static str> {
433        self.tick = self.tick.wrapping_add(1);
434        let ts = self.tick;
435
436        let conn = self
437            .connections
438            .get_mut(&conn_id)
439            .ok_or("connection not found")?;
440
441        let cwnd_before = conn.cwnd;
442        conn.bytes_lost = conn.bytes_lost.saturating_add(lost_bytes);
443        conn.last_ts = ts;
444
445        let min_cwnd = self.config.min_cwnd;
446        let max_cwnd = self.config.max_cwnd;
447
448        let new_cwnd = match self.config.algorithm {
449            CccAlgorithm::Reno => reno_on_loss(conn, min_cwnd),
450            CccAlgorithm::Cubic => cubic_on_loss(conn, min_cwnd),
451            CccAlgorithm::Bbr => bbr_on_loss(conn, min_cwnd),
452            CccAlgorithm::Vegas => vegas_on_loss(conn, min_cwnd),
453            CccAlgorithm::Westwood => westwood_on_loss(conn, min_cwnd),
454        };
455
456        conn.cwnd = new_cwnd.clamp(min_cwnd, max_cwnd);
457        conn.state = CccState::FastRecovery;
458        self.total_losses += 1;
459
460        // Extract values before releasing the borrow.
461        let cwnd_after = conn.cwnd;
462        let new_ssthresh = conn.ssthresh;
463        let sending_rate = conn.rate();
464
465        self.push_event(CccEvent {
466            ts,
467            conn_id,
468            event_type: CccEventType::PacketLost,
469            cwnd_before,
470            cwnd_after,
471        });
472
473        Ok(Decision {
474            new_cwnd: cwnd_after,
475            new_ssthresh,
476            new_state: CccState::FastRecovery,
477            sending_rate,
478        })
479    }
480
481    /// Process a retransmission timeout on `conn_id`.
482    ///
483    /// Resets cwnd to min_cwnd, halves ssthresh, re-enters slow start.
484    pub fn on_timeout(&mut self, conn_id: CccConnId) -> Result<CccDecision, &'static str> {
485        self.tick = self.tick.wrapping_add(1);
486        let ts = self.tick;
487
488        let conn = self
489            .connections
490            .get_mut(&conn_id)
491            .ok_or("connection not found")?;
492
493        let cwnd_before = conn.cwnd;
494        conn.last_ts = ts;
495
496        let min_cwnd = self.config.min_cwnd;
497
498        conn.ssthresh = (conn.cwnd / 2).max(min_cwnd);
499        conn.cwnd = min_cwnd;
500        conn.state = CccState::SlowStart;
501        // Reset Cubic state on timeout.
502        conn.cubic_k = 0.0;
503        conn.cubic_w_max = min_cwnd as f64;
504        self.total_losses += 1;
505
506        // Extract values before releasing the borrow.
507        let cwnd_after = conn.cwnd;
508        let new_ssthresh = conn.ssthresh;
509        let sending_rate = conn.rate();
510
511        self.push_event(CccEvent {
512            ts,
513            conn_id,
514            event_type: CccEventType::Timeout,
515            cwnd_before,
516            cwnd_after,
517        });
518        self.push_event(CccEvent {
519            ts,
520            conn_id,
521            event_type: CccEventType::SlowStartEnter,
522            cwnd_before: cwnd_after,
523            cwnd_after,
524        });
525
526        Ok(Decision {
527            new_cwnd: cwnd_after,
528            new_ssthresh,
529            new_state: CccState::SlowStart,
530            sending_rate,
531        })
532    }
533
534    // ── Queries ──────────────────────────────────────────────────────────────
535
536    /// Current sending rate for a connection, in bytes/sec.
537    ///
538    /// Returns `None` if the connection is unknown or RTT has not been measured.
539    pub fn sending_rate(&self, conn_id: CccConnId) -> Option<f64> {
540        self.connections.get(&conn_id).and_then(|c| c.rate())
541    }
542
543    /// Aggregate statistics across all active connections.
544    pub fn controller_stats(&self) -> CccControllerStats {
545        let n = self.connections.len();
546        if n == 0 {
547            return CccControllerStats {
548                total_acks: self.total_acks,
549                total_losses: self.total_losses,
550                avg_cwnd: 0.0,
551                avg_rtt: 0.0,
552                active_connections: 0,
553            };
554        }
555        let sum_cwnd: u64 = self.connections.values().map(|c| c.cwnd).sum();
556        let sum_rtt: f64 = self.connections.values().map(|c| c.rtt_ms).sum();
557        CccControllerStats {
558            total_acks: self.total_acks,
559            total_losses: self.total_losses,
560            avg_cwnd: sum_cwnd as f64 / n as f64,
561            avg_rtt: sum_rtt / n as f64,
562            active_connections: n,
563        }
564    }
565
566    /// Immutable reference to a connection, if it exists.
567    pub fn connection(&self, conn_id: CccConnId) -> Option<&CccConnection> {
568        self.connections.get(&conn_id)
569    }
570
571    /// Read-only view of the event log.
572    pub fn events(&self) -> &VecDeque<CccEvent> {
573        &self.events
574    }
575
576    // ── Internal ─────────────────────────────────────────────────────────────
577
578    fn push_event(&mut self, event: CccEvent) {
579        if self.events.len() >= 1_000 {
580            self.events.pop_front();
581        }
582        self.events.push_back(event);
583    }
584}
585
586// ─── Reno ────────────────────────────────────────────────────────────────────
587
588/// Reno on-ACK: AIMD with slow-start below ssthresh.
589fn reno_on_ack(conn: &mut CccConnection, bytes_acked: u64, min_cwnd: u64, max_cwnd: u64) -> u64 {
590    match conn.state {
591        CccState::SlowStart | CccState::Idle => {
592            let new_cwnd = conn.cwnd.saturating_add(bytes_acked).min(max_cwnd);
593            if new_cwnd >= conn.ssthresh {
594                conn.state = CccState::CongestionAvoidance;
595            }
596            new_cwnd
597        }
598        CccState::CongestionAvoidance => {
599            // Additive increase: +MSS per RTT ≈ MSS² / cwnd per ACK.
600            let mss: u64 = 1_448;
601            let increase = if conn.cwnd > 0 {
602                (mss.saturating_mul(mss)).saturating_div(conn.cwnd).max(1)
603            } else {
604                mss
605            };
606            conn.cwnd.saturating_add(increase).min(max_cwnd)
607        }
608        CccState::FastRecovery => {
609            // Deflate window: exit recovery once cwnd climbs back to ssthresh.
610            let new_cwnd = conn.cwnd.saturating_add(bytes_acked / 2).min(max_cwnd);
611            if new_cwnd >= conn.ssthresh {
612                conn.state = CccState::CongestionAvoidance;
613            }
614            new_cwnd
615        }
616    }
617    .max(min_cwnd)
618}
619
620/// Reno on-loss: multiplicative decrease, halve cwnd.
621fn reno_on_loss(conn: &mut CccConnection, min_cwnd: u64) -> u64 {
622    conn.ssthresh = (conn.cwnd / 2).max(min_cwnd);
623    conn.ssthresh
624}
625
626// ─── Cubic ───────────────────────────────────────────────────────────────────
627
628/// Cubic growth constant C (standard value 0.4).
629const CUBIC_C: f64 = 0.4;
630/// Cubic beta (standard 0.7 for CUBIC).
631const CUBIC_BETA: f64 = 0.7;
632
633/// Cubic on-ACK: cubic growth function around W_max.
634fn cubic_on_ack(
635    conn: &mut CccConnection,
636    bytes_acked: u64,
637    rtt_ms: f64,
638    min_cwnd: u64,
639    max_cwnd: u64,
640) -> u64 {
641    match conn.state {
642        CccState::SlowStart | CccState::Idle => {
643            let new_cwnd = conn.cwnd.saturating_add(bytes_acked).min(max_cwnd);
644            if new_cwnd >= conn.ssthresh {
645                conn.state = CccState::CongestionAvoidance;
646                // Compute K: cubic root of (W_max * (1-beta) / C).
647                let w_max = conn.cubic_w_max;
648                conn.cubic_k = ((w_max * (1.0 - CUBIC_BETA)) / CUBIC_C).cbrt();
649            }
650            new_cwnd.max(min_cwnd)
651        }
652        CccState::CongestionAvoidance => {
653            let rtt_s = (rtt_ms / 1_000.0).max(0.001);
654            // t = elapsed RTTs since last congestion (approximated by one RTT per call).
655            let t = rtt_s;
656            let k = conn.cubic_k;
657            let w_max = conn.cubic_w_max;
658            // W_cubic(t) = C*(t-K)^3 + W_max
659            let delta = t - k;
660            let w_cubic = CUBIC_C * delta * delta * delta + w_max;
661            let target = w_cubic.max(conn.cwnd as f64);
662            let new_cwnd = (target as u64).clamp(min_cwnd, max_cwnd);
663            if new_cwnd >= conn.ssthresh {
664                conn.state = CccState::CongestionAvoidance; // stays
665            }
666            new_cwnd
667        }
668        CccState::FastRecovery => {
669            let new_cwnd = conn.cwnd.saturating_add(bytes_acked / 2).min(max_cwnd);
670            if new_cwnd >= conn.ssthresh {
671                conn.state = CccState::CongestionAvoidance;
672            }
673            new_cwnd.max(min_cwnd)
674        }
675    }
676}
677
678/// Cubic on-loss: W_max = current cwnd, new cwnd = cwnd * beta.
679fn cubic_on_loss(conn: &mut CccConnection, min_cwnd: u64) -> u64 {
680    conn.cubic_w_max = conn.cwnd as f64;
681    let new_cwnd = ((conn.cwnd as f64 * CUBIC_BETA) as u64).max(min_cwnd);
682    conn.ssthresh = new_cwnd;
683    // Reset cubic K after loss so next CA phase recomputes it.
684    conn.cubic_k = 0.0;
685    new_cwnd
686}
687
688// ─── BBR ─────────────────────────────────────────────────────────────────────
689
690/// BBR on-ACK: pacing window = BDP = bw × min_rtt.
691fn bbr_on_ack(conn: &mut CccConnection, min_cwnd: u64, max_cwnd: u64) -> u64 {
692    if conn.bbr_bw > 0.0 && conn.bbr_min_rtt < f64::MAX && conn.bbr_min_rtt > 0.0 {
693        let bdp = conn.bbr_bw * (conn.bbr_min_rtt / 1_000.0);
694        // Add 1.25× gain for probing.
695        let target = (bdp * 1.25) as u64;
696        // BBR never reduces cwnd on an ACK: the BDP estimate can only raise it.
697        let new_cwnd = target.max(conn.cwnd).clamp(min_cwnd, max_cwnd);
698        if conn.state == CccState::SlowStart && new_cwnd >= conn.ssthresh {
699            conn.state = CccState::CongestionAvoidance;
700        }
701        new_cwnd
702    } else {
703        // Fallback to Reno slow-start until we have measurements.
704        let new_cwnd = conn.cwnd.saturating_add(1_448).min(max_cwnd);
705        if new_cwnd >= conn.ssthresh {
706            conn.state = CccState::CongestionAvoidance;
707        }
708        new_cwnd.max(min_cwnd)
709    }
710}
711
712/// BBR on-loss: do not reduce cwnd; BBR relies on rate not loss.
713/// We do a mild 10% reduction to prevent starvation.
714fn bbr_on_loss(conn: &mut CccConnection, min_cwnd: u64) -> u64 {
715    conn.ssthresh = (conn.cwnd * 9 / 10).max(min_cwnd);
716    conn.ssthresh
717}
718
719// ─── Vegas ───────────────────────────────────────────────────────────────────
720
721/// Vegas alpha/beta thresholds (packets).
722const VEGAS_ALPHA: f64 = 2.0;
723const VEGAS_BETA: f64 = 4.0;
724
725/// Vegas on-ACK: compare actual throughput to expected throughput.
726fn vegas_on_ack(conn: &mut CccConnection, min_cwnd: u64, max_cwnd: u64) -> u64 {
727    if conn.vegas_base_rtt == 0.0 || conn.rtt_ms == 0.0 {
728        // No RTT sample yet — behave like Reno slow-start.
729        let new_cwnd = conn.cwnd.saturating_add(1_448).min(max_cwnd);
730        if new_cwnd >= conn.ssthresh {
731            conn.state = CccState::CongestionAvoidance;
732        }
733        return new_cwnd.max(min_cwnd);
734    }
735
736    let expected = conn.cwnd as f64 / conn.vegas_base_rtt;
737    let actual = conn.cwnd as f64 / conn.rtt_ms;
738    let diff = expected - actual;
739    let mss: f64 = 1_448.0;
740
741    let new_cwnd: u64 = match conn.state {
742        CccState::SlowStart | CccState::Idle => {
743            let grown = conn.cwnd.saturating_add(1_448).min(max_cwnd);
744            if grown >= conn.ssthresh {
745                conn.state = CccState::CongestionAvoidance;
746            }
747            grown
748        }
749        CccState::CongestionAvoidance => {
750            if diff < VEGAS_ALPHA {
751                // Too little queuing — increase.
752                (conn.cwnd as f64 + mss * mss / conn.cwnd as f64) as u64
753            } else if diff > VEGAS_BETA {
754                // Too much queuing — decrease.
755                conn.cwnd
756                    .saturating_sub((mss * mss / conn.cwnd as f64) as u64)
757            } else {
758                conn.cwnd
759            }
760        }
761        CccState::FastRecovery => {
762            let grown = conn.cwnd.saturating_add(1_448 / 2).min(max_cwnd);
763            if grown >= conn.ssthresh {
764                conn.state = CccState::CongestionAvoidance;
765            }
766            grown
767        }
768    };
769    new_cwnd.clamp(min_cwnd, max_cwnd)
770}
771
772/// Vegas on-loss: same as Reno multiplicative decrease.
773fn vegas_on_loss(conn: &mut CccConnection, min_cwnd: u64) -> u64 {
774    conn.ssthresh = (conn.cwnd / 2).max(min_cwnd);
775    conn.ssthresh
776}
777
778// ─── Westwood ────────────────────────────────────────────────────────────────
779
780/// Westwood on-ACK: AIMD but uses bandwidth estimate.
781fn westwood_on_ack(
782    conn: &mut CccConnection,
783    bytes_acked: u64,
784    min_cwnd: u64,
785    max_cwnd: u64,
786) -> u64 {
787    match conn.state {
788        CccState::SlowStart | CccState::Idle => {
789            let new_cwnd = conn.cwnd.saturating_add(bytes_acked).min(max_cwnd);
790            if new_cwnd >= conn.ssthresh {
791                conn.state = CccState::CongestionAvoidance;
792            }
793            new_cwnd.max(min_cwnd)
794        }
795        CccState::CongestionAvoidance => {
796            let mss: u64 = 1_448;
797            let increase = if conn.cwnd > 0 {
798                (mss.saturating_mul(mss)).saturating_div(conn.cwnd).max(1)
799            } else {
800                mss
801            };
802            conn.cwnd
803                .saturating_add(increase)
804                .min(max_cwnd)
805                .max(min_cwnd)
806        }
807        CccState::FastRecovery => {
808            let new_cwnd = conn.cwnd.saturating_add(bytes_acked / 2).min(max_cwnd);
809            if new_cwnd >= conn.ssthresh {
810                conn.state = CccState::CongestionAvoidance;
811            }
812            new_cwnd.max(min_cwnd)
813        }
814    }
815}
816
817/// Westwood on-loss: set ssthresh = BDP estimate.
818fn westwood_on_loss(conn: &mut CccConnection, min_cwnd: u64) -> u64 {
819    if conn.westwood_bw > 0.0 && conn.rtt_ms > 0.0 {
820        let bdp = (conn.westwood_bw * conn.rtt_ms / 1_000.0) as u64;
821        conn.ssthresh = bdp.max(min_cwnd);
822    } else {
823        conn.ssthresh = (conn.cwnd / 2).max(min_cwnd);
824    }
825    conn.ssthresh
826}
827
828// ═══════════════════════════════════════════════════════════════════════════════
829// LEGACY API (retained for backwards compatibility)
830// ═══════════════════════════════════════════════════════════════════════════════
831
832/// Phases of the congestion control state machine (legacy).
833#[derive(Clone, Copy, Debug, PartialEq, Eq)]
834pub enum CongestionState {
835    /// Exponential growth phase: window doubles per RTT.
836    SlowStart,
837    /// Linear growth phase: additive increase.
838    CongestionAvoidance,
839    /// Recovering from a detected packet loss event.
840    FastRecovery,
841}
842
843/// Events that drive congestion controller state transitions (legacy).
844#[derive(Clone, Copy, Debug, PartialEq, Eq)]
845pub enum CongestionEvent {
846    /// A successful acknowledgment was received for `bytes` of data.
847    AckReceived {
848        /// Number of bytes acknowledged.
849        bytes: u64,
850    },
851    /// Packet loss was detected (triggers window reduction).
852    PacketLoss,
853    /// Retransmit timeout — severe reduction, resets to SlowStart.
854    Timeout,
855    /// Explicit Congestion Notification mark received.
856    EcnMark,
857}
858
859/// A snapshot of the current congestion window state for a peer (legacy).
860#[derive(Clone, Debug)]
861pub struct WindowStats {
862    /// Current congestion window size in bytes.
863    pub current_window: u64,
864    /// Slow-start threshold in bytes.
865    pub slow_start_threshold: u64,
866    /// Current congestion control phase.
867    pub state: CongestionState,
868    /// Cumulative number of ACK events processed.
869    pub total_acks: u64,
870    /// Cumulative number of loss/ECN events processed.
871    pub total_losses: u64,
872}
873
874impl WindowStats {
875    /// Returns the fraction of the total capacity represented by the current window.
876    ///
877    /// Returns `0.0` when both `current_window` and `slow_start_threshold` are zero.
878    pub fn utilization(&self) -> f64 {
879        let denom = self.current_window + self.slow_start_threshold;
880        if denom == 0 {
881            return 0.0;
882        }
883        self.current_window as f64 / denom as f64
884    }
885}
886
887/// Configuration parameters for a congestion controller (legacy).
888#[derive(Clone, Debug)]
889pub struct CongestionConfig {
890    /// Initial congestion window size (bytes). Default: 64 KB.
891    pub initial_window: u64,
892    /// Maximum congestion window size (bytes). Default: 16 MB.
893    pub max_window: u64,
894    /// Minimum congestion window size (bytes). Default: 1 MTU (1 448 B).
895    pub min_window: u64,
896    /// Initial slow-start threshold (bytes). Default: 1 MB.
897    pub slow_start_threshold: u64,
898}
899
900impl Default for CongestionConfig {
901    fn default() -> Self {
902        Self {
903            initial_window: 65_536,
904            max_window: 16_777_216,
905            min_window: 1_448,
906            slow_start_threshold: 1_048_576,
907        }
908    }
909}
910
911/// Per-peer CUBIC-inspired congestion controller (legacy).
912pub struct PeerCongestionController {
913    /// Identifier of the remote peer.
914    pub peer_id: String,
915    /// Current congestion window (bytes).
916    pub window: u64,
917    /// Slow-start threshold (bytes).
918    pub ssthresh: u64,
919    /// Current state of the congestion control state machine.
920    pub state: CongestionState,
921    /// Configuration parameters.
922    pub config: CongestionConfig,
923    /// Total number of ACK events received.
924    pub total_acks: u64,
925    /// Total number of loss/ECN events received.
926    pub total_losses: u64,
927}
928
929impl PeerCongestionController {
930    /// Create a new controller for `peer_id`, starting in [`CongestionState::SlowStart`].
931    pub fn new(peer_id: String, config: CongestionConfig) -> Self {
932        let window = config.initial_window;
933        let ssthresh = config.slow_start_threshold;
934        Self {
935            peer_id,
936            window,
937            ssthresh,
938            state: CongestionState::SlowStart,
939            config,
940            total_acks: 0,
941            total_losses: 0,
942        }
943    }
944
945    /// Process a network event and update window / state accordingly.
946    pub fn on_event(&mut self, event: CongestionEvent) {
947        match event {
948            CongestionEvent::AckReceived { bytes } => self.handle_ack(bytes),
949            CongestionEvent::PacketLoss => self.handle_packet_loss(),
950            CongestionEvent::Timeout => self.handle_timeout(),
951            CongestionEvent::EcnMark => self.handle_ecn(),
952        }
953    }
954
955    fn handle_ack(&mut self, bytes: u64) {
956        match self.state {
957            CongestionState::SlowStart => {
958                self.window = self
959                    .window
960                    .saturating_add(bytes)
961                    .min(self.config.max_window);
962                if self.window >= self.ssthresh {
963                    self.state = CongestionState::CongestionAvoidance;
964                }
965                self.total_acks += 1;
966            }
967            CongestionState::CongestionAvoidance => {
968                let increase = if self.window > 0 {
969                    (bytes.saturating_mul(bytes)).saturating_div(self.window)
970                } else {
971                    bytes
972                };
973                self.window = self
974                    .window
975                    .saturating_add(increase)
976                    .min(self.config.max_window);
977                self.total_acks += 1;
978            }
979            CongestionState::FastRecovery => {
980                self.window = self
981                    .window
982                    .saturating_add(bytes / 2)
983                    .min(self.config.max_window);
984                if self.window >= self.ssthresh {
985                    self.state = CongestionState::CongestionAvoidance;
986                }
987                self.total_acks += 1;
988            }
989        }
990    }
991
992    fn handle_packet_loss(&mut self) {
993        self.ssthresh = (self.window / 2).max(self.config.min_window);
994        self.window = self.ssthresh;
995        self.state = CongestionState::FastRecovery;
996        self.total_losses += 1;
997    }
998
999    fn handle_timeout(&mut self) {
1000        self.ssthresh = (self.window / 2).max(self.config.min_window);
1001        self.window = self.config.initial_window.max(self.config.min_window);
1002        self.state = CongestionState::SlowStart;
1003        self.total_losses += 1;
1004    }
1005
1006    fn handle_ecn(&mut self) {
1007        self.ssthresh = ((self.window * 7) / 8).max(self.config.min_window);
1008        self.window = self.ssthresh;
1009        self.state = CongestionState::CongestionAvoidance;
1010        self.total_losses += 1;
1011    }
1012
1013    /// Return a snapshot of the current window statistics.
1014    pub fn window_stats(&self) -> WindowStats {
1015        WindowStats {
1016            current_window: self.window,
1017            slow_start_threshold: self.ssthresh,
1018            state: self.state,
1019            total_acks: self.total_acks,
1020            total_losses: self.total_losses,
1021        }
1022    }
1023
1024    /// Returns `true` if the caller may send `bytes` given the current window.
1025    pub fn can_send(&self, bytes: u64) -> bool {
1026        bytes <= self.window
1027    }
1028}
1029
1030/// Manages [`PeerCongestionController`] instances for multiple peers (legacy).
1031pub struct MultiPeerCongestionManager {
1032    /// Per-peer controllers, keyed by peer ID string.
1033    pub controllers: HashMap<String, PeerCongestionController>,
1034    /// Default configuration applied to newly created controllers.
1035    pub config: CongestionConfig,
1036}
1037
1038impl MultiPeerCongestionManager {
1039    /// Create a new manager with the given default configuration.
1040    pub fn new(config: CongestionConfig) -> Self {
1041        Self {
1042            controllers: HashMap::new(),
1043            config,
1044        }
1045    }
1046
1047    /// Return a mutable reference to the controller for `peer_id`.
1048    pub fn get_or_create(&mut self, peer_id: &str) -> &mut PeerCongestionController {
1049        self.controllers
1050            .entry(peer_id.to_owned())
1051            .or_insert_with(|| {
1052                PeerCongestionController::new(peer_id.to_owned(), self.config.clone())
1053            })
1054    }
1055
1056    /// Deliver a congestion event to the controller for `peer_id`.
1057    pub fn on_event(&mut self, peer_id: &str, event: CongestionEvent) {
1058        let ctrl = self.get_or_create(peer_id);
1059        ctrl.on_event(event);
1060    }
1061
1062    /// Remove the controller for `peer_id`.
1063    pub fn remove_peer(&mut self, peer_id: &str) -> bool {
1064        self.controllers.remove(peer_id).is_some()
1065    }
1066
1067    /// Return the sum of all managed peer windows.
1068    pub fn total_window(&self) -> u64 {
1069        self.controllers.values().map(|c| c.window).sum()
1070    }
1071}
1072
1073// ═══════════════════════════════════════════════════════════════════════════════
1074// TESTS
1075// ═══════════════════════════════════════════════════════════════════════════════
1076
1077#[cfg(test)]
1078mod tests {
1079    use super::*;
1080
1081    // ── helpers ──────────────────────────────────────────────────────────────
1082
1083    fn default_config() -> CongestionConfig {
1084        CongestionConfig::default()
1085    }
1086
1087    fn legacy_ctrl(peer: &str) -> PeerCongestionController {
1088        PeerCongestionController::new(peer.to_owned(), default_config())
1089    }
1090
1091    fn make_ctrl(algo: CccAlgorithm) -> CongestionController {
1092        CongestionController::new(CccControllerConfig {
1093            algorithm: algo,
1094            ..CccControllerConfig::default()
1095        })
1096    }
1097
1098    // ══ LEGACY TESTS (1–24) ══════════════════════════════════════════════════
1099
1100    #[test]
1101    fn test_new_starts_in_slow_start() {
1102        let ctrl = legacy_ctrl("peer-a");
1103        assert_eq!(ctrl.state, CongestionState::SlowStart);
1104        assert_eq!(ctrl.window, 65_536);
1105        assert_eq!(ctrl.ssthresh, 1_048_576);
1106    }
1107
1108    #[test]
1109    fn test_slow_start_ack_grows_window() {
1110        let mut ctrl = legacy_ctrl("p");
1111        let before = ctrl.window;
1112        ctrl.on_event(CongestionEvent::AckReceived { bytes: 1_000 });
1113        assert_eq!(ctrl.window, before + 1_000);
1114    }
1115
1116    #[test]
1117    fn test_slow_start_caps_at_max() {
1118        let cfg = CongestionConfig {
1119            initial_window: 16_777_000,
1120            ..Default::default()
1121        };
1122        let mut ctrl = PeerCongestionController::new("p".into(), cfg.clone());
1123        ctrl.on_event(CongestionEvent::AckReceived { bytes: 1_000_000 });
1124        assert_eq!(ctrl.window, cfg.max_window);
1125    }
1126
1127    #[test]
1128    fn test_slow_start_transitions_at_ssthresh() {
1129        let cfg = CongestionConfig {
1130            initial_window: 500_000,
1131            slow_start_threshold: 600_000,
1132            ..Default::default()
1133        };
1134        let mut ctrl = PeerCongestionController::new("p".into(), cfg);
1135        ctrl.on_event(CongestionEvent::AckReceived { bytes: 200_000 });
1136        assert_eq!(ctrl.state, CongestionState::CongestionAvoidance);
1137    }
1138
1139    #[test]
1140    fn test_congestion_avoidance_increase_smaller() {
1141        let cfg = CongestionConfig {
1142            initial_window: 100_000,
1143            slow_start_threshold: 50_000,
1144            ..Default::default()
1145        };
1146        let mut ctrl = PeerCongestionController::new("p".into(), cfg);
1147        ctrl.state = CongestionState::CongestionAvoidance;
1148        let bytes: u64 = 1_000;
1149        let before = ctrl.window;
1150        ctrl.on_event(CongestionEvent::AckReceived { bytes });
1151        let ca_increase = ctrl.window - before;
1152        assert!(ca_increase < bytes);
1153        assert_eq!(ca_increase, (bytes * bytes) / 100_000);
1154    }
1155
1156    #[test]
1157    fn test_fast_recovery_ack_grows_half() {
1158        let mut ctrl = legacy_ctrl("p");
1159        ctrl.state = CongestionState::FastRecovery;
1160        ctrl.ssthresh = ctrl.window + 100_000;
1161        let before = ctrl.window;
1162        ctrl.on_event(CongestionEvent::AckReceived { bytes: 2_000 });
1163        assert_eq!(ctrl.window, before + 1_000);
1164    }
1165
1166    #[test]
1167    fn test_fast_recovery_transitions_to_ca() {
1168        let cfg = CongestionConfig {
1169            initial_window: 50_000,
1170            slow_start_threshold: 60_000,
1171            ..Default::default()
1172        };
1173        let mut ctrl = PeerCongestionController::new("p".into(), cfg);
1174        ctrl.state = CongestionState::FastRecovery;
1175        ctrl.ssthresh = 51_000;
1176        ctrl.on_event(CongestionEvent::AckReceived { bytes: 10_000 });
1177        assert_eq!(ctrl.state, CongestionState::CongestionAvoidance);
1178    }
1179
1180    #[test]
1181    fn test_packet_loss_sets_ssthresh_and_state() {
1182        let mut ctrl = legacy_ctrl("p");
1183        let orig_window = ctrl.window;
1184        ctrl.on_event(CongestionEvent::PacketLoss);
1185        assert_eq!(ctrl.ssthresh, orig_window / 2);
1186        assert_eq!(ctrl.window, orig_window / 2);
1187        assert_eq!(ctrl.state, CongestionState::FastRecovery);
1188    }
1189
1190    #[test]
1191    fn test_packet_loss_floors_at_min() {
1192        let cfg = CongestionConfig {
1193            initial_window: 2_000,
1194            min_window: 1_448,
1195            ..Default::default()
1196        };
1197        let mut ctrl = PeerCongestionController::new("p".into(), cfg.clone());
1198        ctrl.on_event(CongestionEvent::PacketLoss);
1199        assert_eq!(ctrl.window, cfg.min_window);
1200        assert_eq!(ctrl.ssthresh, cfg.min_window);
1201    }
1202
1203    #[test]
1204    fn test_timeout_resets_window_and_state() {
1205        let mut ctrl = legacy_ctrl("p");
1206        ctrl.window = 500_000;
1207        ctrl.on_event(CongestionEvent::Timeout);
1208        assert_eq!(ctrl.window, 65_536);
1209        assert_eq!(ctrl.state, CongestionState::SlowStart);
1210    }
1211
1212    #[test]
1213    fn test_ecn_mark() {
1214        let mut ctrl = legacy_ctrl("p");
1215        ctrl.window = 800_000;
1216        ctrl.on_event(CongestionEvent::EcnMark);
1217        assert_eq!(ctrl.ssthresh, (800_000u64 * 7) / 8);
1218        assert_eq!(ctrl.window, ctrl.ssthresh);
1219        assert_eq!(ctrl.state, CongestionState::CongestionAvoidance);
1220    }
1221
1222    #[test]
1223    fn test_total_acks_increments() {
1224        let mut ctrl = legacy_ctrl("p");
1225        assert_eq!(ctrl.total_acks, 0);
1226        ctrl.on_event(CongestionEvent::AckReceived { bytes: 100 });
1227        ctrl.on_event(CongestionEvent::AckReceived { bytes: 100 });
1228        assert_eq!(ctrl.total_acks, 2);
1229    }
1230
1231    #[test]
1232    fn test_total_losses_increments() {
1233        let mut ctrl = legacy_ctrl("p");
1234        assert_eq!(ctrl.total_losses, 0);
1235        ctrl.on_event(CongestionEvent::PacketLoss);
1236        ctrl.on_event(CongestionEvent::EcnMark);
1237        ctrl.on_event(CongestionEvent::Timeout);
1238        assert_eq!(ctrl.total_losses, 3);
1239    }
1240
1241    #[test]
1242    fn test_can_send_within_window() {
1243        let ctrl = legacy_ctrl("p");
1244        assert!(ctrl.can_send(ctrl.window));
1245        assert!(ctrl.can_send(1));
1246    }
1247
1248    #[test]
1249    fn test_can_send_exceeds_window() {
1250        let ctrl = legacy_ctrl("p");
1251        assert!(!ctrl.can_send(ctrl.window + 1));
1252    }
1253
1254    #[test]
1255    fn test_utilization() {
1256        let ctrl = legacy_ctrl("p");
1257        let stats = ctrl.window_stats();
1258        let expected = stats.current_window as f64
1259            / (stats.current_window + stats.slow_start_threshold) as f64;
1260        let diff = (stats.utilization() - expected).abs();
1261        assert!(diff < 1e-12);
1262    }
1263
1264    #[test]
1265    fn test_utilization_zero() {
1266        let stats = WindowStats {
1267            current_window: 0,
1268            slow_start_threshold: 0,
1269            state: CongestionState::SlowStart,
1270            total_acks: 0,
1271            total_losses: 0,
1272        };
1273        assert_eq!(stats.utilization(), 0.0);
1274    }
1275
1276    #[test]
1277    fn test_manager_creates_on_first_access() {
1278        let mut mgr = MultiPeerCongestionManager::new(default_config());
1279        let ctrl = mgr.get_or_create("peer-1");
1280        assert_eq!(ctrl.state, CongestionState::SlowStart);
1281        assert_eq!(ctrl.window, 65_536);
1282    }
1283
1284    #[test]
1285    fn test_manager_routes_event() {
1286        let mut mgr = MultiPeerCongestionManager::new(default_config());
1287        mgr.get_or_create("a");
1288        mgr.get_or_create("b");
1289        let initial_a = mgr.controllers["a"].window;
1290        let initial_b = mgr.controllers["b"].window;
1291        mgr.on_event("a", CongestionEvent::AckReceived { bytes: 5_000 });
1292        assert_eq!(mgr.controllers["a"].window, initial_a + 5_000);
1293        assert_eq!(mgr.controllers["b"].window, initial_b);
1294    }
1295
1296    #[test]
1297    fn test_remove_peer() {
1298        let mut mgr = MultiPeerCongestionManager::new(default_config());
1299        mgr.get_or_create("x");
1300        assert!(mgr.remove_peer("x"));
1301        assert!(!mgr.remove_peer("x"));
1302    }
1303
1304    #[test]
1305    fn test_total_window() {
1306        let mut mgr = MultiPeerCongestionManager::new(default_config());
1307        mgr.get_or_create("a");
1308        mgr.get_or_create("b");
1309        let expected = mgr.controllers["a"].window + mgr.controllers["b"].window;
1310        assert_eq!(mgr.total_window(), expected);
1311    }
1312
1313    #[test]
1314    fn test_ssthresh_floor_on_loss() {
1315        let cfg = CongestionConfig {
1316            initial_window: 1_500,
1317            min_window: 1_448,
1318            ..Default::default()
1319        };
1320        let mut ctrl = PeerCongestionController::new("p".into(), cfg.clone());
1321        ctrl.on_event(CongestionEvent::PacketLoss);
1322        assert!(ctrl.ssthresh >= cfg.min_window);
1323        assert!(ctrl.window >= cfg.min_window);
1324    }
1325
1326    #[test]
1327    fn test_state_machine_full_cycle() {
1328        let mut ctrl = legacy_ctrl("p");
1329        assert_eq!(ctrl.state, CongestionState::SlowStart);
1330        for _ in 0..20 {
1331            ctrl.on_event(CongestionEvent::AckReceived { bytes: 100_000 });
1332        }
1333        assert_eq!(ctrl.state, CongestionState::CongestionAvoidance);
1334        ctrl.on_event(CongestionEvent::PacketLoss);
1335        assert_eq!(ctrl.state, CongestionState::FastRecovery);
1336        for _ in 0..30 {
1337            ctrl.on_event(CongestionEvent::AckReceived { bytes: 100_000 });
1338        }
1339        assert_eq!(ctrl.state, CongestionState::CongestionAvoidance);
1340        ctrl.on_event(CongestionEvent::Timeout);
1341        assert_eq!(ctrl.state, CongestionState::SlowStart);
1342    }
1343
1344    #[test]
1345    fn test_ca_aimd_formula() {
1346        let cfg = CongestionConfig {
1347            initial_window: 200_000,
1348            slow_start_threshold: 100_000,
1349            ..Default::default()
1350        };
1351        let mut ctrl = PeerCongestionController::new("p".into(), cfg);
1352        ctrl.state = CongestionState::CongestionAvoidance;
1353        let window_before = ctrl.window;
1354        let bytes: u64 = 4_000;
1355        ctrl.on_event(CongestionEvent::AckReceived { bytes });
1356        let expected_increase = (bytes * bytes) / window_before;
1357        assert_eq!(ctrl.window, window_before + expected_increase);
1358    }
1359
1360    // ══ NEW API TESTS (25+) ══════════════════════════════════════════════════
1361
1362    // ── add / remove / reset connection ─────────────────────────────────────
1363
1364    #[test]
1365    fn test_add_connection_creates_entry() {
1366        let mut cc = make_ctrl(CccAlgorithm::Reno);
1367        cc.add_connection(1);
1368        assert!(cc.connection(1).is_some());
1369    }
1370
1371    #[test]
1372    fn test_add_connection_idempotent() {
1373        let mut cc = make_ctrl(CccAlgorithm::Reno);
1374        cc.add_connection(1);
1375        let cwnd1 = cc.connection(1).map(|c| c.cwnd);
1376        cc.add_connection(1); // should not reset
1377        let cwnd2 = cc.connection(1).map(|c| c.cwnd);
1378        assert_eq!(cwnd1, cwnd2);
1379    }
1380
1381    #[test]
1382    fn test_remove_connection_returns_true() {
1383        let mut cc = make_ctrl(CccAlgorithm::Reno);
1384        cc.add_connection(10);
1385        assert!(cc.remove_connection(10));
1386    }
1387
1388    #[test]
1389    fn test_remove_connection_missing_returns_false() {
1390        let mut cc = make_ctrl(CccAlgorithm::Reno);
1391        assert!(!cc.remove_connection(99));
1392    }
1393
1394    #[test]
1395    fn test_reset_connection_restores_defaults() {
1396        let mut cc = make_ctrl(CccAlgorithm::Reno);
1397        cc.add_connection(5);
1398        // Drive cwnd up.
1399        cc.on_ack(5, 500_000, 10.0).expect("ack ok");
1400        cc.reset_connection(5).expect("reset ok");
1401        let conn = cc.connection(5).expect("still exists");
1402        assert_eq!(conn.cwnd, 65_536);
1403        assert_eq!(conn.state, CccState::SlowStart);
1404    }
1405
1406    #[test]
1407    fn test_reset_connection_unknown_errors() {
1408        let mut cc = make_ctrl(CccAlgorithm::Reno);
1409        assert!(cc.reset_connection(999).is_err());
1410    }
1411
1412    // ── on_ack ───────────────────────────────────────────────────────────────
1413
1414    #[test]
1415    fn test_on_ack_unknown_errors() {
1416        let mut cc = make_ctrl(CccAlgorithm::Reno);
1417        assert!(cc.on_ack(42, 1_000, 10.0).is_err());
1418    }
1419
1420    #[test]
1421    fn test_reno_slow_start_ack_increases_cwnd() {
1422        let mut cc = make_ctrl(CccAlgorithm::Reno);
1423        cc.add_connection(1);
1424        let before = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1425        cc.on_ack(1, 1_000, 20.0).expect("ok");
1426        let after = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1427        assert!(after > before);
1428    }
1429
1430    #[test]
1431    fn test_reno_transitions_to_ca_after_ssthresh() {
1432        let mut cc = CongestionController::new(CccControllerConfig {
1433            algorithm: CccAlgorithm::Reno,
1434            initial_cwnd: 900_000,
1435            ssthresh: 1_000_000,
1436            ..Default::default()
1437        });
1438        cc.add_connection(1);
1439        cc.on_ack(1, 200_000, 10.0).expect("ok");
1440        let state = cc
1441            .connection(1)
1442            .map(|c| c.state)
1443            .expect("test: connection 1 should exist after add_connection");
1444        assert_eq!(state, CccState::CongestionAvoidance);
1445    }
1446
1447    #[test]
1448    fn test_reno_ca_smaller_increase_than_ss() {
1449        let mut cc = CongestionController::new(CccControllerConfig {
1450            algorithm: CccAlgorithm::Reno,
1451            initial_cwnd: 200_000,
1452            ssthresh: 100_000,
1453            ..Default::default()
1454        });
1455        cc.add_connection(1);
1456        // Force into CA.
1457        if let Some(c) = cc.connections.get_mut(&1) {
1458            c.state = CccState::CongestionAvoidance;
1459        }
1460        let before = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1461        cc.on_ack(1, 5_000, 10.0).expect("ok");
1462        let after = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1463        // In CA the increase should be much smaller than 5_000.
1464        let delta = after - before;
1465        assert!(delta < 5_000, "delta={delta}");
1466    }
1467
1468    #[test]
1469    fn test_reno_cwnd_capped_at_max() {
1470        let mut cc = CongestionController::new(CccControllerConfig {
1471            algorithm: CccAlgorithm::Reno,
1472            initial_cwnd: 16_776_000,
1473            max_cwnd: 16_777_216,
1474            ..Default::default()
1475        });
1476        cc.add_connection(1);
1477        cc.on_ack(1, 500_000, 10.0).expect("ok");
1478        let cwnd = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1479        assert!(cwnd <= 16_777_216);
1480    }
1481
1482    // ── on_loss ──────────────────────────────────────────────────────────────
1483
1484    #[test]
1485    fn test_on_loss_unknown_errors() {
1486        let mut cc = make_ctrl(CccAlgorithm::Reno);
1487        assert!(cc.on_loss(42, 1_000).is_err());
1488    }
1489
1490    #[test]
1491    fn test_reno_on_loss_halves_cwnd() {
1492        let mut cc = make_ctrl(CccAlgorithm::Reno);
1493        cc.add_connection(1);
1494        let before = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1495        cc.on_loss(1, 1_000).expect("ok");
1496        let after = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1497        assert!(after <= before / 2 + 1); // allow for min_cwnd floor
1498    }
1499
1500    #[test]
1501    fn test_on_loss_enters_fast_recovery() {
1502        let mut cc = make_ctrl(CccAlgorithm::Reno);
1503        cc.add_connection(1);
1504        cc.on_loss(1, 100).expect("ok");
1505        assert_eq!(
1506            cc.connection(1).map(|c| c.state),
1507            Some(CccState::FastRecovery)
1508        );
1509    }
1510
1511    #[test]
1512    fn test_on_loss_increments_total_losses() {
1513        let mut cc = make_ctrl(CccAlgorithm::Reno);
1514        cc.add_connection(1);
1515        cc.on_loss(1, 100).expect("ok");
1516        cc.on_loss(1, 100).expect("ok");
1517        assert_eq!(cc.controller_stats().total_losses, 2);
1518    }
1519
1520    #[test]
1521    fn test_on_loss_cwnd_never_below_min() {
1522        let mut cc = CongestionController::new(CccControllerConfig {
1523            algorithm: CccAlgorithm::Reno,
1524            initial_cwnd: 1_448,
1525            min_cwnd: 1_448,
1526            ..Default::default()
1527        });
1528        cc.add_connection(1);
1529        cc.on_loss(1, 100).expect("ok");
1530        let cwnd = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1531        assert!(cwnd >= 1_448);
1532    }
1533
1534    // ── on_timeout ───────────────────────────────────────────────────────────
1535
1536    #[test]
1537    fn test_on_timeout_unknown_errors() {
1538        let mut cc = make_ctrl(CccAlgorithm::Reno);
1539        assert!(cc.on_timeout(42).is_err());
1540    }
1541
1542    #[test]
1543    fn test_on_timeout_resets_to_slow_start() {
1544        let mut cc = make_ctrl(CccAlgorithm::Reno);
1545        cc.add_connection(1);
1546        cc.on_ack(1, 500_000, 10.0).expect("ok");
1547        cc.on_timeout(1).expect("ok");
1548        assert_eq!(cc.connection(1).map(|c| c.state), Some(CccState::SlowStart));
1549    }
1550
1551    #[test]
1552    fn test_on_timeout_resets_cwnd_to_min() {
1553        let mut cc = make_ctrl(CccAlgorithm::Reno);
1554        cc.add_connection(1);
1555        cc.on_timeout(1).expect("ok");
1556        let cwnd = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1557        assert_eq!(cwnd, 1_448); // min_cwnd
1558    }
1559
1560    #[test]
1561    fn test_on_timeout_halves_ssthresh() {
1562        let mut cc = make_ctrl(CccAlgorithm::Reno);
1563        cc.add_connection(1);
1564        let before_ssthresh = cc.connection(1).map(|c| c.ssthresh).unwrap_or(0);
1565        let before_cwnd = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1566        cc.on_timeout(1).expect("ok");
1567        let after_ssthresh = cc.connection(1).map(|c| c.ssthresh).unwrap_or(0);
1568        assert!(after_ssthresh <= (before_cwnd / 2).max(1_448));
1569        // ssthresh must be <= what it was (initial cwnd is < initial ssthresh).
1570        assert!(after_ssthresh <= before_ssthresh);
1571    }
1572
1573    #[test]
1574    fn test_on_timeout_increments_total_losses() {
1575        let mut cc = make_ctrl(CccAlgorithm::Reno);
1576        cc.add_connection(1);
1577        cc.on_timeout(1).expect("ok");
1578        assert_eq!(cc.controller_stats().total_losses, 1);
1579    }
1580
1581    // ── sending_rate ─────────────────────────────────────────────────────────
1582
1583    #[test]
1584    fn test_sending_rate_none_before_rtt() {
1585        let mut cc = make_ctrl(CccAlgorithm::Reno);
1586        cc.add_connection(1);
1587        assert_eq!(cc.sending_rate(1), None);
1588    }
1589
1590    #[test]
1591    fn test_sending_rate_some_after_ack() {
1592        let mut cc = make_ctrl(CccAlgorithm::Reno);
1593        cc.add_connection(1);
1594        cc.on_ack(1, 1_000, 20.0).expect("ok");
1595        assert!(cc.sending_rate(1).is_some());
1596    }
1597
1598    #[test]
1599    fn test_sending_rate_unknown_none() {
1600        let cc = make_ctrl(CccAlgorithm::Reno);
1601        assert_eq!(cc.sending_rate(999), None);
1602    }
1603
1604    #[test]
1605    fn test_sending_rate_positive() {
1606        let mut cc = make_ctrl(CccAlgorithm::Reno);
1607        cc.add_connection(1);
1608        cc.on_ack(1, 10_000, 100.0).expect("ok");
1609        let rate = cc.sending_rate(1).unwrap_or(0.0);
1610        assert!(rate > 0.0);
1611    }
1612
1613    // ── controller_stats ─────────────────────────────────────────────────────
1614
1615    #[test]
1616    fn test_stats_empty_controller() {
1617        let cc = make_ctrl(CccAlgorithm::Reno);
1618        let stats = cc.controller_stats();
1619        assert_eq!(stats.active_connections, 0);
1620        assert_eq!(stats.total_acks, 0);
1621        assert_eq!(stats.total_losses, 0);
1622        assert_eq!(stats.avg_cwnd, 0.0);
1623        assert_eq!(stats.avg_rtt, 0.0);
1624    }
1625
1626    #[test]
1627    fn test_stats_counts_connections() {
1628        let mut cc = make_ctrl(CccAlgorithm::Reno);
1629        cc.add_connection(1);
1630        cc.add_connection(2);
1631        assert_eq!(cc.controller_stats().active_connections, 2);
1632    }
1633
1634    #[test]
1635    fn test_stats_total_acks_aggregated() {
1636        let mut cc = make_ctrl(CccAlgorithm::Reno);
1637        cc.add_connection(1);
1638        cc.add_connection(2);
1639        cc.on_ack(1, 1_000, 10.0).expect("ok");
1640        cc.on_ack(2, 1_000, 10.0).expect("ok");
1641        assert_eq!(cc.controller_stats().total_acks, 2);
1642    }
1643
1644    #[test]
1645    fn test_stats_avg_cwnd_reasonable() {
1646        let mut cc = make_ctrl(CccAlgorithm::Reno);
1647        cc.add_connection(1);
1648        let stats = cc.controller_stats();
1649        assert!(stats.avg_cwnd > 0.0);
1650    }
1651
1652    // ── event log ────────────────────────────────────────────────────────────
1653
1654    #[test]
1655    fn test_events_populated_on_ack() {
1656        let mut cc = make_ctrl(CccAlgorithm::Reno);
1657        cc.add_connection(1);
1658        cc.on_ack(1, 1_000, 10.0).expect("ok");
1659        assert!(!cc.events().is_empty());
1660    }
1661
1662    #[test]
1663    fn test_events_populated_on_loss() {
1664        let mut cc = make_ctrl(CccAlgorithm::Reno);
1665        cc.add_connection(1);
1666        cc.on_loss(1, 100).expect("ok");
1667        let has_loss = cc
1668            .events()
1669            .iter()
1670            .any(|e| e.event_type == CccEventType::PacketLost);
1671        assert!(has_loss);
1672    }
1673
1674    #[test]
1675    fn test_events_bounded_at_1000() {
1676        let mut cc = make_ctrl(CccAlgorithm::Reno);
1677        cc.add_connection(1);
1678        for _ in 0..1_100u32 {
1679            cc.on_ack(1, 100, 5.0).expect("ok");
1680        }
1681        assert!(cc.events().len() <= 1_000);
1682    }
1683
1684    #[test]
1685    fn test_events_timeout_type() {
1686        let mut cc = make_ctrl(CccAlgorithm::Reno);
1687        cc.add_connection(1);
1688        cc.on_timeout(1).expect("ok");
1689        let has_timeout = cc
1690            .events()
1691            .iter()
1692            .any(|e| e.event_type == CccEventType::Timeout);
1693        assert!(has_timeout);
1694    }
1695
1696    #[test]
1697    fn test_events_record_cwnd_change() {
1698        let mut cc = make_ctrl(CccAlgorithm::Reno);
1699        cc.add_connection(1);
1700        cc.on_ack(1, 10_000, 10.0).expect("ok");
1701        let event = cc.events().iter().last().expect("event");
1702        // cwnd_before and cwnd_after are both valid u64 values.
1703        assert!(event.cwnd_after > 0);
1704    }
1705
1706    // ── Cubic algorithm ──────────────────────────────────────────────────────
1707
1708    #[test]
1709    fn test_cubic_slow_start_grows() {
1710        let mut cc = make_ctrl(CccAlgorithm::Cubic);
1711        cc.add_connection(1);
1712        let before = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1713        cc.on_ack(1, 10_000, 20.0).expect("ok");
1714        let after = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1715        assert!(after > before);
1716    }
1717
1718    #[test]
1719    fn test_cubic_loss_reduces_cwnd() {
1720        let mut cc = make_ctrl(CccAlgorithm::Cubic);
1721        cc.add_connection(1);
1722        let before = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1723        cc.on_loss(1, 1_000).expect("ok");
1724        let after = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1725        assert!(after <= before);
1726    }
1727
1728    #[test]
1729    fn test_cubic_timeout_resets_to_slowstart() {
1730        let mut cc = make_ctrl(CccAlgorithm::Cubic);
1731        cc.add_connection(1);
1732        cc.on_timeout(1).expect("ok");
1733        assert_eq!(cc.connection(1).map(|c| c.state), Some(CccState::SlowStart));
1734    }
1735
1736    // ── BBR algorithm ────────────────────────────────────────────────────────
1737
1738    #[test]
1739    fn test_bbr_grows_cwnd() {
1740        let mut cc = make_ctrl(CccAlgorithm::Bbr);
1741        cc.add_connection(1);
1742        let before = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1743        cc.on_ack(1, 10_000, 20.0).expect("ok");
1744        let after = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1745        assert!(after >= before);
1746    }
1747
1748    #[test]
1749    fn test_bbr_loss_mild_reduction() {
1750        let mut cc = make_ctrl(CccAlgorithm::Bbr);
1751        cc.add_connection(1);
1752        let before = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1753        cc.on_loss(1, 1_000).expect("ok");
1754        let after = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1755        // BBR does a mild 10% reduction, so after should be > 0 and <= before.
1756        assert!(after > 0 && after <= before);
1757    }
1758
1759    #[test]
1760    fn test_bbr_has_sending_rate_after_ack() {
1761        let mut cc = make_ctrl(CccAlgorithm::Bbr);
1762        cc.add_connection(1);
1763        cc.on_ack(1, 50_000, 15.0).expect("ok");
1764        assert!(cc.sending_rate(1).is_some());
1765    }
1766
1767    // ── Vegas algorithm ──────────────────────────────────────────────────────
1768
1769    #[test]
1770    fn test_vegas_grows_cwnd_without_rtt() {
1771        let mut cc = make_ctrl(CccAlgorithm::Vegas);
1772        cc.add_connection(1);
1773        let before = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1774        cc.on_ack(1, 1_448, 0.0).expect("ok");
1775        let after = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1776        assert!(after >= before);
1777    }
1778
1779    #[test]
1780    fn test_vegas_loss_reduces_cwnd() {
1781        let mut cc = make_ctrl(CccAlgorithm::Vegas);
1782        cc.add_connection(1);
1783        let before = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1784        cc.on_loss(1, 1_000).expect("ok");
1785        let after = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1786        assert!(after <= before);
1787    }
1788
1789    // ── Westwood algorithm ───────────────────────────────────────────────────
1790
1791    #[test]
1792    fn test_westwood_slow_start_grows() {
1793        let mut cc = make_ctrl(CccAlgorithm::Westwood);
1794        cc.add_connection(1);
1795        let before = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1796        cc.on_ack(1, 5_000, 10.0).expect("ok");
1797        let after = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1798        assert!(after > before);
1799    }
1800
1801    #[test]
1802    fn test_westwood_loss_uses_bw_estimate() {
1803        let mut cc = make_ctrl(CccAlgorithm::Westwood);
1804        cc.add_connection(1);
1805        // Give it some bandwidth history.
1806        cc.on_ack(1, 50_000, 20.0).expect("ok");
1807        cc.on_loss(1, 1_000).expect("ok");
1808        // After loss with a BW estimate, ssthresh should be the BDP.
1809        let ssthresh = cc.connection(1).map(|c| c.ssthresh).unwrap_or(0);
1810        assert!(ssthresh >= 1_448);
1811    }
1812
1813    // ── xorshift64 ────────────────────────────────────────────────────────────
1814
1815    #[test]
1816    fn test_xorshift64_produces_nonzero() {
1817        let mut state: u64 = 12345;
1818        let v = xorshift64(&mut state);
1819        assert_ne!(v, 0);
1820    }
1821
1822    #[test]
1823    fn test_xorshift64_changes_state() {
1824        let mut state: u64 = 9999;
1825        let v1 = xorshift64(&mut state);
1826        let v2 = xorshift64(&mut state);
1827        assert_ne!(v1, v2);
1828    }
1829
1830    #[test]
1831    fn test_xorshift64_deterministic() {
1832        let mut s1: u64 = 42;
1833        let mut s2: u64 = 42;
1834        assert_eq!(xorshift64(&mut s1), xorshift64(&mut s2));
1835    }
1836
1837    // ── Decision struct ──────────────────────────────────────────────────────
1838
1839    #[test]
1840    fn test_decision_new_cwnd_matches_connection() {
1841        let mut cc = make_ctrl(CccAlgorithm::Reno);
1842        cc.add_connection(1);
1843        let d = cc.on_ack(1, 1_000, 10.0).expect("ok");
1844        let actual = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1845        assert_eq!(d.new_cwnd, actual);
1846    }
1847
1848    #[test]
1849    fn test_decision_state_matches_connection() {
1850        let mut cc = make_ctrl(CccAlgorithm::Reno);
1851        cc.add_connection(1);
1852        let d = cc.on_loss(1, 100).expect("ok");
1853        assert_eq!(d.new_state, CccState::FastRecovery);
1854    }
1855
1856    #[test]
1857    fn test_decision_sending_rate_consistency() {
1858        let mut cc = make_ctrl(CccAlgorithm::Reno);
1859        cc.add_connection(1);
1860        let d = cc.on_ack(1, 1_000, 50.0).expect("ok");
1861        // Decision sending_rate should match controller's sending_rate.
1862        assert_eq!(d.sending_rate, cc.sending_rate(1));
1863    }
1864
1865    // ── RTT tracking ─────────────────────────────────────────────────────────
1866
1867    #[test]
1868    fn test_rtt_updated_on_ack() {
1869        let mut cc = make_ctrl(CccAlgorithm::Reno);
1870        cc.add_connection(1);
1871        cc.on_ack(1, 1_000, 30.0).expect("ok");
1872        let rtt = cc.connection(1).map(|c| c.rtt_ms).unwrap_or(0.0);
1873        assert!(rtt > 0.0);
1874    }
1875
1876    #[test]
1877    fn test_rtt_ewma_converges() {
1878        let mut cc = make_ctrl(CccAlgorithm::Reno);
1879        cc.add_connection(1);
1880        for _ in 0..20 {
1881            cc.on_ack(1, 1_000, 100.0).expect("ok");
1882        }
1883        let rtt = cc.connection(1).map(|c| c.rtt_ms).unwrap_or(0.0);
1884        // After 20 samples of 100ms, EWMA should be close to 100ms.
1885        assert!((rtt - 100.0).abs() < 20.0, "rtt={rtt}");
1886    }
1887
1888    // ── bytes_acked / bytes_lost accounting ───────────────────────────────────
1889
1890    #[test]
1891    fn test_bytes_acked_accumulates() {
1892        let mut cc = make_ctrl(CccAlgorithm::Reno);
1893        cc.add_connection(1);
1894        cc.on_ack(1, 5_000, 10.0).expect("ok");
1895        cc.on_ack(1, 3_000, 10.0).expect("ok");
1896        let ba = cc.connection(1).map(|c| c.bytes_acked).unwrap_or(0);
1897        assert_eq!(ba, 8_000);
1898    }
1899
1900    #[test]
1901    fn test_bytes_lost_accumulates() {
1902        let mut cc = make_ctrl(CccAlgorithm::Reno);
1903        cc.add_connection(1);
1904        cc.on_loss(1, 1_000).expect("ok");
1905        cc.on_loss(1, 500).expect("ok");
1906        let bl = cc.connection(1).map(|c| c.bytes_lost).unwrap_or(0);
1907        assert_eq!(bl, 1_500);
1908    }
1909
1910    // ── min_cwnd floor guarantee ──────────────────────────────────────────────
1911
1912    #[test]
1913    fn test_cwnd_never_below_min_after_many_losses() {
1914        let mut cc = make_ctrl(CccAlgorithm::Reno);
1915        cc.add_connection(1);
1916        for _ in 0..50 {
1917            cc.on_loss(1, 100_000).expect("ok");
1918        }
1919        let cwnd = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1920        assert!(cwnd >= 1_448);
1921    }
1922
1923    // ── max_cwnd ceiling guarantee ────────────────────────────────────────────
1924
1925    #[test]
1926    fn test_cwnd_never_above_max_after_many_acks() {
1927        let mut cc = make_ctrl(CccAlgorithm::Reno);
1928        cc.add_connection(1);
1929        for _ in 0..5_000u32 {
1930            cc.on_ack(1, 100_000, 5.0).expect("ok");
1931        }
1932        let cwnd = cc.connection(1).map(|c| c.cwnd).unwrap_or(0);
1933        assert!(cwnd <= 16_777_216);
1934    }
1935
1936    // ── multi-connection isolation ────────────────────────────────────────────
1937
1938    #[test]
1939    fn test_connections_isolated() {
1940        let mut cc = make_ctrl(CccAlgorithm::Reno);
1941        cc.add_connection(1);
1942        cc.add_connection(2);
1943        let before_2 = cc.connection(2).map(|c| c.cwnd).unwrap_or(0);
1944        cc.on_loss(1, 10_000).expect("ok");
1945        let after_2 = cc.connection(2).map(|c| c.cwnd).unwrap_or(0);
1946        assert_eq!(before_2, after_2);
1947    }
1948
1949    // ── CccAlgorithm default ──────────────────────────────────────────────────
1950
1951    #[test]
1952    fn test_algorithm_default_is_reno() {
1953        assert_eq!(CccAlgorithm::default(), CccAlgorithm::Reno);
1954    }
1955
1956    // ── CccState default ─────────────────────────────────────────────────────
1957
1958    #[test]
1959    fn test_state_default_is_ca() {
1960        assert_eq!(CccState::default(), CccState::CongestionAvoidance);
1961    }
1962
1963    // ── type alias usability ──────────────────────────────────────────────────
1964
1965    #[test]
1966    fn test_type_aliases_usable() {
1967        let mut cc: CccCongestionController = CccCongestionController::with_defaults();
1968        cc.add_connection(1);
1969        let d: CccDecision = cc.on_ack(1, 1_000, 10.0).expect("ok");
1970        assert!(d.new_cwnd > 0);
1971    }
1972}