// Contract coverage for the synthetic Theia-to-RCT UVC comparison probe.
//
// Scope: inspect the StreamWebcamMedia synthetic injector and the manual RCT
// capture/comparison harness.
// Targets: synthetic UVC frame injection, remote RCT capture, and comparison
// artifacts used by manual media debugging.
// Why: the remaining UVC corruption is rare and stateful, so we need
// repeatable source-to-received frame evidence.

use std::{fs, path::PathBuf, process::Command};

use serde_json::Value;

const PROBE_SRC: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/scripts/manual/run_synthetic_rct_uvc_probe.py"
));
const INJECTOR_SRC: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/server/src/bin/lesavka-synthetic-uplink.rs"
));
const INJECTOR_SUPPORT_SRC: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/server/src/bin/lesavka_synthetic_uplink/support.rs"
));
const SERVER_CARGO: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/server/Cargo.toml"));
const SERVER_INSTALL: &str = include_str!(concat!(
    env!("CARGO_MANIFEST_DIR"),
    "/scripts/install/server.sh"
));

fn repo_script_path() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("scripts/manual/run_synthetic_rct_uvc_probe.py")
}

#[test]
fn synthetic_probe_keeps_bundled_network_ingress_and_rct_comparison_markers() {
    for expected in [
        "lesavka.synthetic-rct-capture.v1",
        "lesavka.synthetic-rct-probe.orchestrator.v1",
        "StreamWebcamMedia",
        "lesavka-synthetic-uplink",
        "https://127.0.0.1:50051",
        "--inject-host",
        "--local-inject",
        "--rct-host",
        "--capture-only",
        "--capture-before-inject",
        "--inject-warmup-s",
        "--capture-finish-grace-s",
        "--pause-local-live-upstream",
        "--media-control-path",
        "--jpeg-quality",
        "--inject-max-frame-bytes",
        "--server-uvc-audit",
        "--server-uvc-audit-host",
        "--server-uvc-audit-control-path",
        "--server-uvc-audit-dir",
        "--server-uvc-audit-sample-frames",
        "--stream-analyze",
        "--deep-capture",
        "--sequence-window",
        "--mix-mae-threshold",
        "--mix-improvement",
        "--mix-min-bands",
        "--source",
        "--mode",
        "1280x720@20,1280x720@30,1920x1080@20,1920x1080@30",
        "marker_decode_failed",
        "frame_repeat",
        "frame_gap",
        "frame_backwards",
        "lower_half_tear",
        "mixed_sequence_bands",
        "lower_half_frame_mix",
        "upper_half_frame_mix",
        "sequence_boundary",
        "sequence_marker_mismatch",
        "marker_sequence_implausible",
        "black_or_gray_slab",
        "horizontal_shift",
        "expected_",
        "expected_best_",
        "suspicious_",
        "decoded_pct",
        "capture_mode",
        "raw_capture_bytes",
        "analysis_duration_s",
        "visual_suspicious",
        "visual_reason_counts",
        "cadence_reason_counts",
        "best_frame_sequence",
        "comparison_sequence",
        "comparison_sequence_counts",
        "best_frame_mae",
        "mixed_band_count",
        "mixed_band_run_pct",
        "band_sequence_counts",
        "upper_dominant_sequence",
        "lower_dominant_sequence",
        "max_mixed_band_count",
        "max_sequence_boundary_count",
        "lesavka.server-uvc-boundary-summary.v1",
        "server_uvc_boundary",
        "server_uvc_audit_artifacts",
        "server_boundary_missing",
        "server_boundary_frame_mode_mismatch",
        "server_boundary_uvc_mode_mismatch",
        "server_boundary_visual_corruption",
        "downstream_uvc_or_browser_corruption",
        "no_visual_corruption_observed",
        "spool-audit.jsonl",
        "matching_frame_records",
        "matching_uvc_mode_records",
        "decoded_sample_count",
        "marker_sample_count",
        "diagnosis",
        "encoded_oversize_frames",
        "sent_frames",
        "exit_reason",
        "decoded_sequence_counts",
        "synthetic uplink completed but RCT capture did not finish",
        "synthetic injector was preempted after sending",
        "synthetic uplink exited before capture warmup completed",
        "paused local live camera upstream",
        "restored local live media control",
        "paused injector-host live camera upstream",
        "restored injector-host live media control",
        "media_control_with_camera",
        "discover_media_control_paths",
        "resolve_media_control_path",
        "pause_remote_live_upstream",
        "restore_remote_live_upstream",
        "run_remote_python",
        "discovered live Lesavka media control path",
        "cadence-only repeat/gap",
        "no visual tear/mixed-frame corruption was detected",
        "max_lower_mae",
        "ffmpeg",
        "v4l2",
        "x11grab",
        "capture.mjpg",
        "usbmon.pcapng",
        "uvcvideo-dynamic-debug",
        "power/autosuspend",
        "analyze_marked_capture.py",
    ] {
        assert!(
            PROBE_SRC.contains(expected),
            "synthetic RCT probe should preserve marker {expected}"
        );
    }
}

#[test]
fn synthetic_injector_enters_the_public_bundled_media_rpc() {
    for expected in [
        "name                    = \"lesavka-synthetic-uplink\"",
        "src/bin/lesavka-synthetic-uplink.rs",
        "stream_webcam_media(Request::new(ReceiverStream::new(rx)))",
        "UpstreamMediaBundle",
        "AudioEncoding::PcmS16le",
        "silence_pcm(args.frame_step_us())",
        "synthetic_rgb_frame",
        "draw_sequence_marker",
        "synthetic_band_marker_luma",
        "add_jpeg_app4_integrity",
        "image/jpeg",
        "jpegenc",
        "client_capture_pts_us: pts_us",
        "client_send_pts_us: pts_us",
        "ClientTlsConfig",
        "--tls-ca",
        "--tls-client-cert",
        "--tls-client-key",
        "--max-frame-bytes",
        "encoded synthetic frame",
        "StreamWebcamMedia closed before accepting synthetic frame",
    ] {
        assert!(
            SERVER_CARGO.contains(expected)
                || INJECTOR_SRC.contains(expected)
                || INJECTOR_SUPPORT_SRC.contains(expected),
            "synthetic injector should preserve marker {expected}"
        );
    }
    assert!(
        SERVER_INSTALL.contains(
            "install_verified_executable \"$SRC_DIR/target/release/lesavka-synthetic-uplink\" /usr/local/bin/lesavka-synthetic-uplink"
        ),
        "server installer should install the synthetic uplink binary for Theia lab runs"
    );
}

#[test]
fn synthetic_probe_self_test_detects_slab_and_shift_categories() {
    let dir = tempfile::tempdir().expect("tempdir");
    let output = Command::new("python3")
        .arg(repo_script_path())
        .arg("--self-test")
        .arg("--artifact-dir")
        .arg(dir.path())
        .output()
        .expect("run synthetic RCT probe self-test");

    assert!(
        output.status.success(),
        "synthetic probe self-test should succeed: stdout={} stderr={}",
        String::from_utf8_lossy(&output.stdout),
        String::from_utf8_lossy(&output.stderr)
    );

    let summary: Value = serde_json::from_str(
        &fs::read_to_string(dir.path().join("summary.json")).expect("summary json"),
    )
    .expect("parse summary json");
    assert_eq!(
        summary["schema"],
        "lesavka.synthetic-rct-probe.self-test.v1"
    );
    assert!(
        summary["suspicious_frames"].as_u64().unwrap_or_default() >= 3,
        "self-test should detect at least the slab, shifted, and mixed synthetic frames: {summary}"
    );
    let records = summary["records"].as_array().expect("records array");
    assert!(
        records.iter().any(|record| record["reasons"]
            .as_array()
            .is_some_and(|reasons| reasons.iter().any(|reason| reason == "black_or_gray_slab"))),
        "self-test should include a slab category: {summary}"
    );
    assert!(
        records.iter().any(|record| record["reasons"]
            .as_array()
            .is_some_and(|reasons| reasons.iter().any(|reason| reason == "horizontal_shift"))),
        "self-test should include a horizontal shift category: {summary}"
    );
    assert!(
        records.iter().any(
            |record| record["reasons"].as_array().is_some_and(|reasons| reasons
                .iter()
                .any(|reason| reason == "mixed_sequence_bands"))
        ),
        "self-test should include a mixed-frame category: {summary}"
    );
}
