#[cfg(not(coverage))]
use super::EncodedProbeFrames;
#[cfg(coverage)]
use super::coverage_output_delay_summary;
use super::{
    DARK_FRAME_RGB, EVENT_COLORS, EVENT_FREQUENCIES_HZ, OutputDelayProbeTimeline, ProbeConfig,
    draw_frame_continuity_watermark, duration_mul, duration_us, event_frequency_hz,
    parse_event_width_codes, positive_delay, probe_color_for_code, render_audio_chunk,
    unix_ns_from_start,
};
#[cfg(coverage)]
use crate::audio::Voice;
use crate::camera::{CameraCodec, CameraConfig, CameraOutput};
#[cfg(coverage)]
use crate::video_sinks::CameraRelay;
use lesavka_common::lesavka::OutputDelayProbeRequest;
use std::collections::BTreeSet;
#[cfg(coverage)]
use std::sync::Arc;
use std::time::Duration;

#[test]
fn request_defaults_to_long_coded_server_probe() {
    let config =
        ProbeConfig::from_request(&OutputDelayProbeRequest::default()).expect("default config");

    assert_eq!(config.duration, Duration::from_secs(20));
    assert_eq!(config.warmup, Duration::from_secs(4));
    assert_eq!(config.event_count(), 16);
    assert_eq!(config.event_code_at(Duration::from_secs(4)), Some(1));
    assert_eq!(config.event_code_at(Duration::from_secs(5)), Some(2));
}

#[test]
fn event_codes_reject_unsupported_signatures() {
    let request = OutputDelayProbeRequest {
        event_width_codes: "1,17".to_string(),
        ..Default::default()
    };

    assert!(ProbeConfig::from_request(&request).is_err());
}

#[test]
fn request_validation_rejects_non_live_probe_shapes() {
    let period_too_large = OutputDelayProbeRequest {
        duration_seconds: u32::MAX,
        ..Default::default()
    };
    assert!(ProbeConfig::from_request(&period_too_large).is_err());

    let width_not_smaller_than_period = OutputDelayProbeRequest {
        pulse_period_ms: 120,
        pulse_width_ms: 120,
        ..Default::default()
    };
    assert!(ProbeConfig::from_request(&width_not_smaller_than_period).is_err());
    assert!(positive_delay(-1, "video_delay_us").is_err());
    assert!(parse_event_width_codes(" , , ").is_err());
}

#[test]
#[cfg(coverage)]
/// Keeps `coverage_probe_summary_uses_validated_request_and_timeline_shape` explicit because the coverage harness must preserve the same request validation and timeline fields as live UVC/UAC probing.
/// Inputs are a synthetic HEVC camera profile and probe request; output is a compact coverage summary with timeline JSON.
fn coverage_probe_summary_uses_validated_request_and_timeline_shape() {
    let camera = CameraConfig {
        output: CameraOutput::Uvc,
        codec: CameraCodec::Hevc,
        width: 1280,
        height: 720,
        fps: 30,
        hdmi: None,
    };
    let request = OutputDelayProbeRequest {
        duration_seconds: 6,
        warmup_seconds: 1,
        pulse_period_ms: 1_000,
        pulse_width_ms: 120,
        event_width_codes: "1,2,3".to_string(),
        audio_delay_us: 0,
        video_delay_us: 150_000,
    };

    let summary = coverage_output_delay_summary(&camera, &request).expect("coverage summary");
    let timeline =
        serde_json::from_str::<serde_json::Value>(&summary.timeline_json).expect("timeline json");

    assert_eq!(summary.video_frames, 1);
    assert_eq!(summary.audio_packets, 1);
    assert_eq!(summary.event_count, 5);
    assert_eq!(timeline["camera_width"].as_u64(), Some(1280));
    assert_eq!(timeline["video_delay_us"].as_u64(), Some(150_000));
}

#[tokio::test]
#[cfg(coverage)]
/// Keeps `coverage_probe_runtime_entrypoint_accepts_noop_sinks` explicit because the coverage harness should validate the public probe entrypoint without touching physical UVC/UAC devices.
/// Inputs are noop video/audio sinks plus a short probe request; output is a serialized timeline summary from the same entrypoint the RPC uses.
async fn coverage_probe_runtime_entrypoint_accepts_noop_sinks() {
    let camera = CameraConfig {
        output: CameraOutput::Uvc,
        codec: CameraCodec::Mjpeg,
        width: 640,
        height: 480,
        fps: 20,
        hdmi: None,
    };
    let request = OutputDelayProbeRequest {
        duration_seconds: 3,
        warmup_seconds: 1,
        pulse_period_ms: 1_000,
        pulse_width_ms: 120,
        event_width_codes: "1,2".to_string(),
        audio_delay_us: 0,
        video_delay_us: 0,
    };
    let relay = Arc::new(CameraRelay::new_noop(0));
    let mut voice = Voice::new("coverage-audio").await.expect("coverage voice");

    let summary = super::run_server_output_delay_probe(relay, &mut voice, &camera, &request)
        .await
        .expect("coverage output-delay probe");

    assert_eq!(summary.video_frames, 1);
    assert_eq!(summary.audio_packets, 1);
    assert_eq!(summary.event_count, 2);
    assert!(summary.timeline_json.contains("640"));
}

#[test]
fn default_probe_signatures_are_unique_for_all_coded_pairs() {
    assert_eq!(EVENT_COLORS.len(), 16);
    assert_eq!(EVENT_FREQUENCIES_HZ.len(), 16);

    let colors = EVENT_COLORS
        .iter()
        .map(|color| (color.r, color.g, color.b))
        .collect::<BTreeSet<_>>();
    let frequencies = EVENT_FREQUENCIES_HZ
        .iter()
        .map(|frequency| (*frequency * 10.0).round() as i64)
        .collect::<BTreeSet<_>>();

    assert_eq!(colors.len(), EVENT_COLORS.len());
    assert_eq!(frequencies.len(), EVENT_FREQUENCIES_HZ.len());
}

#[test]
/// Keeps `continuity_watermark_encodes_sequence_bits_and_timing_helpers_saturate` explicit because the analyzer relies on the watermark to detect smoothness without mistaking overflow for a huge timestamp.
/// Inputs are a raw RGB frame, sequence number, and edge timing values; output is an in-place watermark plus bounded helper results.
fn continuity_watermark_encodes_sequence_bits_and_timing_helpers_saturate() {
    let width = 40usize;
    let height = 36usize;
    let mut frame = vec![127u8; width * height * 3];

    draw_frame_continuity_watermark(&mut frame, width, height, 0b1010_0101_0000_1111);

    assert_eq!(frame[(height - 1) * width * 3], 255);
    assert!(frame.contains(&0));
    assert!(frame.contains(&255));
    assert_eq!(event_frequency_hz(0), None);
    assert_eq!(event_frequency_hz(17), None);
    assert_eq!(event_frequency_hz(1), Some(EVENT_FREQUENCIES_HZ[0]));
    assert_eq!(
        duration_mul(Duration::from_nanos(u64::MAX), 2),
        Duration::from_nanos(u64::MAX)
    );
}

#[test]
fn audio_chunk_contains_tone_only_during_coded_pulse() {
    let config = ProbeConfig::from_request(&OutputDelayProbeRequest {
        duration_seconds: 6,
        warmup_seconds: 1,
        pulse_period_ms: 1_000,
        pulse_width_ms: 120,
        event_width_codes: "3".to_string(),
        audio_delay_us: 0,
        video_delay_us: 0,
    })
    .expect("config");

    let active = render_audio_chunk(&config, Duration::from_secs(1), 480);
    let idle = render_audio_chunk(&config, Duration::from_millis(500), 480);

    assert!(active.iter().any(|byte| *byte != 0));
    assert!(idle.iter().any(|byte| *byte != 0));
    assert!(rms_i16_le(&active) > rms_i16_le(&idle) * 10.0);
}

#[test]
fn generated_video_and_audio_share_the_same_event_schedule() {
    let config = ProbeConfig::from_request(&OutputDelayProbeRequest {
        duration_seconds: 6,
        warmup_seconds: 1,
        pulse_period_ms: 1_000,
        pulse_width_ms: 120,
        event_width_codes: "2".to_string(),
        audio_delay_us: 0,
        video_delay_us: 0,
    })
    .expect("config");
    let idle_pts = Duration::from_millis(500);
    let active_pts = Duration::from_secs(1);

    let idle_color = probe_color_for_code(config.event_code_at(idle_pts));
    let active_color = probe_color_for_code(config.event_code_at(active_pts));
    let idle_audio = render_audio_chunk(&config, idle_pts, 480);
    let active_audio = render_audio_chunk(&config, active_pts, 480);

    assert_eq!(
        (idle_color.r, idle_color.g, idle_color.b),
        (DARK_FRAME_RGB.r, DARK_FRAME_RGB.g, DARK_FRAME_RGB.b)
    );
    assert_ne!(
        (active_color.r, active_color.g, active_color.b),
        (DARK_FRAME_RGB.r, DARK_FRAME_RGB.g, DARK_FRAME_RGB.b)
    );
    assert!(rms_i16_le(&active_audio) > rms_i16_le(&idle_audio) * 10.0);
}

#[test]
#[cfg(not(coverage))]
fn hevc_probe_frame_encoder_builds_when_x265_is_available() {
    gstreamer::init().expect("initialize gstreamer");
    if gstreamer::ElementFactory::find("x265enc").is_none() {
        return;
    }
    let config = ProbeConfig::from_request(&OutputDelayProbeRequest {
        duration_seconds: 3,
        warmup_seconds: 1,
        pulse_period_ms: 1_000,
        pulse_width_ms: 120,
        event_width_codes: "1".to_string(),
        audio_delay_us: 0,
        video_delay_us: 0,
    })
    .expect("config");
    let camera = CameraConfig {
        output: CameraOutput::Uvc,
        codec: CameraCodec::Hevc,
        width: 320,
        height: 180,
        fps: 10,
        hdmi: None,
    };

    let frames =
        EncodedProbeFrames::new(&camera, &config, Duration::from_millis(100)).expect("hevc frames");

    assert!(
        !frames
            .packet_for_frame(0)
            .expect("first HEVC frame")
            .is_empty()
    );
    assert!(
        !frames
            .packet_for_frame(10)
            .expect("event HEVC frame")
            .is_empty()
    );
}

#[test]
fn timeline_exports_wall_clock_fields_for_freshness() {
    let config = ProbeConfig::from_request(&OutputDelayProbeRequest {
        duration_seconds: 6,
        warmup_seconds: 1,
        pulse_period_ms: 1_000,
        pulse_width_ms: 120,
        event_width_codes: "1".to_string(),
        audio_delay_us: 0,
        video_delay_us: 0,
    })
    .expect("config");
    let camera = CameraConfig {
        output: CameraOutput::Uvc,
        codec: CameraCodec::Mjpeg,
        width: 640,
        height: 480,
        fps: 20,
        hdmi: None,
    };
    let start_unix_ns = 1_700_000_000_000_000_000u128;
    let mut timeline = OutputDelayProbeTimeline::new(&config, &camera, start_unix_ns);
    let slot = config.event_slot_by_id(0);
    let video_us = duration_us(Duration::from_micros(slot.planned_start_us));
    let audio_us = video_us.saturating_add(500);

    timeline.mark_video(
        slot,
        1,
        video_us,
        unix_ns_from_start(start_unix_ns, video_us),
    );
    timeline.mark_audio(
        slot,
        1,
        audio_us,
        unix_ns_from_start(start_unix_ns, audio_us),
    );

    let json = serde_json::to_value(timeline).expect("timeline json");
    assert_eq!(
        json["injection_scope"].as_str(),
        Some("server-final-output-handoff")
    );
    assert_eq!(
        json["sink_handoff_path"].as_str(),
        Some("video CameraRelay::feed; audio Voice::push")
    );
    assert_eq!(json["client_uplink_included"].as_bool(), Some(false));
    assert_eq!(
        json["server_start_unix_ns"].as_u64(),
        Some(start_unix_ns as u64)
    );
    assert_eq!(
        json["events"][0]["video_feed_unix_ns"].as_u64(),
        Some(unix_ns_from_start(start_unix_ns, video_us) as u64)
    );
    assert_eq!(
        json["events"][0]["audio_push_unix_ns"].as_u64(),
        Some(unix_ns_from_start(start_unix_ns, audio_us) as u64)
    );
    assert_eq!(
        json["events"][0]["server_feed_delta_ms"].as_f64(),
        Some(0.5)
    );
}

#[test]
fn timeline_ignores_out_of_range_and_duplicate_marks() {
    let config = ProbeConfig::from_request(&OutputDelayProbeRequest {
        duration_seconds: 2,
        warmup_seconds: 3,
        pulse_period_ms: 1_000,
        pulse_width_ms: 120,
        event_width_codes: "1".to_string(),
        audio_delay_us: 0,
        video_delay_us: 0,
    })
    .expect("config");
    assert_eq!(config.event_count(), 0);

    let camera = CameraConfig {
        output: CameraOutput::Uvc,
        codec: CameraCodec::Mjpeg,
        width: 640,
        height: 480,
        fps: 20,
        hdmi: None,
    };
    let mut timeline = OutputDelayProbeTimeline::new(&config, &camera, 0);
    timeline.mark_audio(config.event_slot_by_id(0), 1, 1_000, 1_000_000);
    timeline.mark_video(config.event_slot_by_id(0), 1, 1_000, 1_000_000);
    assert!(timeline.events.is_empty());

    let config = ProbeConfig::from_request(&OutputDelayProbeRequest {
        duration_seconds: 6,
        warmup_seconds: 1,
        pulse_period_ms: 1_000,
        pulse_width_ms: 120,
        event_width_codes: "1".to_string(),
        audio_delay_us: 0,
        video_delay_us: 0,
    })
    .expect("config");
    let mut timeline = OutputDelayProbeTimeline::new(&config, &camera, 0);
    let slot = config.event_slot_by_id(0);

    timeline.mark_video(slot, 1, 1_000, 1_000_000);
    timeline.mark_video(slot, 2, 2_000, 2_000_000);
    timeline.mark_audio(slot, 1, 1_500, 1_500_000);
    timeline.mark_audio(slot, 2, 2_500, 2_500_000);

    assert_eq!(timeline.events[0].video_seq, Some(1));
    assert_eq!(timeline.events[0].audio_seq, Some(1));
    assert_eq!(timeline.events[0].server_feed_delta_ms, Some(0.5));
}

fn rms_i16_le(bytes: &[u8]) -> f64 {
    let samples = bytes
        .chunks_exact(2)
        .map(|chunk| f64::from(i16::from_le_bytes([chunk[0], chunk[1]])))
        .collect::<Vec<_>>();
    let mean_square =
        samples.iter().map(|sample| sample * sample).sum::<f64>() / samples.len().max(1) as f64;
    mean_square.sqrt()
}
