// Regression coverage for the direct-MJPEG normalizer RSS leak.
//
// Scope: lock down the safe production defaults and lifecycle cleanup for the
// server UVC MJPEG path.
// Targets: server/src/video_sinks/hevc_mjpeg_guard.rs,
// server/src/video_sinks/webcam_sink.rs, server/src/camera_runtime.rs, and
// the camera/upstream media RPC lifecycles.
// Why: the native GStreamer jpegdec/jpegenc normalizer can retain process RSS
// during long calls; future changes must keep it opt-in, guarded, and cleaned
// up when the client stream closes.

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 normalizer_enabled() -> bool {
            direct_mjpeg_normalize_enabled()
        }

        pub fn normalizer_rss_limit_kb() -> Option<u64> {
            direct_mjpeg_normalize_rss_limit_kb()
        }
    }
}
use video_sinks::hevc_mjpeg_guard as guard;

const SERVER_INSTALL: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/scripts/install/server.sh"
));
const WEBCAM_SINK: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/server/src/video_sinks/webcam_sink.rs"
));
const WEBCAM_CONSTRUCTOR: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/server/src/video_sinks/webcam_sink/constructor.rs"
));
const WEBCAM_FRAME_HANDOFF: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/server/src/video_sinks/webcam_sink/frame_handoff.rs"
));
const CAMERA_RUNTIME: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/server/src/camera_runtime.rs"
));
const CAMERA_RPC: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/server/src/main/relay_service/camera_stream_rpc.rs"
));
const UPSTREAM_MEDIA_RPC: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/server/src/main/relay_service/upstream_media_rpc.rs"
));

fn assert_ordered(haystack: &str, earlier: &str, later: &str) {
    let earlier_pos = haystack
        .find(earlier)
        .unwrap_or_else(|| panic!("missing earlier marker {earlier:?}"));
    let later_pos = haystack
        .find(later)
        .unwrap_or_else(|| panic!("missing later marker {later:?}"));
    assert!(
        earlier_pos < later_pos,
        "expected marker {earlier:?} before {later:?}"
    );
}

#[test]
fn direct_mjpeg_normalizer_defaults_on_with_rss_fuse_after_native_rss_leak() {
    temp_env::with_vars(
        [
            ("LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE", None::<&str>),
            (
                "LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB",
                None::<&str>,
            ),
        ],
        || {
            assert!(
                guard::normalizer_enabled(),
                "direct MJPEG normalization should be active but RSS-fused while chasing UVC artifacts"
            );
            assert_eq!(guard::normalizer_rss_limit_kb(), Some(384 * 1024));
        },
    );

    temp_env::with_vars(
        [
            ("LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE", Some("on")),
            ("LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB", Some("0")),
        ],
        || {
            assert!(guard::normalizer_enabled());
            assert_eq!(guard::normalizer_rss_limit_kb(), None);
        },
    );
}

#[test]
fn installer_keeps_the_native_normalizer_memory_bounded_by_default() {
    assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE:-1}"));
    assert!(!SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE:-0}"));
    assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_RSS_LIMIT_MB:-384}"));
    assert!(SERVER_INSTALL.contains("${LESAVKA_UVC_DIRECT_MJPEG_NORMALIZE_PULL_TIMEOUT_MS:-0}"));
}

#[test]
fn native_mjpeg_sanitizer_parses_jpeg_caps_after_reencode() {
    for marker in [
        "direct_mjpeg_normalize_input_parse",
        "direct_mjpeg_normalize_output_parse",
        "dynamic_hevc_mjpeg_output_parse",
    ] {
        assert!(
            WEBCAM_SINK.contains(marker),
            "normalizer/spool branches should preserve JPEG parser marker {marker}"
        );
    }
    assert!(WEBCAM_CONSTRUCTOR.contains("hevc_mjpeg_output_parse"));
    assert_ordered(
        WEBCAM_SINK,
        "direct_mjpeg_normalize_output_parse",
        ".property(\"caps\", &caps_mjpeg)",
    );
    let dynamic_parse_pos = WEBCAM_SINK
        .find("dynamic_hevc_mjpeg_output_parse")
        .expect("dynamic HEVC spool parser marker");
    assert!(
        WEBCAM_SINK[dynamic_parse_pos..].contains(".property(\"caps\", &caps_mjpeg)"),
        "dynamic HEVC spool capsfilter should sit after jpegparse, not directly after jpegenc"
    );
    assert!(
        !WEBCAM_SINK.contains(".field(\"colorimetry\", \"2:4:7:1\")")
            && !WEBCAM_CONSTRUCTOR.contains(".field(\"colorimetry\", \"2:4:7:1\")"),
        "encoded JPEG parser output advertises bt709-style colorimetry, so hard-coded 2:4:7:1 caps can starve the branch"
    );
}

#[test]
fn opt_in_normalizer_has_rss_fuse_before_per_frame_gstreamer_allocation() {
    assert!(WEBCAM_FRAME_HANDOFF.contains("current_process_rss_kb"));
    assert!(WEBCAM_FRAME_HANDOFF.contains("direct_mjpeg_normalize_rss_limit_kb"));
    assert!(WEBCAM_FRAME_HANDOFF.contains(
        "direct MJPEG normalization disabled because server RSS exceeded its safety limit"
    ));
    assert_ordered(
        WEBCAM_FRAME_HANDOFF,
        "direct_mjpeg_normalize_rss_limit_kb()",
        "gst::Buffer::from_slice(pkt.data.clone())",
    );
    let normalizer_push = WEBCAM_FRAME_HANDOFF
        .split("gst::Buffer::from_slice(pkt.data.clone())")
        .nth(1)
        .expect("direct normalizer buffer allocation block");
    assert_ordered(
        normalizer_push,
        "meta.set_duration(Some(gst::ClockTime::from_useconds(self.frame_step_us)))",
        "src.push_buffer(buf)",
    );
}

#[test]
fn closed_camera_streams_release_native_uvc_pipeline_state() {
    assert!(CAMERA_RUNTIME.contains("pub async fn release_if_active"));
    assert!(CAMERA_RUNTIME.contains("slot.take()"));
    assert!(WEBCAM_SINK.contains("impl Drop for WebcamSink"));
    assert!(WEBCAM_SINK.contains("set_state(gst::State::Null)"));
    assert!(CAMERA_RPC.contains("camera_rt.release_if_active(camera_session_id).await"));
    assert!(UPSTREAM_MEDIA_RPC.contains("camera_rt.release_if_active(camera_session_id).await"));
}
