// Chaos-style coverage for decoded HEVC-to-MJPEG visual damage handling.
//
// Scope: model bursts of partial, black, stale, and recovered decoded frames.
// Targets: server/src/video_sinks/hevc_mjpeg_guard.rs,
// server/src/video_sinks/webcam_sink.rs, and server/src/main/relay_service.rs.
// Why: the known-good user experience is sync/freshness first, with freezes
// preferred over showing corrupted grey or torn frames in conferencing apps.

mod video_support {
    pub fn env_u32(name: &str, default: u32) -> u32 {
        std::env::var(name)
            .ok()
            .and_then(|value| value.trim().parse().ok())
            .unwrap_or(default)
    }
}

mod video_sinks {
    #[allow(clippy::items_after_test_module)]
    pub mod hevc_mjpeg_guard {
        include!(concat!(
            env!("CARGO_MANIFEST_DIR"),
            "/server/src/video_sinks/hevc_mjpeg_guard.rs"
        ));

        pub fn should_freeze(previous_bytes: u64, next_bytes: usize) -> bool {
            should_freeze_decoded_mjpeg(previous_bytes, next_bytes)
        }

        pub fn should_freeze_frame(previous_bytes: u64, decoded_mjpeg: &[u8]) -> bool {
            should_freeze_decoded_mjpeg_frame(previous_bytes, decoded_mjpeg)
        }

        pub fn should_reject_direct_frame(previous_bytes: u64, mjpeg: &[u8]) -> bool {
            should_reject_direct_mjpeg_frame(previous_bytes, mjpeg)
        }

        pub fn should_reject_direct_frame_with_budget(
            previous_bytes: u64,
            max_bytes: usize,
            mjpeg: &[u8],
        ) -> bool {
            direct_mjpeg_reject_reason(previous_bytes, Some(max_bytes), None, mjpeg).is_some()
        }
    }
}
use video_sinks::hevc_mjpeg_guard as guard;

const WEBCAM_SINK: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/server/src/video_sinks/webcam_sink.rs"
));
const WEBCAM_HANDOFF: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/server/src/video_sinks/webcam_sink/frame_handoff.rs"
));
const RELAY_SERVICE: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/server/src/main/relay_service.rs"
));
const WEBCAM_RPC: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/server/src/main/relay_service/upstream_media_rpc.rs"
));

#[derive(Debug, Clone, Copy)]
enum FrameEvent {
    Emitted(usize),
    Frozen(usize),
}

fn run_damage_burst(frames: &[usize]) -> Vec<FrameEvent> {
    let mut last_good = 0;
    let mut events = Vec::with_capacity(frames.len());
    for &bytes in frames {
        if guard::should_freeze(last_good, bytes) {
            events.push(FrameEvent::Frozen(bytes));
        } else {
            last_good = bytes as u64;
            events.push(FrameEvent::Emitted(bytes));
        }
    }
    events
}

#[test]
fn corrupt_partial_and_black_frames_freeze_until_recovery_frame_arrives() {
    temp_env::with_vars(
        [
            ("LESAVKA_UVC_HEVC_FREEZE_ON_SIZE_DROP", Some("1")),
            ("LESAVKA_UVC_HEVC_SIZE_DROP_PCT", Some("45")),
            ("LESAVKA_UVC_HEVC_MIN_REFERENCE_BYTES", Some("65536")),
        ],
        || {
            let events = run_damage_burst(&[230_000, 100_000, 12_000, 0, 225_000, 190_000]);

            assert!(matches!(events[0], FrameEvent::Emitted(230_000)));
            assert!(matches!(events[1], FrameEvent::Frozen(100_000)));
            assert!(matches!(events[2], FrameEvent::Frozen(12_000)));
            assert!(matches!(events[3], FrameEvent::Frozen(0)));
            assert!(matches!(events[4], FrameEvent::Emitted(225_000)));
            assert!(matches!(events[5], FrameEvent::Emitted(190_000)));
        },
    );
}

#[test]
fn tiny_startup_frames_do_not_poison_later_decode_baselines() {
    temp_env::with_vars(
        [
            ("LESAVKA_UVC_HEVC_FREEZE_ON_SIZE_DROP", Some("1")),
            ("LESAVKA_UVC_HEVC_SIZE_DROP_PCT", Some("45")),
            ("LESAVKA_UVC_HEVC_MIN_REFERENCE_BYTES", Some("65536")),
        ],
        || {
            let events = run_damage_burst(&[8_000, 7_000, 120_000, 53_000]);

            assert!(matches!(events[0], FrameEvent::Emitted(8_000)));
            assert!(matches!(events[1], FrameEvent::Emitted(7_000)));
            assert!(matches!(events[2], FrameEvent::Emitted(120_000)));
            assert!(matches!(events[3], FrameEvent::Frozen(53_000)));
        },
    );
}

#[test]
fn server_hevc_recovery_and_freshest_spool_paths_remain_wired() {
    for marker in [
        "freshest_mjpeg_sample(sink)",
        "last_decoded_mjpeg_bytes",
        "last_mjpeg_passthrough_bytes",
        "direct_mjpeg_normalize_src",
        "direct_mjpeg_normalize_bypassed",
        "mjpeg_normalized",
        "current_process_rss_kb",
        "direct_mjpeg_normalize_rss_limit_kb",
        "decoded_mjpeg_reject_reason(previous_bytes, map.as_slice())",
        "direct_mjpeg_reject_reason(",
        "spool_direct_mjpeg_frame",
        "spool_passthrough_direct_mjpeg_frame",
        "freezing suspicious decoded HEVC->MJPEG frame",
        "freezing suspicious direct MJPEG frame before UVC spool",
        "direct MJPEG normalization starved; falling back to guarded passthrough",
    ] {
        assert!(
            WEBCAM_SINK.contains(marker) || WEBCAM_HANDOFF.contains(marker),
            "HEVC output damage handling should preserve marker {marker}"
        );
    }

    for marker in [
        "media_v2_should_hold_hevc_video_for_recovery",
        "media_v2_has_hevc_recovery_keyframe",
        "held HEVC delta frame until next recovery keyframe",
    ] {
        assert!(
            RELAY_SERVICE.contains(marker) || WEBCAM_RPC.contains(marker),
            "server missing-keyframe recovery should preserve marker {marker}"
        );
    }
}

#[test]
fn direct_mjpeg_guard_is_conservative_but_filters_obvious_black_or_truncated_frames() {
    let healthy_payload: Vec<u8> = (0..140_000).map(|idx| (idx % 251) as u8).collect();
    let flat = jpeg_with_payload(&vec![0x80; 140_000]);
    let healthy = jpeg_with_payload(&healthy_payload);
    let tiny = jpeg_with_payload(&vec![0x42; 8_000]);
    let mut truncated = healthy.clone();
    truncated.pop();

    temp_env::with_vars(
        [
            ("LESAVKA_UVC_DIRECT_MJPEG_VISUAL_GUARD", Some("1")),
            ("LESAVKA_UVC_DIRECT_MJPEG_SIZE_DROP_PCT", Some("18")),
            (
                "LESAVKA_UVC_DIRECT_MJPEG_MIN_REFERENCE_BYTES",
                Some("49152"),
            ),
        ],
        || {
            assert!(!guard::should_reject_direct_frame(0, &flat));
            assert!(!guard::should_reject_direct_frame(220_000, &healthy));
            assert!(guard::should_reject_direct_frame_with_budget(
                220_000,
                healthy.len() - 1,
                &healthy
            ));
            assert!(guard::should_reject_direct_frame(220_000, &flat));
            assert!(guard::should_reject_direct_frame(220_000, &tiny));
            assert!(guard::should_reject_direct_frame(220_000, &truncated));
        },
    );
}

fn jpeg_with_payload(payload: &[u8]) -> Vec<u8> {
    let mut bytes = vec![0xff, 0xd8, 0xff, 0xda];
    bytes.extend_from_slice(payload);
    bytes.extend_from_slice(&[0xff, 0xd9]);
    bytes
}

#[test]
fn grey_and_black_slab_bursts_freeze_instead_of_reaching_uvc() {
    temp_env::with_vars(
        [
            ("LESAVKA_UVC_HEVC_FREEZE_ON_SIZE_DROP", Some("1")),
            ("LESAVKA_UVC_HEVC_SIZE_DROP_PCT", Some("45")),
            ("LESAVKA_UVC_HEVC_MIN_REFERENCE_BYTES", Some("65536")),
            ("LESAVKA_UVC_HEVC_MIN_PAYLOAD_DISTINCT_BYTES", Some("12")),
            ("LESAVKA_UVC_HEVC_DOMINANT_BYTE_PCT", Some("92")),
        ],
        || {
            let prior_good = 240_000;
            let healthy_payload: Vec<u8> = (0..140_000).map(|idx| (idx % 251) as u8).collect();
            let grey = jpeg_with_payload(&vec![0x80; 140_000]);
            let black = jpeg_with_payload(&vec![0x00; 140_000]);
            let healthy = jpeg_with_payload(&healthy_payload);

            assert!(guard::should_freeze_frame(prior_good, &grey));
            assert!(guard::should_freeze_frame(prior_good, &black));
            assert!(guard::should_freeze_frame(0, &black));
            assert!(!guard::should_freeze_frame(prior_good, &healthy));
        },
    );
}
