use super::{
    SyncProbeCapture, build_dark_probe_frame, build_marker_probe_frame, build_regular_probe_frame,
};
use crate::input::camera::{CameraCodec, CameraConfig};
use crate::sync_probe::analyze::detect_audio_onsets;
use crate::sync_probe::schedule::PulseSchedule;
use lesavka_common::lesavka::{AudioPacket, VideoPacket};
use std::time::Duration;
use std::time::Instant;

fn stub_camera() -> CameraConfig {
    CameraConfig {
        codec: CameraCodec::Mjpeg,
        width: 1280,
        height: 720,
        fps: 30,
    }
}

fn decode_interleaved_pcm_to_mono_samples(pcm_bytes: &[u8]) -> Vec<i16> {
    pcm_bytes
        .chunks_exact(2)
        .map(|chunk| i16::from_le_bytes([chunk[0], chunk[1]]))
        .collect::<Vec<_>>()
        .chunks_exact(super::AUDIO_CHANNELS)
        .map(|frame| frame[0])
        .collect()
}

#[tokio::test]
async fn coverage_stub_exposes_live_video_and_audio_queues() {
    let capture = SyncProbeCapture::new(
        stub_camera(),
        PulseSchedule::new(
            Duration::from_secs(4),
            Duration::from_secs(1),
            Duration::from_millis(100),
            5,
        ),
        Duration::from_secs(2),
    )
    .expect("stub capture");

    let video_queue = capture.video_queue();
    let audio_queue = capture.audio_queue();
    let _ = video_queue.push(
        VideoPacket {
            id: 2,
            pts: 1,
            data: vec![1, 2, 3],
            ..Default::default()
        },
        Duration::ZERO,
    );
    let _ = audio_queue.push(
        AudioPacket {
            id: 0,
            pts: 2,
            data: vec![4, 5, 6],
        },
        Duration::ZERO,
    );

    let video = video_queue.pop_fresh().await;
    let audio = audio_queue.pop_fresh().await;
    assert_eq!(video.packet.expect("video packet").data, vec![1, 2, 3]);
    assert_eq!(audio.packet.expect("audio packet").data, vec![4, 5, 6]);
}

#[test]
fn probe_pts_duration_cutoff_matches_video_boundary_rule() {
    assert!(!super::probe_pts_exceeds_duration(
        Duration::from_secs(2).as_micros() as u64,
        Duration::from_secs(2),
    ));
    assert!(super::probe_pts_exceeds_duration(
        Duration::from_secs(2).as_micros() as u64 + 1,
        Duration::from_secs(2),
    ));
}

#[test]
fn synthesized_audio_pulses_track_video_flash_boundaries() {
    let schedule = PulseSchedule::new(
        Duration::from_secs(4),
        Duration::from_secs(1),
        Duration::from_millis(120),
        5,
    );
    let chunk_samples =
        (super::AUDIO_SAMPLE_RATE as usize * super::AUDIO_CHUNK_MS as usize / 1_000).max(1);
    let mut samples = Vec::<i16>::new();
    let chunk_duration = Duration::from_millis(super::AUDIO_CHUNK_MS);
    for chunk_index in 0..600u64 {
        let pts = chunk_duration.saturating_mul(chunk_index as u32);
        if pts > Duration::from_secs(6) {
            break;
        }
        let chunk = super::render_audio_chunk(&schedule, pts, chunk_samples);
        samples.extend(
            chunk
                .chunks_exact(std::mem::size_of::<i16>())
                .map(|bytes| i16::from_le_bytes([bytes[0], bytes[1]])),
        );
    }

    let mono = samples
        .chunks_exact(super::AUDIO_CHANNELS)
        .map(|frame| frame[0])
        .collect::<Vec<_>>();
    let onsets =
        detect_audio_onsets(&mono, super::AUDIO_SAMPLE_RATE as u32, 5).expect("audio onsets");
    assert!(
        onsets.len() >= 2,
        "expected at least 2 onsets, got {onsets:?}"
    );
    assert!(
        onsets
            .iter()
            .zip([4.0, 5.0])
            .all(|(actual, expected)| { (*actual - expected).abs() <= 0.02 })
    );
}

#[test]
fn probe_video_frames_render_distinct_idle_regular_and_marker_patterns() {
    let dark = build_dark_probe_frame(64, 36);
    let regular = build_regular_probe_frame(64, 36);
    let marker = build_marker_probe_frame(64, 36);

    assert_eq!(dark.len(), regular.len());
    assert_eq!(dark.len(), marker.len());
    assert!(
        regular.iter().map(|byte| u64::from(*byte)).sum::<u64>()
            != dark.iter().map(|byte| u64::from(*byte)).sum::<u64>()
    );
    assert!(
        marker.iter().map(|byte| u64::from(*byte)).sum::<u64>()
            != dark.iter().map(|byte| u64::from(*byte)).sum::<u64>()
    );
    assert_ne!(regular, marker);
}

#[test]
fn probe_video_pts_are_lag_capped_like_audio() {
    let rebaser = crate::live_capture_clock::DurationPacedSourcePtsRebaser::default();
    let _first =
        super::runtime::rebase_probe_packet_pts(&rebaser, 1_000_000, Duration::from_millis(2));
    std::thread::sleep(Duration::from_millis(8));
    let second =
        super::runtime::rebase_probe_packet_pts(&rebaser, 1_000_001, Duration::from_millis(2));

    assert!(
        second >= 6_000,
        "delayed probe video packets should be pulled forward instead of looking ancient"
    );
}

#[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;

    loop {
        let next = audio_queue.pop_fresh().await;
        let Some(packet) = next.packet else {
            break;
        };
        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_span = video_last.expect("video last pts") - video_first.expect("video first pts");
    let audio_span = audio_last.expect("audio last pts") - audio_first.expect("audio first pts");
    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_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
    );
}
