/// Verifies HEVC decoded-frame polling defaults to a freshness-first wait.
///
/// Input: unset timeout env var. Output: 20ms appsink poll timeout. Why:
/// server-side hardware decode/JPEG encode often lands inside one 30fps
/// frame interval, and a 5ms poll was starving Meet-visible UVC output.
#[test]
fn decoded_mjpeg_pull_timeout_defaults_to_short_bounded_wait() {
    temp_env::with_var_unset("LESAVKA_UVC_HEVC_SPOOL_PULL_TIMEOUT_MS", || {
        assert_eq!(
            super::decoded_mjpeg_pull_timeout(),
            gstreamer::ClockTime::from_mseconds(20)
        );
    });
}

/// Verifies explicit HEVC spool polling overrides stay bounded.
///
/// Input: zero and oversized timeout values. Output: direct zero polling
/// and a 50ms safety cap. Why: lab tuning may need aggressive polling, but
/// no override should recreate the multi-second decoded-frame backlog.
#[test]
fn decoded_mjpeg_pull_timeout_allows_fast_poll_and_clamps_slow_waits() {
    temp_env::with_var("LESAVKA_UVC_HEVC_SPOOL_PULL_TIMEOUT_MS", Some("0"), || {
        assert_eq!(
            super::decoded_mjpeg_pull_timeout(),
            gstreamer::ClockTime::from_mseconds(0)
        );
    });

    temp_env::with_var("LESAVKA_UVC_HEVC_SPOOL_PULL_TIMEOUT_MS", Some("250"), || {
        assert_eq!(
            super::decoded_mjpeg_pull_timeout(),
            gstreamer::ClockTime::from_mseconds(50)
        );
    });
}

#[test]
fn mjpeg_spool_frame_budget_uses_live_budget_when_zero_or_unset() {
    temp_env::with_vars(
        [
            ("LESAVKA_UVC_FRAME_SIZE_GUARD", None::<&str>),
            ("LESAVKA_UVC_FRAME_MAX_BYTES", None::<&str>),
            ("LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC", None::<&str>),
        ],
        || {
            assert_eq!(super::mjpeg_spool_frame_max_bytes(30), 150_000);
        },
    );

    temp_env::with_vars(
        [
            ("LESAVKA_UVC_FRAME_SIZE_GUARD", Some("1")),
            ("LESAVKA_UVC_FRAME_MAX_BYTES", Some("0")),
            ("LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC", Some("9")),
        ],
        || {
            assert_eq!(super::mjpeg_spool_frame_max_bytes(30), 1);
        },
    );

    temp_env::with_vars(
        [
            ("LESAVKA_UVC_FRAME_SIZE_GUARD", Some("1")),
            ("LESAVKA_UVC_FRAME_MAX_BYTES", None::<&str>),
            ("LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC", Some("10000000")),
            ("LESAVKA_UVC_BULK", Some("0")),
            ("LESAVKA_UVC_MAXPACKET", Some("1024")),
            ("LESAVKA_UVC_ISOCHRONOUS_LIMIT_PCT", None::<&str>),
        ],
        || {
            assert_eq!(super::mjpeg_spool_frame_max_bytes(30), 232_106);
        },
    );
}

#[test]
fn mjpeg_spool_frame_budget_clamps_explicit_limit_and_diagnostic_flags() {
    temp_env::with_vars(
        [
            ("LESAVKA_UVC_FRAME_SIZE_GUARD", Some("1")),
            ("LESAVKA_UVC_FRAME_MAX_BYTES", Some("123456")),
            ("LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC", Some("1")),
        ],
        || {
            assert_eq!(super::mjpeg_spool_frame_max_bytes(30), 1);
        },
    );

    temp_env::with_vars(
        [
            ("LESAVKA_UVC_FRAME_SIZE_GUARD", Some("0")),
            ("LESAVKA_UVC_FRAME_MAX_BYTES", Some("123456")),
            ("LESAVKA_UVC_MJPEG_BUDGET_BYTES_PER_SEC", Some("1")),
        ],
        || {
            assert_eq!(super::mjpeg_spool_frame_max_bytes(30), 1);
        },
    );
}

/// Verifies spool metadata remains opt-in and path-configurable.
///
/// Input: default and explicit metadata env vars. Output: disabled by
/// default plus deterministic sidecar path selection. Why: diagnostics must
/// not add per-frame writes unless the operator asks for timing evidence.
#[test]
fn mjpeg_spool_metadata_is_opt_in_and_path_configurable() {
    temp_env::with_var_unset("LESAVKA_UVC_FRAME_META", || {
        assert!(!super::mjpeg_spool_metadata_enabled());
    });
    temp_env::with_var("LESAVKA_UVC_FRAME_META", Some("yes"), || {
        assert!(super::mjpeg_spool_metadata_enabled());
    });

    let frame = std::path::Path::new("/tmp/lesavka-frame.mjpg");
    temp_env::with_var_unset("LESAVKA_UVC_FRAME_META_PATH", || {
        assert_eq!(
            super::mjpeg_spool_metadata_path(frame),
            std::path::PathBuf::from("/tmp/lesavka-frame.mjpg.meta.json")
        );
    });
    temp_env::with_var("LESAVKA_UVC_FRAME_META_PATH", Some("/tmp/custom-meta.json"), || {
        assert_eq!(
            super::mjpeg_spool_metadata_path(frame),
            std::path::PathBuf::from("/tmp/custom-meta.json")
        );
    });

    temp_env::with_var_unset("LESAVKA_UVC_FRAME_META_LOG_PATH", || {
        assert_eq!(super::mjpeg_spool_metadata_log_path(), None);
    });
    temp_env::with_var("LESAVKA_UVC_FRAME_META_LOG_PATH", Some("  "), || {
        assert_eq!(super::mjpeg_spool_metadata_log_path(), None);
    });
}

/// Verifies audit env controls default safely and clamp invalid sampling.
///
/// Input: unset and explicit audit env vars plus sampler examples. Output:
/// disabled-by-default directory, every-frame default, and bounded sampling.
/// Why: boundary audits should be easy for short repros but impossible to
/// enable accidentally during normal calls.
#[test]
fn mjpeg_spool_audit_knobs_are_opt_in_and_bounded() {
    temp_env::with_vars(
        [
            ("LESAVKA_UVC_FRAME_AUDIT_DIR", None::<&str>),
            ("LESAVKA_UVC_FRAME_AUDIT_EVERY", None::<&str>),
            ("LESAVKA_UVC_FRAME_AUDIT_MAX_FRAMES", None::<&str>),
            ("LESAVKA_UVC_FRAME_AUDIT_ROLLING", None::<&str>),
        ],
        || {
            assert_eq!(super::audit::mjpeg_spool_audit_dir(), None);
            assert_eq!(super::audit::mjpeg_spool_audit_every(), 1);
            assert_eq!(super::audit::mjpeg_spool_audit_max_frames(), 1800);
            assert!(super::audit::mjpeg_spool_audit_rolling_enabled());
        },
    );

    temp_env::with_vars(
        [
            ("LESAVKA_UVC_FRAME_AUDIT_DIR", Some("/tmp/uvc-audit")),
            ("LESAVKA_UVC_FRAME_AUDIT_EVERY", Some("0")),
            ("LESAVKA_UVC_FRAME_AUDIT_MAX_FRAMES", Some("0")),
            ("LESAVKA_UVC_FRAME_AUDIT_ROLLING", Some("off")),
        ],
        || {
            assert_eq!(
                super::audit::mjpeg_spool_audit_dir(),
                Some(std::path::PathBuf::from("/tmp/uvc-audit"))
            );
            assert_eq!(super::audit::mjpeg_spool_audit_every(), 1);
            assert_eq!(super::audit::mjpeg_spool_audit_max_frames(), 0);
            assert!(!super::audit::mjpeg_spool_audit_rolling_enabled());
        },
    );

    assert!(super::audit::should_sample_spool_audit_frame(1, 3));
    assert!(!super::audit::should_sample_spool_audit_frame(2, 3));
    assert!(super::audit::should_sample_spool_audit_frame(4, 3));
}

/// Verifies runtime audit control can enable boundary evidence without a
/// server restart.
///
/// Input: a temporary control file containing an audit directory. Output:
/// audit dir resolution follows the control file when the env dir is unset.
/// Why: hardware/lab probes need to turn on exact UVC-bound frame evidence
/// around one run without making every installed call write MJPEG dumps.
#[test]
fn mjpeg_spool_audit_can_be_enabled_by_runtime_control_file() {
    let dir = tempfile::tempdir().expect("tempdir");
    let control = dir.path().join("uvc-audit.control");
    let audit = dir.path().join("audit");
    std::fs::write(&control, format!("{}\n", audit.display())).expect("write control");

    temp_env::with_vars(
        [
            ("LESAVKA_UVC_FRAME_AUDIT_DIR", None::<&str>),
            (
                "LESAVKA_UVC_FRAME_AUDIT_CONTROL_PATH",
                Some(control.to_str().expect("utf8 path")),
            ),
        ],
        || {
            assert_eq!(super::audit::mjpeg_spool_audit_dir(), Some(audit));
        },
    );

    std::fs::write(&control, "off\n").expect("disable control");
    temp_env::with_vars(
        [
            ("LESAVKA_UVC_FRAME_AUDIT_DIR", None::<&str>),
            (
                "LESAVKA_UVC_FRAME_AUDIT_CONTROL_PATH",
                Some(control.to_str().expect("utf8 path")),
            ),
        ],
        || {
            assert_eq!(super::audit::mjpeg_spool_audit_dir(), None);
        },
    );
}

/// Verifies metadata records carry enough timing evidence for RCT analysis.
///
/// Input: HEVC-decoded spool timing. Output: JSON fields for source and
/// decoded PTS. Why: future blind end-to-end probes need to tell whether a
/// bad RCT result came from transport/decode or from the UVC helper/browser.
#[test]
fn mjpeg_spool_metadata_formats_timing_fields() {
    let record = super::format_mjpeg_spool_metadata(
        7,
        1234,
        super::MjpegSpoolTiming::hevc_decoded_mjpeg(42_000, Some(43_000)),
    );

    assert!(record.contains("\"schema\":\"lesavka.uvc-mjpeg-spool-meta.v1\""));
    assert!(record.contains("\"sequence\":7"));
    assert!(record.contains("\"profile\":\"hevc-decoded-mjpeg\""));
    assert!(record.contains("\"bytes\":1234"));
    assert!(record.contains("\"source_pts_us\":42000"));
    assert!(record.contains("\"decoded_pts_us\":43000"));
}

/// Verifies direct MJPEG metadata explicitly marks passthrough timing.
///
/// Input: an upstream MJPEG packet PTS. Output: metadata with no decoded
/// PTS. Why: direct MJPEG ingress must remain distinguishable from HEVC
/// decode when later RCT timing evidence is compared across profiles.
#[test]
fn mjpeg_passthrough_metadata_uses_source_pts_and_null_decode_pts() {
    let record = super::format_mjpeg_spool_metadata(
        8,
        99,
        super::MjpegSpoolTiming::mjpeg_passthrough(55_000),
    );

    assert!(record.contains("\"profile\":\"mjpeg-passthrough\""));
    assert!(record.contains("\"source_pts_us\":55000"));
    assert!(record.contains("\"decoded_pts_us\":null"));
}

/// Verifies normalized direct-MJPEG handoffs are distinguishable.
///
/// Input: an upstream MJPEG packet PTS after decode/re-encode. Output:
/// metadata with the normalized profile marker. Why: RCT artifact probes
/// need to separate raw passthrough from the safer browser-facing path.
#[test]
fn mjpeg_normalized_metadata_uses_source_pts_and_profile_marker() {
    let record = super::format_mjpeg_spool_metadata(
        9,
        101,
        super::MjpegSpoolTiming::mjpeg_normalized(66_000),
    );

    assert!(record.contains("\"profile\":\"mjpeg-normalized\""));
    assert!(record.contains("\"source_pts_us\":66000"));
    assert!(record.contains("\"decoded_pts_us\":null"));
}

/// Verifies frame spooling preserves default behavior unless metadata is enabled.
///
/// Input: a temporary frame path plus disabled metadata env vars. Output:
/// the frame file is atomically written and no sidecar appears. Why:
/// diagnostics must not alter the normal UVC helper handoff during calls.
#[test]
fn spool_mjpeg_frame_writes_frame_without_default_sidecar() {
    let dir = tempfile::tempdir().expect("tempdir");
    let frame = dir.path().join("nested").join("frame.mjpg");
    let meta = frame.with_extension("mjpg.meta.json");

    temp_env::with_vars(
        [
            ("LESAVKA_UVC_FRAME_META", None::<&str>),
            ("LESAVKA_UVC_FRAME_AUDIT_DIR", None::<&str>),
        ],
        || {
            super::spool_mjpeg_frame_with_timing(
                &frame,
                b"jpeg-bytes",
                Some(super::MjpegSpoolTiming::mjpeg_passthrough(10)),
            )
            .expect("spool frame");
        },
    );

    assert_eq!(std::fs::read(&frame).expect("read frame"), b"jpeg-bytes");
    assert!(!meta.exists());
}

/// Verifies enabled frame metadata is atomically written beside the frame.
///
/// Input: explicit metadata enablement, custom sidecar path, and HEVC
/// timing. Output: both frame and sidecar are published. Why: this gives
/// client-to-RCT probes a precise server decode/spool boundary without
/// requiring invasive server logging.
#[test]
fn spool_mjpeg_frame_writes_enabled_sidecar_with_timing() {
    let dir = tempfile::tempdir().expect("tempdir");
    let frame = dir.path().join("frame.mjpg");
    let meta = dir.path().join("frame-meta.json");
    let log = dir.path().join("frames.jsonl");

    temp_env::with_vars(
        [
            ("LESAVKA_UVC_FRAME_META", Some("on")),
            (
                "LESAVKA_UVC_FRAME_META_PATH",
                Some(meta.to_str().expect("utf8 path")),
            ),
            (
                "LESAVKA_UVC_FRAME_META_LOG_PATH",
                Some(log.to_str().expect("utf8 path")),
            ),
            ("LESAVKA_UVC_FRAME_AUDIT_DIR", None::<&str>),
        ],
        || {
            super::spool_mjpeg_frame_with_timing(
                &frame,
                b"decoded-jpeg",
                Some(super::MjpegSpoolTiming::hevc_decoded_mjpeg(
                    100_000,
                    Some(101_000),
                )),
            )
            .expect("spool frame with metadata");
        },
    );

    assert_eq!(std::fs::read(&frame).expect("read frame"), b"decoded-jpeg");
    let record = std::fs::read_to_string(&meta).expect("read metadata");
    assert!(record.contains("\"profile\":\"hevc-decoded-mjpeg\""));
    assert!(record.contains("\"bytes\":12"));
    assert!(record.contains("\"source_pts_us\":100000"));
    assert!(record.contains("\"decoded_pts_us\":101000"));

    let log_record = std::fs::read_to_string(&log).expect("read metadata log");
    assert_eq!(log_record.lines().count(), 1);
    assert!(log_record.contains("\"profile\":\"hevc-decoded-mjpeg\""));
}

/// Verifies boundary audit saves exact UVC-bound MJPEG payloads and an index.
///
/// Input: enabled audit dir with every-frame capture. Output: frame copy plus
/// JSONL timing/hash record. Why: this is the evidence needed to decide
/// whether corruption exists before or after the server-to-UVC boundary.
#[test]
#[serial_test::serial(audit)]
fn spool_mjpeg_frame_writes_enabled_boundary_audit() {
    super::audit::reset_spool_audit_counter_for_test();
    let dir = tempfile::tempdir().expect("tempdir");
    let frame = dir.path().join("frame.mjpg");
    let audit = dir.path().join("audit");

    temp_env::with_vars(
        [
            ("LESAVKA_UVC_FRAME_META", None::<&str>),
            (
                "LESAVKA_UVC_FRAME_AUDIT_DIR",
                Some(audit.to_str().expect("utf8 path")),
            ),
            ("LESAVKA_UVC_FRAME_AUDIT_EVERY", Some("1")),
            ("LESAVKA_UVC_FRAME_AUDIT_MAX_FRAMES", Some("1")),
            ("LESAVKA_UVC_FRAME_AUDIT_ROLLING", Some("0")),
        ],
        || {
            super::spool_mjpeg_frame_with_timing(
                &frame,
                b"audit-jpeg",
                Some(
                    super::MjpegSpoolTiming::mjpeg_passthrough(222)
                        .with_uvc_mode(1280, 720, 30),
                ),
            )
            .expect("spool audited frame");
            super::spool_mjpeg_frame_with_timing(
                &frame,
                b"second-jpeg",
                Some(super::MjpegSpoolTiming::mjpeg_passthrough(333)),
            )
            .expect("spool second audited frame");
        },
    );

    let audited_frames = std::fs::read_dir(&audit)
        .expect("audit dir")
        .filter_map(Result::ok)
        .filter(|entry| entry.file_name().to_string_lossy().ends_with(".mjpg"))
        .collect::<Vec<_>>();
    assert_eq!(audited_frames.len(), 1);
    assert_eq!(
        std::fs::read(audited_frames[0].path()).expect("audit frame"),
        b"audit-jpeg"
    );
    let index = std::fs::read_to_string(audit.join("spool-audit.jsonl")).expect("audit index");
    assert_eq!(index.lines().count(), 1);
    assert!(index.contains("\"schema\":\"lesavka.uvc-mjpeg-spool-audit.v1\""));
    assert!(index.contains("\"profile\":\"mjpeg-passthrough\""));
    assert!(index.contains("\"source_pts_us\":222"));
    assert!(index.contains("\"jpeg_complete\":false"));
    assert!(index.contains("\"frame_width\":null"));
    assert!(index.contains("\"uvc_width\":1280"));
    assert!(index.contains("\"uvc_height\":720"));
    assert!(index.contains("\"uvc_fps\":30"));
    assert!(index.contains("\"file\":\"frame-"));
}

/// Verifies rolling audit keeps the latest bounded frame evidence.
///
/// Input: a one-frame rolling audit and two spooled MJPEG payloads. Output:
/// one overwritten frame file plus two index rows with distinct source
/// sequences. Why: long live repro sessions must not leave only stale startup
/// frames when the user records a later artifact.
#[test]
#[serial_test::serial(audit)]
fn spool_mjpeg_frame_rolls_enabled_boundary_audit() {
    super::audit::reset_spool_audit_counter_for_test();
    let dir = tempfile::tempdir().expect("tempdir");
    let frame = dir.path().join("frame.mjpg");
    let audit = dir.path().join("audit");

    temp_env::with_vars(
        [
            ("LESAVKA_UVC_FRAME_META", None::<&str>),
            (
                "LESAVKA_UVC_FRAME_AUDIT_DIR",
                Some(audit.to_str().expect("utf8 path")),
            ),
            ("LESAVKA_UVC_FRAME_AUDIT_EVERY", Some("1")),
            ("LESAVKA_UVC_FRAME_AUDIT_MAX_FRAMES", Some("1")),
            ("LESAVKA_UVC_FRAME_AUDIT_ROLLING", Some("1")),
        ],
        || {
            super::spool_mjpeg_frame_with_timing(
                &frame,
                b"first-jpeg",
                Some(super::MjpegSpoolTiming::mjpeg_passthrough(111)),
            )
            .expect("spool first audited frame");
            super::spool_mjpeg_frame_with_timing(
                &frame,
                b"latest-jpeg",
                Some(super::MjpegSpoolTiming::mjpeg_passthrough(222)),
            )
            .expect("spool rolling audited frame");
        },
    );

    let audited_frames = std::fs::read_dir(&audit)
        .expect("audit dir")
        .filter_map(Result::ok)
        .filter(|entry| entry.file_name().to_string_lossy().ends_with(".mjpg"))
        .collect::<Vec<_>>();
    assert_eq!(audited_frames.len(), 1);
    assert_eq!(
        std::fs::read(audited_frames[0].path()).expect("audit frame"),
        b"latest-jpeg"
    );
    let index = std::fs::read_to_string(audit.join("spool-audit.jsonl")).expect("audit index");
    assert_eq!(index.lines().count(), 2);
    assert!(index.contains("\"slot\":1"));
    assert!(index.contains("\"source_pts_us\":111"));
    assert!(index.contains("\"source_pts_us\":222"));
}
