use super::*;

#[cfg(not(coverage))]
#[tokio::test]
async fn runtime_audio_probe_emits_nontrivial_pcm_packets() {
    let capture = SyncProbeCapture::new(
        stub_camera(),
        PulseSchedule::new(
            Duration::from_secs(1),
            Duration::from_millis(500),
            Duration::from_millis(120),
            4,
        ),
        Duration::from_secs(3),
    )
    .expect("runtime capture");

    let audio_queue = capture.audio_queue();
    let mut packet_count = 0usize;
    let mut total_bytes = 0usize;
    let mut largest_packet = 0usize;
    let expected_duration_us = 20_000;

    loop {
        let next = audio_queue.pop_fresh().await;
        let Some(packet) = next.packet else {
            break;
        };
        assert_eq!(
            packet.frame_duration_us, expected_duration_us,
            "synthetic audio metadata should match its PCM payload duration"
        );
        packet_count += 1;
        total_bytes += packet.data.len();
        largest_packet = largest_packet.max(packet.data.len());
    }

    assert!(
        packet_count >= 120,
        "expected the runtime probe to emit many PCM packets, got {packet_count}"
    );
    assert!(
        total_bytes >= 200_000,
        "expected the runtime probe to emit a meaningful PCM payload, got {total_bytes} bytes"
    );
    assert!(
        largest_packet >= 1_000,
        "expected at least one non-trivial PCM packet, largest was {largest_packet} bytes"
    );
}

#[cfg(not(coverage))]
#[tokio::test]
async fn runtime_audio_probe_decodes_detectable_click_onsets() {
    let schedule = PulseSchedule::new(
        Duration::from_secs(1),
        Duration::from_millis(500),
        Duration::from_millis(120),
        4,
    );
    let capture = SyncProbeCapture::new(stub_camera(), schedule.clone(), Duration::from_secs(3))
        .expect("runtime capture");

    let audio_queue = capture.audio_queue();
    let mut pcm = Vec::new();
    loop {
        let next = audio_queue.pop_fresh().await;
        let Some(packet) = next.packet else {
            break;
        };
        pcm.extend_from_slice(&packet.data);
    }

    assert!(
        pcm.len() >= 200_000,
        "expected the runtime probe PCM stream to carry a meaningful payload, got {} bytes",
        pcm.len()
    );

    let decoded = decode_interleaved_pcm_to_mono_samples(&pcm);
    let onsets =
        detect_audio_onsets(&decoded, super::AUDIO_SAMPLE_RATE as u32, 5).expect("audio onsets");
    assert!(
        onsets.len() >= 4,
        "expected at least four decoded click onsets, got {onsets:?}"
    );

    let expected = [1.0, 1.5, 2.0, 2.5];
    for (actual, expected) in onsets.iter().zip(expected) {
        assert!(
            (*actual - expected).abs() <= 0.08,
            "expected onset near {expected:.3}s, got {actual:.3}s"
        );
    }
}

#[cfg(not(coverage))]
#[tokio::test]
async fn runtime_audio_probe_decodes_detectable_click_onsets_for_manual_harness_timing() {
    let schedule = PulseSchedule::new(
        Duration::from_secs(4),
        Duration::from_secs(1),
        Duration::from_millis(120),
        5,
    );
    let capture = SyncProbeCapture::new(stub_camera(), schedule.clone(), Duration::from_secs(10))
        .expect("runtime capture");

    let audio_queue = capture.audio_queue();
    let mut pcm = Vec::new();
    loop {
        let next = audio_queue.pop_fresh().await;
        let Some(packet) = next.packet else {
            break;
        };
        pcm.extend_from_slice(&packet.data);
    }

    let decoded = decode_interleaved_pcm_to_mono_samples(&pcm);
    let onsets =
        detect_audio_onsets(&decoded, super::AUDIO_SAMPLE_RATE as u32, 5).expect("audio onsets");
    assert!(
        onsets.len() >= 6,
        "expected at least six decoded click onsets, got {onsets:?}"
    );

    let expected = [4.0, 5.0, 6.0, 7.0, 8.0, 9.0];
    for (actual, expected) in onsets.iter().zip(expected) {
        assert!(
            (*actual - expected).abs() <= 0.1,
            "expected onset near {expected:.3}s, got {actual:.3}s"
        );
    }
}

#[cfg(not(coverage))]
#[tokio::test]
async fn runtime_probe_audio_and_video_pts_advance_near_real_time() {
    let capture_duration = Duration::from_secs(3);
    let capture = SyncProbeCapture::new(
        stub_camera(),
        PulseSchedule::new(
            Duration::from_secs(1),
            Duration::from_millis(500),
            Duration::from_millis(120),
            4,
        ),
        capture_duration,
    )
    .expect("runtime capture");

    let video_queue = capture.video_queue();
    let audio_queue = capture.audio_queue();
    let started = Instant::now();

    let video_task = tokio::spawn(async move {
        let mut first_pts = None;
        let mut last_pts = None;
        let mut count = 0usize;
        loop {
            let next = video_queue.pop_fresh().await;
            let Some(packet) = next.packet else {
                break;
            };
            first_pts.get_or_insert(packet.pts);
            last_pts = Some(packet.pts);
            count = count.saturating_add(1);
        }
        (first_pts, last_pts, count)
    });

    let audio_task = tokio::spawn(async move {
        let mut first_pts = None;
        let mut last_pts = None;
        let mut count = 0usize;
        loop {
            let next = audio_queue.pop_fresh().await;
            let Some(packet) = next.packet else {
                break;
            };
            first_pts.get_or_insert(packet.pts);
            last_pts = Some(packet.pts);
            count = count.saturating_add(1);
        }
        (first_pts, last_pts, count)
    });

    let (video_first, video_last, video_count) = video_task.await.expect("video drain");
    let (audio_first, audio_last, audio_count) = audio_task.await.expect("audio drain");
    let wall_elapsed = started.elapsed();

    let video_first = video_first.expect("video first pts");
    let audio_first = audio_first.expect("audio first pts");
    let video_span = video_last.expect("video last pts") - video_first;
    let audio_span = audio_last.expect("audio last pts") - audio_first;
    eprintln!(
        "runtime probe spans: video_count={video_count} video_span_us={video_span} audio_count={audio_count} audio_span_us={audio_span} wall_elapsed={wall_elapsed:?}"
    );

    assert!(
        video_count >= 60,
        "expected many runtime probe video packets, got {video_count}"
    );
    assert!(
        audio_count >= 60,
        "expected many runtime probe audio packets, got {audio_count}"
    );
    assert!(
        wall_elapsed <= Duration::from_secs(5),
        "runtime probe should not take excessively long locally, took {wall_elapsed:?}"
    );
    assert!(
        video_first <= 50_000,
        "video PTS should stay anchored to synthetic capture start, got {video_first} us"
    );
    assert!(
        audio_first <= 50_000,
        "audio PTS should stay anchored to synthetic capture start, got {audio_first} us"
    );
    assert!(
        video_span >= 2_400_000,
        "video pts should span most of the 3s capture, got {} us",
        video_span
    );
    assert!(
        audio_span >= 2_400_000,
        "audio pts should span most of the 3s capture, got {} us",
        audio_span
    );
    assert!(
        audio_span <= 3_400_000,
        "audio pts should stay near the 3s capture duration, got {} us",
        audio_span
    );
}

#[cfg(not(coverage))]
#[tokio::test]
async fn runtime_probe_1080p_mjpeg_packets_are_clock_paced() {
    let capture = SyncProbeCapture::new(
        CameraConfig {
            codec: CameraCodec::Mjpeg,
            width: 1920,
            height: 1080,
            fps: 30,
        },
        PulseSchedule::new(
            Duration::from_secs(1),
            Duration::from_millis(500),
            Duration::from_millis(120),
            4,
        ),
        Duration::from_secs(2),
    )
    .expect("runtime capture");

    let started = Instant::now();
    let video_queue = capture.video_queue();
    let mut count = 0usize;
    loop {
        let next = video_queue.pop_fresh().await;
        let Some(packet) = next.packet else {
            break;
        };
        assert!(!packet.data.is_empty());
        count += 1;
    }

    assert!(count >= 45, "expected real-time 1080p packets, got {count}");
    assert!(
        started.elapsed() <= Duration::from_secs(4),
        "1080p MJPEG probe should be packet-paced, not encoder-paced"
    );
}

#[cfg(not(coverage))]
#[tokio::test]
/// Verifies local synthetic HEVC video and PCM audio can be paired before transport.
///
/// Inputs: a tiny HEVC sync-probe camera profile and the normal synthetic audio
/// schedule. Outputs: assertions over locally generated packets only. Why: the
/// server can only use the HEVC ingress path optimally if the client leaves the
/// machine with HEVC access units and nearby audio on the same capture clock.
async fn runtime_probe_hevc_video_and_audio_can_form_one_local_bundle() {
    gst::init().expect("gst init");
    if !hevc_encoder_available() {
        eprintln!("skipping local HEVC bundle contract because no HEVC encoder is installed");
        return;
    }

    let capture = SyncProbeCapture::new(
        CameraConfig {
            codec: CameraCodec::Hevc,
            width: 640,
            height: 360,
            fps: 10,
        },
        PulseSchedule::with_event_width_codes(
            Duration::from_millis(500),
            Duration::from_millis(500),
            Duration::from_millis(120),
            4,
            vec![1, 2, 3],
        ),
        Duration::from_secs(2),
    )
    .expect("runtime HEVC capture");

    let video_queue = capture.video_queue();
    let audio_queue = capture.audio_queue();
    let video_task = tokio::spawn(async move {
        let mut packets = Vec::new();
        loop {
            let next = video_queue.pop_fresh().await;
            let Some(packet) = next.packet else {
                break;
            };
            packets.push(packet);
        }
        packets
    });
    let audio_task = tokio::spawn(async move {
        let mut packets = Vec::new();
        loop {
            let next = audio_queue.pop_fresh().await;
            let Some(packet) = next.packet else {
                break;
            };
            packets.push(packet);
        }
        packets
    });

    let videos = video_task.await.expect("video drain");
    let audios = audio_task.await.expect("audio drain");
    assert!(
        videos.len() >= 8,
        "expected enough local HEVC probe video packets to form a bundle, got {}",
        videos.len()
    );
    assert!(
        audios.len() >= 90,
        "expected local PCM audio packets, got {}",
        audios.len()
    );

    let video = videos
        .iter()
        .find(|packet| packet_has_annex_b_start_code(&packet.data))
        .expect("HEVC Annex-B video packet");
    let paired_audio = audios
        .iter()
        .filter(|packet| {
            packet.pts >= video.pts.saturating_sub(120_000)
                && packet.pts <= video.pts.saturating_add(40_000)
        })
        .collect::<Vec<_>>();

    assert!(
        !paired_audio.is_empty(),
        "expected at least one PCM packet close to HEVC video pts {}",
        video.pts
    );
    assert!(
        paired_audio.iter().any(|packet| packet.data.len() >= 3_840),
        "expected paired audio to carry full 20ms stereo PCM packets"
    );
}

#[cfg(not(coverage))]
#[tokio::test]
async fn runtime_probe_video_packets_change_across_a_pulse_boundary() {
    let capture = SyncProbeCapture::new(
        stub_camera(),
        PulseSchedule::new(
            Duration::from_secs(1),
            Duration::from_secs(1),
            Duration::from_millis(120),
            4,
        ),
        Duration::from_secs(2),
    )
    .expect("runtime capture");

    let video_queue = capture.video_queue();
    let mut darkest_packet = None;
    let mut brightest_packet = None;

    loop {
        let next = video_queue.pop_fresh().await;
        let Some(packet) = next.packet else {
            break;
        };
        let mean = decode_mjpeg_packet_mean_luma(&packet);
        if mean <= 40 && darkest_packet.is_none() {
            darkest_packet = Some((packet.clone(), mean));
        }
        if mean >= 180 && brightest_packet.is_none() {
            brightest_packet = Some((packet.clone(), mean));
        }
        if darkest_packet.is_some() && brightest_packet.is_some() {
            break;
        }
    }

    let (dark_packet, dark_mean) = darkest_packet.expect("dark packet");
    let (pulse_packet, pulse_mean) = brightest_packet.expect("pulse packet");
    assert_ne!(dark_packet.data, pulse_packet.data);
    assert!(!dark_packet.data.is_empty());
    assert!(!pulse_packet.data.is_empty());
    assert!(
        pulse_mean > dark_mean.saturating_add(100),
        "expected decoded pulse frame to be much brighter than decoded dark frame, got dark={dark_mean} pulse={pulse_mean}"
    );
}

#[cfg(not(coverage))]
#[tokio::test]
async fn runtime_probe_dark_video_packets_do_not_alternate_frame_to_frame() {
    let schedule = PulseSchedule::new(
        Duration::from_secs(4),
        Duration::from_secs(1),
        Duration::from_millis(120),
        5,
    );
    let capture = SyncProbeCapture::new(
        CameraConfig {
            codec: CameraCodec::Mjpeg,
            width: 640,
            height: 480,
            fps: 20,
        },
        schedule,
        Duration::from_secs(3),
    )
    .expect("runtime capture");

    let video_queue = capture.video_queue();
    let mut dark_means = Vec::new();

    loop {
        let next = video_queue.pop_fresh().await;
        let Some(packet) = next.packet else {
            break;
        };
        dark_means.push(decode_mjpeg_packet_mean_luma(&packet));
        if dark_means.len() >= 8 {
            break;
        }
    }

    assert!(
        dark_means.len() >= 3,
        "expected several dark packets before the first pulse, got {dark_means:?}"
    );
    let min = *dark_means.iter().min().expect("dark min");
    let max = *dark_means.iter().max().expect("dark max");
    assert!(
        max.saturating_sub(min) <= 2,
        "expected consecutive dark MJPEG packets to stay visually stable, got {dark_means:?}"
    );
}

#[cfg(not(coverage))]
/// Returns whether this developer host can encode local HEVC probe media.
///
/// Inputs: installed GStreamer plugin registry. Outputs: true when one of the
/// supported HEVC encoders is available. Why: the local bundle contract should
/// prove HEVC behavior when the host can do it without making unrelated CI
/// workers fail just because they lack codec plugins.
fn hevc_encoder_available() -> bool {
    [
        "x265enc",
        "nvh265enc",
        "vah265enc",
        "vaapih265enc",
        "v4l2h265enc",
    ]
    .iter()
    .any(|encoder| gst::ElementFactory::find(encoder).is_some())
}

#[cfg(not(coverage))]
/// Detects Annex-B framing in an encoded video packet.
///
/// Inputs: encoded video bytes. Outputs: true when a 3- or 4-byte start code is
/// present. Why: the server-side HEVC decoder expects byte-stream access units,
/// not an opaque test payload that only looks non-empty.
fn packet_has_annex_b_start_code(data: &[u8]) -> bool {
    data.windows(4).any(|window| window == [0, 0, 0, 1])
        || data.windows(3).any(|window| window == [0, 0, 1])
}
