// Unit coverage for the HEVC-to-MJPEG decoded-frame guard.
//
// Scope: include the server guard in the root test taxonomy and model the UVC
// helper decision that freezes suspicious frames.
// Targets: server/src/video_sinks/hevc_mjpeg_guard.rs.
// Why: when HEVC decode produces a syntactically valid but visually damaged
// JPEG, users should see a short last-good-frame freeze instead of grey slabs,
// black frames, or tearing.

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 jpeg_quality() -> u32 {
            hevc_jpeg_quality()
        }

        pub fn min_reference() -> u32 {
            min_reference_bytes()
        }

        pub fn drop_pct() -> u32 {
            size_drop_pct()
        }

        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)
        }
    }
}
use video_sinks::hevc_mjpeg_guard as guard;

#[derive(Default)]
struct GuardedFrameEmitter {
    last_good_bytes: u64,
    emitted_bytes: Vec<usize>,
    frozen_frames: usize,
}

impl GuardedFrameEmitter {
    fn push(&mut self, bytes: usize) -> bool {
        if guard::should_freeze(self.last_good_bytes, bytes) {
            self.frozen_frames += 1;
            return false;
        }

        self.last_good_bytes = bytes as u64;
        self.emitted_bytes.push(bytes);
        true
    }
}

#[test]
fn guard_freezes_suspicious_decoded_collapses_but_allows_normal_variation() {
    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")),
        ],
        || {
            assert!(!guard::should_freeze(200_000, 110_000));
            assert!(guard::should_freeze(200_000, 80_000));
            assert!(!guard::should_freeze(20_000, 1_000));
        },
    );
}

#[test]
fn last_good_frame_model_does_not_emit_damaged_hevc_mjpeg_frames() {
    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 mut emitter = GuardedFrameEmitter::default();

            assert!(emitter.push(220_000));
            assert!(!emitter.push(20_000));
            assert!(!emitter.push(60_000));
            assert!(emitter.push(210_000));

            assert_eq!(emitter.frozen_frames, 2);
            assert_eq!(emitter.emitted_bytes, vec![220_000, 210_000]);
            assert_eq!(emitter.last_good_bytes, 210_000);
        },
    );
}

#[test]
fn guard_tuning_env_is_clamped_to_safe_runtime_bounds() {
    temp_env::with_vars(
        [
            ("LESAVKA_UVC_HEVC_JPEG_QUALITY", Some("0")),
            ("LESAVKA_UVC_HEVC_SIZE_DROP_PCT", Some("0")),
            ("LESAVKA_UVC_HEVC_MIN_REFERENCE_BYTES", Some("0")),
        ],
        || {
            assert_eq!(guard::jpeg_quality(), 1);
            assert_eq!(guard::drop_pct(), 1);
            assert_eq!(guard::min_reference(), 1);
        },
    );

    temp_env::with_vars(
        [
            ("LESAVKA_UVC_HEVC_JPEG_QUALITY", Some("101")),
            ("LESAVKA_UVC_HEVC_SIZE_DROP_PCT", Some("100")),
            ("LESAVKA_UVC_HEVC_MIN_REFERENCE_BYTES", Some("2048")),
        ],
        || {
            assert_eq!(guard::jpeg_quality(), 100);
            assert_eq!(guard::drop_pct(), 95);
            assert_eq!(guard::min_reference(), 2048);
        },
    );
}

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 guard_freezes_incomplete_and_low_entropy_decoded_frames() {
    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 healthy_payload: Vec<u8> = (0..130_000).map(|idx| (idx % 251) as u8).collect();
            let grey_slab_payload = vec![0x80; 130_000];
            let black_slab_payload = vec![0x00; 130_000];
            let mut truncated = jpeg_with_payload(&healthy_payload);
            truncated.truncate(truncated.len() - 1);

            assert!(!guard::should_freeze_frame(
                220_000,
                &jpeg_with_payload(&healthy_payload),
            ));
            assert!(guard::should_freeze_frame(
                220_000,
                &jpeg_with_payload(&grey_slab_payload),
            ));
            assert!(guard::should_freeze_frame(
                220_000,
                &jpeg_with_payload(&black_slab_payload),
            ));
            assert!(guard::should_freeze_frame(220_000, &truncated));
        },
    );
}
