// Unit coverage for client upstream bundled media queue behavior.
//
// Scope: exercise the freshness queue with complete `UpstreamMediaBundle`
// values rather than split audio/video packets.
// Targets: `client/src/uplink_fresh_queue.rs`.
// Why: the upstream sync win depends on dropping stale media as complete A/V
// bundles so the server never receives an orphaned tone or flash.

#[path = "../../../../client/src/uplink_fresh_queue.rs"]
#[allow(warnings)]
mod uplink_fresh_queue;

use lesavka_common::lesavka::{AudioEncoding, AudioPacket, UpstreamMediaBundle, VideoPacket};
use std::time::Duration;
use uplink_fresh_queue::{FreshPacketQueue, FreshQueueConfig, FreshQueuePolicy};

fn bundle(seq: u64, capture_pts_us: u64) -> UpstreamMediaBundle {
    UpstreamMediaBundle {
        session_id: 7,
        seq,
        capture_start_us: capture_pts_us.saturating_sub(20_000),
        capture_end_us: capture_pts_us.saturating_add(20_000),
        video: Some(VideoPacket {
            seq,
            pts: capture_pts_us,
            data: vec![0, 0, 0, 1, 0x26, 0x01, seq as u8],
            client_capture_pts_us: capture_pts_us,
            client_send_pts_us: capture_pts_us.saturating_add(5_000),
            client_queue_age_ms: 5,
            ..Default::default()
        }),
        audio: vec![
            AudioPacket {
                seq: seq.saturating_mul(2),
                pts: capture_pts_us.saturating_sub(20_000),
                data: vec![0x11; 1_920],
                client_capture_pts_us: capture_pts_us.saturating_sub(20_000),
                client_send_pts_us: capture_pts_us.saturating_add(5_000),
                client_queue_age_ms: 25,
                ..Default::default()
            },
            AudioPacket {
                seq: seq.saturating_mul(2).saturating_add(1),
                pts: capture_pts_us.saturating_add(20_000),
                data: vec![0x22; 1_920],
                client_capture_pts_us: capture_pts_us.saturating_add(20_000),
                client_send_pts_us: capture_pts_us.saturating_add(25_000),
                client_queue_age_ms: 5,
                ..Default::default()
            },
        ],
        audio_sample_rate: 48_000,
        audio_channels: 2,
        audio_encoding: AudioEncoding::PcmS16le as i32,
        video_width: 1280,
        video_height: 720,
        video_fps: 30,
    }
}

fn assert_complete_bundle(bundle: &UpstreamMediaBundle, expected_seq: u64) {
    assert_eq!(bundle.seq, expected_seq);
    assert!(
        bundle.video.is_some(),
        "freshness queue must not split video out of its bundle"
    );
    assert_eq!(
        bundle.audio.len(),
        2,
        "freshness queue must not split audio out of its bundle"
    );
    let video = bundle.video.as_ref().expect("video packet");
    assert!(
        bundle.capture_start_us <= video.client_capture_pts_us
            && bundle.capture_end_us >= video.client_capture_pts_us
    );
    assert!(bundle.audio.iter().all(|audio| {
        bundle.capture_start_us <= audio.client_capture_pts_us
            && bundle.capture_end_us >= audio.client_capture_pts_us
    }));
}

#[tokio::test]
async fn latest_only_policy_drops_superseded_bundles_as_complete_av_units() {
    let queue = FreshPacketQueue::new(FreshQueueConfig {
        capacity: 8,
        max_age: Duration::from_secs(1),
        policy: FreshQueuePolicy::LatestOnly,
    });

    for seq in 1..=3 {
        let _ = queue.push(
            bundle(seq, 1_000_000 + seq * 33_333),
            Duration::from_millis(10),
        );
    }

    let popped = queue.pop_fresh().await;
    assert_eq!(popped.dropped_stale, 2);
    assert_eq!(popped.queue_depth, 0);
    assert_complete_bundle(&popped.packet.expect("newest bundle"), 3);
}

#[tokio::test]
async fn stale_bundle_drop_never_delivers_audio_or_video_halves() {
    let queue = FreshPacketQueue::new(FreshQueueConfig {
        capacity: 4,
        max_age: Duration::from_millis(200),
        policy: FreshQueuePolicy::DrainOldest,
    });

    let _ = queue.push(bundle(1, 1_000_000), Duration::from_millis(250));
    let _ = queue.push(bundle(2, 1_033_333), Duration::from_millis(25));

    let popped = queue.pop_fresh().await;
    assert_eq!(popped.dropped_stale, 1);
    assert_complete_bundle(&popped.packet.expect("fresh bundle"), 2);
}

#[tokio::test]
async fn capacity_pressure_drops_oldest_bundles_instead_of_backlogging_media() {
    let queue = FreshPacketQueue::new(FreshQueueConfig {
        capacity: 2,
        max_age: Duration::from_secs(1),
        policy: FreshQueuePolicy::DrainOldest,
    });

    let first = queue.push(bundle(1, 1_000_000), Duration::from_millis(5));
    let second = queue.push(bundle(2, 1_033_333), Duration::from_millis(5));
    let third = queue.push(bundle(3, 1_066_666), Duration::from_millis(5));

    assert_eq!(first.dropped_queue_full, 0);
    assert_eq!(second.queue_depth, 2);
    assert_eq!(third.dropped_queue_full, 1);
    assert_complete_bundle(&queue.pop_fresh().await.packet.expect("second bundle"), 2);
    assert_complete_bundle(&queue.pop_fresh().await.packet.expect("third bundle"), 3);
}
