use super::{
    AUDIO_SAMPLE_RATE, SyncProbeCapture, build_coded_probe_frame, 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;
#[cfg(not(coverage))]
use gstreamer as gst;
#[cfg(not(coverage))]
use gstreamer::prelude::*;
#[cfg(not(coverage))]
use gstreamer_app as gst_app;
#[cfg(coverage)]
use lesavka_common::lesavka::AudioPacket;
use lesavka_common::lesavka::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]
#[cfg(coverage)]
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],
            ..Default::default()
        },
        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]);
    assert!(capture.start_unix_ns() > 0);
}

#[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);
    let coded = build_coded_probe_frame(64, 36, 3);

    assert_eq!(dark.len(), regular.len());
    assert_eq!(dark.len(), marker.len());
    assert_eq!(dark.len(), coded.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);
    assert_ne!(coded, regular);
    assert_ne!(coded, dark);
}

#[test]
/// Keeps `probe_origin_timestamp_is_unix_epoch_based` explicit because client-to-RCT freshness depends on comparing client-origin media timestamps with Tethys observations.
/// Inputs are none; output is a current Unix nanosecond timestamp.
fn probe_origin_timestamp_is_unix_epoch_based() {
    let before = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .expect("system clock after epoch")
        .as_nanos() as u64;
    let captured = super::unix_now_ns();
    let after = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .expect("system clock after epoch")
        .as_nanos() as u64;

    assert!(captured >= before);
    assert!(captured <= after);
}

#[cfg(not(coverage))]
fn decode_mjpeg_packet_mean_luma(packet: &VideoPacket) -> u8 {
    gst::init().expect("gst init");
    let pipeline = gst::parse::launch(
        "appsrc name=src is-live=false format=time do-timestamp=false \
         caps=image/jpeg,parsed=true ! jpegdec ! videoconvert ! \
         video/x-raw,format=GRAY8 ! \
         appsink name=sink emit-signals=false sync=false max-buffers=1 drop=false",
    )
    .expect("decode pipeline")
    .downcast::<gst::Pipeline>()
    .expect("pipeline");
    let src = pipeline
        .by_name("src")
        .expect("src")
        .downcast::<gst_app::AppSrc>()
        .expect("appsrc");
    let sink = pipeline
        .by_name("sink")
        .expect("sink")
        .downcast::<gst_app::AppSink>()
        .expect("appsink");

    pipeline
        .set_state(gst::State::Playing)
        .expect("pipeline playing");
    let mut buffer = gst::Buffer::from_slice(packet.data.clone());
    if let Some(meta) = buffer.get_mut() {
        meta.set_pts(Some(gst::ClockTime::from_useconds(packet.pts)));
    }
    src.push_buffer(buffer).expect("push buffer");
    src.end_of_stream().expect("end of stream");
    let sample = sink.pull_sample().expect("decoded sample");
    pipeline.set_state(gst::State::Null).expect("pipeline null");
    let buffer = sample.buffer().expect("sample buffer");
    let map = buffer.map_readable().expect("buffer readable");
    let bytes = map.as_slice();
    let mean = bytes.iter().map(|value| u64::from(*value)).sum::<u64>() / bytes.len().max(1) as u64;
    mean.min(u64::from(u8::MAX)) as u8
}

#[cfg(not(coverage))]
#[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))]
mod runtime_packets;
