use parking_lot::Mutex;
use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::atomic::{AtomicI32, AtomicU64, Ordering};
use std::time::Instant;

use crate::protocol::{
    AggregateInterval, StreamInterval, StreamResult, TcpInfoSnapshot, UdpIntervalProgress, UdpStats,
};

/// Maximum number of intervals to keep in history (1 minute at 1-second intervals)
const MAX_INTERVAL_HISTORY: usize = 60;

pub struct StreamStats {
    pub stream_id: u8,
    pub bytes_sent: AtomicU64,
    pub bytes_received: AtomicU64,
    pub start_time: Instant,
    pub intervals: Mutex<VecDeque<IntervalStats>>,
    pub retransmits: AtomicU64,
    pub interval_retransmits_total: AtomicU64,
    pub last_bytes: AtomicU64,
    // Per-direction `last_bytes` mirrors for bidir interval deltas. When the
    // test is unidirectional, one of these stays at 0 and `last_bytes` is
    // sufficient on its own.
    pub last_bytes_sent: AtomicU64,
    pub last_bytes_received: AtomicU64,
    pub last_tcp_retransmits: AtomicU64,
    // UDP stats (updated live by receiver)
    pub udp_jitter_us: AtomicU64, // Jitter in microseconds (convert to ms when reading)
    pub udp_lost: AtomicU64,      // Total lost packets
    pub last_udp_lost: AtomicU64, // For interval calculation
    /// Cumulative UDP packets received on this stream — counted only on
    /// successful header decode (valid xfr packets). Junk/foreign datagrams
    /// still bump `bytes_received` because they consumed wire bandwidth, but
    /// must not dilute the loss denominator. Used by the aggregator to
    /// compute a running loss percentage that matches what the sequence
    /// tracker actually saw.
    pub udp_packets_received: AtomicU64,
    /// Raw file descriptor for TCP_INFO polling (-1 = not set)
    pub tcp_info_fd: AtomicI32,
    /// Final TCP_INFO snapshot captured when the stream task exits.
    /// Survives after clear_tcp_info_fd(), so the Result handler can read it.
    final_tcp_info: Mutex<Option<TcpInfoSnapshot>>,
}

#[derive(Debug, Clone)]
pub struct IntervalStats {
    pub timestamp: Instant,
    pub bytes: u64,
    pub throughput_mbps: f64,
    pub retransmits: u64,
    pub jitter_ms: f64,
    pub lost: u64,
    pub rtt_us: Option<u32>,
    pub cwnd: Option<u32>,
    /// Per-direction interval deltas. Zero for unidirectional tests (the
    /// single-direction count is already in `bytes`).
    pub bytes_sent: u64,
    pub bytes_received: u64,
    /// Cumulative UDP packets received on this stream as of this interval's
    /// snapshot moment. Captured in `record_interval` so the aggregator can
    /// compute `udp_progress` from a consistent point in time rather than
    /// re-reading live atomics later (which would attribute packets that
    /// arrived after the snapshot to the wrong interval).
    pub cumulative_packets_received: u64,
    /// Cumulative UDP packets lost on this stream as of this interval's
    /// snapshot. See `cumulative_packets_received` for why this is captured
    /// at snapshot time rather than re-loaded from `udp_lost`.
    pub cumulative_packets_lost: u64,
}

impl StreamStats {
    pub fn new(stream_id: u8) -> Self {
        Self {
            stream_id,
            bytes_sent: AtomicU64::new(0),
            bytes_received: AtomicU64::new(0),
            start_time: Instant::now(),
            intervals: Mutex::new(VecDeque::new()),
            retransmits: AtomicU64::new(0),
            interval_retransmits_total: AtomicU64::new(0),
            last_bytes: AtomicU64::new(0),
            last_bytes_sent: AtomicU64::new(0),
            last_bytes_received: AtomicU64::new(0),
            last_tcp_retransmits: AtomicU64::new(0),
            udp_jitter_us: AtomicU64::new(0),
            udp_lost: AtomicU64::new(0),
            last_udp_lost: AtomicU64::new(0),
            udp_packets_received: AtomicU64::new(0),
            tcp_info_fd: AtomicI32::new(-1),
            final_tcp_info: Mutex::new(None),
        }
    }

    /// Store raw file descriptor for TCP_INFO polling.
    /// Must be called before the TcpStream is moved or split.
    pub fn set_tcp_info_fd(&self, fd: i32) {
        *self.final_tcp_info.lock() = None;
        self.tcp_info_fd.store(fd, Ordering::Relaxed);
    }

    /// Clear the stored fd to prevent stale fd reuse after stream closes.
    pub fn clear_tcp_info_fd(&self) {
        self.tcp_info_fd.store(-1, Ordering::Relaxed);
    }

    /// Store a final TCP_INFO snapshot captured before the socket is dropped.
    pub fn set_final_tcp_info(&self, info: TcpInfoSnapshot) {
        *self.final_tcp_info.lock() = Some(info);
    }

    /// Poll current TCP_INFO snapshot using stored fd.
    /// Returns None if fd is not set or getsockopt fails.
    pub fn poll_tcp_info(&self) -> Option<TcpInfoSnapshot> {
        let fd = self.tcp_info_fd.load(Ordering::Relaxed);
        if fd < 0 {
            return None;
        }
        crate::tcp_info::get_tcp_info_from_fd(fd).ok()
    }

    /// Get the final TCP_INFO snapshot captured when the stream task exited,
    /// or poll live if the fd is still open.
    pub fn final_tcp_info(&self) -> Option<TcpInfoSnapshot> {
        self.final_tcp_info
            .lock()
            .clone()
            .or_else(|| self.poll_tcp_info())
    }

    pub fn add_bytes_sent(&self, bytes: u64) {
        self.bytes_sent.fetch_add(bytes, Ordering::Relaxed);
    }

    pub fn add_bytes_received(&self, bytes: u64) {
        self.bytes_received.fetch_add(bytes, Ordering::Relaxed);
    }

    pub fn add_retransmits(&self, count: u64) {
        self.retransmits.fetch_add(count, Ordering::Relaxed);
    }

    /// Update live UDP jitter (in microseconds)
    pub fn set_udp_jitter_us(&self, jitter_us: u64) {
        self.udp_jitter_us.store(jitter_us, Ordering::Relaxed);
    }

    /// Add lost packets to cumulative count
    pub fn add_udp_lost(&self, count: u64) {
        self.udp_lost.fetch_add(count, Ordering::Relaxed);
    }

    /// Add to cumulative UDP packets-received count.
    pub fn add_udp_received(&self, count: u64) {
        self.udp_packets_received
            .fetch_add(count, Ordering::Relaxed);
    }

    /// Snapshot the cumulative UDP `(received, lost)` pair as a single
    /// `UdpIntervalProgress`. Reads under Relaxed ordering — packaged so
    /// callers cannot accidentally read received and lost at independently
    /// drifting points in time.
    pub fn udp_progress_snapshot(&self) -> UdpIntervalProgress {
        UdpIntervalProgress {
            packets_received: self.udp_packets_received.load(Ordering::Relaxed),
            packets_lost: self.udp_lost.load(Ordering::Relaxed),
        }
    }

    /// Get current jitter in milliseconds
    pub fn udp_jitter_ms(&self) -> f64 {
        self.udp_jitter_us.load(Ordering::Relaxed) as f64 / 1000.0
    }

    pub fn total_bytes(&self) -> u64 {
        self.bytes_sent.load(Ordering::Relaxed) + self.bytes_received.load(Ordering::Relaxed)
    }

    pub fn retransmits(&self) -> u64 {
        self.retransmits.load(Ordering::Relaxed)
    }

    pub fn throughput_mbps(&self) -> f64 {
        let elapsed = self.start_time.elapsed().as_secs_f64();
        if elapsed > 0.0 {
            (self.total_bytes() as f64 * 8.0) / (elapsed * 1_000_000.0)
        } else {
            0.0
        }
    }

    pub fn record_interval(&self) -> IntervalStats {
        let now = Instant::now();
        let total_bytes = self.total_bytes();
        let last_bytes = self.last_bytes.swap(total_bytes, Ordering::Relaxed);
        let interval_bytes = total_bytes.saturating_sub(last_bytes);

        // Per-direction deltas for bidir interval reporting.
        let total_sent = self.bytes_sent.load(Ordering::Relaxed);
        let total_recv = self.bytes_received.load(Ordering::Relaxed);
        let last_sent = self.last_bytes_sent.swap(total_sent, Ordering::Relaxed);
        let last_recv = self.last_bytes_received.swap(total_recv, Ordering::Relaxed);
        let interval_bytes_sent = total_sent.saturating_sub(last_sent);
        let interval_bytes_received = total_recv.saturating_sub(last_recv);

        let elapsed = now.duration_since(self.start_time);
        let intervals = self.intervals.lock();
        let interval_duration = if let Some(last) = intervals.back() {
            now.duration_since(last.timestamp)
        } else {
            elapsed
        };
        drop(intervals);

        let throughput_mbps = if interval_duration.as_secs_f64() > 0.0 {
            (interval_bytes as f64 * 8.0) / (interval_duration.as_secs_f64() * 1_000_000.0)
        } else {
            0.0
        };

        // UDP stats for this interval. Snapshot the live atomics here, at
        // the same moment we capture per-interval deltas, and stash them in
        // `IntervalStats` so the aggregator pulls cumulative counts from a
        // consistent point in time. Re-reading the atomics later in
        // `to_aggregate_with_direction` would attribute packets that arrived
        // between the two reads to the wrong sparkline sample.
        let jitter_ms = self.udp_jitter_ms();
        let total_lost = self.udp_lost.load(Ordering::Relaxed);
        let last_lost = self.last_udp_lost.swap(total_lost, Ordering::Relaxed);
        let interval_lost = total_lost.saturating_sub(last_lost);
        let cumulative_packets_received = self.udp_packets_received.load(Ordering::Relaxed);
        let cumulative_packets_lost = total_lost;

        // TCP_INFO: live RTT, cwnd, and retransmit deltas
        let tcp_info = self.poll_tcp_info();
        let interval_retransmits = if let Some(ref info) = tcp_info {
            let total = info.retransmits;
            let last = self.last_tcp_retransmits.swap(total, Ordering::Relaxed);
            total.saturating_sub(last)
        } else {
            0
        };
        if interval_retransmits > 0 {
            let _ = self.interval_retransmits_total.fetch_update(
                Ordering::Relaxed,
                Ordering::Relaxed,
                |current| Some(current.saturating_add(interval_retransmits)),
            );
        }
        let stats = IntervalStats {
            timestamp: now,
            bytes: interval_bytes,
            throughput_mbps,
            retransmits: interval_retransmits,
            jitter_ms,
            lost: interval_lost,
            rtt_us: tcp_info.as_ref().map(|t| t.rtt_us),
            cwnd: tcp_info.as_ref().map(|t| t.cwnd),
            bytes_sent: interval_bytes_sent,
            bytes_received: interval_bytes_received,
            cumulative_packets_received,
            cumulative_packets_lost,
        };

        let mut intervals = self.intervals.lock();
        intervals.push_back(stats.clone());
        // Keep only last MAX_INTERVAL_HISTORY intervals to bound memory
        if intervals.len() > MAX_INTERVAL_HISTORY {
            intervals.pop_front();
        }
        drop(intervals);
        stats
    }

    pub fn to_interval(&self, interval_stats: &IntervalStats) -> StreamInterval {
        StreamInterval {
            id: self.stream_id,
            bytes: interval_stats.bytes,
            retransmits: Some(interval_stats.retransmits),
            jitter_ms: if interval_stats.jitter_ms > 0.0 {
                Some(interval_stats.jitter_ms)
            } else {
                None
            },
            lost: if interval_stats.lost > 0 {
                Some(interval_stats.lost)
            } else {
                None
            },
            error: None,
            rtt_us: interval_stats.rtt_us,
            cwnd: interval_stats.cwnd,
        }
    }

    pub fn to_result(&self, duration_ms: u64) -> StreamResult {
        let bytes = self.total_bytes();
        let throughput_mbps = if duration_ms > 0 {
            (bytes as f64 * 8.0) / (duration_ms as f64 / 1000.0) / 1_000_000.0
        } else {
            0.0
        };

        StreamResult {
            id: self.stream_id,
            bytes,
            throughput_mbps,
            retransmits: Some(
                self.retransmits()
                    .max(self.interval_retransmits_total.load(Ordering::Relaxed)),
            ),
            jitter_ms: None,
            lost: None,
        }
    }
}

pub struct TestStats {
    pub test_id: String,
    pub streams: Vec<Arc<StreamStats>>,
    pub start_time: Instant,
    /// Per-stream TCP info snapshots (indexed by stream_id)
    pub tcp_info: Mutex<Vec<TcpInfoSnapshot>>,
    /// Per-stream UDP stats (indexed by stream_id)
    pub udp_stats: Mutex<Vec<UdpStats>>,
}

impl TestStats {
    pub fn new(test_id: String, num_streams: u8) -> Self {
        let streams = (0..num_streams)
            .map(|i| Arc::new(StreamStats::new(i)))
            .collect();
        Self {
            test_id,
            streams,
            start_time: Instant::now(),
            tcp_info: Mutex::new(Vec::new()),
            udp_stats: Mutex::new(Vec::new()),
        }
    }

    pub fn add_udp_stats(&self, stats: UdpStats) {
        self.udp_stats.lock().push(stats);
    }

    /// Aggregate all UDP stats across streams
    pub fn aggregate_udp_stats(&self) -> Option<UdpStats> {
        let stats = self.udp_stats.lock();
        if stats.is_empty() {
            return None;
        }

        let mut total = UdpStats {
            packets_sent: 0,
            packets_received: 0,
            lost: 0,
            lost_percent: 0.0,
            jitter_ms: 0.0,
            out_of_order: 0,
            jitter_max_ms: None,
            packet_size: None,
        };

        let mut jitter_max = 0.0f64;
        let mut any_jitter_max = false;
        for s in stats.iter() {
            total.packets_sent += s.packets_sent;
            total.packets_received += s.packets_received;
            total.lost += s.lost;
            total.out_of_order += s.out_of_order;
            total.jitter_ms += s.jitter_ms;
            if let Some(max) = s.jitter_max_ms {
                jitter_max = jitter_max.max(max);
                any_jitter_max = true;
            }
            if total.packet_size.is_none() {
                total.packet_size = s.packet_size;
            }
        }

        // Average jitter across streams
        total.jitter_ms /= stats.len() as f64;
        if any_jitter_max {
            total.jitter_max_ms = Some(jitter_max);
        }

        // Recalculate loss percent from totals
        if total.packets_sent > 0 {
            total.lost_percent = (total.lost as f64 / total.packets_sent as f64) * 100.0;
        }

        Some(total)
    }

    pub fn elapsed_ms(&self) -> u64 {
        self.start_time.elapsed().as_millis() as u64
    }

    pub fn record_intervals(&self) -> Vec<IntervalStats> {
        self.streams.iter().map(|s| s.record_interval()).collect()
    }

    pub fn to_aggregate(&self, intervals: &[IntervalStats]) -> AggregateInterval {
        self.to_aggregate_with_direction(intervals, false)
    }

    /// Build an `AggregateInterval` for the given per-stream interval snapshots.
    /// When `is_bidir` is true, also populates the per-direction split fields
    /// (swapped from the server's view so the client reads its own direction:
    /// `bytes_sent` = bytes the client sent = what the server received, etc.).
    pub fn to_aggregate_with_direction(
        &self,
        intervals: &[IntervalStats],
        is_bidir: bool,
    ) -> AggregateInterval {
        let total_bytes: u64 = intervals.iter().map(|i| i.bytes).sum();
        let total_throughput: f64 = intervals.iter().map(|i| i.throughput_mbps).sum();
        let total_retransmits: u64 = intervals.iter().map(|i| i.retransmits).sum();
        let total_lost: u64 = intervals.iter().map(|i| i.lost).sum();

        // Per-direction interval deltas from the reporting side, swapped for
        // the client's perspective when this is bidirectional.
        let (split_bytes_sent, split_bytes_received, split_ts_send, split_ts_recv) = if is_bidir {
            // Server-local `bytes_sent` is what the server sent — from the
            // client's perspective that's `bytes_received`. Swap.
            let server_sent: u64 = intervals.iter().map(|i| i.bytes_sent).sum();
            let server_recv: u64 = intervals.iter().map(|i| i.bytes_received).sum();
            let client_sent = server_recv;
            let client_recv = server_sent;
            // Per-direction throughput, using the same interval window the
            // aggregate `throughput_mbps` was computed against. If total_bytes
            // is nonzero, distribute total_throughput proportionally; otherwise
            // both are zero.
            let (ts, tr) = if total_bytes > 0 {
                (
                    total_throughput * (client_sent as f64) / (total_bytes as f64),
                    total_throughput * (client_recv as f64) / (total_bytes as f64),
                )
            } else {
                (0.0, 0.0)
            };
            (Some(client_sent), Some(client_recv), Some(ts), Some(tr))
        } else {
            (None, None, None, None)
        };

        // Average jitter across streams that have jitter data
        let jitter_values: Vec<f64> = intervals
            .iter()
            .filter(|i| i.jitter_ms > 0.0)
            .map(|i| i.jitter_ms)
            .collect();
        let avg_jitter = if jitter_values.is_empty() {
            None
        } else {
            Some(jitter_values.iter().sum::<f64>() / jitter_values.len() as f64)
        };

        // Cumulative UDP packet counts across all streams. Read from the
        // per-stream `IntervalStats` snapshot the caller passed in — same
        // point in time as the per-interval `lost` count — so the deltas
        // the client computes between two consecutive `udp_progress`
        // samples actually correspond to the same window. Sent as raw
        // counts (not a derived percent) so the client can compute its own
        // percent and treat absence-of-field as "unknown".
        let cumulative_received: u64 = intervals
            .iter()
            .map(|i| i.cumulative_packets_received)
            .sum();
        let cumulative_lost: u64 = intervals.iter().map(|i| i.cumulative_packets_lost).sum();
        let udp_progress = if cumulative_received > 0 || cumulative_lost > 0 {
            Some(UdpIntervalProgress {
                packets_received: cumulative_received,
                packets_lost: cumulative_lost,
            })
        } else {
            None
        };

        // Average RTT across streams that have TCP_INFO
        let rtt_values: Vec<u32> = intervals.iter().filter_map(|i| i.rtt_us).collect();
        let avg_rtt = if rtt_values.is_empty() {
            None
        } else {
            Some(
                (rtt_values.iter().map(|&r| r as u64).sum::<u64>() / rtt_values.len() as u64)
                    as u32,
            )
        };

        // Sum cwnd across streams (total sending capacity)
        let cwnd_values: Vec<u32> = intervals.iter().filter_map(|i| i.cwnd).collect();
        let total_cwnd = if cwnd_values.is_empty() {
            None
        } else {
            Some(cwnd_values.iter().sum())
        };

        AggregateInterval {
            bytes: total_bytes,
            throughput_mbps: total_throughput,
            retransmits: Some(total_retransmits),
            jitter_ms: avg_jitter,
            lost: if total_lost > 0 {
                Some(total_lost)
            } else {
                None
            },
            udp_progress,
            rtt_us: avg_rtt,
            cwnd: total_cwnd,
            bytes_sent: split_bytes_sent,
            bytes_received: split_bytes_received,
            throughput_send_mbps: split_ts_send,
            throughput_recv_mbps: split_ts_recv,
        }
    }

    pub fn total_bytes(&self) -> u64 {
        self.streams.iter().map(|s| s.total_bytes()).sum()
    }

    /// Bytes this side has sent, summed across all streams. Used to separate
    /// upload vs download accounting in bidirectional tests.
    pub fn total_bytes_sent(&self) -> u64 {
        self.streams
            .iter()
            .map(|s| s.bytes_sent.load(Ordering::Relaxed))
            .sum()
    }

    /// Bytes this side has received, summed across all streams.
    pub fn total_bytes_received(&self) -> u64 {
        self.streams
            .iter()
            .map(|s| s.bytes_received.load(Ordering::Relaxed))
            .sum()
    }

    pub fn add_tcp_info(&self, info: TcpInfoSnapshot) {
        self.tcp_info.lock().push(info);
    }

    /// Get final TCP info (last snapshot represents post-transfer state)
    pub fn get_tcp_info(&self) -> Option<TcpInfoSnapshot> {
        let infos = self.tcp_info.lock();
        infos.last().cloned()
    }

    /// Poll local TCP_INFO across all streams for live client-side metrics.
    /// Returns (avg_rtt_us, total_retransmits, total_cwnd) if any stream has a valid fd.
    pub fn poll_local_tcp_info(&self) -> Option<(u32, u64, u32)> {
        let mut rtt_sum = 0u64;
        let mut retransmits = 0u64;
        let mut cwnd = 0u32;
        let mut count = 0u64;
        for stream in &self.streams {
            if let Some(info) = stream.poll_tcp_info() {
                rtt_sum += info.rtt_us as u64;
                retransmits += info.retransmits;
                cwnd += info.cwnd;
                count += 1;
            }
        }
        (rtt_sum.checked_div(count)).map(|avg_rtt| (avg_rtt as u32, retransmits, cwnd))
    }

    /// Get final TCP_INFO across all streams, using saved snapshots from completed tasks.
    /// Unlike poll_local_tcp_info(), this works after stream tasks have exited.
    pub fn final_local_tcp_info(&self) -> Option<TcpInfoSnapshot> {
        let mut rtt_sum = 0u64;
        let mut rtt_var_sum = 0u64;
        let mut retransmits = 0u64;
        let mut cwnd = 0u32;
        let mut count = 0u64;
        let mut bytes_acked_sum = 0u64;
        let mut any_bytes_acked = false;
        for stream in &self.streams {
            if let Some(info) = stream.final_tcp_info() {
                rtt_sum += info.rtt_us as u64;
                rtt_var_sum += info.rtt_var_us as u64;
                retransmits += info.retransmits;
                cwnd += info.cwnd;
                if let Some(acked) = info.bytes_acked {
                    bytes_acked_sum += acked;
                    any_bytes_acked = true;
                }
                count += 1;
            }
        }
        rtt_sum.checked_div(count).map(|avg_rtt| {
            let avg_rtt_var = rtt_var_sum / count; // safe: checked_div confirmed count > 0
            TcpInfoSnapshot {
                retransmits,
                rtt_us: avg_rtt as u32,
                rtt_var_us: avg_rtt_var as u32,
                cwnd,
                bytes_acked: if any_bytes_acked {
                    Some(bytes_acked_sum)
                } else {
                    None
                },
            }
        })
    }
}

pub fn bytes_to_human(bytes: u64) -> String {
    const KB: u64 = 1024;
    const MB: u64 = KB * 1024;
    const GB: u64 = MB * 1024;
    const TB: u64 = GB * 1024;

    if bytes >= TB {
        format!("{:.2} TB", bytes as f64 / TB as f64)
    } else if bytes >= GB {
        format!("{:.2} GB", bytes as f64 / GB as f64)
    } else if bytes >= MB {
        format!("{:.2} MB", bytes as f64 / MB as f64)
    } else if bytes >= KB {
        format!("{:.2} KB", bytes as f64 / KB as f64)
    } else {
        format!("{} B", bytes)
    }
}

/// Normalize a float for display at a given decimal precision.
/// Handles both IEEE 754 `-0.0` and tiny negatives (e.g. `-1e-12`)
/// that would round to `-0.0` at the given precision.
#[inline]
pub fn normalize_for_display(v: f64, decimals: i32) -> f64 {
    let scale = 10f64.powi(decimals);
    let rounded = (v * scale).round() / scale;
    if rounded == 0.0 { 0.0 } else { rounded }
}

pub fn mbps_to_human(mbps: f64) -> String {
    // Branch on rounded value so e.g. 999.95 displays as "1.00 Gbps" not "1000.0 Mbps"
    let rounded_1dp = normalize_for_display(mbps, 1);
    if rounded_1dp >= 1000.0 {
        format!("{:.2} Gbps", normalize_for_display(mbps / 1000.0, 2))
    } else {
        format!("{:.1} Mbps", rounded_1dp)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_stream_stats_bytes() {
        let stats = StreamStats::new(0);
        stats.add_bytes_sent(1000);
        stats.add_bytes_received(500);
        assert_eq!(stats.total_bytes(), 1500);
    }

    #[test]
    fn test_to_result_uses_max_retransmit_source() {
        let stats = StreamStats::new(0);

        // Interval-based total can be higher when final TCP_INFO snapshot is unavailable.
        stats.retransmits.store(3, Ordering::Relaxed);
        stats.interval_retransmits_total.store(7, Ordering::Relaxed);
        let result = stats.to_result(1000);
        assert_eq!(result.retransmits, Some(7));

        // Final TCP_INFO snapshot can include tail retransmits after last interval.
        stats.retransmits.store(11, Ordering::Relaxed);
        stats.interval_retransmits_total.store(7, Ordering::Relaxed);
        let result = stats.to_result(1000);
        assert_eq!(result.retransmits, Some(11));
    }

    #[test]
    fn test_final_tcp_info_survives_fd_clear() {
        let stats = StreamStats::new(0);
        let info = TcpInfoSnapshot {
            retransmits: 7,
            rtt_us: 1200,
            rtt_var_us: 300,
            cwnd: 64 * 1024,
            bytes_acked: None,
        };

        stats.set_tcp_info_fd(123);
        stats.set_final_tcp_info(info.clone());
        stats.clear_tcp_info_fd();

        let final_info = stats.final_tcp_info().expect("missing final tcp info");
        assert_eq!(final_info.retransmits, info.retransmits);
        assert_eq!(final_info.rtt_us, info.rtt_us);
        assert_eq!(final_info.rtt_var_us, info.rtt_var_us);
        assert_eq!(final_info.cwnd, info.cwnd);
    }

    #[test]
    fn test_final_local_tcp_info_uses_saved_snapshots() {
        let stats = TestStats::new("test".to_string(), 2);

        stats.streams[0].set_final_tcp_info(TcpInfoSnapshot {
            retransmits: 3,
            rtt_us: 1000,
            rtt_var_us: 200,
            cwnd: 32 * 1024,
            bytes_acked: None,
        });
        stats.streams[1].set_final_tcp_info(TcpInfoSnapshot {
            retransmits: 5,
            rtt_us: 3000,
            rtt_var_us: 600,
            cwnd: 64 * 1024,
            bytes_acked: None,
        });

        let info = stats
            .final_local_tcp_info()
            .expect("missing aggregated final tcp info");
        assert_eq!(info.retransmits, 8);
        assert_eq!(info.rtt_us, 2000);
        assert_eq!(info.rtt_var_us, 400);
        assert_eq!(info.cwnd, 96 * 1024);
    }

    #[test]
    fn test_bytes_to_human() {
        assert_eq!(bytes_to_human(500), "500 B");
        assert_eq!(bytes_to_human(1024), "1.00 KB");
        assert_eq!(bytes_to_human(1024 * 1024), "1.00 MB");
        assert_eq!(bytes_to_human(1024 * 1024 * 1024), "1.00 GB");
    }

    #[test]
    fn test_aggregate_udp_stats_takes_max_jitter_across_streams() {
        let stats = TestStats::new("test".to_string(), 3);
        stats.add_udp_stats(UdpStats {
            packets_sent: 100,
            packets_received: 100,
            lost: 0,
            lost_percent: 0.0,
            jitter_ms: 1.0,
            out_of_order: 0,
            jitter_max_ms: Some(2.5),
            packet_size: Some(1400),
        });
        stats.add_udp_stats(UdpStats {
            packets_sent: 100,
            packets_received: 99,
            lost: 1,
            lost_percent: 1.0,
            jitter_ms: 0.5,
            out_of_order: 0,
            jitter_max_ms: Some(7.25), // highest peak
            packet_size: Some(1400),
        });
        stats.add_udp_stats(UdpStats {
            packets_sent: 100,
            packets_received: 100,
            lost: 0,
            lost_percent: 0.0,
            jitter_ms: 2.0,
            out_of_order: 0,
            jitter_max_ms: Some(4.0),
            packet_size: Some(1400),
        });

        let agg = stats.aggregate_udp_stats().expect("expected aggregation");
        assert_eq!(
            agg.jitter_max_ms,
            Some(7.25),
            "max jitter across streams should pick the per-stream peak, not an average"
        );
        assert_eq!(agg.packet_size, Some(1400));
        // Avg jitter for sanity
        assert!((agg.jitter_ms - (1.0 + 0.5 + 2.0) / 3.0).abs() < 1e-9);
    }

    #[test]
    fn test_aggregate_udp_stats_preserves_none_when_no_streams_report_max() {
        let stats = TestStats::new("test".to_string(), 1);
        stats.add_udp_stats(UdpStats {
            packets_sent: 10,
            packets_received: 10,
            lost: 0,
            lost_percent: 0.0,
            jitter_ms: 0.1,
            out_of_order: 0,
            jitter_max_ms: None, // e.g. older server without the field
            packet_size: None,
        });
        let agg = stats.aggregate_udp_stats().expect("expected aggregation");
        assert_eq!(agg.jitter_max_ms, None);
        assert_eq!(agg.packet_size, None);
    }

    #[test]
    fn test_total_bytes_sent_and_received_split() {
        let stats = TestStats::new("test".to_string(), 2);
        stats.streams[0].add_bytes_sent(100);
        stats.streams[0].add_bytes_received(50);
        stats.streams[1].add_bytes_sent(200);
        stats.streams[1].add_bytes_received(75);

        assert_eq!(stats.total_bytes_sent(), 300);
        assert_eq!(stats.total_bytes_received(), 125);
        // Existing total_bytes() still includes both.
        assert_eq!(stats.total_bytes(), 425);
    }

    #[test]
    fn test_mbps_to_human() {
        assert_eq!(mbps_to_human(500.0), "500.0 Mbps");
        assert_eq!(mbps_to_human(1500.0), "1.50 Gbps");
    }

    #[test]
    fn test_mbps_to_human_negative_zero() {
        assert_eq!(mbps_to_human(-0.0), "0.0 Mbps");
        assert_eq!(mbps_to_human(-0.0 / 1.0), "0.0 Mbps");
    }

    #[test]
    fn test_mbps_to_human_boundary() {
        // 999.95 rounds to 1000.0 at 1dp — should switch to Gbps
        assert_eq!(mbps_to_human(999.95), "1.00 Gbps");
        // 999.94 rounds to 999.9 — stays Mbps
        assert_eq!(mbps_to_human(999.94), "999.9 Mbps");
        // Exact boundary
        assert_eq!(mbps_to_human(1000.0), "1.00 Gbps");
        assert_eq!(mbps_to_human(999.9), "999.9 Mbps");
    }

    #[test]
    fn test_normalize_for_display_special_values() {
        // NaN propagates (not a display concern — throughput is never NaN)
        assert!(normalize_for_display(f64::NAN, 1).is_nan());
        // Infinity propagates
        assert_eq!(normalize_for_display(f64::INFINITY, 1), f64::INFINITY);
        assert_eq!(
            normalize_for_display(f64::NEG_INFINITY, 1),
            f64::NEG_INFINITY
        );
    }

    #[test]
    fn test_normalize_for_display() {
        // Exact -0.0 normalizes to 0.0
        assert_eq!(normalize_for_display(-0.0, 1), 0.0);
        assert!(normalize_for_display(-0.0, 1).is_sign_positive());

        // Tiny negative that rounds to zero at given precision
        assert_eq!(normalize_for_display(-1e-12, 1), 0.0);
        assert!(normalize_for_display(-1e-12, 1).is_sign_positive());

        // Normal values pass through
        assert_eq!(normalize_for_display(42.56, 1), 42.6);
        assert_eq!(normalize_for_display(-5.3, 1), -5.3);

        // Zero stays zero
        assert_eq!(normalize_for_display(0.0, 1), 0.0);
        assert!(normalize_for_display(0.0, 1).is_sign_positive());
    }
}
