//! Per-peer jitter buffer with Opus packet-loss concealment.
//!
//! Incoming audio arrives as unreliable QUIC datagrams that can be reordered,
//! duplicated, or dropped on real networks. Each packet carries a monotonic
//! sequence number (assigned by the sender). This buffer reorders packets by
//! sequence, holds an *adaptive* playout delay to absorb jitter, and — when a
//! sequence is missing but later packets have already arrived — synthesizes a
//! concealment frame via Opus PLC instead of emitting a click of silence.
//!
//! ## Adaptive playout delay
//!
//! The playout delay (how many frames we accumulate before (re)starting
//! playout) is a feedback controller, not a fixed constant. It reacts to the
//! buffer's own observations, with no wall clock required:
//!
//! * **Grow** (jitter beat the cushion): a late-arriving packet (one for a
//!   sequence we already played past) or a gap that forced Opus PLC each bumps
//!   the target up one frame.
//! * **Shrink** (comfortably ahead): a long unbroken run of real decoded frames
//!   shaves the target back down one frame.
//!
//! Growth is fast and shrink is slow (AIMD-style) so we react to badness
//! immediately but reclaim latency cautiously. Benign silence — a talker
//! pausing, so packets simply stop — produces none of these signals, so the
//! target is left untouched across quiet stretches.

use crate::codec::{AudioDecoder, CodecError, opus_impl::OpusDecoder};
use opus::Channels;
use std::collections::BTreeMap;

/// Samples per channel in one transmitted frame (20ms @ 48kHz mono).
pub const FRAME_SAMPLES: usize = 960;

/// Starting (and most common) playout delay (~60ms): the number of frames to
/// accumulate before playout begins. The adaptive controller moves the live
/// target up and down from here within `[MIN_DELAY_FRAMES, MAX_DELAY_FRAMES]`.
const DEFAULT_DELAY_FRAMES: usize = 3;

/// Floor for the adaptive delay (~40ms). Below this there's no slack left to
/// reorder even a single packet, so we never shrink past it.
const MIN_DELAY_FRAMES: usize = 2;

/// Ceiling for the adaptive delay (~240ms). Kept well under
/// `MAX_BUFFERED_FRAMES` so a deep cushion still leaves reorder headroom, and
/// bounded so a pathological link can't drive playout latency unboundedly.
const MAX_DELAY_FRAMES: usize = 12;

/// Consecutive cleanly-played real frames (~5s) required to shave one frame off
/// the target. Deliberately long so we reclaim latency slowly and don't flap.
const CLEAN_RUN_TO_SHRINK: usize = 250;

/// While buffering, prime playout after this many `pop_frame` polls even if the
/// target delay isn't met yet (~500ms; the mixer polls every 20ms). This
/// rescues a short utterance that never reaches a grown target, and bounds the
/// worst-case startup latency.
const PRIME_TIMEOUT_TICKS: usize = 25;

/// Hard cap on buffered frames (~640ms). If we ever exceed this we've fallen
/// badly behind, so we drop the oldest and resync rather than grow unbounded.
const MAX_BUFFERED_FRAMES: usize = 32;

/// Sequence discontinuities larger than this (~10s at 20ms/frame) are treated
/// as a restarted/new stream, not ordinary packet loss or reordering.
const MAX_REASONABLE_SEQ_GAP: u32 = 500;

pub struct JitterBuffer {
    decoder: OpusDecoder,
    /// Reorder window: sequence number -> encoded Opus payload.
    packets: BTreeMap<u32, Vec<u8>>,
    /// Next sequence we expect to play. `None` means idle/buffering: we are
    /// waiting to accumulate `target_delay` frames before (re)starting playout.
    next_seq: Option<u32>,
    /// Live adaptive playout delay, in frames. Moved by the controller within
    /// `[MIN_DELAY_FRAMES, MAX_DELAY_FRAMES]`.
    target_delay: usize,
    /// Consecutive cleanly-played real frames since the last disruption; drives
    /// the slow shrink toward `MIN_DELAY_FRAMES`.
    clean_run: usize,
    /// `pop_frame` polls spent buffering with packets present; drives the
    /// `PRIME_TIMEOUT_TICKS` safety prime.
    buffering_ticks: usize,
}

/// Wrapping-aware "is `a` strictly before `b`" for sequence numbers.
fn seq_before(a: u32, b: u32) -> bool {
    a != b && b.wrapping_sub(a) < (1 << 31)
}

impl JitterBuffer {
    pub fn new() -> Result<Self, CodecError> {
        Ok(Self {
            decoder: OpusDecoder::new(48000, Channels::Mono, FRAME_SAMPLES)?,
            packets: BTreeMap::new(),
            next_seq: None,
            target_delay: DEFAULT_DELAY_FRAMES,
            clean_run: 0,
            buffering_ticks: 0,
        })
    }

    /// Current adaptive playout delay, in frames. Exposed for metrics/tests.
    pub fn target_delay(&self) -> usize {
        self.target_delay
    }

    /// Grow the playout delay one frame (bounded): jitter beat the cushion, so
    /// next time we (re)prime we hold a deeper buffer. Resets the clean run.
    fn note_disruption(&mut self) {
        self.target_delay = (self.target_delay + 1).min(MAX_DELAY_FRAMES);
        self.clean_run = 0;
    }

    /// Count one cleanly-played real frame; after a long unbroken run, shave one
    /// frame off the delay (bounded below) to reclaim latency on a calm link.
    fn note_clean(&mut self) {
        self.clean_run += 1;
        if self.clean_run >= CLEAN_RUN_TO_SHRINK {
            self.target_delay = self.target_delay.saturating_sub(1).max(MIN_DELAY_FRAMES);
            self.clean_run = 0;
        }
    }

    fn reset_to_stream(&mut self, seq: u32, payload: Vec<u8>) {
        self.packets.clear();
        self.packets.insert(seq, payload);
        self.next_seq = None;
        self.clean_run = 0;
        self.buffering_ticks = 0;
    }

    /// Store a received packet, dropping ones we've already played past and
    /// bounding total depth.
    pub fn insert(&mut self, seq: u32, payload: Vec<u8>) {
        // Too late: this sequence has already been played (or concealed). Its
        // arrival after the playout head means our cushion was too shallow.
        if let Some(next) = self.next_seq
            && seq_before(seq, next)
        {
            if next.wrapping_sub(seq) > MAX_REASONABLE_SEQ_GAP {
                self.reset_to_stream(seq, payload);
                return;
            }
            self.note_disruption();
            return;
        }
        if let Some(next) = self.next_seq
            && seq.wrapping_sub(next) > MAX_REASONABLE_SEQ_GAP
        {
            self.reset_to_stream(seq, payload);
            return;
        }
        self.packets.insert(seq, payload);

        while self.packets.len() > MAX_BUFFERED_FRAMES {
            let oldest = *self.packets.keys().next().expect("non-empty");
            self.packets.remove(&oldest);
            // We've discarded backlog; resync the playout head to the new front.
            self.next_seq = self.packets.keys().next().copied();
            // The resync breaks sequence continuity; restart the clean run.
            self.clean_run = 0;
        }
    }

    /// Produce the next 20ms PCM frame for playout, or `None` when idle or
    /// still buffering (the caller should treat `None` as silence).
    pub fn pop_frame(&mut self) -> Option<Vec<i16>> {
        match self.next_seq {
            None => {
                // Idle with nothing buffered: genuinely silent, no prime pending.
                if self.packets.is_empty() {
                    self.buffering_ticks = 0;
                    return None;
                }
                // Buffering: prime once we've accumulated the adaptive target, or
                // after a bounded wait so a short utterance isn't held forever.
                self.buffering_ticks += 1;
                if self.packets.len() >= self.target_delay
                    || self.buffering_ticks >= PRIME_TIMEOUT_TICKS
                {
                    self.buffering_ticks = 0;
                    self.clean_run = 0;
                    self.next_seq = self.packets.keys().next().copied();
                    self.pop_frame()
                } else {
                    None
                }
            }
            Some(next) => {
                if let Some(payload) = self.packets.remove(&next) {
                    self.next_seq = Some(next.wrapping_add(1));
                    let frame = self.decoder.decode(Some(&payload)).ok();
                    // A real, in-order frame played: the link is keeping up.
                    self.note_clean();
                    frame
                } else if self.packets.is_empty() {
                    // Underrun: the talker has gone quiet (or stopped). Go idle
                    // and re-buffer before resuming, rather than concealing forever.
                    // This is benign (silence), so we don't grow the delay; just
                    // end the clean run since playout is breaking.
                    self.next_seq = None;
                    self.clean_run = 0;
                    None
                } else {
                    // Gap with later packets already buffered: a packet was lost
                    // or reordered out of window. Try Opus in-band FEC from the
                    // packet right after the gap; if that packet isn't buffered
                    // (burst loss) or FEC fails, fall back to plain PLC.
                    self.next_seq = Some(next.wrapping_add(1));
                    self.note_disruption();
                    let (&smallest, next_payload) = self.packets.iter().next().expect("non-empty");
                    if fec_covers_gap(next, smallest) {
                        self.decoder
                            .decode_fec(next_payload)
                            .or_else(|_| self.decoder.decode(None))
                            .ok()
                    } else {
                        self.decoder.decode(None).ok()
                    }
                }
            }
        }
    }

    /// True when nothing is buffered and playout is idle (talker silent).
    pub fn is_idle(&self) -> bool {
        self.next_seq.is_none() && self.packets.is_empty()
    }
}

/// Opus in-band FEC in packet N carries a low-fidelity copy of frame N-1 and
/// nothing else — a lost frame `next` is FEC-recoverable solely from packet
/// `next+1`. Any later successor's FEC data is a different frame's audio, and
/// splicing it into this gap plays sound from the wrong position; the caller
/// must conceal with plain PLC instead.
fn fec_covers_gap(next: u32, smallest_buffered: u32) -> bool {
    smallest_buffered == next.wrapping_add(1)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::codec::opus_impl::{OpusDecoder, OpusEncoder, OpusParams};
    use crate::codec::{AudioDecoder, AudioEncoder};
    use opus::{Application, Channels};

    /// A real, decodable Opus packet for one 20ms mono frame at amplitude `amp`.
    fn frame(enc: &mut OpusEncoder, amp: i16) -> Vec<u8> {
        let pcm: Vec<i16> = (0..FRAME_SAMPLES)
            .map(|i| if i % 2 == 0 { amp } else { -amp })
            .collect();
        enc.encode(&pcm).unwrap()
    }

    fn tone_frame(enc: &mut OpusEncoder, amp: i16, frame_index: usize) -> Vec<u8> {
        let pcm: Vec<i16> = (0..FRAME_SAMPLES)
            .map(|i| {
                let sample_index = frame_index * FRAME_SAMPLES + i;
                let t = sample_index as f32 / 48_000.0;
                let fundamental = (t * 220.0 * 2.0 * std::f32::consts::PI).sin();
                let harmonic = (t * 440.0 * 2.0 * std::f32::consts::PI).sin();
                ((fundamental * 0.7 + harmonic * 0.3) * amp as f32) as i16
            })
            .collect();
        enc.encode(&pcm).unwrap()
    }

    fn rms_error(a: &[i16], b: &[i16]) -> f64 {
        assert_eq!(a.len(), b.len());
        let sum_sq: f64 = a
            .iter()
            .zip(b)
            .map(|(&left, &right)| {
                let diff = left as f64 - right as f64;
                diff * diff
            })
            .sum();
        (sum_sq / a.len() as f64).sqrt()
    }

    #[test]
    fn buffers_then_plays_in_order() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        let mut jb = JitterBuffer::new().unwrap();

        // Below the target delay, playout hasn't primed yet.
        jb.insert(0, frame(&mut enc, 1000));
        assert!(jb.pop_frame().is_none());

        // Reaching the target delay primes playout and yields the first frame.
        jb.insert(1, frame(&mut enc, 1000));
        jb.insert(2, frame(&mut enc, 1000));
        assert_eq!(jb.pop_frame().map(|f| f.len()), Some(FRAME_SAMPLES));
        assert_eq!(jb.pop_frame().map(|f| f.len()), Some(FRAME_SAMPLES));
        assert_eq!(jb.pop_frame().map(|f| f.len()), Some(FRAME_SAMPLES));
        // Drained: idle again.
        assert!(jb.pop_frame().is_none());
        assert!(jb.is_idle());
    }

    #[test]
    fn reorders_out_of_order_arrivals() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        let mut jb = JitterBuffer::new().unwrap();

        // Arrive scrambled but within the buffering window.
        jb.insert(2, frame(&mut enc, 800));
        jb.insert(0, frame(&mut enc, 800));
        jb.insert(1, frame(&mut enc, 800));

        // Three real frames come out (in sequence order), then idle.
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_none());
    }

    #[test]
    fn conceals_gap_when_later_packets_present() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        let mut jb = JitterBuffer::new().unwrap();

        // Seq 2 is missing, but 0,1,3 arrive — enough to prime.
        jb.insert(0, frame(&mut enc, 1200));
        jb.insert(1, frame(&mut enc, 1200));
        jb.insert(3, frame(&mut enc, 1200));

        assert!(jb.pop_frame().is_some()); // seq 0
        assert!(jb.pop_frame().is_some()); // seq 1
        // seq 2 missing but seq 3 buffered -> Opus PLC produces a concealment frame.
        let concealed = jb.pop_frame();
        assert_eq!(concealed.map(|f| f.len()), Some(FRAME_SAMPLES));
        assert!(jb.pop_frame().is_some()); // seq 3
        assert!(jb.pop_frame().is_none());
    }

    #[test]
    fn uses_in_band_fec_from_next_packet_for_gap() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        enc.apply_params(&OpusParams {
            bitrate: 20_000,
            inband_fec: true,
            packet_loss_perc: 60,
            dtx: false,
        })
        .unwrap();

        let dropped_seq = 5usize;
        let amps = [1800, 1800, 1800, 1800, 1800, 12_000, 12_000, 12_000];
        let packets: Vec<Vec<u8>> = amps
            .into_iter()
            .enumerate()
            .map(|(seq, amp)| tone_frame(&mut enc, amp, seq))
            .collect();

        let mut expected_decoder = OpusDecoder::new(48000, Channels::Mono, FRAME_SAMPLES).unwrap();
        for packet in packets.iter().take(dropped_seq) {
            expected_decoder.decode(Some(packet)).unwrap();
        }
        let expected_lost = expected_decoder
            .decode(Some(&packets[dropped_seq]))
            .unwrap();

        let mut plc_decoder = OpusDecoder::new(48000, Channels::Mono, FRAME_SAMPLES).unwrap();
        for packet in packets.iter().take(dropped_seq) {
            plc_decoder.decode(Some(packet)).unwrap();
        }
        let pure_plc = plc_decoder.decode(None).unwrap();

        let mut jb = JitterBuffer::new().unwrap();
        for (seq, packet) in packets.iter().enumerate() {
            if seq != dropped_seq {
                jb.insert(seq as u32, packet.clone());
            }
        }

        for _ in 0..dropped_seq {
            assert_eq!(jb.pop_frame().map(|frame| frame.len()), Some(FRAME_SAMPLES));
        }

        let recovered = jb.pop_frame().expect("gap should be reconstructed");
        assert_eq!(recovered.len(), FRAME_SAMPLES);
        assert!(
            jb.packets.contains_key(&(dropped_seq as u32 + 1)),
            "FEC source packet must remain buffered for normal decode"
        );
        assert_eq!(jb.pop_frame().map(|frame| frame.len()), Some(FRAME_SAMPLES));

        let fec_error = rms_error(&recovered, &expected_lost);
        let plc_error = rms_error(&pure_plc, &expected_lost);
        assert!(
            fec_error < plc_error * 0.75,
            "FEC reconstruction should be materially closer than PLC (fec_error={fec_error}, plc_error={plc_error})"
        );
    }

    #[test]
    fn fec_covers_gap_only_for_the_immediate_successor() {
        // Packet next+1 is the only one whose in-band FEC describes frame `next`.
        assert!(fec_covers_gap(4, 5));
        // A burst gap: the smallest survivor's FEC is some other frame's audio.
        assert!(!fec_covers_gap(3, 5));
        assert!(!fec_covers_gap(3, 3_000));
        // Sequence wraparound still counts as adjacent.
        assert!(fec_covers_gap(u32::MAX, 0));
    }

    #[test]
    fn burst_gap_falls_back_to_plc_not_wrong_position_fec() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        enc.apply_params(&OpusParams {
            bitrate: 20_000,
            inband_fec: true,
            packet_loss_perc: 60,
            dtx: false,
        })
        .unwrap();

        // Frames 0..=6; 3 and 4 are lost as a burst, so when playout reaches
        // seq 3 the smallest buffered packet is 5 — whose FEC data is frame 4,
        // NOT frame 3. The buffer must conceal 3 with plain PLC rather than
        // splice frame 4's audio into the wrong position.
        let packets: Vec<Vec<u8>> = (0..7).map(|seq| tone_frame(&mut enc, 8_000, seq)).collect();

        // Twin decoder replaying the exact call sequence the jitter buffer
        // should make for seq 3: decode 0,1,2 then a plain PLC conceal.
        let mut twin = OpusDecoder::new(48000, Channels::Mono, FRAME_SAMPLES).unwrap();
        for packet in packets.iter().take(3) {
            twin.decode(Some(packet)).unwrap();
        }
        let expected_plc = twin.decode(None).unwrap();

        let mut jb = JitterBuffer::new().unwrap();
        for (seq, packet) in packets.iter().enumerate() {
            if seq != 3 && seq != 4 {
                jb.insert(seq as u32, packet.clone());
            }
        }

        for _ in 0..3 {
            assert_eq!(jb.pop_frame().map(|frame| frame.len()), Some(FRAME_SAMPLES));
        }

        // Seq 3: burst gap — bit-exact PLC (same decoder state, same inputs),
        // which decode_fec(packet 5) could never produce.
        let concealed = jb.pop_frame().expect("gap should be concealed");
        assert_eq!(concealed, expected_plc, "burst gap must use plain PLC");

        // Seq 4: packet 5 IS the immediate successor, so its FEC data is
        // frame 4's audio — the correctly-positioned recovery still applies.
        let recovered = jb
            .pop_frame()
            .expect("adjacent gap should be reconstructed");
        let mut fec_twin = OpusDecoder::new(48000, Channels::Mono, FRAME_SAMPLES).unwrap();
        for packet in packets.iter().take(3) {
            fec_twin.decode(Some(packet)).unwrap();
        }
        fec_twin.decode(None).unwrap();
        let expected_fec = fec_twin.decode_fec(&packets[5]).unwrap();
        assert_eq!(recovered, expected_fec, "adjacent gap should still use FEC");

        // Then 5 and 6 play normally.
        assert_eq!(jb.pop_frame().map(|frame| frame.len()), Some(FRAME_SAMPLES));
        assert_eq!(jb.pop_frame().map(|frame| frame.len()), Some(FRAME_SAMPLES));
        assert!(jb.pop_frame().is_none());
    }

    #[test]
    fn drops_packets_already_played() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        let mut jb = JitterBuffer::new().unwrap();

        jb.insert(5, frame(&mut enc, 600));
        jb.insert(6, frame(&mut enc, 600));
        jb.insert(7, frame(&mut enc, 600));
        assert!(jb.pop_frame().is_some()); // primes at seq 5, plays 5
        assert!(jb.pop_frame().is_some()); // 6

        // A straggler for an already-played sequence must be discarded.
        jb.insert(5, frame(&mut enc, 600));
        assert_eq!(jb.packets.len(), 1); // only seq 7 remains buffered
    }

    #[test]
    fn far_behind_sequence_resets_as_restarted_stream() {
        let mut jb = JitterBuffer::new().unwrap();
        jb.next_seq = Some(5_000);
        jb.packets.insert(5_000, vec![9]);
        jb.clean_run = 12;
        jb.buffering_ticks = 4;

        jb.insert(0, vec![1]);

        assert_eq!(jb.next_seq, None);
        assert_eq!(jb.packets.len(), 1);
        assert_eq!(jb.packets.get(&0).map(Vec::as_slice), Some(&[1][..]));
        assert_eq!(jb.clean_run, 0);
        assert_eq!(jb.buffering_ticks, 0);
    }

    #[test]
    fn far_ahead_sequence_resets_to_bound_plc_run() {
        let mut jb = JitterBuffer::new().unwrap();
        jb.next_seq = Some(10);
        jb.packets.insert(10, vec![9]);
        jb.clean_run = 12;
        jb.buffering_ticks = 4;

        let jumped_seq = 10 + MAX_REASONABLE_SEQ_GAP + 1;
        jb.insert(jumped_seq, vec![2]);

        assert_eq!(jb.next_seq, None);
        assert_eq!(jb.packets.len(), 1);
        assert_eq!(
            jb.packets.get(&jumped_seq).map(Vec::as_slice),
            Some(&[2][..])
        );
        assert_eq!(jb.clean_run, 0);
        assert_eq!(jb.buffering_ticks, 0);
    }

    #[test]
    fn test_seq_before_ordering() {
        // Basic ordering
        assert!(seq_before(0, 1));
        assert!(!seq_before(1, 0));
        assert!(!seq_before(5, 5));
        assert!(seq_before(100, 101));
        assert!(!seq_before(101, 100));

        // Wraparound
        assert!(seq_before(u32::MAX, 0));
        assert!(!seq_before(0, u32::MAX));

        // Half-range boundary
        assert!(seq_before(0, 0x7FFF_FFFF));
        assert!(!seq_before(0, 0x8000_0000));
    }

    #[test]
    fn test_jitter_buffer_overflow_resync() {
        let mut jb = JitterBuffer::new().unwrap();
        assert!(jb.next_seq.is_none());

        let count = MAX_BUFFERED_FRAMES + 1;
        for seq in 0..count {
            jb.insert(seq as u32, vec![0u8]);
        }

        assert_eq!(jb.packets.len(), MAX_BUFFERED_FRAMES);
        assert!(!jb.packets.contains_key(&0));
        assert_eq!(jb.next_seq, Some(1));
    }

    #[test]
    fn reprimes_after_underrun_idle() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        let mut jb = JitterBuffer::new().unwrap();

        // Prime with seq 0, 1, 2
        jb.insert(0, frame(&mut enc, 1000));
        jb.insert(1, frame(&mut enc, 1000));
        jb.insert(2, frame(&mut enc, 1000));

        // pop_frame() 3x -> 3 Some
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_some());

        // A 4th pop_frame() -> None, and jb.is_idle() is true
        assert!(jb.pop_frame().is_none());
        assert!(jb.is_idle());

        // Insert ONE new frame (seq 3): pop_frame() must still be None (must re-accumulate TARGET_DELAY_FRAMES)
        jb.insert(3, frame(&mut enc, 1000));
        assert!(jb.pop_frame().is_none());
        assert!(!jb.is_idle());

        // Insert seq 4 and 5 (now 3 buffered) -> pop_frame() yields Some (re-primed)
        jb.insert(4, frame(&mut enc, 1000));
        jb.insert(5, frame(&mut enc, 1000));
        assert!(jb.pop_frame().is_some());
    }

    #[test]
    fn duplicate_insert_does_not_grow_buffer() {
        let mut jb = JitterBuffer::new().unwrap();
        jb.insert(0, vec![0u8]);
        jb.insert(0, vec![1u8]);
        assert_eq!(jb.packets.len(), 1);
    }

    #[test]
    fn is_idle_reflects_buffer_state() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        let mut jb = JitterBuffer::new().unwrap();

        // Fresh buffer
        assert!(jb.is_idle());

        // After a single insert
        jb.insert(0, frame(&mut enc, 1000));
        assert!(!jb.is_idle());

        // Prime (3 frames)
        jb.insert(1, frame(&mut enc, 1000));
        jb.insert(2, frame(&mut enc, 1000));

        // Drain past the end so it underruns
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_none());

        assert!(jb.is_idle());
    }

    #[test]
    fn overflow_resync_while_playing() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        let mut jb = JitterBuffer::new().unwrap();

        // Prime with seq 0, 1, 2
        jb.insert(0, frame(&mut enc, 1000));
        jb.insert(1, frame(&mut enc, 1000));
        jb.insert(2, frame(&mut enc, 1000));

        // pop_frame() twice (now next_seq == Some(2))
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_some());
        assert_eq!(jb.next_seq, Some(2));

        // Insert a contiguous run of higher sequences to exceed MAX_BUFFERED_FRAMES
        let start = 3;
        let end = 3 + MAX_BUFFERED_FRAMES + 2;
        for seq in start..end {
            jb.insert(seq as u32, frame(&mut enc, 1000));
        }

        assert_eq!(jb.packets.len(), MAX_BUFFERED_FRAMES);
        assert_eq!(jb.next_seq, jb.packets.keys().next().copied());
    }

    // ---- Adaptive playout delay ------------------------------------------

    #[test]
    fn starts_at_default_delay() {
        let jb = JitterBuffer::new().unwrap();
        assert_eq!(jb.target_delay(), DEFAULT_DELAY_FRAMES);
    }

    #[test]
    fn grows_delay_on_late_arrival() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        let mut jb = JitterBuffer::new().unwrap();

        // Prime and play two frames so the playout head sits at seq 2.
        jb.insert(0, frame(&mut enc, 1000));
        jb.insert(1, frame(&mut enc, 1000));
        jb.insert(2, frame(&mut enc, 1000));
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_some());
        assert_eq!(jb.next_seq, Some(2));
        assert_eq!(jb.target_delay(), DEFAULT_DELAY_FRAMES);

        // A packet for an already-played sequence arrives too late: grow by one.
        jb.insert(0, vec![0u8]);
        assert_eq!(jb.target_delay(), DEFAULT_DELAY_FRAMES + 1);
        // The stale payload was dropped, not buffered.
        assert!(!jb.packets.contains_key(&0));
    }

    #[test]
    fn grows_delay_on_gap_conceal() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        let mut jb = JitterBuffer::new().unwrap();

        // Seq 2 is missing but 0, 1, 3 arrive — enough to prime.
        jb.insert(0, frame(&mut enc, 1200));
        jb.insert(1, frame(&mut enc, 1200));
        jb.insert(3, frame(&mut enc, 1200));

        assert!(jb.pop_frame().is_some()); // seq 0 (clean)
        assert!(jb.pop_frame().is_some()); // seq 1 (clean)
        assert_eq!(jb.target_delay(), DEFAULT_DELAY_FRAMES);

        // Seq 2 missing with seq 3 buffered -> PLC conceal -> grow by one.
        assert!(jb.pop_frame().is_some());
        assert_eq!(jb.target_delay(), DEFAULT_DELAY_FRAMES + 1);
    }

    #[test]
    fn silence_does_not_change_delay() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        let mut jb = JitterBuffer::new().unwrap();

        // A clean short utterance that drains to an underrun (talker stops).
        jb.insert(0, frame(&mut enc, 1000));
        jb.insert(1, frame(&mut enc, 1000));
        jb.insert(2, frame(&mut enc, 1000));
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_some());

        // Underrun + further idle polls must leave the delay untouched: a quiet
        // talker is not a network problem.
        assert!(jb.pop_frame().is_none());
        assert!(jb.is_idle());
        assert!(jb.pop_frame().is_none());
        assert!(jb.pop_frame().is_none());
        assert_eq!(jb.target_delay(), DEFAULT_DELAY_FRAMES);
    }

    #[test]
    fn shrinks_delay_after_clean_run() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        let mut jb = JitterBuffer::new().unwrap();

        // Steady state: keep the cushion topped up so every pop yields a real,
        // in-order frame (no conceal, no underrun, no overflow).
        let mut next = 0u32;
        for _ in 0..DEFAULT_DELAY_FRAMES {
            jb.insert(next, frame(&mut enc, 800));
            next += 1;
        }
        for _ in 0..CLEAN_RUN_TO_SHRINK {
            assert!(jb.pop_frame().is_some());
            jb.insert(next, frame(&mut enc, 800));
            next += 1;
        }

        // One clean run's worth of frames shaves exactly one off the delay.
        assert_eq!(jb.target_delay(), DEFAULT_DELAY_FRAMES - 1);
    }

    #[test]
    fn delay_is_bounded_above() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        let mut jb = JitterBuffer::new().unwrap();

        // Prime and advance the head, then hammer late arrivals.
        jb.insert(0, frame(&mut enc, 500));
        jb.insert(1, frame(&mut enc, 500));
        jb.insert(2, frame(&mut enc, 500));
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_some());

        for _ in 0..100 {
            jb.insert(0, vec![0u8]); // always "too late" -> disruption
        }
        assert_eq!(jb.target_delay(), MAX_DELAY_FRAMES);
    }

    #[test]
    fn delay_is_bounded_below() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        let mut jb = JitterBuffer::new().unwrap();

        // Many clean runs would shrink forever; it must stop at the floor.
        let mut next = 0u32;
        for _ in 0..DEFAULT_DELAY_FRAMES {
            jb.insert(next, frame(&mut enc, 800));
            next += 1;
        }
        for _ in 0..(CLEAN_RUN_TO_SHRINK * 4) {
            assert!(jb.pop_frame().is_some());
            jb.insert(next, frame(&mut enc, 800));
            next += 1;
            assert!(jb.target_delay() >= MIN_DELAY_FRAMES);
        }
        assert_eq!(jb.target_delay(), MIN_DELAY_FRAMES);
    }

    #[test]
    fn prime_timeout_rescues_short_utterance() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        let mut jb = JitterBuffer::new().unwrap();

        // Drive the target above what a short utterance can reach.
        jb.insert(0, frame(&mut enc, 500));
        jb.insert(1, frame(&mut enc, 500));
        jb.insert(2, frame(&mut enc, 500));
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_some());
        while jb.target_delay() < 6 {
            jb.insert(0, vec![0u8]);
        }
        assert!(jb.pop_frame().is_some()); // drain seq 2
        assert!(jb.pop_frame().is_none()); // underrun -> idle
        assert!(jb.is_idle());

        // A 2-frame utterance is below the grown target of 6, so only the
        // timeout can start it — and it must, exactly at PRIME_TIMEOUT_TICKS.
        jb.insert(100, frame(&mut enc, 700));
        jb.insert(101, frame(&mut enc, 700));
        let mut polls = 0;
        loop {
            polls += 1;
            assert!(polls <= PRIME_TIMEOUT_TICKS, "must prime by the timeout");
            if jb.pop_frame().is_some() {
                break;
            }
        }
        assert_eq!(polls, PRIME_TIMEOUT_TICKS);
    }

    #[test]
    fn grown_target_requires_deeper_reprime() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        let mut jb = JitterBuffer::new().unwrap();

        assert_eq!(jb.target_delay(), DEFAULT_DELAY_FRAMES);

        // Prime with seq 0,1,2
        jb.insert(0, frame(&mut enc, 1000));
        jb.insert(1, frame(&mut enc, 1000));
        jb.insert(2, frame(&mut enc, 1000));

        // pop_frame() twice -> head now at seq 2
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_some());
        assert_eq!(jb.next_seq, Some(2));
        assert_eq!(jb.target_delay(), DEFAULT_DELAY_FRAMES);

        // A late packet for an already-played sequence (0) arrives: grows delay to 4
        jb.insert(0, vec![0u8]);
        assert_eq!(jb.target_delay(), 4);

        // Drain: plays seq 2, then underruns (goes idle)
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_none());
        assert!(jb.is_idle());

        // Insert three fresh contiguous frames (seq 100, 101, 102)
        jb.insert(100, frame(&mut enc, 1000));
        jb.insert(101, frame(&mut enc, 1000));
        jb.insert(102, frame(&mut enc, 1000));

        // Playout must NOT prime yet (3 < grown target of 4)
        assert!(jb.pop_frame().is_none());
        assert!(!jb.is_idle());

        // Insert a fourth frame (seq 103) -> primes and plays seq 100
        jb.insert(103, frame(&mut enc, 1000));
        assert!(jb.pop_frame().is_some());
    }

    #[test]
    fn overflow_resync_resets_clean_run() {
        let mut enc = OpusEncoder::new(48000, Channels::Mono, Application::Voip).unwrap();
        let mut jb = JitterBuffer::new().unwrap();

        // Prime with seq 0,1,2
        jb.insert(0, frame(&mut enc, 1000));
        jb.insert(1, frame(&mut enc, 1000));
        jb.insert(2, frame(&mut enc, 1000));

        // Play a few in-order real frames so clean_run > 0
        assert!(jb.pop_frame().is_some());
        assert!(jb.pop_frame().is_some());
        assert!(jb.clean_run > 0);

        // Insert a contiguous run long enough to exceed MAX_BUFFERED_FRAMES
        // packets currently contains seq 2 (length 1).
        // Inserting seq 3..=34 (32 frames) makes total length 33, exceeding MAX_BUFFERED_FRAMES (32)
        for seq in 3..=34 {
            jb.insert(seq, frame(&mut enc, 1000));
        }

        // Assert overflow occurred and reset clean_run
        assert_eq!(jb.clean_run, 0);
    }
}
