use super::*;
use crate::input::camera::{CameraCodec, CameraConfig};

const SUPPORTED_HEVC_AUDIT_MODES: &[(u32, u32, u32)] = &[
    (1280, 720, 20),
    (1280, 720, 30),
    (1920, 1080, 20),
    (1920, 1080, 30),
];

#[test]
/// Verifies the local HEVC probe bundle contains exactly the metadata the server needs.
///
/// Inputs: one synthetic HEVC access unit and nearby PCM audio packets on the
/// same capture clock. Outputs: assertions only. Why: this catches regressions
/// where the client silently sends plausible media bytes but drops the bundled
/// timing/profile fields that let the server map HEVC video and audio together.
fn hevc_probe_bundle_preserves_paired_media_and_server_metadata() {
    let camera = CameraConfig {
        codec: CameraCodec::Hevc,
        width: 1280,
        height: 720,
        fps: 30,
    };
    assert!(matches!(camera.codec, CameraCodec::Hevc));

    let probe_start = Instant::now();
    let mut video = VideoPacket {
        pts: 1_000_000,
        data: vec![0, 0, 0, 1, 0x26, 0xaa, 0xbb],
        ..Default::default()
    };
    let mut video_seq = 0;
    stamp_probe_video_packet(&mut video, &mut video_seq, 2, camera.fps, probe_start);

    let mut audio = vec![
        AudioPacket {
            pts: 950_000,
            data: vec![0; 1_920],
            ..Default::default()
        },
        AudioPacket {
            pts: 1_010_000,
            data: vec![1; 1_920],
            ..Default::default()
        },
    ];
    let mut audio_seq = 0;
    for packet in &mut audio {
        stamp_probe_audio_packet(packet, &mut audio_seq, 3, probe_start);
    }
    retain_probe_audio_for_video(&mut audio, packet_video_capture_pts_us(&video));
    assert_eq!(
        audio.len(),
        2,
        "nearby audio should travel with the HEVC frame"
    );

    let (capture_start_us, capture_end_us) = probe_bundle_capture_bounds(Some(&video), &audio);
    let bundle = build_probe_bundle(
        PROBE_BUNDLE_SESSION_ID,
        7,
        &camera,
        Some(video),
        audio,
        capture_start_us,
        capture_end_us,
    );

    let video = bundle.video.as_ref().expect("bundled HEVC video");
    assert_eq!(bundle.session_id, PROBE_BUNDLE_SESSION_ID);
    assert_eq!(bundle.seq, 7);
    assert_eq!(bundle.video_width, 1280);
    assert_eq!(bundle.video_height, 720);
    assert_eq!(bundle.video_fps, 30);
    assert_eq!(bundle.audio_sample_rate, 48_000);
    assert_eq!(bundle.audio_channels, 2);
    assert!(!bundle.audio.is_empty());
    assert!(video.data.windows(4).any(|window| window == [0, 0, 0, 1]));
    assert!(bundle.capture_start_us <= video.client_capture_pts_us);
    assert!(bundle.capture_end_us >= video.client_capture_pts_us);
    assert!(
        bundle.audio.iter().all(
            |packet| packet.client_capture_pts_us <= bundle.capture_end_us
                && packet.client_capture_pts_us >= bundle.capture_start_us
        ),
        "bundle capture bounds should include every paired audio packet"
    );
}

#[test]
/// Verifies the blind sync probe can describe compressed Opus audio bundles.
///
/// Inputs: one synthetic video packet plus a pre-compressed Opus-like audio
/// packet. Outputs: assertions over bundle metadata. Why: the client-to-RCT
/// hardware probe must not silently report PCM evidence when the operator asked
/// to validate the Opus upstream route.
fn probe_bundle_uses_opus_metadata_when_audio_packets_are_opus() {
    let camera = CameraConfig {
        codec: CameraCodec::Hevc,
        width: 1280,
        height: 720,
        fps: 30,
    };
    let probe_start = Instant::now();
    let mut video = VideoPacket {
        pts: 1_000_000,
        data: vec![0, 0, 0, 1, 0x26, 0xaa, 0xbb],
        ..Default::default()
    };
    let mut video_seq = 0;
    stamp_probe_video_packet(&mut video, &mut video_seq, 1, camera.fps, probe_start);

    let mut audio = AudioPacket {
        pts: 1_000_000,
        data: vec![0x7f; 160],
        frame_duration_us: 20_000,
        ..Default::default()
    };
    lesavka_common::audio_transport::mark_packet_opus(&mut audio);
    let mut audio_seq = 0;
    stamp_probe_audio_packet(&mut audio, &mut audio_seq, 1, probe_start);

    let bundle = build_probe_bundle(
        PROBE_BUNDLE_SESSION_ID,
        42,
        &camera,
        Some(video),
        vec![audio],
        1_000_000,
        1_000_000,
    );

    assert_eq!(
        bundle.audio_encoding,
        lesavka_common::lesavka::AudioEncoding::Opus as i32
    );
    assert_eq!(bundle.audio_sample_rate, 48_000);
    assert_eq!(bundle.audio_channels, 2);
    assert_eq!(bundle.audio[0].frame_duration_us, 20_000);
    assert_eq!(bundle.audio[0].data.len(), 160);
}

#[test]
/// Verifies a synthetic HEVC event train leaves as paired A/V bundles.
///
/// Inputs: sixteen fake HEVC access units and nearby PCM packets on the same
/// client capture clock. Outputs: assertions over the exact
/// `UpstreamMediaBundle` messages that would enter the gRPC stream. Why: the
/// client-to-server probe is only useful if every coded flash keeps its nearby
/// tone packets and profile metadata before the server sees it.
fn hevc_probe_bundle_train_keeps_every_coded_flash_with_nearby_audio() {
    let camera = default_hevc_probe_camera();
    let bundles = build_hevc_probe_bundle_train_for_camera(camera);

    assert_hevc_probe_bundle_train(&camera, &bundles);
}

#[test]
/// Proves every supported HEVC transport mode can build a complete local A/V train.
///
/// Inputs: the four resolution/fps combinations Lesavka advertises for the RCT
/// path. Outputs: assertions over the bundled media units only. Why: the next
/// hardware loop should fail on transport or server decode if it fails at all,
/// not because one mode's synthetic flash/tone source cannot produce all 16
/// physically paired HEVC+audio bundles.
fn hevc_probe_bundle_train_covers_every_supported_mode() {
    for &(width, height, fps) in SUPPORTED_HEVC_AUDIT_MODES {
        let camera = hevc_probe_camera(width, height, fps);
        let bundles = build_hevc_probe_bundle_train_for_camera(camera);
        assert_hevc_probe_bundle_train(&camera, &bundles);
    }
}

/// Checks one synthetic bundle train against the transport invariants.
///
/// Inputs: the camera profile used to build the train and the resulting
/// `UpstreamMediaBundle` messages. Outputs: assertions only. Why: mode-specific
/// tests should share one invariant definition so 20fps/30fps coverage cannot
/// quietly drift from the canonical HEVC probe behavior.
fn assert_hevc_probe_bundle_train(camera: &CameraConfig, bundles: &[UpstreamMediaBundle]) {
    assert_eq!(bundles.len(), 16);
    for (idx, bundle) in bundles.iter().enumerate() {
        let event_code = idx as u64 + 1;
        let video = bundle.video.as_ref().expect("HEVC video packet");
        assert_eq!(bundle.session_id, PROBE_BUNDLE_SESSION_ID);
        assert_eq!(bundle.seq, event_code);
        assert_eq!(bundle.video_width, camera.width);
        assert_eq!(bundle.video_height, camera.height);
        assert_eq!(bundle.video_fps, camera.fps);
        assert_eq!(bundle.audio_sample_rate, 48_000);
        assert_eq!(bundle.audio_channels, 2);
        assert_eq!(bundle.audio.len(), 2);
        assert!(video.data.windows(4).any(|window| window == [0, 0, 0, 1]));
        assert_eq!(video.effective_fps, camera.fps);
        assert!(video.client_capture_pts_us > 0);
        assert!(video.client_send_pts_us >= video.client_capture_pts_us);
        assert!(
            bundle.capture_start_us <= video.client_capture_pts_us
                && bundle.capture_end_us >= video.client_capture_pts_us
        );
        assert!(bundle.audio.iter().all(|packet| {
            packet.client_capture_pts_us >= bundle.capture_start_us
                && packet.client_capture_pts_us <= bundle.capture_end_us
                && packet.client_send_pts_us >= packet.client_capture_pts_us
                && packet.data.len() == 1_920
        }));
        assert!(
            bundle
                .audio
                .iter()
                .any(|packet| { packet.client_capture_pts_us < video.client_capture_pts_us })
                && bundle
                    .audio
                    .iter()
                    .any(|packet| { packet.client_capture_pts_us > video.client_capture_pts_us }),
            "event {event_code} should carry tone packets around the flash PTS"
        );
    }
}

#[test]
/// Stress-tests the local HEVC bundle train under freshness-biased delivery.
///
/// Inputs: the same sixteen synthetic bundles used by the client-to-server-RCT
/// probe plus a deterministic WAN jitter profile. Outputs: assertions that
/// stale bundles are dropped as whole A/V units while the remaining evidence
/// still meets the analyzer's 13-pair floor. Why: the next hardware run should
/// test real transport, not discover that our synthetic client media can split
/// tones from flashes when freshness pressure appears.
fn hevc_probe_bundle_train_drops_stale_events_as_complete_av_units_under_jitter() {
    let bundles = build_hevc_probe_bundle_train();
    let simulated_network_delay_ms = [
        44_u64, 72, 96, 1_450, 118, 64, 88, 77, 1_820, 91, 70, 109, 83, 1_120, 93, 66,
    ];
    let max_age_ms = 1_000_u64;
    let mut delivered = Vec::new();
    let mut dropped = Vec::new();

    for (bundle, delay_ms) in bundles.iter().zip(simulated_network_delay_ms) {
        let video = bundle.video.as_ref().expect("HEVC video packet");
        let observed_age_ms = delay_ms
            + video
                .client_send_pts_us
                .saturating_sub(video.client_capture_pts_us)
                / 1_000;
        if observed_age_ms > max_age_ms {
            dropped.push(bundle.seq);
            continue;
        }
        assert!(
            bundle.audio.iter().all(|packet| {
                let skew_us = packet
                    .client_capture_pts_us
                    .abs_diff(video.client_capture_pts_us);
                skew_us <= 120_000
            }),
            "fresh bundles must keep only nearby tone packets"
        );
        delivered.push(bundle.seq);
    }

    assert_eq!(dropped, vec![4, 9, 14]);
    assert_eq!(delivered.len(), 13);
    assert!(
        delivered.windows(2).all(|window| window[1] > window[0]),
        "freshness drops must not reorder the surviving coded event train"
    );
}

#[test]
/// Writes and validates a local manifest for the outgoing synthetic HEVC bundle train.
///
/// Inputs: sixteen synthetic HEVC flash packets plus nearby PCM tone packets.
/// Outputs: a JSON audit manifest, optionally copied to
/// `LESAVKA_LOCAL_HEVC_BUNDLE_AUDIT_JSON`. Why: this is a passwordless
/// preflight for the client side of the HEVC plan, proving the server will
/// receive one complete bundled train before any WAN/RCT hardware can add
/// noise.
fn hevc_probe_bundle_audit_writes_manifest_for_local_preflight() {
    let bundles = build_hevc_probe_bundle_train();
    let manifest = hevc_bundle_audit_manifest(&bundles);

    assert_eq!(manifest["schema"], "lesavka.local-hevc-bundle-audit.v1");
    assert_eq!(manifest["summary"]["bundles"], 16);
    assert_eq!(manifest["summary"]["coded_video_events"], 16);
    assert_eq!(manifest["summary"]["bundles_with_audio_before_video"], 16);
    assert_eq!(manifest["summary"]["bundles_with_audio_after_video"], 16);
    assert_eq!(manifest["summary"]["annex_b_video_events"], 16);
    assert_eq!(manifest["summary"]["monotonic_bundle_sequences"], true);
    assert_eq!(manifest["summary"]["metadata_mode"], "1920x1080@30");
    assert_eq!(manifest["summary"]["video_codec"], "hevc");

    let audit_file = tempfile::NamedTempFile::new().expect("audit tempfile");
    write_hevc_bundle_audit(audit_file.path(), &manifest);
    let persisted: serde_json::Value =
        serde_json::from_slice(&std::fs::read(audit_file.path()).expect("audit bytes"))
            .expect("audit json");
    assert_eq!(persisted["summary"]["bundles"], 16);

    if let Some(path) = std::env::var_os("LESAVKA_LOCAL_HEVC_BUNDLE_AUDIT_JSON") {
        write_hevc_bundle_audit(std::path::Path::new(&path), &manifest);
    }
}

/// Builds the local HEVC probe train exactly like the client transport harness should emit it.
///
/// Inputs are fixed so the audit remains deterministic: 16 coded video flashes at
/// `1920x1080@30` plus nearby audio tone packets. The output is a sequence of
/// bundled media units with one HEVC-like video packet and the audio packets that
/// should stay physically paired before transport.
fn build_hevc_probe_bundle_train() -> Vec<UpstreamMediaBundle> {
    build_hevc_probe_bundle_train_for_camera(default_hevc_probe_camera())
}

/// Returns the canonical local audit mode used by the persisted manifest.
///
/// Inputs: none. Output: the 1080p30 HEVC camera profile. Why: the manifest is
/// intentionally stable for easy comparison across runs, while a separate test
/// covers every advertised transport mode.
fn default_hevc_probe_camera() -> CameraConfig {
    hevc_probe_camera(1920, 1080, 30)
}

/// Builds one HEVC camera profile for local bundle-audit tests.
///
/// Inputs: width, height, and fps. Output: `CameraConfig` with HEVC selected.
/// Why: keeping mode construction in one place avoids accidentally testing MJPEG
/// metadata while hardening the HEVC client transport path.
fn hevc_probe_camera(width: u32, height: u32, fps: u32) -> CameraConfig {
    CameraConfig {
        codec: CameraCodec::Hevc,
        width,
        height,
        fps,
    }
}

/// Builds the local HEVC probe train for one camera mode.
///
/// Inputs are fixed except for the camera profile: 16 coded video flashes plus
/// nearby audio tone packets. Output: bundled media units with one HEVC-like
/// video packet and the audio packets that should stay physically paired before
/// transport.
fn build_hevc_probe_bundle_train_for_camera(camera: CameraConfig) -> Vec<UpstreamMediaBundle> {
    let probe_start = Instant::now();
    let mut video_seq = 0;
    let mut audio_seq = 0;
    let mut bundles = Vec::new();

    for event_index in 0..16_u64 {
        let event_pts_us = 4_000_000 + event_index * 1_000_000;
        let event_code = event_index + 1;
        let mut video = VideoPacket {
            pts: event_pts_us,
            data: vec![0, 0, 0, 1, 0x26, event_code as u8, 0xaa],
            ..Default::default()
        };
        stamp_probe_video_packet(&mut video, &mut video_seq, 1, camera.fps, probe_start);

        let mut audio = vec![
            AudioPacket {
                pts: event_pts_us.saturating_sub(200_000),
                data: vec![0xee; 1_920],
                ..Default::default()
            },
            AudioPacket {
                pts: event_pts_us.saturating_sub(20_000),
                data: vec![event_code as u8; 1_920],
                ..Default::default()
            },
            AudioPacket {
                pts: event_pts_us.saturating_add(10_000),
                data: vec![event_code as u8; 1_920],
                ..Default::default()
            },
        ];
        for packet in &mut audio {
            stamp_probe_audio_packet(packet, &mut audio_seq, 2, probe_start);
        }

        retain_probe_audio_for_video(&mut audio, packet_video_capture_pts_us(&video));
        assert_eq!(
            audio.len(),
            2,
            "event {event_code} should keep only nearby tone packets"
        );
        let (capture_start_us, capture_end_us) = probe_bundle_capture_bounds(Some(&video), &audio);
        bundles.push(build_probe_bundle(
            PROBE_BUNDLE_SESSION_ID,
            event_code,
            &camera,
            Some(video),
            audio,
            capture_start_us,
            capture_end_us,
        ));
    }

    bundles
}

/// Converts a bundle train into the JSON evidence consumed by the local audit script.
///
/// The input transport bundle train is recorded before involving the server: coded video visibility,
/// Annex-B framing, monotonic bundle sequence numbers, and audio packets on both
/// sides of each video timestamp.
fn hevc_bundle_audit_manifest(bundles: &[UpstreamMediaBundle]) -> serde_json::Value {
    let mut previous_seq = 0_u64;
    let mut monotonic = true;
    let mut coded_video_events = 0usize;
    let mut annex_b_video_events = 0usize;
    let mut audio_before_video = 0usize;
    let mut audio_after_video = 0usize;
    let mut total_audio_packets = 0usize;
    let events = bundles
        .iter()
        .map(|bundle| {
            monotonic &= bundle.seq > previous_seq;
            previous_seq = bundle.seq;
            let video = bundle.video.as_ref().expect("audit video packet");
            let has_annex_b = video.data.windows(4).any(|window| window == [0, 0, 0, 1]);
            annex_b_video_events += usize::from(has_annex_b);
            coded_video_events += usize::from(!video.data.is_empty());
            let before = bundle
                .audio
                .iter()
                .filter(|packet| packet.client_capture_pts_us < video.client_capture_pts_us)
                .count();
            let after = bundle
                .audio
                .iter()
                .filter(|packet| packet.client_capture_pts_us > video.client_capture_pts_us)
                .count();
            let audio_capture_pts_us = bundle
                .audio
                .iter()
                .map(|packet| packet.client_capture_pts_us)
                .collect::<Vec<_>>();
            let max_audio_video_skew_us = bundle
                .audio
                .iter()
                .map(|packet| {
                    packet
                        .client_capture_pts_us
                        .abs_diff(video.client_capture_pts_us)
                })
                .max()
                .unwrap_or(0);
            audio_before_video += usize::from(before > 0);
            audio_after_video += usize::from(after > 0);
            total_audio_packets += bundle.audio.len();
            serde_json::json!({
                "bundle_seq": bundle.seq,
                "event_code": video.data.get(5).copied().unwrap_or_default(),
                "video_capture_pts_us": video.client_capture_pts_us,
                "video_send_pts_us": video.client_send_pts_us,
                "video_bytes": video.data.len(),
                "has_annex_b_start_code": has_annex_b,
                "audio_packets": bundle.audio.len(),
                "audio_capture_pts_us": audio_capture_pts_us,
                "max_audio_video_skew_us": max_audio_video_skew_us,
                "audio_before_video": before,
                "audio_after_video": after,
                "capture_start_us": bundle.capture_start_us,
                "capture_end_us": bundle.capture_end_us,
            })
        })
        .collect::<Vec<_>>();

    serde_json::json!({
        "schema": "lesavka.local-hevc-bundle-audit.v1",
        "summary": {
            "video_codec": "hevc",
            "metadata_mode": "1920x1080@30",
            "bundles": bundles.len(),
            "coded_video_events": coded_video_events,
            "annex_b_video_events": annex_b_video_events,
            "audio_packets": total_audio_packets,
            "bundles_with_audio_before_video": audio_before_video,
            "bundles_with_audio_after_video": audio_after_video,
            "monotonic_bundle_sequences": monotonic,
        },
        "events": events,
    })
}

/// Persists local audit evidence so later remote failures have a known-good client-side artifact.
///
/// `path` is the destination JSON file; parent directories are created on demand.
/// `manifest` is written as pretty JSON and the function panics in tests if the
/// local filesystem cannot store the evidence.
fn write_hevc_bundle_audit(path: &std::path::Path, manifest: &serde_json::Value) {
    if let Some(parent) = path.parent() {
        std::fs::create_dir_all(parent).expect("audit parent");
    }
    let file = std::fs::File::create(path).expect("audit file");
    serde_json::to_writer_pretty(file, manifest).expect("audit json write");
}
