// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use crate::time::Timestamp;

//= https://www.rfc-editor.org/rfc/rfc9002#section-7.3.2
//# If the congestion window is reduced immediately, a
//# single packet can be sent prior to reduction.  This speeds up loss
//# recovery if the data in the lost packet is retransmitted and is
//# similar to TCP as described in Section 5 of [RFC6675].
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum FastRetransmission {
    Idle,
    RequiresTransmission,
}

/// Tracks the state of recovery for BBR
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum State {
    /// Not currently in recovery
    Recovered,
    /// Recovering
    ///
    /// If a packet sent after the `Timestamp` is acknowledged, recovery is exited
    /// `FastRetransmission` allows for one packet to be transmitted after entering recovery.
    Recovering(Timestamp, FastRetransmission),
}

impl State {
    /// True if a single packet may be transmitted despite a cwnd constraint
    #[inline]
    pub fn requires_fast_retransmission(&self) -> bool {
        matches!(
            self,
            State::Recovering(_, FastRetransmission::RequiresTransmission)
        )
    }

    /// Called when a packet is transmitted
    #[inline]
    pub fn on_packet_sent(&mut self) {
        if let State::Recovering(_, transmission @ FastRetransmission::RequiresTransmission) = self
        {
            // A packet has been sent since we entered recovery (fast retransmission)
            // so flip the state back to idle.
            *transmission = FastRetransmission::Idle;
        }
    }

    /// Called on each ack
    ///
    /// Returns `true` if the ack caused recovery to be exited
    #[inline]
    pub fn on_ack(&mut self, time_sent: Timestamp) -> bool {
        match self {
            // Check if this ack causes the controller to exit recovery
            State::Recovering(recovery_start_time, _) => {
                if time_sent > *recovery_start_time {
                    //= https://www.rfc-editor.org/rfc/rfc9002#section-7.3.2
                    //# A recovery period ends and the sender enters congestion avoidance
                    //# when a packet sent during the recovery period is acknowledged.
                    *self = State::Recovered;
                    return true;
                }
            }
            State::Recovered => {}
        }

        false
    }

    /// Called when a congestion event occurs (packet loss or ECN CE count increase)
    ///
    /// Returns `true` if the congestion event caused recovery to be entered
    #[inline]
    pub fn on_congestion_event(&mut self, now: Timestamp) -> bool {
        match self {
            State::Recovered => {
                //= https://www.rfc-editor.org/rfc/rfc9002#section-7.3.2
                //# If the congestion window is reduced immediately, a
                //# single packet can be sent prior to reduction.  This speeds up loss
                //# recovery if the data in the lost packet is retransmitted and is
                //# similar to TCP as described in Section 5 of [RFC6675].
                *self = State::Recovering(now, FastRetransmission::RequiresTransmission);
                true
            }
            _ => false,
        }
    }

    #[inline]
    pub fn on_packet_discarded(&mut self) {
        if let State::Recovering(_, transmission @ FastRetransmission::RequiresTransmission) = self
        {
            // If any of the discarded packets were lost, they will no longer be retransmitted
            // so flip the Recovery status back to Idle so it is not waiting for a
            // retransmission that may never come.
            *transmission = FastRetransmission::Idle;
        }
    }

    /// True if currently in recovery
    #[cfg(test)]
    pub fn in_recovery(&self) -> bool {
        *self != State::Recovered
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::time::{Clock as _, NoopClock};
    use std::time::Duration;

    #[test]
    fn recovered() {
        let state = State::Recovered;

        assert!(!state.in_recovery());
        assert!(!state.requires_fast_retransmission());
    }

    #[test]
    fn in_recovery() {
        let now = NoopClock.get_time();
        let state = State::Recovering(now, FastRetransmission::RequiresTransmission);

        assert!(state.in_recovery());
        assert!(state.requires_fast_retransmission());
    }

    #[test]
    fn state_transitions() {
        let now = NoopClock.get_time() + Duration::from_secs(10);
        let mut state = State::Recovered;

        // Acking a packet while Recovered does not change the state
        assert!(!state.on_ack(now));
        assert_eq!(state, State::Recovered);

        // Congestion event moves Recovered to InRecovery
        assert!(state.on_congestion_event(now));
        assert_eq!(
            state,
            State::Recovering(now, FastRetransmission::RequiresTransmission)
        );
        assert!(state.requires_fast_retransmission());

        // Sending a packet moves FastRetransmission to Idle
        state.on_packet_sent();
        assert!(!state.requires_fast_retransmission());

        // Ack for a packet sent before the recovery start time does not exit recovery
        let sent_time = now - Duration::from_secs(1);
        assert!(!state.on_ack(sent_time));
        assert_eq!(state, State::Recovering(now, FastRetransmission::Idle));

        // Ack for a packet sent after the recovery start time exits recovery
        let sent_time = now + Duration::from_secs(1);
        assert!(state.on_ack(sent_time));
        assert_eq!(state, State::Recovered);

        // Discarded packet sets FastRetransmission back to Idle
        let mut state = State::Recovering(now, FastRetransmission::RequiresTransmission);
        state.on_packet_discarded();
        assert_eq!(state, State::Recovering(now, FastRetransmission::Idle));
    }
}


// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use crate::recovery::bandwidth::PacketInfo;

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.5.1
//# Several aspects of the BBR algorithm depend on counting the progress of "packet-timed" round
//# trips, which start at the transmission of some segment, and then end at the acknowledgement
//# of that segment. BBR.round_count is a count of the number of these "packet-timed" round trips
//# elapsed so far.
#[derive(Clone, Debug, Default)]
pub(crate) struct Counter {
    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.5.1
    //# BBRInitRoundCounting():
    //#   BBR.next_round_delivered = 0
    //#   BBR.round_start = false
    //#   BBR.round_count = 0
    /// The `delivered_bytes` at which the next round begins
    next_round_delivered_bytes: u64,
    /// True if the current ack being processed started a new round
    round_start: bool,
    /// The number of rounds counted since initialization
    round_count: u64,
}

impl Counter {
    /// Called for each acknowledgement of one or more packets
    pub fn on_ack(&mut self, packet_info: PacketInfo, delivered_bytes: u64) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.5.1
        //# BBRUpdateRound():
        //#   if (packet.delivered >= BBR.next_round_delivered)
        //#     BBRStartRound()
        //#     BBR.round_count++
        //#     BBR.rounds_since_probe++
        //#     BBR.round_start = true
        //#   else
        //#     BBR.round_start = false
        if packet_info.delivered_bytes >= self.next_round_delivered_bytes {
            self.set_round_end(delivered_bytes);
            self.round_count += 1;
            self.round_start = true;
        } else {
            self.round_start = false;
        }
    }

    /// Sets the end of the current round to the given `delivered_bytes`
    pub fn set_round_end(&mut self, delivered_bytes: u64) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.5.1
        //# BBRStartRound():
        //#   BBR.next_round_delivered = C.delivered

        debug_assert!(
            delivered_bytes >= self.next_round_delivered_bytes,
            "The end of the round can only be extended, not shortened"
        );
        self.next_round_delivered_bytes = delivered_bytes;
    }

    /// True if the latest acknowledgement started a new round, false otherwise
    pub fn round_start(&self) -> bool {
        self.round_start
    }

    /// The number of rounds counted since initialization
    pub fn round_count(&self) -> u64 {
        self.round_count
    }

    #[cfg(test)]
    pub fn round_end(&self) -> u64 {
        self.next_round_delivered_bytes
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::time::{Clock, NoopClock};

    #[test]
    fn counter() {
        let mut counter = Counter::default();

        assert!(!counter.round_start());
        assert_eq!(0, counter.round_count());

        let now = NoopClock.get_time();
        let mut packet_info = PacketInfo {
            delivered_bytes: 0,
            delivered_time: now,
            lost_bytes: 0,
            ecn_ce_count: 0,
            first_sent_time: now,
            bytes_in_flight: 0,
            is_app_limited: false,
        };

        let round_end_at = 100;
        counter.on_ack(packet_info, round_end_at);

        assert!(counter.round_start());
        assert_eq!(1, counter.round_count());

        let mut delivered_bytes = round_end_at;

        for i in 0..round_end_at {
            packet_info.delivered_bytes = i;
            delivered_bytes += packet_info.delivered_bytes;
            counter.on_ack(packet_info, delivered_bytes);

            // No new round started since we haven't reach the round end
            assert!(!counter.round_start());
            assert_eq!(1, counter.round_count());
        }

        // Now we have reached the end of the round
        packet_info.delivered_bytes = round_end_at;
        delivered_bytes += round_end_at;

        counter.on_ack(packet_info, delivered_bytes);

        assert!(counter.round_start());
        assert_eq!(2, counter.round_count());
    }

    #[test]
    fn set_round_end() {
        let mut counter = Counter::default();

        assert!(!counter.round_start());
        assert_eq!(0, counter.round_count());

        let now = NoopClock.get_time();
        let mut packet_info = PacketInfo {
            delivered_bytes: 0,
            delivered_time: now,
            lost_bytes: 0,
            ecn_ce_count: 0,
            first_sent_time: now,
            bytes_in_flight: 0,
            is_app_limited: false,
        };

        let round_end_at = 100;
        counter.on_ack(packet_info, round_end_at);

        assert!(counter.round_start());
        assert_eq!(1, counter.round_count());

        packet_info.delivered_bytes += 1;

        counter.on_ack(packet_info, round_end_at);

        assert!(!counter.round_start());
        assert_eq!(1, counter.round_count());

        let new_round_end = round_end_at + 1;
        counter.set_round_end(new_round_end);

        // `set_round_end` does not start a new round
        assert!(!counter.round_start());
        assert_eq!(1, counter.round_count());

        packet_info.delivered_bytes = round_end_at;
        counter.on_ack(packet_info, round_end_at);

        // Since the end of the round has been updated, a new round is not started
        // when the original round end has been reached
        assert!(!counter.round_start());
        assert_eq!(1, counter.round_count());

        packet_info.delivered_bytes = new_round_end;
        counter.on_ack(packet_info, new_round_end + 100);

        // We've reached the new round end, so a new round is started
        assert!(counter.round_start());
        assert_eq!(2, counter.round_count());
    }
}


// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use crate::{
    event::builder::SlowStartExitCause,
    recovery::{
        bbr::{BbrCongestionController, State},
        congestion_controller::Publisher,
    },
};
use num_rational::Ratio;

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.6
//# A constant specifying the minimum gain value for calculating the pacing rate that will
//# allow the sending rate to double each round (4*ln(2) ~= 2.77)
pub(crate) const PACING_GAIN: Ratio<u64> = Ratio::new_raw(277, 100);

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.6
//# A constant specifying the minimum gain value for calculating the
//# cwnd that will allow the sending rate to double each round (2.0)
pub(crate) const CWND_GAIN: Ratio<u64> = Ratio::new_raw(2, 1);

/// Methods related to the Startup state
impl BbrCongestionController {
    /// Enter the `Startup` state
    #[inline]
    pub(super) fn enter_startup<Pub: Publisher>(&mut self, publisher: &mut Pub) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.1.1
        //# BBREnterStartup():
        //#   BBR.state = Startup
        //#   BBR.pacing_gain = BBRStartupPacingGain
        //#   BBR.cwnd_gain = BBRStartupCwndGain

        // pacing_gain and cwnd_gain are managed with the State enum

        // New BBR state requires updating the model
        self.try_fast_path = false;
        self.state.transition_to(State::Startup, publisher);
    }

    /// Checks if the `Startup` state is done and enters `Drain` if so
    #[inline]
    pub(super) fn check_startup_done<Pub: Publisher>(&mut self, publisher: &mut Pub) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.1.1
        //# BBRCheckStartupDone():
        //#   BBRCheckStartupFullBandwidth()
        //#   BBRCheckStartupHighLoss()
        //#   if (BBR.state == Startup and BBR.filled_pipe)
        //#     BBREnterDrain()
        if self.round_counter.round_start() {
            self.full_pipe_estimator.on_round_start(
                self.bw_estimator.rate_sample(),
                self.data_rate_model.max_bw(),
                self.ecn_state.is_ce_too_high_in_round(),
            );
        }

        if self.congestion_state.loss_round_start() {
            // Excessive inflight is checked at the end of a loss round, not a regular round, as done
            // in tcp_bbr2.c/bbr2_check_loss_too_high_in_startup
            //
            // See https://github.com/google/bbr/blob/1a45fd4faf30229a3d3116de7bfe9d2f933d3562/net/ipv4/tcp_bbr2.c#L2133
            self.full_pipe_estimator.on_loss_round_start(
                self.bw_estimator.rate_sample(),
                self.congestion_state.loss_bursts_in_round(),
                self.max_datagram_size,
            )
        }

        if self.state.is_startup() && self.full_pipe_estimator.filled_pipe() {
            publisher.on_slow_start_exited(SlowStartExitCause::Other, self.cwnd);
            self.enter_drain(publisher);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        event, path,
        path::MINIMUM_MTU,
        recovery::{bandwidth::PacketInfo, bbr::probe_rtt, congestion_controller::PathPublisher},
        time::{Clock, NoopClock},
    };

    #[test]
    fn enter_startup() {
        let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());

        // Startup can only be re-entered from ProbeRtt
        bbr.state = State::ProbeRtt(probe_rtt::State::default());

        bbr.enter_startup(&mut publisher);

        assert!(bbr.state.is_startup());
        assert!(!bbr.try_fast_path);
    }

    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.1.1
    //= type=test
    //# BBRCheckStartupDone():
    //#   BBRCheckStartupFullBandwidth()
    //#   BBRCheckStartupHighLoss()
    //#   if (BBR.state == Startup and BBR.filled_pipe)
    //#     BBREnterDrain()
    #[test]
    fn check_startup_done() {
        let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());

        // Not in startup
        bbr.state = State::ProbeRtt(probe_rtt::State::default());
        bbr.full_pipe_estimator.set_filled_pipe_for_test(true);

        bbr.check_startup_done(&mut publisher);

        assert!(bbr.state.is_probing_rtt());

        bbr.state = State::Startup;
        bbr.full_pipe_estimator.set_filled_pipe_for_test(false);

        // Filled pipe = false
        bbr.check_startup_done(&mut publisher);
        assert!(bbr.state.is_startup());

        // Now startup is done
        bbr.state = State::Startup;
        bbr.full_pipe_estimator.set_filled_pipe_for_test(true);
        bbr.check_startup_done(&mut publisher);
        assert!(bbr.state.is_drain());
    }

    #[test]
    fn check_startup_done_filled_pipe_on_round_start() {
        let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        let now = NoopClock.get_time();

        // Set ECN state to be too high, which would cause the full pipe estimator to be filled
        bbr.ecn_state.on_explicit_congestion(1000);
        bbr.ecn_state
            .on_round_start(1000 * MINIMUM_MTU as u64, MINIMUM_MTU);
        assert!(!bbr.round_counter.round_start());

        // ECN must be too high over 2 rounds to fill the pipe
        bbr.check_startup_done(&mut publisher);
        bbr.check_startup_done(&mut publisher);

        // Still in startup since it wasn't the start of a round when ECN was measured
        assert!(!bbr.full_pipe_estimator.filled_pipe());
        assert!(bbr.state.is_startup());

        bbr.round_counter.set_round_end(100);
        let packet_info = PacketInfo {
            delivered_bytes: 100,
            delivered_time: now,
            lost_bytes: 0,
            ecn_ce_count: 0,
            first_sent_time: now,
            bytes_in_flight: 0,
            is_app_limited: false,
        };
        bbr.round_counter.on_ack(packet_info, 200);
        assert!(bbr.round_counter.round_start());

        // ECN must be too high over 2 rounds to fill the pipe
        bbr.check_startup_done(&mut publisher);
        bbr.check_startup_done(&mut publisher);

        assert!(bbr.full_pipe_estimator.filled_pipe());
        assert!(bbr.state.is_drain());
    }

    #[test]
    fn check_startup_done_filled_pipe_on_loss_round_start() {
        let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
        let mut publisher = event::testing::Publisher::snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        let now = NoopClock.get_time();

        // Set loss to be too high, which would cause the full pipe estimator to be filled
        bbr.bw_estimator.on_loss(1000);
        bbr.bw_estimator.set_delivered_bytes_for_test(100);
        // 8 loss bursts must occur for the pipe to be full
        for _ in 0..8 {
            bbr.congestion_state.on_packet_lost(100, true);
        }
        assert!(!bbr.congestion_state.loss_round_start());
        bbr.check_startup_done(&mut publisher);

        // Still in startup since it wasn't the start of a loss round when loss was measured
        assert!(!bbr.full_pipe_estimator.filled_pipe());
        assert!(bbr.state.is_startup());

        let packet_info = PacketInfo {
            delivered_bytes: 100,
            delivered_time: now,
            lost_bytes: 0,
            ecn_ce_count: 0,
            first_sent_time: now,
            bytes_in_flight: 0,
            is_app_limited: false,
        };
        bbr.update_latest_signals(packet_info);
        assert!(bbr.congestion_state.loss_round_start());

        bbr.check_startup_done(&mut publisher);

        assert!(bbr.full_pipe_estimator.filled_pipe());
        assert!(bbr.state.is_drain());
    }
}


// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use crate::{
    assert_delta,
    counter::Counter,
    event, path,
    path::MINIMUM_MTU,
    random,
    recovery::{
        bandwidth::{Bandwidth, PacketInfo, RateSample},
        bbr,
        bbr::{probe_bw::CyclePhase, probe_rtt, BbrCongestionController, State},
        congestion_controller::{PathPublisher, Publisher},
        CongestionController,
    },
    time::{Clock, NoopClock},
};
use num_rational::Ratio;
use num_traits::{Inv, One, ToPrimitive};
use std::time::Duration;

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.5.6.3
//= type=test
//# When not explicitly accelerating to probe for bandwidth (Drain, ProbeRTT,
//# ProbeBW_DOWN, ProbeBW_CRUISE), BBR responds to loss by slowing down to some extent.
#[test]
fn is_probing_for_bandwidth() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());

    // States that are not explicitly accelerating to probe for bandwidth
    // ie Drain, ProbeRtt, ProbeBW_DOWN, ProbeBW_CRUISE
    assert!(!State::Drain.is_probing_for_bandwidth());
    assert!(!State::ProbeRtt(probe_rtt::State::default()).is_probing_for_bandwidth());

    enter_probe_bw_state(&mut bbr, CyclePhase::Down, &mut publisher);
    assert!(!bbr.state.is_probing_for_bandwidth());

    enter_probe_bw_state(&mut bbr, CyclePhase::Cruise, &mut publisher);
    assert!(!bbr.state.is_probing_for_bandwidth());

    // States that are explicitly accelerating to probe for bandwidth
    assert!(State::Startup.is_probing_for_bandwidth());

    enter_probe_bw_state(&mut bbr, CyclePhase::Up, &mut publisher);
    assert!(bbr.state.is_probing_for_bandwidth());

    enter_probe_bw_state(&mut bbr, CyclePhase::Refill, &mut publisher);
    assert!(bbr.state.is_probing_for_bandwidth());
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.5.6.2
//= type=test
//# BBRInflightHiFromLostPacket(rs, packet):
//#   size = packet.size
//#   /* What was in flight before this packet? */
//#   inflight_prev = rs.tx_in_flight - size
//#   /* What was lost before this packet? */
//#   lost_prev = rs.lost - size
//#   lost_prefix = (BBRLossThresh * inflight_prev - lost_prev) /
//#                 (1 - BBRLossThresh)
//#   /* At what inflight value did losses cross BBRLossThresh? */
//#   inflight = inflight_prev + lost_prefix
//#   return inflight
#[test]
fn inflight_hi_from_lost_packet() {
    let now = NoopClock.get_time();
    let packet_info = PacketInfo {
        delivered_bytes: 0,
        delivered_time: now,
        lost_bytes: 0,
        ecn_ce_count: 0,
        first_sent_time: now,
        bytes_in_flight: 3000,
        is_app_limited: false,
    };

    let inflight_prev = packet_info.bytes_in_flight - MINIMUM_MTU as u32;
    assert_eq!(inflight_prev, 1800);

    // lost prefix = (BBRLossThresh * inflight_prev - lost_prev) / (1 - BBRLossThresh)
    // lost prefix = (1/50 * 1800 - (1210 - 1200)) / (1 - 1/50) = ~26
    assert_eq!(
        inflight_prev + 26,
        BbrCongestionController::inflight_hi_from_lost_packet(
            MINIMUM_MTU as u32,
            1210,
            packet_info
        )
    );

    // lost prefix is zero since LOSS_THRESH * 1800 < 3000 - 1200
    assert_eq!(
        inflight_prev,
        BbrCongestionController::inflight_hi_from_lost_packet(
            MINIMUM_MTU as u32,
            3000,
            packet_info
        )
    );

    // Test losing the first sent packet when nothing is inflight yet
    let packet_info = PacketInfo {
        delivered_bytes: 0,
        delivered_time: now,
        lost_bytes: 0,
        ecn_ce_count: 0,
        first_sent_time: now,
        bytes_in_flight: 0,
        is_app_limited: false,
    };

    // inflight_prev = 0 since bytes_in_flight when the lost packet was sent was 0
    let inflight_prev = 0;

    // lost prefix is zero since inflight_prev = 0
    assert_eq!(
        inflight_prev,
        BbrCongestionController::inflight_hi_from_lost_packet(
            MINIMUM_MTU as u32,
            MINIMUM_MTU as u32,
            packet_info
        )
    );
}

#[test]
fn pacing_cwnd_gain() {
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());

    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.6
    //= type=test
    //# A constant specifying the minimum gain value for calculating the pacing rate that will
    //# allow the sending rate to double each round (4*ln(2) ~= 2.77)
    assert_delta!(State::Startup.pacing_gain().to_f32().unwrap(), 2.77, 0.001);

    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.6
    //= type=test
    //# A constant specifying the minimum gain value for calculating the
    //# cwnd that will allow the sending rate to double each round (2.0)
    assert_delta!(State::Startup.cwnd_gain().to_f32().unwrap(), 2.0, 0.001);

    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.2
    //= type=test
    //# In Drain, BBR aims to quickly drain any queue created in Startup by switching to a
    //# pacing_gain well below 1.0, until any estimated queue has been drained. It uses a
    //# pacing_gain that is the inverse of the value used during Startup, chosen to try to
    //# drain the queue in one round

    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.2
    //= type=test
    //# BBREnterDrain():
    //#     BBR.state = Drain
    //#     BBR.pacing_gain = 1/BBRStartupCwndGain  /* pace slowly */
    //#     BBR.cwnd_gain = BBRStartupCwndGain      /* maintain cwnd */
    assert_eq!(State::Drain.pacing_gain(), State::Startup.cwnd_gain().inv());
    assert_eq!(State::Drain.cwnd_gain(), State::Startup.cwnd_gain());

    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    let now = NoopClock.get_time();
    bbr.enter_drain(&mut publisher);
    bbr.enter_probe_bw(
        false,
        &mut random::testing::Generator::default(),
        now,
        &mut publisher,
    );
    assert!(bbr.state.is_probing_bw());

    // ProbeBw cwnd gain from https://www.ietf.org/archive/id/draft-cardwell-iccrg-bbr-congestion-control-02.html#section-4.6.1
    assert_delta!(bbr.state.cwnd_gain().to_f32().unwrap(), 2.0, 0.001);

    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.1
    //= type=test
    //# In the ProbeBW_DOWN phase of the cycle, a BBR flow pursues the deceleration tactic,
    //# to try to send slower than the network is delivering data, to reduce the amount of data
    //# in flight, with all of the standard motivations for the deceleration tactic (discussed
    //# in "State Machine Tactics", above). It does this by switching to a BBR.pacing_gain of
    //# 0.9, sending at 90% of BBR.bw.
    assert_delta!(bbr.state.pacing_gain().to_f32().unwrap(), 0.9, 0.001);

    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.4.4
    //= type=test
    //# BBREnterProbeRTT():
    //#     BBR.state = ProbeRTT
    //#     BBR.pacing_gain = 1
    assert_delta!(
        State::ProbeRtt(probe_rtt::State::default())
            .pacing_gain()
            .to_f32()
            .unwrap(),
        1.0,
        0.001
    );

    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.14.2
    //= type=test
    //# A constant specifying the gain value for calculating the cwnd during ProbeRTT: 0.5
    assert_delta!(
        State::ProbeRtt(probe_rtt::State::default())
            .cwnd_gain()
            .to_f32()
            .unwrap(),
        0.5,
        0.001
    );
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.2.1
//= type=test
//# BBROnInit():
//#   init_windowed_max_filter(filter=BBR.MaxBwFilter, value=0, time=0)
//#   BBR.min_rtt = SRTT ? SRTT : Inf
//#   BBR.min_rtt_stamp = Now()
//#   BBR.probe_rtt_done_stamp = 0
//#   BBR.probe_rtt_round_done = false
//#   BBR.prior_cwnd = 0
//#   BBR.idle_restart = false
//#   BBR.extra_acked_interval_start = Now()
//#   BBR.extra_acked_delivered = 0
//#   BBRResetCongestionSignals()
//#   BBRResetLowerBounds()
//#   BBRInitRoundCounting()
//#   BBRInitFullPipe()
//#   BBRInitPacingRate()
//#   BBREnterStartup()
#[test]
fn new() {
    let bbr = BbrCongestionController::new(MINIMUM_MTU);

    assert_eq!(Bandwidth::ZERO, bbr.data_rate_model.max_bw());
    assert_eq!(None, bbr.data_volume_model.min_rtt());
    assert_eq!(0, bbr.prior_cwnd);
    assert!(!bbr.idle_restart);
    assert_eq!(0, bbr.data_volume_model.extra_acked());

    // BBRResetCongestionSignals()
    bbr::congestion::testing::assert_reset(bbr.congestion_state);

    // BBRResetLowerBounds()
    assert_eq!(u64::MAX, bbr.data_volume_model.inflight_lo());
    assert_eq!(Bandwidth::INFINITY, bbr.data_rate_model.bw_lo());

    // BBRInitRoundCounting()
    assert!(!bbr.round_counter.round_start());
    assert_eq!(0, bbr.round_counter.round_count());

    // BBRInitFullPipe()
    assert!(!bbr.full_pipe_estimator.filled_pipe());

    // BBREnterStartup()
    assert!(bbr.state.is_startup());
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.4.2
//= type=test
//# BBRBDPMultiple(gain):
//#   if (BBR.min_rtt == Inf)
//#       return InitialCwnd /* no valid RTT samples yet */
//#     BBR.bdp = BBR.bw * BBR.min_rtt
//#     return gain * BBR.bdp
#[test]
fn bdp_multiple() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    let now = NoopClock.get_time();

    // No min_rtt yet, so bdp is the initial window
    assert_eq!(12_000, bbr.bdp_multiple(Bandwidth::ZERO, Ratio::one()));

    // Set an RTT so min_rtt is populated
    let rtt = Duration::from_millis(100);
    bbr.data_volume_model.update_min_rtt(rtt, now);
    assert_eq!(Some(rtt), bbr.data_volume_model.min_rtt());

    let bandwidth = Bandwidth::new(1000, Duration::from_millis(1));
    let gain = Ratio::new(2, 1);

    // bdp_multiple = bandwidth * min_rtt * gain = 1000bytes/ms * 100ms * 2 = 200000 bytes
    assert_eq!(200000, bbr.bdp_multiple(bandwidth, gain));

    // Infinite bandwidth should not panic
    assert_eq!(u64::MAX, bbr.bdp_multiple(Bandwidth::INFINITY, gain));

    // Zero bandwidth should not panic
    assert_eq!(0, bbr.bdp_multiple(Bandwidth::ZERO, gain));
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.5.3
//# BBRTargetInflight()
//#   return min(BBR.bdp, cwnd)
#[test]
fn target_inflight() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    let now = NoopClock.get_time();

    let rate_sample = RateSample {
        delivered_bytes: 1000,
        interval: Duration::from_millis(1),
        ..Default::default()
    };
    bbr.data_rate_model.update_max_bw(rate_sample);
    bbr.data_rate_model.bound_bw_for_model();

    // Set an RTT so min_rtt is populated
    let rtt = Duration::from_millis(100);
    bbr.data_volume_model.update_min_rtt(rtt, now);
    assert_eq!(Some(rtt), bbr.data_volume_model.min_rtt());

    // bdp = 100ms * 1000bytes/ms = 100000bytes
    // cwnd = 12000
    // bdp < cwnd, so cwnd is returned
    assert_eq!(12000, bbr.target_inflight());

    bbr.cwnd = 200000;

    // bdp > cwnd, so bdp is returned
    assert_eq!(100000, bbr.target_inflight());
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.4.2
//= type=test
//# BBRUpdateMaxInflight()
//#   BBRUpdateAggregationBudget()
//#   inflight = BBRBDPMultiple(BBR.cwnd_gain)
//#   inflight += BBR.extra_acked
//#   BBR.max_inflight = BBRQuantizationBudget(inflight)
#[test]
fn max_inflight() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);

    bbr.data_volume_model.set_extra_acked_for_test(1000, 0);
    // bdp = initial_window = 12000 since min_rtt is not populated
    // inflight = bdp + extra_acked = 13000
    // max_inflight = quantization_budget(13000) = 3 * MAX_BURST_PACKETS * MINIMUM_MTU = 36000
    assert_eq!(36000, bbr.max_inflight());
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.4.2
//= type=test
//# BBRInflight(gain)
//#   inflight = BBRBDPMultiple(gain)
//#   return BBRQuantizationBudget(inflight)
#[test]
fn inflight() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    let now = NoopClock.get_time();

    // Set an RTT so min_rtt is populated
    let rtt = Duration::from_millis(100);
    bbr.data_volume_model.update_min_rtt(rtt, now);
    assert_eq!(Some(rtt), bbr.data_volume_model.min_rtt());

    let bandwidth = Bandwidth::new(2000, Duration::from_millis(1));
    // bdp = 2000bytes/ms * 100ms = 200000bytes
    // max_inflight = quantization_budget(200000) = 200000
    assert_eq!(200000, bbr.inflight(bandwidth, Ratio::one()));

    // Infinite bandwidth should not panic
    assert_eq!(
        u32::MAX,
        bbr.inflight(Bandwidth::INFINITY, Ratio::new(2, 1))
    );
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.3.6
//= type=test
//# BBRInflightWithHeadroom()
//#   if (BBR.inflight_hi == Infinity)
//#     return Infinity
//#   headroom = max(1, BBRHeadroom * BBR.inflight_hi)
//#     return max(BBR.inflight_hi - headroom,
//#                BBRMinPipeCwnd)
#[test]
fn inflight_with_headroom() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);

    // inflight_hi has not been initialized so inflight is u32::MAX
    assert_eq!(u32::MAX, bbr.inflight_with_headroom());

    bbr.data_volume_model.update_upper_bound(10_000);

    // inflight_with_headroom = HEADROOM * inflight_hi = .85 * 10_0000 = 8500
    assert_eq!(8500, bbr.inflight_with_headroom());

    // Set a lower inflight_hi so that the minimum window is larger
    bbr.data_volume_model.update_upper_bound(1000);

    // inflight_with_headroom = HEADROOM * inflight_hi = .85 * 1000 = 850
    // inflight_with_headroom < minimum_window, return minimum_window
    assert_eq!(4800, bbr.inflight_with_headroom());
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.4.2
//= type=test
//# BBRQuantizationBudget(inflight)
//#   BBRUpdateOffloadBudget()
//#   inflight = max(inflight, BBR.offload_budget)
//#   inflight = max(inflight, BBRMinPipeCwnd)
//#   if (BBR.state == ProbeBW && BBR.cycle_idx == ProbeBW_UP)
//#     inflight += 2
//#   return inflight
#[test]
fn quantization_budget() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    bbr.pacer.set_send_quantum_for_test(4000);

    let send_quantum = bbr.pacer.send_quantum();

    // offload_budget = 3 * send_quantum

    // offload_budget > inflight > minimum_window, return offload_budget
    assert_eq!(3 * send_quantum as u64, bbr.quantization_budget(6000));

    // offload_budget < inflight, return inflight
    assert_eq!(14000, bbr.quantization_budget(14000));

    bbr.pacer.set_send_quantum_for_test(1000);
    // offload_budget = 3 * send_quantum = 3000

    // minimum_window = 4 * mtu = 4800
    // offload_budget < inflight < minimum_window
    assert_eq!(4800, bbr.quantization_budget(2000));

    enter_probe_bw_state(&mut bbr, CyclePhase::Up, &mut publisher);
    assert!(bbr.state.is_probing_bw_up());

    // since probe bw up, add 2 packets to the budget
    assert_eq!(4800 + 2 * MINIMUM_MTU as u64, bbr.quantization_budget(2000));
}

#[test]
fn is_inflight_too_high() {
    let rate_sample = RateSample {
        lost_bytes: 3,
        bytes_in_flight: 100,
        ..Default::default()
    };
    // loss rate higher than 2% threshold and loss bursts = limit
    assert!(BbrCongestionController::is_inflight_too_high(
        rate_sample,
        MINIMUM_MTU,
        2,
        2
    ));

    // loss rate higher than 2% threshold but loss bursts < limit
    assert!(!BbrCongestionController::is_inflight_too_high(
        rate_sample,
        MINIMUM_MTU,
        1,
        2
    ));

    let rate_sample = RateSample {
        lost_bytes: 2,
        bytes_in_flight: 100,
        ..Default::default()
    };
    // loss rate <= 2% threshold
    assert!(!BbrCongestionController::is_inflight_too_high(
        rate_sample,
        MINIMUM_MTU,
        2,
        2
    ));

    let rate_sample = RateSample {
        delivered_bytes: 100 * MINIMUM_MTU as u64,
        ecn_ce_count: 51,
        ..Default::default()
    };
    // ecn rate higher than 50% threshold
    assert!(BbrCongestionController::is_inflight_too_high(
        rate_sample,
        MINIMUM_MTU,
        0,
        2
    ));

    let rate_sample = RateSample {
        delivered_bytes: 100 * MINIMUM_MTU as u64,
        ecn_ce_count: 50,
        ..Default::default()
    };
    // ecn rate <= 50% threshold
    assert!(!BbrCongestionController::is_inflight_too_high(
        rate_sample,
        MINIMUM_MTU,
        0,
        2,
    ));
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.4.7
//= type=test
//# BBRBoundCwndForModel():
//#   cap = Infinity
//#   if (IsInAProbeBWState() and
//#       BBR.state != ProbeBW_CRUISE)
//#     cap = BBR.inflight_hi
//#   else if (BBR.state == ProbeRTT or
//#            BBR.state == ProbeBW_CRUISE)
//#     cap = BBRInflightWithHeadroom()
//#
//#   /* apply inflight_lo (possibly infinite): */
//#   cap = min(cap, BBR.inflight_lo)
//#   cap = max(cap, BBRMinPipeCwnd)
//#   cwnd = min(cwnd, cap)
#[test]
fn bound_cwnd_for_model() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    enter_probe_bw_state(&mut bbr, CyclePhase::Down, &mut publisher);

    bbr.data_volume_model.update_upper_bound(10000);

    assert_eq!(10000, bbr.bound_cwnd_for_model());

    enter_probe_bw_state(&mut bbr, CyclePhase::Cruise, &mut publisher);

    // inflight_with_headroom = .85 * 10000 = 8500
    assert_eq!(8500, bbr.bound_cwnd_for_model());

    bbr.state = State::ProbeRtt(probe_rtt::State::default());
    // inflight_with_headroom = .85 * 10000 = 8500
    assert_eq!(8500, bbr.bound_cwnd_for_model());

    // now the limiting factor is inflight_lo
    bbr.state = State::Startup;
    // Set inflight_lo to 5000
    bbr.data_volume_model
        .update_lower_bound(1000, 5000, true, false, 0.0);
    assert_eq!(5000, bbr.data_volume_model.inflight_lo());

    assert_eq!(5000, bbr.bound_cwnd_for_model());

    // now the limiting factor is minimum window (4800)
    // Set inflight_lo to 4000
    bbr.data_volume_model
        .update_lower_bound(1000, 4000, true, false, 0.0);
    assert_eq!(4000, bbr.data_volume_model.inflight_lo());

    assert_eq!(4800, bbr.bound_cwnd_for_model());
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.4.6
//= type=test
//#     if (BBR.filled_pipe)
//#       cwnd = min(cwnd + rs.newly_acked, BBR.max_inflight)
#[test]
fn set_cwnd_filled_pipe() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    assert_eq!(36_000, bbr.max_inflight());

    bbr.full_pipe_estimator.set_filled_pipe_for_test(true);
    assert!(bbr.full_pipe_estimator.filled_pipe());

    bbr.cwnd = 12_000;
    bbr.set_cwnd(1000);
    assert_eq!(13_000, bbr.cwnd);
    assert!(!bbr.try_fast_path);

    bbr.cwnd = 40_000;
    bbr.set_cwnd(1000);
    assert_eq!(36_000, bbr.cwnd);
    assert!(bbr.try_fast_path);
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.4.6
//= type=test
//#     else if (cwnd < BBR.max_inflight || C.delivered < InitialCwnd)
//#       cwnd = cwnd + rs.newly_acked
#[test]
fn set_cwnd_not_filled_pipe() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let now = NoopClock.get_time();
    assert_eq!(36_000, bbr.max_inflight());

    assert!(!bbr.full_pipe_estimator.filled_pipe());

    bbr.cwnd = 12_000;
    bbr.set_cwnd(1000);
    assert_eq!(13_000, bbr.cwnd);

    // cwnd > BBR.max_inflight, but C.delivered < 2 * InitialCwnd
    bbr.cwnd = 40_000;
    bbr.set_cwnd(1000);
    assert_eq!(41_000, bbr.cwnd);

    // Set C.delivered > 2 * InitialCwnd
    let packet_info = PacketInfo {
        delivered_bytes: 0,
        delivered_time: now,
        lost_bytes: 0,
        ecn_ce_count: 0,
        first_sent_time: now,
        bytes_in_flight: 0,
        is_app_limited: false,
    };
    bbr.bw_estimator.on_ack(
        2 * BbrCongestionController::initial_window(MINIMUM_MTU) as usize + 1,
        now,
        packet_info,
        now,
        &mut publisher,
    );
    bbr.cwnd = 12_000;
    bbr.set_cwnd(1000);
    assert_eq!(13_000, bbr.cwnd);
    assert!(!bbr.try_fast_path);

    // cwnd > BBR.max_inflight and C.delivered > InitialCwnd
    bbr.cwnd = 40_000;
    bbr.set_cwnd(1000);
    assert_eq!(40_000, bbr.cwnd); // No change
    assert!(bbr.try_fast_path);
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.4.5
//= type=test
//# BBRBoundCwndForProbeRTT():
//#   if (BBR.state == ProbeRTT)
//#     cwnd = min(cwnd, BBRProbeRTTCwnd())
#[test]
fn set_cwnd_probing_rtt() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    assert_eq!(36_000, bbr.max_inflight());
    assert_eq!(12_000, bbr.probe_rtt_cwnd());

    bbr.state = State::ProbeRtt(probe_rtt::State::default());

    // cwnd > probe_rtt_cwnd
    bbr.cwnd = 13_000;
    bbr.set_cwnd(1000);
    assert_eq!(12_000, bbr.cwnd);

    // cwnd < probe_rtt_cwnd. Since cwnd < max_flight, newly_acked is added
    bbr.cwnd = 10_000;
    bbr.set_cwnd(1000);
    assert_eq!(11_000, bbr.cwnd);
}

#[test]
fn set_cwnd_clamp() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    assert_eq!(36_000, bbr.max_inflight());

    // cwnd < min
    bbr.cwnd = bbr.minimum_window() - 1000;
    bbr.set_cwnd(500);
    assert_eq!(bbr.minimum_window(), bbr.cwnd);

    // cwnd > bound_cwnd_for_model
    bbr.data_volume_model.set_inflight_lo_for_test(30_000);
    assert_eq!(30_000, bbr.bound_cwnd_for_model());

    bbr.cwnd = 40_000;
    bbr.set_cwnd(1000);
    assert_eq!(30_000, bbr.cwnd);
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.4.4
//= type=test
//# BBRSaveCwnd()
//#   if (!InLossRecovery() and BBR.state != ProbeRTT)
//#     return cwnd
//#   else
//#     return max(BBR.prior_cwnd, cwnd)
#[test]
fn save_cwnd() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    bbr.state = State::ProbeRtt(probe_rtt::State::default());

    bbr.prior_cwnd = 2000;
    bbr.cwnd = 1000;
    bbr.save_cwnd();
    assert_eq!(2000, bbr.prior_cwnd);

    bbr.prior_cwnd = 4000;
    bbr.cwnd = 5000;
    bbr.save_cwnd();
    assert_eq!(5000, bbr.prior_cwnd);
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.4.4
//= type=test
//# BBRRestoreCwnd()
//#   cwnd = max(cwnd, BBR.prior_cwnd)
#[test]
fn restore_cwnd() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    bbr.state = State::ProbeRtt(probe_rtt::State::default());

    bbr.prior_cwnd = 1000;
    bbr.cwnd = 2000;

    bbr.restore_cwnd();

    assert_eq!(2000, bbr.cwnd);

    bbr.prior_cwnd = 2000;
    bbr.cwnd = 1000;

    bbr.restore_cwnd();

    assert_eq!(2000, bbr.cwnd);
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.6.4.4
//= type=test
//# Upon entering Fast Recovery, set cwnd to the number of packets still in flight
//# (allowing at least one for a fast retransmit):
//#
//# BBROnEnterFastRecovery():
//#   BBR.prior_cwnd = BBRSaveCwnd()
//#   cwnd = packets_in_flight + max(rs.newly_acked, 1)
//#   BBR.packet_conservation = true

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.5.6.2
//= type=test
//# if (!BBR.bw_probe_samples)
//#   return /* not a packet sent while probing bandwidth */
//# rs.tx_in_flight = packet.tx_in_flight /* inflight at transmit */
//# rs.lost = C.lost - packet.lost /* data lost since transmit */
//# rs.is_app_limited = packet.is_app_limited;
//# if (IsInflightTooHigh(rs))
//#   rs.tx_in_flight = BBRInflightHiFromLostPacket(rs, packet)
//#   BBRHandleInflightTooHigh(rs)
#[test]
fn handle_lost_packet() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    let now = NoopClock.get_time();
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    bbr.bw_probe_samples = true;

    bbr.bw_estimator.on_loss(1000);

    let lost_packet = PacketInfo {
        delivered_bytes: 0,
        delivered_time: now,
        lost_bytes: 0,
        ecn_ce_count: 0,
        first_sent_time: now,
        bytes_in_flight: 10000,
        is_app_limited: false,
    };

    enter_probe_bw_state(&mut bbr, CyclePhase::Up, &mut publisher);
    // Two lost bursts to trigger inflight being too high
    bbr.congestion_state.on_packet_lost(500, true);
    bbr.congestion_state.on_packet_lost(500, true);
    bbr.handle_lost_packet(
        1000,
        lost_packet,
        &mut random::testing::Generator::default(),
        now,
        &mut publisher,
    );

    let inflight_hi_from_lost_packet =
        BbrCongestionController::inflight_hi_from_lost_packet(1000, 1000, lost_packet) as u64;

    // Only react once per bw probe
    assert!(!bbr.bw_probe_samples);

    // inflight_hi_from_lost_packet > BETA * target_inflight, so that becomes inflight_hi
    assert!(inflight_hi_from_lost_packet > (bbr::BETA * bbr.target_inflight() as u64).to_integer());
    assert_eq!(
        inflight_hi_from_lost_packet,
        bbr.data_volume_model.inflight_hi()
    );

    if let State::ProbeBw(probe_bw_state) = bbr.state {
        assert_eq!(CyclePhase::Down, probe_bw_state.cycle_phase());
    } else {
        panic!("Must be in ProbeBw Down state");
    }

    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    bbr.bw_estimator.on_loss(1000);

    // This time set cwnd and max_bw higher so that BETA * target_inflight is higher than inflight_hi_from_lost_packet
    bbr.bw_probe_samples = true;
    bbr.cwnd = 100_000;
    let rate_sample = RateSample {
        delivered_bytes: 100_000,
        interval: Duration::from_millis(1),
        ..Default::default()
    };
    bbr.data_volume_model
        .update_min_rtt(Duration::from_millis(10), now);
    bbr.data_rate_model.update_max_bw(rate_sample);
    bbr.data_rate_model.bound_bw_for_model();
    // Two lost bursts to trigger inflight being too high
    bbr.congestion_state.on_packet_lost(500, true);
    bbr.congestion_state.on_packet_lost(500, true);

    bbr.handle_lost_packet(
        1000,
        lost_packet,
        &mut random::testing::Generator::default(),
        now,
        &mut publisher,
    );

    let inflight_hi_from_lost_packet =
        BbrCongestionController::inflight_hi_from_lost_packet(1000, 1000, lost_packet) as u64;

    // Only react once per bw probe
    assert!(!bbr.bw_probe_samples);

    // inflight_hi_from_lost_packet < BETA * target_inflight, so that becomes inflight_hi
    assert!(inflight_hi_from_lost_packet < (bbr::BETA * bbr.target_inflight() as u64).to_integer());
    assert_eq!(
        (bbr::BETA * bbr.target_inflight() as u64).to_integer(),
        bbr.data_volume_model.inflight_hi()
    );
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.4.3
//= type=test
//# BBRHandleRestartFromIdle():
//#   if (packets_in_flight == 0 and C.app_limited)
//#     BBR.idle_restart = true
//#        BBR.extra_acked_interval_start = Now()
//#     if (IsInAProbeBWState())
//#       BBRSetPacingRateWithGain(1)
#[test]
fn handle_restart_from_idle() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
    let now = NoopClock.get_time();
    let pacing_rate = bbr.pacer.pacing_rate();

    bbr.handle_restart_from_idle(now, &mut publisher);

    // Not app limited
    assert!(!bbr.idle_restart);

    // App limited, but bytes in flight > 0
    bbr.bytes_in_flight = Counter::new(1);
    bbr.bw_estimator.on_app_limited(100);
    bbr.handle_restart_from_idle(now, &mut publisher);

    assert!(!bbr.idle_restart);

    bbr.bytes_in_flight = Counter::default();

    bbr.handle_restart_from_idle(now, &mut publisher);
    assert!(bbr.idle_restart);
    assert_eq!(
        Some(now),
        bbr.data_volume_model.extra_acked_interval_start()
    );
    assert_eq!(pacing_rate, bbr.pacer.pacing_rate());

    enter_probe_bw_state(&mut bbr, CyclePhase::Down, &mut publisher);
    let rate_sample = RateSample {
        delivered_bytes: 100_000,
        interval: Duration::from_millis(1),
        ..Default::default()
    };
    bbr.data_rate_model.update_max_bw(rate_sample);
    bbr.data_rate_model.bound_bw_for_model();
    assert!(bbr.data_rate_model.bw() > bbr.pacer.pacing_rate());

    let now = now + Duration::from_secs(5);
    bbr.handle_restart_from_idle(now, &mut publisher);
    assert!(bbr.idle_restart);
    assert_eq!(
        Some(now),
        bbr.data_volume_model.extra_acked_interval_start()
    );
    assert!(bbr.pacer.pacing_rate() > pacing_rate);
}

#[test]
fn model_update_required() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    let rate_sample = RateSample {
        delivered_bytes: 100_000,
        interval: Duration::from_millis(1),
        ..Default::default()
    };
    bbr.data_rate_model.update_max_bw(rate_sample);
    bbr.data_rate_model.bound_bw_for_model();
    let rate_sample = RateSample {
        delivered_bytes: 10_000,
        interval: Duration::from_millis(1),
        is_app_limited: true,
        ..Default::default()
    };
    bbr.bw_estimator.set_rate_sample_for_test(rate_sample);

    assert!(bbr.bw_estimator.rate_sample().is_app_limited);
    assert!(bbr.bw_estimator.rate_sample().delivery_rate() < bbr.data_rate_model.max_bw());
    assert!(!bbr.congestion_state.loss_in_round());
    assert!(!bbr.congestion_state.ecn_in_round());

    // try_fast_path
    bbr.try_fast_path = false;
    assert!(bbr.model_update_required());
    bbr.try_fast_path = true;
    assert!(!bbr.model_update_required());

    // app limited
    let rate_sample = RateSample {
        delivered_bytes: 10_000,
        interval: Duration::from_millis(1),
        is_app_limited: false,
        ..Default::default()
    };
    bbr.bw_estimator.set_rate_sample_for_test(rate_sample);
    assert!(bbr.model_update_required());
    let rate_sample = RateSample {
        delivered_bytes: 10_000,
        interval: Duration::from_millis(1),
        is_app_limited: true,
        ..Default::default()
    };
    bbr.bw_estimator.set_rate_sample_for_test(rate_sample);
    assert!(!bbr.model_update_required());

    // delivery_rate >= max_bw
    let rate_sample = RateSample {
        delivered_bytes: 500_000,
        interval: Duration::from_millis(1),
        is_app_limited: true,
        ..Default::default()
    };
    bbr.bw_estimator.set_rate_sample_for_test(rate_sample);
    assert!(bbr.bw_estimator.rate_sample().delivery_rate() >= bbr.data_rate_model.max_bw());
    assert!(bbr.model_update_required());
    let rate_sample = RateSample {
        delivered_bytes: 10_000,
        interval: Duration::from_millis(1),
        is_app_limited: true,
        ..Default::default()
    };
    bbr.bw_estimator.set_rate_sample_for_test(rate_sample);
    assert!(bbr.bw_estimator.rate_sample().delivery_rate() < bbr.data_rate_model.max_bw());
    assert!(!bbr.model_update_required());

    // loss in round
    bbr.congestion_state.on_packet_lost(100, true);
    assert!(bbr.model_update_required());
    bbr.congestion_state.reset();
    assert!(!bbr.model_update_required());

    // ecn in round
    bbr.congestion_state.on_explicit_congestion();
    assert!(bbr.model_update_required());
    bbr.congestion_state.reset();
    assert!(!bbr.model_update_required());
}

#[test]
fn control_update_required() {
    let mut bbr = BbrCongestionController::new(MINIMUM_MTU);
    let now = NoopClock.get_time();

    bbr.try_fast_path = true;
    let mut model_updated = false;
    assert!(bbr.data_volume_model.min_rtt().is_none());

    assert!(!bbr.control_update_required(model_updated, None));

    // try_fast_path
    bbr.try_fast_path = false;
    assert!(bbr.control_update_required(model_updated, None));
    bbr.try_fast_path = true;
    assert!(!bbr.control_update_required(model_updated, None));

    // model_updated
    model_updated = true;
    assert!(bbr.control_update_required(model_updated, None));
    model_updated = false;
    assert!(!bbr.control_update_required(model_updated, None));

    // prev_min_rtt != self.data_volume_model.min_rtt()
    assert!(bbr.control_update_required(model_updated, Some(Duration::from_millis(100))));
    bbr.data_volume_model
        .update_min_rtt(Duration::from_millis(100), now);
    assert!(bbr.control_update_required(model_updated, None));
    assert!(bbr.control_update_required(model_updated, Some(Duration::from_millis(200))));
    assert!(!bbr.control_update_required(model_updated, Some(Duration::from_millis(100))));
}

#[test]
fn on_mtu_update() {
    let mut mtu = 5000;
    let mut bbr = BbrCongestionController::new(mtu);
    let mut publisher = event::testing::Publisher::snapshot();
    let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());

    bbr.cwnd = 100_000;

    mtu = 10000;
    bbr.on_mtu_update(mtu, &mut publisher);

    assert_eq!(bbr.max_datagram_size, mtu);
    assert_eq!(bbr.cwnd, 200_000);
}

/// Helper method to move the given BBR congestion controller into the
/// ProbeBW state with the given CyclePhase
fn enter_probe_bw_state<Pub: Publisher>(
    bbr: &mut BbrCongestionController,
    cycle_phase: CyclePhase,
    publisher: &mut Pub,
) {
    let now = NoopClock.get_time();

    match bbr.state {
        State::Startup => {
            bbr.enter_drain(publisher);
            bbr.enter_probe_bw(
                false,
                &mut random::testing::Generator::default(),
                now,
                publisher,
            );
        }
        State::Drain | State::ProbeRtt(_) => {
            bbr.enter_probe_bw(
                false,
                &mut random::testing::Generator::default(),
                now,
                publisher,
            );
        }
        State::ProbeBw(_) => {}
    }

    if let State::ProbeBw(ref mut probe_bw_state) = bbr.state {
        probe_bw_state.set_cycle_phase_for_test(cycle_phase);
    }
}


// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use crate::time::Timestamp;
use core::{marker::PhantomData, time::Duration};

/// Data structure for tracking the minimum or maximum value seen over a configurable
/// time period specified by the `window_length`
///
/// Based on https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=f672258391b42a5c7cc2732c9c063e56a85c8dbe
#[derive(Clone, Debug)]
pub(crate) struct WindowedFilter<T, TimeType, DurationType, FilterType> {
    current_value: Option<T>,
    last_updated: Option<TimeType>,
    window_length: DurationType,
    filter: PhantomData<FilterType>,
}

pub(crate) trait Filter<T> {
    /// Returns true if the `new` value should replace the `current` value
    fn supersedes(new: T, current: Option<T>) -> bool;
}

#[derive(Clone, Debug)]
pub(crate) struct MaxFilter;
#[derive(Clone, Debug)]
pub(crate) struct MinFilter;

impl<T: core::cmp::PartialOrd> Filter<T> for MaxFilter {
    fn supersedes(new: T, current: Option<T>) -> bool {
        current.map_or(true, |current| new >= current)
    }
}

impl<T: core::cmp::PartialOrd> Filter<T> for MinFilter {
    fn supersedes(new: T, current: Option<T>) -> bool {
        current.map_or(true, |current| new <= current)
    }
}

/// Filter that maintains the maximum value seen over the window
pub(crate) type WindowedMaxFilter<T, TimeType, DurationType> =
    WindowedFilter<T, TimeType, DurationType, MaxFilter>;
/// Filter that maintains the minimum value seen over the window
pub(crate) type WindowedMinFilter<T, TimeType, DurationType> =
    WindowedFilter<T, TimeType, DurationType, MinFilter>;

impl<
        T: Copy + PartialOrd,
        TimeType: Copy + PartialOrd + core::ops::Sub<Output = DurationType>,
        DurationType: PartialOrd,
        FilterType: Filter<T>,
    > WindowedFilter<T, TimeType, DurationType, FilterType>
{
    /// Constructs a new `WindowedFilter` with the specified `window_length`
    pub fn new(window_length: DurationType) -> Self {
        Self {
            current_value: None,
            last_updated: None,
            window_length,
            filter: Default::default(),
        }
    }

    /// Updates the `WindowedFilter` with the given sample
    ///
    /// If the `new_sample` supersedes the current value according to the `Filter` or the current
    /// value has expired according to the window length and the `now` value, the new sample will
    /// become the current value.
    ///
    /// `now` must be monotonically increasing, unless the `TimeType` supports wrapping (such as
    /// `core::num::Wrapping`)
    pub fn update(&mut self, new_sample: T, now: TimeType) {
        if self.window_expired(now) || FilterType::supersedes(new_sample, self.current_value) {
            self.current_value = Some(new_sample);
            self.last_updated = Some(now);
        }
    }

    /// Returns the current value if one has been recorded yet
    pub fn value(&self) -> Option<T> {
        self.current_value
    }

    #[inline]
    fn window_expired(&self, now: TimeType) -> bool {
        self.last_updated.map_or(false, |last_updated| {
            now - last_updated >= self.window_length
        })
    }
}
//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.5.3.2
//# A BBR implementation MAY use a generic windowed min filter to track BBR.min_rtt.
//# However, a significant savings in space and improvement in freshness can be achieved
//# by integrating the BBR.min_rtt estimation into the ProbeRTT state machine

/// Specialized WindowedMinFilter for tracking min round trip time
///
/// BBRv2 tracks both a min probe RTT that is refreshed at least every `PROBE_RTT_INTERVAL`,
/// and an overall min_rtt that is refreshed at least every `MIN_RTT_FILTER_LEN`.
#[derive(Clone, Debug)]
pub(crate) struct MinRttWindowedFilter {
    min_probe_rtt: WindowedMinFilter<Duration, Timestamp, Duration>,
    min_rtt: WindowedMinFilter<Duration, Timestamp, Duration>,
    //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.14.2
    //# A boolean recording whether the BBR.probe_rtt_min_delay has expired and is due for a
    //# refresh with an application idle period or a transition into ProbeRTT state.
    probe_rtt_expired: bool,
}

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.14.2
//# A constant specifying the minimum time interval between ProbeRTT states: 5 secs.
pub const PROBE_RTT_INTERVAL: Duration = Duration::from_secs(5);

//= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#2.14.1
//# A constant specifying the length of the BBR.min_rtt min filter window,
//# MinRTTFilterLen is 10 secs.
const MIN_RTT_FILTER_LEN: Duration = Duration::from_secs(10);

impl MinRttWindowedFilter {
    /// Constructs a new MinRttWindowedFilter
    pub fn new() -> Self {
        Self {
            min_probe_rtt: WindowedMinFilter::new(PROBE_RTT_INTERVAL),
            min_rtt: WindowedMinFilter::new(MIN_RTT_FILTER_LEN),
            probe_rtt_expired: false,
        }
    }

    /// Updates the min_probe_rtt and min_rtt estimates with the given `rtt`
    pub fn update(&mut self, rtt: Duration, now: Timestamp) {
        //= https://tools.ietf.org/id/draft-cardwell-iccrg-bbr-congestion-control-02#4.3.4.4
        //# BBRUpdateMinRTT()
        //#   BBR.probe_rtt_expired =
        //#     Now() > BBR.probe_rtt_min_stamp + ProbeRTTInterval
        //#   if (rs.rtt >= 0 and
        //#       (rs.rtt < BBR.probe_rtt_min_delay or
        //#        BBR.probe_rtt_expired))
        //#      BBR.probe_rtt_min_delay = rs.rtt
        //#      BBR.probe_rtt_min_stamp = Now()
        //#
        //#   min_rtt_expired =
        //#     Now() > BBR.min_rtt_stamp + MinRTTFilterLen
        //#   if (BBR.probe_rtt_min_delay < BBR.min_rtt or
        //#       min_rtt_expired)
        //#     BBR.min_rtt       = BBR.probe_rtt_min_delay
        //#     BBR.min_rtt_stamp = BBR.probe_rtt_min_stamp

        self.probe_rtt_expired = self.min_probe_rtt.window_expired(now);
        self.min_probe_rtt.update(rtt, now);

        let probe_rtt = self
            .min_probe_rtt
            .value()
            .expect("probe_rtt is updated just prior");

        if self.min_rtt.window_expired(now)
            || MinFilter::supersedes(probe_rtt, self.min_rtt.value())
        {
            // When the min_rtt expires or is superseded, it is replaced with the
            // min_probe_rtt value and the last updated time from min_probe_rtt
            // rather than the latest RTT and current time to keep min_probe_rtt and
            // min_rtt coordinated.
            self.min_rtt.current_value = self.min_probe_rtt.value();
            self.min_rtt.last_updated = self.min_probe_rtt.last_updated;
        }
    }

    /// The current windowed estimate of minimum round trip time
    pub fn min_rtt(&self) -> Option<Duration> {
        self.min_rtt.value()
    }

    /// True if the probe RTT has expired and is due for a refresh by entering the ProbeRTT state
    pub fn probe_rtt_expired(&self) -> bool {
        self.probe_rtt_expired
    }

    /// Overrides the last updated time for Min Probe RTT to ensure ProbeRTT is not entered until
    /// the next `PROBE_RTT_INTERVAL`.
    pub fn schedule_next_probe_rtt(&mut self, now: Timestamp) {
        self.min_probe_rtt.last_updated = Some(now);
    }

    #[cfg(test)]
    pub fn next_probe_rtt(&self) -> Option<Timestamp> {
        self.min_probe_rtt
            .last_updated
            .map(|last_updated| last_updated + PROBE_RTT_INTERVAL)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::time::{Clock, NoopClock};
    use core::time::Duration;

    #[test]
    fn min_filter() {
        let mut filter = WindowedMinFilter::new(Duration::from_secs(10));

        // Filter has not received an update, so no value should be present
        assert_eq!(None, filter.value());
        assert_eq!(None, filter.last_updated);

        // After the first update, the first value is the min
        let now = NoopClock.get_time();
        filter.update(7, now);
        assert_eq!(Some(7), filter.value());
        assert_eq!(Some(now), filter.last_updated);

        // A lower value is received
        let now = now + Duration::from_secs(5);
        filter.update(3, now);
        assert_eq!(Some(3), filter.value());
        assert_eq!(Some(now), filter.last_updated);

        // A value higher than the min is received, no update to the value
        let now = now + Duration::from_secs(9);
        filter.update(4, now);
        assert_eq!(Some(3), filter.value());
        assert!(filter.last_updated.unwrap() < now);

        // A value higher than the min is received, but the current min has expired
        let now = now + Duration::from_secs(1);
        filter.update(4, now);
        assert_eq!(Some(4), filter.value());
        assert_eq!(Some(now), filter.last_updated);
    }

    #[test]
    fn max_filter() {
        let mut filter = WindowedMaxFilter::new(10);

        // Filter has not received an update, so no value should be present
        assert_eq!(None, filter.value());
        assert_eq!(None, filter.last_updated);

        // After the first update, the first value is the max
        let mut now = 0;
        filter.update(7, now);
        assert_eq!(Some(7), filter.value());
        assert_eq!(Some(now), filter.last_updated);

        // A higher value is received
        now += 1;
        filter.update(8, now);
        assert_eq!(Some(8), filter.value());
        assert_eq!(Some(now), filter.last_updated);

        // A value lower than the max is received, no update to the value
        now += 9;
        filter.update(4, now);
        assert_eq!(Some(8), filter.value());
        assert!(filter.last_updated.unwrap() < now);

        // A value lower than the max is received, but the current max has expired
        now += 1;
        filter.update(4, now);
        assert_eq!(Some(4), filter.value());
        assert_eq!(Some(now), filter.last_updated);
    }

    #[test]
    fn wrapping() {
        let mut filter = WindowedMaxFilter::new(core::num::Wrapping(2_u8));

        // Filter has not received an update, so no value should be present
        assert_eq!(None, filter.value());
        assert_eq!(None, filter.last_updated);

        // After the first update, the first value is the max
        let mut now = core::num::Wrapping(0);
        filter.update(7, now);
        assert_eq!(Some(7), filter.value());
        assert_eq!(Some(now), filter.last_updated);

        // now is set to the maximum for the time type in use
        now = core::num::Wrapping(u8::MAX);
        filter.update(2, now);
        assert_eq!(Some(2), filter.value());
        assert_eq!(Some(now), filter.last_updated);

        // Wrapping around, the current value should not be considered expired
        now = core::num::Wrapping(0);
        filter.update(1, now);
        assert_eq!(Some(2), filter.value());
        assert_eq!(Some(core::num::Wrapping(u8::MAX)), filter.last_updated);

        // Now the current value has expired
        now += core::num::Wrapping(1);
        filter.update(1, now);
        assert_eq!(Some(1), filter.value());
        assert_eq!(Some(now), filter.last_updated);
    }

    #[test]
    fn min_rtt_filter() {
        let mut filter = MinRttWindowedFilter::new();

        // Filter has not received an update, so no value should be present
        assert_eq!(None, filter.min_rtt());
        // Probe RTT is not expired upon initialization
        assert!(!filter.probe_rtt_expired());

        // After the first update, the first value is the min
        let now = NoopClock.get_time();
        filter.update(Duration::from_millis(5), now);
        assert_eq!(Some(Duration::from_millis(5)), filter.min_rtt());
        assert!(!filter.probe_rtt_expired());

        // PROBE_RTT_INTERVAL has passed
        let now = now + PROBE_RTT_INTERVAL;
        filter.update(Duration::from_millis(9), now);
        // min_rtt is not updated since it has not expired and is lower than the new rtt
        assert_eq!(Some(Duration::from_millis(5)), filter.min_rtt());
        assert!(filter.probe_rtt_expired());

        // Midway through the Probe RTT period the RTT drops
        let now = now + Duration::from_secs(2);
        let probe_rtt_update_time = now;
        filter.update(Duration::from_millis(7), now);
        // min_rtt is not updated since it has not expired and is lower than the new rtt
        assert_eq!(Some(Duration::from_millis(5)), filter.min_rtt());
        assert!(!filter.probe_rtt_expired());
        assert_eq!(Some(Duration::from_millis(7)), filter.min_probe_rtt.value());

        // Now the Min RTT has expired, since it has been MIN_RTT_FILTER_LEN (10 seconds) since the
        // min_rtt value was first set (PROBE_RTT_INTERVAL + 2 seconds + 3 seconds)
        let now = now + Duration::from_secs(3);
        filter.update(Duration::from_millis(10), now);
        // min_rtt is updated since it has expired. The value is set to the current probe_rtt
        assert_eq!(Some(Duration::from_millis(7)), filter.min_rtt());
        // min_rtt last_updated is set to the probe_rtt last updated time
        assert_eq!(Some(probe_rtt_update_time), filter.min_rtt.last_updated);
        assert!(!filter.probe_rtt_expired());

        filter.schedule_next_probe_rtt(now);

        let now = now + PROBE_RTT_INTERVAL;
        assert!(!filter.probe_rtt_expired());
        filter.update(Duration::from_secs(10), now);
        assert!(filter.probe_rtt_expired());
    }
}


// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

use crate::{
    event,
    packet::number::PacketNumberSpace,
    path,
    path::MINIMUM_MTU,
    random,
    recovery::{
        bbr::BbrCongestionController, congestion_controller::PathPublisher, CongestionController,
        CubicCongestionController, RttEstimator,
    },
    time::{testing::Clock, Clock as _, Timestamp},
};
use bolero::{check, generator::*};
use core::time::Duration;
use std::collections::VecDeque;

struct SentPacketInfo<PacketInfo> {
    sent_bytes: u16,
    time_sent: Timestamp,
    cc_packet_info: PacketInfo,
}

#[derive(Debug, TypeGenerator)]
enum Operation {
    IncrementTime {
        /// The milli-second value by which to increase the timestamp
        millis: u16,
    },
    PacketSent {
        #[generator(1..=255)]
        count: u8,
        #[generator(0..=9000)]
        bytes_sent: u16,
        app_limited: Option<bool>,
    },
    AckReceived {
        index: u8,
        #[generator(1..=255)]
        count: u8,
        #[generator(1..=2000)]
        rtt: u16,
    },
    PacketLost {
        index: u8,
    },
    ExplicitCongestion {
        #[generator(1..=255)]
        ce_count: u64,
    },
    MtuUpdated {
        #[generator(1200..=9000)]
        mtu: u16,
    },
    PacketDiscarded,
}

struct Model<CC: CongestionController> {
    /// The congestion controller being fuzzed
    subject: CC,
    /// Tracks packets inflight
    sent_packets: VecDeque<SentPacketInfo<CC::PacketInfo>>,
    /// The round trip time estimator
    rtt_estimator: RttEstimator,
    /// A monotonically increasing timestamp
    timestamp: Timestamp,
}

impl<CC: CongestionController> Model<CC> {
    fn new(congestion_controller: CC) -> Self {
        Self {
            subject: congestion_controller,
            sent_packets: VecDeque::new(),
            rtt_estimator: RttEstimator::default(),
            timestamp: Clock::default().get_time(),
        }
    }

    fn apply(&mut self, operation: &Operation, rng: &mut dyn random::Generator) {
        match operation {
            Operation::IncrementTime { millis } => {
                self.timestamp += Duration::from_millis(*millis as u64);
            }
            Operation::PacketSent {
                count,
                bytes_sent,
                app_limited,
            } => {
                self.on_packet_sent(*count, *bytes_sent, *app_limited);
            }
            Operation::AckReceived { index, count, rtt } => {
                self.on_ack_received(*index, *count, Duration::from_millis(*rtt as u64), rng);
            }
            Operation::PacketLost { index } => {
                self.on_packet_lost(*index, rng);
            }
            Operation::ExplicitCongestion { ce_count } => self.on_explicit_congestion(*ce_count),
            Operation::MtuUpdated { mtu } => self.on_mtu_updated(*mtu),
            Operation::PacketDiscarded => self.on_packet_discarded(),
        }
    }

    fn on_packet_sent(&mut self, count: u8, bytes_sent: u16, app_limited: Option<bool>) {
        let mut publisher = event::testing::Publisher::no_snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());

        for _ in 0..count {
            if !self.subject.is_congestion_limited()
                || self.subject.requires_fast_retransmission()
                || bytes_sent == 0
            {
                let packet_info = self.subject.on_packet_sent(
                    self.timestamp,
                    bytes_sent as usize,
                    app_limited,
                    &self.rtt_estimator,
                    &mut publisher,
                );
                self.sent_packets.push_back(SentPacketInfo {
                    sent_bytes: bytes_sent,
                    time_sent: self.timestamp,
                    cc_packet_info: packet_info,
                });
            }
        }
    }

    fn on_ack_received(
        &mut self,
        index: u8,
        count: u8,
        rtt: Duration,
        rng: &mut dyn random::Generator,
    ) {
        let mut publisher = event::testing::Publisher::no_snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        let index = (index as usize).min(self.sent_packets.len().saturating_sub(1));
        let mut rtt_updated = false;

        // Acknowledge `count` amount of packets, starting at the random `index`
        for _ in 0..count {
            if let Some(sent_packet_info) = self.sent_packets.remove(index) {
                if sent_packet_info.sent_bytes > 0 {
                    // Update the RTT once for each ack range received
                    if !rtt_updated {
                        self.on_rtt_updated(sent_packet_info.time_sent, rtt);
                        rtt_updated = true;
                    }

                    // `recovery::Manager` does not call `on_ack` if sent_bytes = 0
                    self.subject.on_ack(
                        sent_packet_info.time_sent,
                        sent_packet_info.sent_bytes as usize,
                        sent_packet_info.cc_packet_info,
                        &self.rtt_estimator,
                        rng,
                        self.timestamp,
                        &mut publisher,
                    );
                }
            } else {
                break;
            }
        }
    }

    fn on_packet_lost(&mut self, index: u8, rng: &mut dyn random::Generator) {
        let mut publisher = event::testing::Publisher::no_snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        let index = (index as usize).min(self.sent_packets.len().saturating_sub(1));

        // Report the packet at the random `index` as lost
        if let Some(sent_packet_info) = self.sent_packets.remove(index) {
            if sent_packet_info.sent_bytes > 0 {
                // `recovery::Manager` does not call `on_packet_lost` if sent_bytes = 0
                self.subject.on_packet_lost(
                    sent_packet_info.sent_bytes as u32,
                    sent_packet_info.cc_packet_info,
                    false,
                    false,
                    rng,
                    self.timestamp,
                    &mut publisher,
                );
            }
        }
    }

    fn on_rtt_updated(&mut self, time_sent: Timestamp, rtt: Duration) {
        let mut publisher = event::testing::Publisher::no_snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());

        self.rtt_estimator.update_rtt(
            Duration::ZERO,
            rtt,
            self.timestamp,
            false,
            PacketNumberSpace::Initial,
        );
        self.subject.on_rtt_update(
            time_sent,
            self.timestamp,
            &self.rtt_estimator,
            &mut publisher,
        );
    }

    fn on_explicit_congestion(&mut self, ce_count: u64) {
        let mut publisher = event::testing::Publisher::no_snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        let ce_count = ce_count.min(self.sent_packets.len() as u64);
        self.subject
            .on_explicit_congestion(ce_count, self.timestamp, &mut publisher)
    }

    fn on_packet_discarded(&mut self) {
        let mut publisher = event::testing::Publisher::no_snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        if let Some(sent_packet_info) = self.sent_packets.pop_front() {
            self.subject
                .on_packet_discarded(sent_packet_info.sent_bytes as usize, &mut publisher)
        }
    }

    fn on_mtu_updated(&mut self, mtu: u16) {
        let mut publisher = event::testing::Publisher::no_snapshot();
        let mut publisher = PathPublisher::new(&mut publisher, path::Id::test_id());
        self.subject.on_mtu_update(mtu, &mut publisher)
    }

    fn invariants(&self) {
        let bytes_in_flight: u32 = self
            .sent_packets
            .iter()
            .map(|sent_packet_info| sent_packet_info.sent_bytes as u32)
            .sum();

        assert_eq!(bytes_in_flight, self.subject.bytes_in_flight());
    }
}

#[cfg_attr(miri, ignore)]
#[test]
fn cubic_fuzz() {
    check!()
        .with_generator((MINIMUM_MTU..=9000, gen(), gen::<Vec<Operation>>()))
        .for_each(|(max_datagram_size, seed, operations)| {
            let mut model = Model::new(CubicCongestionController::new(*max_datagram_size));
            let mut rng = random::testing::Generator(*seed);

            for operation in operations.iter() {
                model.apply(operation, &mut rng);
            }

            model.invariants();
        });
}

#[cfg_attr(miri, ignore)]
#[test]
fn bbr_fuzz() {
    check!()
        .with_generator((MINIMUM_MTU..=9000, gen(), gen::<Vec<Operation>>()))
        .for_each(|(max_datagram_size, seed, operations)| {
            let mut model = Model::new(BbrCongestionController::new(*max_datagram_size));
            let mut rng = random::testing::Generator(*seed);

            for operation in operations.iter() {
                model.apply(operation, &mut rng);
            }

            model.invariants();
        });
}