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::fs;
use std::process::Command;
use std::time::Duration;
use tempfile::tempdir;

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

fn decode_adts_aac_to_mono_samples(aac_bytes: &[u8]) -> Vec<i16> {
    let dir = tempdir().expect("tempdir");
    let input = dir.path().join("runtime-probe.aac");
    fs::write(&input, aac_bytes).expect("write runtime AAC");

    let output = Command::new("ffmpeg")
        .arg("-hide_banner")
        .arg("-loglevel")
        .arg("error")
        .arg("-i")
        .arg(&input)
        .arg("-ac")
        .arg("1")
        .arg("-ar")
        .arg(super::AUDIO_SAMPLE_RATE.to_string())
        .arg("-f")
        .arg("s16le")
        .arg("-acodec")
        .arg("pcm_s16le")
        .arg("-")
        .output()
        .expect("decode runtime AAC with ffmpeg");
    assert!(
        output.status.success(),
        "ffmpeg decode failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    assert!(
        output.stdout.len() >= 2,
        "decoded runtime AAC did not yield enough PCM bytes"
    );

    output
        .stdout
        .chunks_exact(2)
        .map(|chunk| i16::from_le_bytes([chunk[0], chunk[1]]))
        .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);
}

#[cfg(not(coverage))]
#[tokio::test]
async fn runtime_audio_probe_emits_nontrivial_aac_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 >= 16,
        "expected the runtime probe to emit many AAC packets, got {packet_count}"
    );
    assert!(
        total_bytes >= 8_000,
        "expected the runtime probe to emit a meaningful AAC payload, got {total_bytes} bytes"
    );
    assert!(
        largest_packet >= 64,
        "expected at least one non-trivial AAC 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 aac = Vec::new();
    loop {
        let next = audio_queue.pop_fresh().await;
        let Some(packet) = next.packet else {
            break;
        };
        aac.extend_from_slice(&packet.data);
    }

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

    let decoded = decode_adts_aac_to_mono_samples(&aac);
    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 aac = Vec::new();
    loop {
        let next = audio_queue.pop_fresh().await;
        let Some(packet) = next.packet else {
            break;
        };
        aac.extend_from_slice(&packet.data);
    }

    let decoded = decode_adts_aac_to_mono_samples(&aac);
    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"
        );
    }
}
