use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use std::io::{BufRead, BufReader, Read, Write};
use std::net::TcpStream;
use std::path::{Path, PathBuf};
use std::process::{Child, Command as ProcessCommand, Stdio};

#[test]
fn imports_whisperx_fixture_to_stdout() {
    let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../tests/fixtures/whisperx-parity-sample.json");
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("import-whisperx")
        .arg(fixture)
        .assert()
        .success()
        .stdout(predicate::str::contains("hello world second speaker"));
}

#[test]
fn speakers_path_local_scope_prints_project_speaker_directory() {
    let temp = tempfile::tempdir().expect("tempdir");
    let expected = temp.path().join(".native-whisperx/speakers");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .current_dir(temp.path())
        .args(["speakers", "path", "--scope", "local"])
        .assert()
        .success()
        .stdout(predicate::str::contains(
            expected.to_string_lossy().as_ref().to_string(),
        ));
}

#[test]
fn speakers_path_auto_prefers_existing_local_speaker_directory() {
    let temp = tempfile::tempdir().expect("tempdir");
    let local = temp.path().join(".native-whisperx/speakers");
    fs::create_dir_all(&local).expect("local speaker directory");
    let global_root = temp.path().join("global-data");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .current_dir(temp.path())
        .env("XDG_DATA_HOME", &global_root)
        .args(["speakers", "path"])
        .assert()
        .success()
        .stdout(predicate::str::contains(
            local.to_string_lossy().as_ref().to_string(),
        ));
}

#[test]
fn speakers_path_explicit_directory_overrides_local_directory() {
    let temp = tempfile::tempdir().expect("tempdir");
    fs::create_dir_all(temp.path().join(".native-whisperx/speakers"))
        .expect("local speaker directory");
    let expected = temp.path().join("controlled-speakers");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .current_dir(temp.path())
        .args([
            "speakers",
            "path",
            "--speaker-directory",
            "controlled-speakers",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains(
            expected.to_string_lossy().as_ref().to_string(),
        ));
}

#[test]
fn speakers_path_accepts_speaker_store_alias() {
    let temp = tempfile::tempdir().expect("tempdir");
    let expected = temp.path().join("controlled-speakers");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .current_dir(temp.path())
        .args(["speakers", "path", "--speaker-store", "controlled-speakers"])
        .assert()
        .success()
        .stdout(predicate::str::contains(
            expected.to_string_lossy().as_ref().to_string(),
        ));
}

#[test]
fn speakers_path_rejects_conflicting_directory_and_store_aliases() {
    let temp = tempfile::tempdir().expect("tempdir");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .current_dir(temp.path())
        .args([
            "speakers",
            "path",
            "--speaker-directory",
            "one",
            "--speaker-store",
            "two",
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains(
            "--speaker-directory and --speaker-store must point to the same path",
        ));
}

#[cfg(target_os = "linux")]
#[test]
fn speakers_path_global_scope_uses_xdg_data_home_convention() {
    let temp = tempfile::tempdir().expect("tempdir");
    let global_root = temp.path().join("global-data");
    let expected = global_root.join("native-whisperx/speakers");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .current_dir(temp.path())
        .env("XDG_DATA_HOME", &global_root)
        .args(["speakers", "path", "--scope", "global"])
        .assert()
        .success()
        .stdout(predicate::str::contains(
            expected.to_string_lossy().as_ref().to_string(),
        ));
}

#[test]
fn speakers_validate_accepts_valid_speaker_library() {
    let temp = tempfile::tempdir().expect("tempdir");
    let directory = temp.path().join("speakers");
    fs::create_dir_all(&directory).expect("speaker directory");
    fs::write(directory.join("library.json"), valid_speaker_library_json()).expect("library");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args(["speakers", "validate", "--speaker-directory"])
        .arg(&directory)
        .assert()
        .success()
        .stdout(predicate::str::contains("Speaker Library valid"))
        .stdout(predicate::str::contains("profiles: 1"));
}

#[test]
fn speakers_validate_reports_specific_error_for_incompatible_library() {
    let temp = tempfile::tempdir().expect("tempdir");
    let directory = temp.path().join("speakers");
    fs::create_dir_all(&directory).expect("speaker directory");
    fs::write(
        directory.join("library.json"),
        valid_speaker_library_json().replace("\"values\": [1.0, 0.0]", "\"values\": [2.0, 0.0]"),
    )
    .expect("library");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args(["speakers", "validate", "--speaker-directory"])
        .arg(&directory)
        .assert()
        .failure()
        .stderr(predicate::str::contains("malformed or incompatible"))
        .stderr(predicate::str::contains("normalized"));
}

#[test]
fn speakers_list_excludes_drafts_by_default() {
    let temp = tempfile::tempdir().expect("tempdir");
    let directory = temp.path().join("speakers");
    fs::create_dir_all(&directory).expect("speaker directory");
    fs::write(
        directory.join("library.json"),
        draft_and_confirmed_speaker_library_json(),
    )
    .expect("library");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    let output = command
        .args(["speakers", "list", "--speaker-directory"])
        .arg(&directory)
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();
    let profiles: serde_json::Value = serde_json::from_slice(&output).expect("profiles json");

    assert_eq!(profiles.as_array().expect("profiles").len(), 1);
    assert_eq!(profiles[0]["speakerId"], "speaker-a");
    assert_eq!(profiles[0]["status"], "confirmed");
}

#[test]
fn speakers_list_include_drafts_outputs_confirmed_and_draft_profiles() {
    let temp = tempfile::tempdir().expect("tempdir");
    let directory = temp.path().join("speakers");
    fs::create_dir_all(&directory).expect("speaker directory");
    fs::write(
        directory.join("library.json"),
        draft_and_confirmed_speaker_library_json(),
    )
    .expect("library");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    let output = command
        .args(["speakers", "list", "--speaker-store"])
        .arg(&directory)
        .arg("--include-drafts")
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();
    let profiles: serde_json::Value = serde_json::from_slice(&output).expect("profiles json");

    let profiles = profiles.as_array().expect("profiles");
    assert_eq!(profiles.len(), 2);
    assert!(profiles
        .iter()
        .any(|profile| profile["speakerId"] == "draft-speaker-b" && profile["status"] == "draft"));
}

#[test]
fn speakers_list_missing_library_outputs_empty_json_array() {
    let temp = tempfile::tempdir().expect("tempdir");
    let directory = temp.path().join("speakers");
    fs::create_dir_all(&directory).expect("speaker directory");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args(["speakers", "list", "--speaker-directory"])
        .arg(&directory)
        .assert()
        .success()
        .stdout(predicate::str::contains("[]"));
}

#[test]
fn speakers_rebuild_trace_uses_local_project_scan_root_by_default() {
    let temp = tempfile::tempdir().expect("tempdir");
    let directory = temp.path().join(".native-whisperx/speakers");
    fs::create_dir_all(&directory).expect("speaker directory");
    fs::write(directory.join("library.json"), valid_speaker_library_json()).expect("library");
    fs::write(
        temp.path().join("transcript.json"),
        r#"{"segments": [{"id": 0, "start": 0.0, "end": 1.25, "text": "hello", "speaker": "speaker-a"}]}"#,
    )
    .expect("transcript");
    fs::write(temp.path().join("transcript.srt"), "ignored").expect("srt");
    fs::write(temp.path().join("broken.json"), "{").expect("broken json");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .current_dir(temp.path())
        .args(["speakers", "rebuild-trace", "--scope", "local"])
        .assert()
        .success()
        .stdout(predicate::str::contains("Speaker Trace rebuilt"))
        .stdout(predicate::str::contains("speakers: 1"))
        .stdout(predicate::str::contains("errors: 1"))
        .stderr(predicate::str::contains("broken.json"))
        .stderr(predicate::str::contains("malformed transcript JSON"));

    let trace = fs::read_to_string(directory.join("speaker-trace.json")).expect("trace");
    assert!(trace.contains("\"profileId\": \"speaker-a\""));
    assert!(trace.contains("\"segmentCount\": 1"));
    assert!(trace.contains("\"totalDuration\": 1.25"));
    assert!(trace.contains("\"snippet\": \"hello\""));
    assert!(!trace.contains("transcript.srt"));
}

#[cfg(target_os = "linux")]
#[test]
fn speakers_rebuild_trace_global_scope_requires_scan_root() {
    let temp = tempfile::tempdir().expect("tempdir");
    let global_root = temp.path().join("global-data");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .current_dir(temp.path())
        .env("XDG_DATA_HOME", &global_root)
        .args(["speakers", "rebuild-trace", "--scope", "global"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("require --scan-root"));
}

#[test]
fn speakers_open_print_url_prints_bare_loopback_url() {
    let temp = tempfile::tempdir().expect("tempdir");
    let mut child = ProcessCommand::new(env!("CARGO_BIN_EXE_native-whisperx"))
        .current_dir(temp.path())
        .args(["speakers", "open", "--scope", "local", "--print-url"])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn speakers open");
    let stdout = child.stdout.take().expect("stdout");
    let mut child = ChildGuard(child);
    let mut stdout = BufReader::new(stdout);
    let mut first_line = String::new();
    stdout.read_line(&mut first_line).expect("url line");

    assert!(
        first_line.starts_with("http://127.0.0.1:"),
        "expected bare loopback URL, got {first_line}"
    );

    child.stop();
}

#[test]
fn speakers_open_no_browser_serves_read_only_loopback_state() {
    let temp = tempfile::tempdir().expect("tempdir");
    let directory = temp.path().join(".native-whisperx/speakers");
    fs::create_dir_all(&directory).expect("speaker directory");
    fs::write(directory.join("library.json"), valid_speaker_library_json()).expect("library");
    fs::write(
        directory.join("speaker-trace.json"),
        r#"{
          "version": 1,
          "scanRoot": "/tmp/native-whisperx-test-output",
          "speakers": [{
            "kind": "enrolled",
            "profileId": "speaker-a",
            "label": "Speaker A",
            "files": [{
              "sourceFile": "/tmp/native-whisperx-test-output/transcript.json",
              "segmentCount": 2,
              "totalDuration": 2.5,
              "spans": [
                {"startSeconds": 0.0, "endSeconds": 1.0, "snippet": "hello"},
                {"startSeconds": 1.0, "endSeconds": 2.5, "snippet": "world"}
              ]
            }]
          }],
          "errors": []
        }"#,
    )
    .expect("trace");

    let mut child = ProcessCommand::new(env!("CARGO_BIN_EXE_native-whisperx"))
        .current_dir(temp.path())
        .args(["speakers", "open", "--scope", "local", "--no-browser"])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn speakers open");
    let stdout = child.stdout.take().expect("stdout");
    let mut child = ChildGuard(child);
    let mut stdout = BufReader::new(stdout);
    let mut first_line = String::new();
    stdout.read_line(&mut first_line).expect("url line");
    let url = extract_url(&first_line);

    assert!(
        url.starts_with("http://127.0.0.1:"),
        "expected loopback URL, got {url}"
    );

    let (status, body) = http_request("GET", &format!("{}/api/state", url.trim_end_matches('/')));
    assert_eq!(status, 200);
    let state: serde_json::Value = serde_json::from_str(&body).expect("state json");
    assert_eq!(state["scope"], "local");
    assert_eq!(state["path"], directory.to_string_lossy().as_ref());
    assert_eq!(state["library"]["status"], "valid");
    assert_eq!(state["library"]["profileCount"], 1);
    assert_eq!(state["profiles"][0]["id"], "speaker-a");
    assert_eq!(state["profiles"][0]["label"], "Speaker A");
    assert_eq!(state["profiles"][0]["metadata"]["note"], "fixture");
    assert_eq!(state["trace"]["status"], "valid");
    assert_eq!(state["trace"]["speakers"][0]["profileId"], "speaker-a");
    assert_eq!(
        state["trace"]["speakers"][0]["files"][0]["sourceFile"],
        "/tmp/native-whisperx-test-output/transcript.json"
    );
    assert_eq!(state["trace"]["speakers"][0]["files"][0]["segmentCount"], 2);
    assert_eq!(
        state["trace"]["speakers"][0]["files"][0]["totalDuration"],
        2.5
    );
    assert_eq!(
        state["trace"]["speakers"][0]["files"][0]["spans"][0]["snippet"],
        "hello"
    );

    let (status, body) = http_request("POST", &format!("{}/api/state", url.trim_end_matches('/')));
    assert_eq!(status, 405);
    assert!(body.contains("does not support this request"));

    child.stop();
}

#[test]
fn speakers_open_no_browser_rescans_trace_with_session_token() {
    let temp = tempfile::tempdir().expect("tempdir");
    let directory = temp.path().join(".native-whisperx/speakers");
    fs::create_dir_all(&directory).expect("speaker directory");
    fs::write(directory.join("library.json"), valid_speaker_library_json()).expect("library");
    fs::write(
        temp.path().join("transcript.json"),
        r#"{"segments": [{"id": 0, "start": 0.0, "end": 1.25, "text": "hello", "speaker": "speaker-a"}]}"#,
    )
    .expect("transcript");
    fs::write(temp.path().join("transcript.srt"), "ignored").expect("srt");

    let mut child = ProcessCommand::new(env!("CARGO_BIN_EXE_native-whisperx"))
        .current_dir(temp.path())
        .args(["speakers", "open", "--scope", "local", "--no-browser"])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn speakers open");
    let stdout = child.stdout.take().expect("stdout");
    let mut child = ChildGuard(child);
    let mut stdout = BufReader::new(stdout);
    let mut first_line = String::new();
    stdout.read_line(&mut first_line).expect("url line");
    let url = extract_url(&first_line);
    let base = url.trim_end_matches('/');
    let (_, html) = http_request("GET", base);
    let token = extract_session_token(&html);

    let (status, body) = http_request_with_body(
        "POST",
        &format!("{base}/api/trace/rebuild"),
        &[],
        Some("{}"),
    );
    assert_eq!(status, 403);
    assert!(body.contains("missing or invalid"));
    assert!(!directory.join("speaker-trace.json").exists());

    let (status, body) = http_request_with_body(
        "POST",
        &format!("{base}/api/trace/rebuild"),
        &[("X-Native-Whisperx-Session-Token", &token)],
        Some("{}"),
    );
    assert_eq!(status, 200, "{body}");
    let response: serde_json::Value = serde_json::from_str(&body).expect("rescan json");
    assert_eq!(response["report"]["stats"]["scannedFiles"], 3);
    assert_eq!(response["report"]["stats"]["acceptedEntries"], 1);
    assert_eq!(response["report"]["stats"]["ignoredNonJsonFiles"], 1);
    assert_eq!(response["report"]["stats"]["malformedJsonErrors"], 0);
    assert_eq!(response["state"]["trace"]["status"], "valid");
    assert_eq!(
        response["state"]["trace"]["speakers"][0]["profileId"],
        "speaker-a"
    );
    assert_eq!(
        response["state"]["trace"]["speakers"][0]["files"][0]["spans"][0]["snippet"],
        "hello"
    );

    let trace = fs::read_to_string(directory.join("speaker-trace.json")).expect("trace");
    assert!(trace.contains("\"profileId\": \"speaker-a\""));
    assert!(!trace.contains("transcript.srt"));

    child.stop();
}

#[test]
fn speakers_open_no_browser_rescan_reports_malformed_json_errors() {
    let temp = tempfile::tempdir().expect("tempdir");
    let directory = temp.path().join(".native-whisperx/speakers");
    fs::create_dir_all(&directory).expect("speaker directory");
    fs::write(directory.join("library.json"), valid_speaker_library_json()).expect("library");
    fs::write(
        temp.path().join("valid.json"),
        r#"{"segments": [{"id": 0, "start": 0.0, "end": 1.0, "text": "ok", "speaker": "speaker-a"}]}"#,
    )
    .expect("valid json");
    fs::write(temp.path().join("broken.json"), "{").expect("broken json");
    fs::write(temp.path().join("notes.txt"), "ignored").expect("text");

    let mut child = ProcessCommand::new(env!("CARGO_BIN_EXE_native-whisperx"))
        .current_dir(temp.path())
        .args(["speakers", "open", "--scope", "local", "--no-browser"])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn speakers open");
    let stdout = child.stdout.take().expect("stdout");
    let mut child = ChildGuard(child);
    let mut stdout = BufReader::new(stdout);
    let mut first_line = String::new();
    stdout.read_line(&mut first_line).expect("url line");
    let url = extract_url(&first_line);
    let base = url.trim_end_matches('/');
    let (_, html) = http_request("GET", base);
    let token = extract_session_token(&html);

    let (status, body) = http_request_with_body(
        "POST",
        &format!("{base}/api/trace/rebuild"),
        &[("X-Native-Whisperx-Session-Token", &token)],
        Some("{}"),
    );
    assert_eq!(status, 200, "{body}");
    let response: serde_json::Value = serde_json::from_str(&body).expect("rescan json");
    assert_eq!(response["report"]["stats"]["scannedFiles"], 4);
    assert_eq!(response["report"]["stats"]["acceptedEntries"], 1);
    assert_eq!(response["report"]["stats"]["ignoredNonJsonFiles"], 1);
    assert_eq!(response["report"]["stats"]["malformedJsonErrors"], 1);
    assert_eq!(
        response["state"]["trace"]["errors"]
            .as_array()
            .unwrap()
            .len(),
        1
    );
    assert!(response["state"]["trace"]["errors"][0]["sourceFile"]
        .as_str()
        .unwrap()
        .ends_with("broken.json"));
    assert!(response["state"]["trace"]["errors"][0]["message"]
        .as_str()
        .unwrap()
        .contains("malformed transcript JSON"));

    let trace = fs::read_to_string(directory.join("speaker-trace.json")).expect("trace");
    assert!(trace.contains("broken.json"));
    assert!(trace.contains("speaker-a"));

    child.stop();
}

#[cfg(target_os = "linux")]
#[test]
fn speakers_open_no_browser_global_rescan_requires_scan_root() {
    let temp = tempfile::tempdir().expect("tempdir");
    let global_root = temp.path().join("global-data");

    let mut child = ProcessCommand::new(env!("CARGO_BIN_EXE_native-whisperx"))
        .current_dir(temp.path())
        .env("XDG_DATA_HOME", &global_root)
        .args(["speakers", "open", "--scope", "global", "--no-browser"])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn speakers open");
    let stdout = child.stdout.take().expect("stdout");
    let mut child = ChildGuard(child);
    let mut stdout = BufReader::new(stdout);
    let mut first_line = String::new();
    stdout.read_line(&mut first_line).expect("url line");
    let url = extract_url(&first_line);
    let base = url.trim_end_matches('/');
    let (_, html) = http_request("GET", base);
    let token = extract_session_token(&html);

    let (status, body) = http_request_with_body(
        "POST",
        &format!("{base}/api/trace/rebuild"),
        &[("X-Native-Whisperx-Session-Token", &token)],
        Some("{}"),
    );
    assert_eq!(status, 400);
    assert!(body.contains("require scanRoot"));
    assert!(!global_root
        .join("native-whisperx/speakers/speaker-trace.json")
        .exists());

    child.stop();
}

#[test]
fn speakers_open_no_browser_edits_and_deletes_profiles_with_session_token() {
    let temp = tempfile::tempdir().expect("tempdir");
    let directory = temp.path().join(".native-whisperx/speakers");
    fs::create_dir_all(&directory).expect("speaker directory");
    fs::write(
        directory.join("library.json"),
        two_profile_speaker_library_json(),
    )
    .expect("library");

    let mut child = ProcessCommand::new(env!("CARGO_BIN_EXE_native-whisperx"))
        .current_dir(temp.path())
        .args(["speakers", "open", "--scope", "local", "--no-browser"])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn speakers open");
    let stdout = child.stdout.take().expect("stdout");
    let mut child = ChildGuard(child);
    let mut stdout = BufReader::new(stdout);
    let mut first_line = String::new();
    stdout.read_line(&mut first_line).expect("url line");
    let url = extract_url(&first_line);
    let base = url.trim_end_matches('/');
    let (_, html) = http_request("GET", base);
    let token = extract_session_token(&html);

    let (status, body) = http_request_with_body(
        "PUT",
        &format!("{base}/api/profiles/speaker-a"),
        &[("X-Native-Whisperx-Session-Token", &token)],
        Some(
            r#"{"id":"speaker-a","label":"Renamed Speaker","metadata":{"note":"changed","role":"host"}}"#,
        ),
    );
    assert_eq!(status, 200, "{body}");
    let state: serde_json::Value = serde_json::from_str(&body).expect("state json");
    assert_eq!(state["profiles"][0]["id"], "speaker-a");
    assert_eq!(state["profiles"][0]["label"], "Renamed Speaker");
    assert_eq!(state["profiles"][0]["metadata"]["role"], "host");
    let saved = fs::read_to_string(directory.join("library.json")).expect("saved library");
    assert!(saved.contains("\"id\": \"speaker-a\""));
    assert!(saved.contains("\"label\": \"Renamed Speaker\""));

    let (status, body) = http_request_with_body(
        "DELETE",
        &format!("{base}/api/profiles/speaker-b"),
        &[("X-Native-Whisperx-Session-Token", &token)],
        None,
    );
    assert_eq!(status, 200, "{body}");
    let state: serde_json::Value = serde_json::from_str(&body).expect("state json");
    assert_eq!(state["library"]["profileCount"], 1);
    assert_eq!(state["profiles"].as_array().expect("profiles").len(), 1);
    let saved = fs::read_to_string(directory.join("library.json")).expect("saved library");
    assert!(!saved.contains("\"id\": \"speaker-b\""));

    let (status, body) = http_request_with_body(
        "POST",
        &format!("{base}/api/profiles"),
        &[("X-Native-Whisperx-Session-Token", &token)],
        Some(r#"{"id":"draft","label":"Draft"}"#),
    );
    assert_eq!(status, 400);
    assert!(body.contains("without embeddings is not supported"));

    child.stop();
}

#[test]
fn speakers_open_no_browser_rejects_missing_or_invalid_session_token() {
    let temp = tempfile::tempdir().expect("tempdir");
    let directory = temp.path().join(".native-whisperx/speakers");
    fs::create_dir_all(&directory).expect("speaker directory");
    fs::write(directory.join("library.json"), valid_speaker_library_json()).expect("library");

    let mut child = ProcessCommand::new(env!("CARGO_BIN_EXE_native-whisperx"))
        .current_dir(temp.path())
        .args(["speakers", "open", "--scope", "local", "--no-browser"])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn speakers open");
    let stdout = child.stdout.take().expect("stdout");
    let mut child = ChildGuard(child);
    let mut stdout = BufReader::new(stdout);
    let mut first_line = String::new();
    stdout.read_line(&mut first_line).expect("url line");
    let url = extract_url(&first_line);
    let base = url.trim_end_matches('/');

    let (status, body) = http_request_with_body(
        "PUT",
        &format!("{base}/api/profiles/speaker-a"),
        &[],
        Some(r#"{"label":"Unauthorized"}"#),
    );
    assert_eq!(status, 403);
    assert!(body.contains("missing or invalid"));

    let (status, body) = http_request_with_body(
        "PUT",
        &format!("{base}/api/profiles/speaker-a"),
        &[("X-Native-Whisperx-Session-Token", "wrong-token")],
        Some(r#"{"label":"Unauthorized"}"#),
    );
    assert_eq!(status, 403);
    assert!(body.contains("missing or invalid"));
    let saved = fs::read_to_string(directory.join("library.json")).expect("saved library");
    assert!(saved.contains("\"label\": \"Speaker A\""));

    child.stop();
}

#[test]
fn speakers_open_no_browser_rejects_invalid_profile_edit_without_writing() {
    let temp = tempfile::tempdir().expect("tempdir");
    let directory = temp.path().join(".native-whisperx/speakers");
    fs::create_dir_all(&directory).expect("speaker directory");
    fs::write(directory.join("library.json"), valid_speaker_library_json()).expect("library");

    let mut child = ProcessCommand::new(env!("CARGO_BIN_EXE_native-whisperx"))
        .current_dir(temp.path())
        .args(["speakers", "open", "--scope", "local", "--no-browser"])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn speakers open");
    let stdout = child.stdout.take().expect("stdout");
    let mut child = ChildGuard(child);
    let mut stdout = BufReader::new(stdout);
    let mut first_line = String::new();
    stdout.read_line(&mut first_line).expect("url line");
    let url = extract_url(&first_line);
    let base = url.trim_end_matches('/');
    let (_, html) = http_request("GET", base);
    let token = extract_session_token(&html);

    let (status, body) = http_request_with_body(
        "PUT",
        &format!("{base}/api/profiles/speaker-a"),
        &[("X-Native-Whisperx-Session-Token", &token)],
        Some(r#"{"id":"speaker-renamed","label":"Renamed"}"#),
    );
    assert_eq!(status, 400);
    assert!(body.contains("profile ids are immutable"));
    let saved = fs::read_to_string(directory.join("library.json")).expect("saved library");
    assert!(saved.contains("\"id\": \"speaker-a\""));
    assert!(saved.contains("\"label\": \"Speaker A\""));

    let (status, body) = http_request_with_body(
        "PUT",
        &format!("{base}/api/profiles/speaker-a"),
        &[("X-Native-Whisperx-Session-Token", &token)],
        Some(r#"{"profileId":"speaker-renamed","label":"Renamed"}"#),
    );
    assert_eq!(status, 400);
    assert!(body.contains("unknown field"));
    let saved = fs::read_to_string(directory.join("library.json")).expect("saved library");
    assert!(saved.contains("\"id\": \"speaker-a\""));
    assert!(saved.contains("\"label\": \"Speaker A\""));

    let (status, body) = http_request_with_body(
        "PUT",
        &format!("{base}/api/profiles/speaker-a"),
        &[("X-Native-Whisperx-Session-Token", &token)],
        Some(r#"{"label":"   "}"#),
    );
    assert_eq!(status, 400);
    assert!(body.contains("profile label must not be empty"));
    let saved = fs::read_to_string(directory.join("library.json")).expect("saved library");
    assert!(saved.contains("\"label\": \"Speaker A\""));

    child.stop();
}

#[test]
fn inspect_models_prints_request_shape() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args([
            "inspect-models",
            "--whisper-bundle",
            "models/whisper",
            "--alignment-bundle",
            "models/wav2vec2",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("candleWhisper"))
        .stdout(predicate::str::contains("models/wav2vec2"));
}

#[test]
fn inspect_models_shows_alignment_enabled_by_default() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args(["inspect-models", "--whisper-bundle", "models/whisper"])
        .assert()
        .success()
        .stdout(predicate::str::contains("\"enabled\": true"))
        .stdout(predicate::str::contains("facebook/wav2vec2-base-960h"));
}

#[test]
fn inspect_models_no_align_maps_to_disabled_alignment() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args([
            "inspect-models",
            "--whisper-bundle",
            "models/whisper",
            "--no-align",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("\"enabled\": false"));
}

#[test]
fn inspect_models_maps_model_cache_to_native_asr() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args([
            "inspect-models",
            "--model",
            "tiny.en",
            "--model-dir",
            "models",
            "--model-cache-only",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("\"modelId\": \"tiny.en\""))
        .stdout(predicate::str::contains("\"modelDir\": \"models\""))
        .stdout(predicate::str::contains("\"modelCacheOnly\": true"));
}

#[test]
fn inspect_models_prints_translation_section_when_requested() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args([
            "inspect-models",
            "--translation-model",
            "Helsinki-NLP/opus-mt-de-en",
            "--model-dir",
            "models",
            "--model-cache-only",
            "--translation-target-language",
            "en",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("\"translation\""))
        .stdout(predicate::str::contains(
            "\"modelId\": \"Helsinki-NLP/opus-mt-de-en\"",
        ))
        .stdout(predicate::str::contains("\"targetLanguage\": \"en\""))
        .stdout(predicate::str::contains("\"modelDir\": \"models\""))
        .stdout(predicate::str::contains("\"modelCacheOnly\": true"));
}

#[test]
fn inspect_models_parses_translation_model_underscore_alias() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args(["inspect-models", "--translation_model", "opus-mt-de-en"])
        .assert()
        .success()
        .stdout(predicate::str::contains("\"translation\""))
        .stdout(predicate::str::contains(
            "\"modelId\": \"Helsinki-NLP/opus-mt-de-en\"",
        ));
}

#[test]
fn transcribe_help_lists_whisperx_386_contract() {
    let help = command_stdout(["transcribe", "--help"]);
    for expected in [
        "<INPUT>...",
        "--provider",
        "--model",
        "--task",
        "--language",
        "--device",
        "--device-index",
        "--batch-size",
        "--compute-type",
        "--verbose [<VERBOSE>]",
        "--log-level",
        "--print-progress",
        "--no-align",
        "--align-model",
        "--model-dir",
        "--model-cache-only",
        "--translation-model",
        "--translation-bundle",
        "--translation-source-language",
        "--translation-target-language",
        "--translation-max-new-tokens",
        "--interpolate-method",
        "--return-char-alignments",
        "--vad-method",
        "--vad-onset",
        "--vad-offset",
        "--chunk-size",
        "--vad-model-bundle",
        "--vad-model-file",
        "--vad-input-name",
        "--vad-output-name",
        "--diarize",
        "--diarize-model",
        "--speaker-embeddings",
        "--hf-token",
        "--min-speakers",
        "--max-speakers",
        "--speaker-assignment-policy",
        "--scope",
        "--speaker-directory",
        "--no-speaker-library",
        "--temperature",
        "--best-of",
        "--beam-size",
        "--patience",
        "--length-penalty",
        "--suppress-tokens",
        "--suppress-numerals",
        "--initial-prompt",
        "--hotwords",
        "--condition-on-previous-text",
        "--fp16",
        "--compression-ratio-threshold",
        "--logprob-threshold",
        "--no-speech-threshold",
        "--threads",
        "--max-line-width",
        "--max-line-count",
        "--highlight-words",
        "--segment-resolution",
        "--output-dir",
        "--format",
        "all",
        "tsv",
        "aud",
        "native-json",
        "sentence",
        "chunk",
    ] {
        assert!(help.contains(expected), "help should contain `{expected}`");
    }
}

#[test]
fn inspect_models_native_diarization_defaults_to_native_model() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args([
            "inspect-models",
            "--speaker_embedding_bundle",
            "models/speakers",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains(
            "\"modelId\": \"native-spectral-speaker-baseline\"",
        ));
}

#[test]
fn inspect_models_maps_strict_contained_speaker_assignment_policy() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args([
            "inspect-models",
            "--speaker_embedding_bundle",
            "models/speakers",
            "--speaker-assignment-policy",
            "strict-contained",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains(
            "\"assignmentPolicy\": \"strictContained\"",
        ));
}

#[test]
fn transcribe_segment_resolution_accepts_whisperx_values_and_legacy_alias() {
    for value in ["sentence", "chunk", "segment"] {
        let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
        command
            .args(["input.wav", "--segment_resolution", value, "--help"])
            .assert()
            .success()
            .stdout(predicate::str::contains("--segment-resolution"));
    }
}

#[test]
fn top_level_help_lists_underscore_aliases() {
    let help = command_stdout(["input.wav", "--help"]);
    for expected in [
        "--output_format",
        "--align_model",
        "--vad_method",
        "--vad_model_bundle",
        "--vad_model_file",
        "--translation_model",
        "--translation_bundle",
        "--translation_source_language",
        "--translation_target_language",
        "--translation_max_new_tokens",
        "--hf_token",
        "--max_line_width",
    ] {
        assert!(help.contains(expected), "help should contain `{expected}`");
    }
}

#[test]
fn transcribe_help_lists_native_json_format() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args(["transcribe", "--help"])
        .assert()
        .success()
        .stdout(predicate::str::contains("native-json"))
        .stdout(predicate::str::contains("tsv"))
        .stdout(predicate::str::contains("aud"))
        .stdout(predicate::str::contains("output_format"));
}

#[test]
fn top_level_help_lists_parity_fixtures() {
    let help = command_stdout(["--help"]);
    assert!(help.contains("parity-fixtures"));
    assert!(help.contains("parity-bench"));
    assert!(help.contains("parity-summary"));
    assert!(help.contains("parity-preflight"));
    assert!(help.contains("parity-goldens"));
}

#[test]
fn parity_fixtures_workflow_exposes_final_full_surface_gate_with_performance_gate() {
    let workflow =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../../.github/workflows/parity-fixtures.yml");
    let workflow = fs::read_to_string(workflow).expect("workflow should exist");

    assert!(workflow.contains("- final-full-surface"));
    assert!(workflow.contains("manifest=\"tests/parity/full-resource-fixtures.json\""));
    assert!(workflow.contains("fixture_args+=(\"--require-non-gating-passed\")"));
    assert!(workflow.contains("preflight_report=$output_dir/preflight.json"));
    assert!(workflow.contains("\"--allow-missing-report\""));
    assert!(workflow.contains("\"--preflight-report\""));
    assert!(workflow.contains("${{ steps.parity.outputs.raw_report }}"));
    assert!(workflow.contains("${{ steps.parity.outputs.preflight_report }}"));
    assert!(workflow.contains("${{ steps.parity.outputs.summary_report }}"));
    assert!(workflow.contains("${{ steps.parity.outputs.benchmark_report }}"));
    assert!(workflow.contains("${{ steps.parity.outputs.progress_log }}"));
    assert!(workflow.contains("Run Rust-Native benchmark ladder"));
    assert!(workflow.contains("tests/parity/rust-native-bench-fixtures.json"));
    assert!(workflow.contains("nativeFasterThanWhisperx"));
    assert!(workflow.contains("nativeSpeedupRatio >= 1.001"));
    assert!(workflow.contains("benchmark report passed="));
    assert!(!workflow.contains("\"--native-only\""));
}

#[test]
fn parity_fixtures_help_lists_local_suite_options() {
    let help = command_stdout(["parity-fixtures", "--help"]);
    for expected in [
        "<MANIFEST>",
        "--root",
        "--whisperx-command",
        "--output-dir",
        "--model-dir",
        "--model-cache-only",
        "--case",
        "--case-timeout-seconds",
        "--require-non-gating-passed",
    ] {
        assert!(help.contains(expected), "help should contain `{expected}`");
    }
}

#[test]
fn parity_bench_help_lists_benchmark_options() {
    let help = command_stdout(["parity-bench", "--help"]);
    for expected in [
        "<MANIFEST>",
        "--root",
        "--whisperx-command",
        "--model-dir",
        "--model-cache-only",
        "--iterations",
        "--warmups",
        "--case",
        "--case-timeout-seconds",
        "--native-only",
        "--json",
    ] {
        assert!(help.contains(expected), "help should contain `{expected}`");
    }
}

#[test]
fn parity_bench_json_empty_manifest_has_stable_top_level_shape() {
    let temp = tempfile::tempdir().expect("tempdir");
    let manifest = temp.path().join("fixtures.json");
    fs::write(&manifest, r#"{"fixtures":[]}"#).expect("manifest");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("parity-bench")
        .arg(&manifest)
        .arg("--root")
        .arg(temp.path())
        .arg("--iterations")
        .arg("1")
        .arg("--warmups")
        .arg("1")
        .arg("--native-only")
        .arg("--case-timeout-seconds")
        .arg("900")
        .arg("--json")
        .assert()
        .success()
        .stdout(predicate::str::contains("\"passed\": true"))
        .stdout(predicate::str::contains("\"iterations\": 1"))
        .stdout(predicate::str::contains("\"warmups\": 1"))
        .stdout(predicate::str::contains("\"nativeOnly\": true"))
        .stdout(predicate::str::contains("\"caseTimeoutSeconds\": 900"))
        .stdout(predicate::str::contains("\"cases\": []"));
}

#[test]
fn parity_bench_rust_native_ladder_cases_are_selectable_with_timeout_reporting() {
    let temp = tempfile::tempdir().expect("tempdir");
    let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../tests/parity/rust-native-bench-fixtures.json");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("parity-bench")
        .arg(fixture)
        .arg("--root")
        .arg(temp.path())
        .arg("--native-only")
        .arg("--case")
        .arg("shrek-retold-3m-large-v3-turbo-cuda")
        .arg("--case")
        .arg("shrek-retold-10m-large-v3-turbo-cuda")
        .arg("--case-timeout-seconds")
        .arg("0")
        .arg("--json")
        .assert()
        .failure()
        .stdout(predicate::str::contains("\"passed\": false"))
        .stdout(predicate::str::contains("\"timedOut\": true"))
        .stdout(predicate::str::contains(
            "\"name\": \"shrek-retold-3m-large-v3-turbo-cuda\"",
        ))
        .stdout(predicate::str::contains(
            "\"name\": \"shrek-retold-10m-large-v3-turbo-cuda\"",
        ))
        .stdout(
            predicate::str::contains("\"name\": \"shrek-retold-30s-large-v3-turbo-cuda\"").not(),
        );
}

#[test]
fn parity_bench_native_only_case_error_still_emits_json_report() {
    let temp = tempfile::tempdir().expect("tempdir");
    let manifest = temp.path().join("fixtures.json");
    fs::write(
        &manifest,
        r#"{
          "fixtures": [
            {
              "name": "missing-audio",
              "input": "audio/missing.wav",
              "nativeAsr": { "modelId": "tiny.en" },
              "alignment": { "enabled": false }
            }
          ]
        }"#,
    )
    .expect("manifest");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("parity-bench")
        .arg(&manifest)
        .arg("--root")
        .arg(temp.path())
        .arg("--native-only")
        .arg("--iterations")
        .arg("1")
        .arg("--warmups")
        .arg("0")
        .arg("--json")
        .assert()
        .failure()
        .stdout(predicate::str::contains("\"passed\": false"))
        .stdout(predicate::str::contains("\"name\": \"missing-audio\""))
        .stdout(predicate::str::contains("\"error\""));
}

#[test]
#[ignore = "requires SMOKE_ROOT with Shrek-derived 30s audio, cached large-v3-turbo CUDA assets, and Silero VAD"]
fn parity_bench_rust_native_ladder_30s_native_only_smoke_emits_failure_json() {
    let smoke_root = std::env::var_os("SMOKE_ROOT").expect("SMOKE_ROOT must be set");
    let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../tests/parity/rust-native-bench-fixtures.json");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("parity-bench")
        .arg(fixture)
        .arg("--root")
        .arg(smoke_root)
        .arg("--native-only")
        .arg("--model-cache-only")
        .arg("--case")
        .arg("shrek-retold-30s-large-v3-turbo-cuda")
        .arg("--case-timeout-seconds")
        .arg("900")
        .arg("--json")
        .assert()
        .failure()
        .stdout(predicate::str::contains(
            "\"name\": \"shrek-retold-30s-large-v3-turbo-cuda\"",
        ))
        .stdout(predicate::str::contains("\"nativeOnly\": true"))
        .stdout(predicate::str::contains("\"native\""))
        .stdout(predicate::str::contains("\"phases\""))
        .stdout(predicate::str::contains("\"realtimeFactor\""));
}

#[test]
fn parity_summary_compacts_fixture_report() {
    let temp = tempfile::tempdir().expect("tempdir");
    let report = temp.path().join("report.json");
    fs::write(
        &report,
        r#"{
          "passed": true,
          "cases": [
            {
              "name": "case-a",
              "gating": true,
              "passed": true,
              "startedAt": "1710000000.000",
              "elapsedSeconds": 1.25,
              "timedOut": false,
              "missingRequiredDiagnostics": [],
              "expectedOutputMatches": [
                {
                  "format": "srt",
                  "comparison": "exact",
                  "gating": false,
                  "expectedPath": "expected.srt",
                  "actualPath": "actual.srt",
                  "passed": false,
                  "difference": "byte mismatch"
                }
              ],
              "failureSummary": []
            }
          ]
        }"#,
    )
    .expect("report");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("parity-summary")
        .arg(&report)
        .assert()
        .success()
        .stdout(predicate::str::contains("\"passed\": true"))
        .stdout(predicate::str::contains("\"elapsedSeconds\": 1.25"))
        .stdout(predicate::str::contains("\"strictComparisonFailures\": []"))
        .stdout(predicate::str::contains("\"reportOnlyDifferences\": ["))
        .stdout(predicate::str::contains(
            "srt exact output differs: byte mismatch",
        ));
}

#[test]
fn parity_summary_reports_gating_failures() {
    let temp = tempfile::tempdir().expect("tempdir");
    let report = temp.path().join("report.json");
    fs::write(
        &report,
        r#"{
          "passed": false,
          "cases": [
            {
              "name": "gating-case",
              "gating": true,
              "passed": false,
              "missingRequiredDiagnostics": [
                "asrModelSource=hugging-face-cache"
              ],
              "error": "segment timing differs at segment 0: native start=0.000s native end=1.000s, reference start=0.250s reference end=1.000s, start_delta=0.250s end_delta=0.000s tolerance=0.100s",
              "expectedOutputMatches": [],
              "failureSummary": []
            },
            {
              "name": "report-only-case",
              "gating": false,
              "passed": false,
              "error": "non-gating failed",
              "missingRequiredDiagnostics": [],
              "expectedOutputMatches": [],
              "failureSummary": []
            }
          ]
        }"#,
    )
    .expect("report");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    let output = command
        .arg("parity-summary")
        .arg(&report)
        .output()
        .expect("summary command should run");

    assert!(
        output.status.success(),
        "summary should succeed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let summary: serde_json::Value =
        serde_json::from_slice(&output.stdout).expect("summary should be json");
    let failures = summary["gatingFailures"]
        .as_array()
        .expect("summary should include gatingFailures");
    assert_eq!(failures.len(), 1);
    assert_eq!(failures[0]["name"], "gating-case");
    let failure_text = serde_json::to_string(&failures[0]).expect("failure json");
    assert!(failure_text.contains("missing required diagnostic: asrModelSource=hugging-face-cache"));
    assert!(failure_text.contains("reference start=0.250s"));
}

#[test]
fn parity_summary_reports_preflight_failures_when_fixture_report_is_missing() {
    let temp = tempfile::tempdir().expect("tempdir");
    let report = temp.path().join("missing-report.json");
    let preflight = temp.path().join("preflight.json");
    let smoke_root = temp.path().join("smoke");
    let model_dir = smoke_root.join("models");
    let output_dir = smoke_root.join("out/final-full-surface-parity");
    let progress_log = output_dir.join("progress.log");
    fs::write(
        &preflight,
        format!(
            r#"{{
          "passed": false,
          "manifest": "{manifest}",
          "root": "{root}",
          "whisperxCommand": "{whisperx_command}",
          "modelDir": "{model_dir}",
          "sourceCheckoutTag": null,
          "cases": [
            {{
              "name": "gating-case",
              "gating": true,
              "passed": false,
              "missing": ["expected JSON {root}/expected/gating-case.json does not exist"],
              "warnings": []
            }},
            {{
              "name": "report-only-case",
              "gating": false,
              "passed": false,
              "missing": ["audio {root}/audio/report-only.wav does not exist"],
              "warnings": []
            }}
          ]
        }}"#,
            manifest = "tests/parity/full-resource-fixtures.json",
            root = smoke_root.display(),
            whisperx_command = ".audio-tools/whisperx-venv/bin/whisperx",
            model_dir = model_dir.display()
        ),
    )
    .expect("preflight");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    let output = command
        .arg("parity-summary")
        .arg(&report)
        .arg("--allow-missing-report")
        .arg("--preflight-report")
        .arg(&preflight)
        .arg("--suite")
        .arg("final-full-surface")
        .arg("--features")
        .arg("whisperx-compat,media-decode,silero-vad,pyannote-vad,pyannote-diarization,cuda")
        .arg("--runner")
        .arg("self-hosted")
        .arg("--manifest")
        .arg("tests/parity/full-resource-fixtures.json")
        .arg("--output-dir")
        .arg(&output_dir)
        .arg("--smoke-root")
        .arg(&smoke_root)
        .arg("--model-dir")
        .arg(&model_dir)
        .arg("--whisperx-command")
        .arg(".audio-tools/whisperx-venv/bin/whisperx")
        .arg("--progress-log")
        .arg(&progress_log)
        .arg("--ort-dylib-path")
        .arg("/opt/onnxruntime/lib/libonnxruntime.so")
        .output()
        .expect("summary command should run");

    assert!(
        output.status.success(),
        "summary should succeed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    let summary: serde_json::Value =
        serde_json::from_slice(&output.stdout).expect("summary should be json");
    assert_eq!(summary["passed"], false);
    assert_eq!(summary["workflow"]["suite"], "final-full-surface");
    assert_eq!(summary["workflow"]["features"][0], "whisperx-compat");
    assert_eq!(summary["workflow"]["runner"], "self-hosted");
    assert_eq!(
        summary["workflow"]["smokeRoot"],
        smoke_root.display().to_string()
    );
    assert_eq!(
        summary["workflow"]["modelDir"],
        model_dir.display().to_string()
    );
    assert_eq!(
        summary["workflow"]["ortDylibPath"],
        "/opt/onnxruntime/lib/libonnxruntime.so"
    );
    assert_eq!(summary["rawReportMissing"], true);
    assert_eq!(summary["preflight"]["passed"], false);
    assert_eq!(summary["gatingFailures"][0]["name"], "gating-case");
    assert_eq!(summary["gatingFailures"][0]["kind"], "preflight");
    assert_eq!(summary["nonGatingFailures"][0]["name"], "report-only-case");
    assert_eq!(summary["nonGatingFailures"][0]["kind"], "preflight");
    assert_eq!(summary["skippedCases"].as_array().unwrap().len(), 2);
    assert_eq!(summary["skippedCases"][0]["reason"], "preflight failed");
}

#[test]
fn parity_fixtures_requires_root_or_smoke_root() {
    let temp = tempfile::tempdir().expect("tempdir");
    let manifest = temp.path().join("fixtures.json");
    fs::write(&manifest, r#"{"fixtures":[]}"#).expect("manifest");
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .env_remove("SMOKE_ROOT")
        .current_dir(temp.path())
        .arg("parity-fixtures")
        .arg(&manifest)
        .assert()
        .failure()
        .stderr(predicate::str::contains("SMOKE_ROOT"))
        .stderr(predicate::str::contains("--root"));
}

#[test]
fn parity_fixtures_manifest_parse_errors_name_manifest() {
    let temp = tempfile::tempdir().expect("tempdir");
    let manifest = temp.path().join("fixtures.json");
    fs::write(&manifest, b"not json").expect("manifest");
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("parity-fixtures")
        .arg(&manifest)
        .arg("--root")
        .arg(temp.path())
        .assert()
        .failure()
        .stderr(predicate::str::contains("failed to parse"))
        .stderr(predicate::str::contains(
            manifest.to_string_lossy().as_ref(),
        ));
}

#[test]
fn parity_fixtures_case_filter_runs_matching_case() {
    let temp = tempfile::tempdir().expect("tempdir");
    let manifest = temp.path().join("fixtures.json");
    fs::write(
        &manifest,
        r#"{
          "fixtures": [
            { "name": "case-a", "input": "audio/a.wav" },
            { "name": "case-b", "input": "audio/b.wav" }
          ]
        }"#,
    )
    .expect("manifest");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("parity-fixtures")
        .arg(&manifest)
        .arg("--root")
        .arg(temp.path())
        .arg("--case")
        .arg("case-a")
        .assert()
        .failure()
        .stdout(predicate::str::contains("\"name\": \"case-a\""))
        .stdout(predicate::str::contains("\"name\": \"case-b\"").not())
        .stderr(predicate::str::contains(
            "parity-fixtures: starting case 1/1: case-a",
        ))
        .stderr(predicate::str::contains(
            "parity-fixtures: completed case 1/1: case-a failed",
        ))
        .stderr(predicate::str::contains(
            "one or more parity fixtures failed",
        ));
}

#[test]
fn parity_fixtures_case_filter_rejects_missing_case() {
    let temp = tempfile::tempdir().expect("tempdir");
    let manifest = temp.path().join("fixtures.json");
    fs::write(
        &manifest,
        r#"{
          "fixtures": [
            { "name": "case-a", "input": "audio/a.wav" }
          ]
        }"#,
    )
    .expect("manifest");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("parity-fixtures")
        .arg(&manifest)
        .arg("--root")
        .arg(temp.path())
        .arg("--case")
        .arg("missing")
        .assert()
        .failure()
        .stdout(predicate::str::is_empty())
        .stderr(predicate::str::contains(
            "no fixture case named missing matched the manifest",
        ));
}

#[test]
fn parity_fixtures_case_filter_accepts_multiple_cases() {
    let temp = tempfile::tempdir().expect("tempdir");
    let manifest = temp.path().join("fixtures.json");
    fs::write(
        &manifest,
        r#"{
          "fixtures": [
            { "name": "case-a", "input": "audio/a.wav" },
            { "name": "case-b", "input": "audio/b.wav" },
            { "name": "case-c", "input": "audio/c.wav" }
          ]
        }"#,
    )
    .expect("manifest");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("parity-fixtures")
        .arg(&manifest)
        .arg("--root")
        .arg(temp.path())
        .arg("--case")
        .arg("case-a")
        .arg("--case")
        .arg("case-c")
        .assert()
        .failure()
        .stdout(predicate::str::contains("\"name\": \"case-a\""))
        .stdout(predicate::str::contains("\"name\": \"case-c\""))
        .stdout(predicate::str::contains("\"name\": \"case-b\"").not())
        .stderr(predicate::str::contains(
            "one or more parity fixtures failed",
        ));
}

#[test]
fn parity_fixtures_case_timeout_reports_bounded_failure() {
    let temp = tempfile::tempdir().expect("tempdir");
    let manifest = temp.path().join("fixtures.json");
    let output_dir = temp.path().join("out");
    fs::write(
        &manifest,
        r#"{
          "fixtures": [
            { "name": "slow-case", "input": "audio/a.wav" }
          ]
        }"#,
    )
    .expect("manifest");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("parity-fixtures")
        .arg(&manifest)
        .arg("--root")
        .arg(temp.path())
        .arg("--output-dir")
        .arg(&output_dir)
        .arg("--case-timeout-seconds")
        .arg("0")
        .assert()
        .failure()
        .stdout(predicate::str::contains("\"name\": \"slow-case\""))
        .stdout(predicate::str::contains("exceeded timeout"))
        .stderr(predicate::str::contains(
            "parity-fixtures: timed out case 1/1: slow-case",
        ))
        .stderr(predicate::str::contains(
            "one or more parity fixtures failed",
        ));

    let report = fs::read_to_string(output_dir.join("report.json")).expect("fixture report");
    assert!(report.contains("\"name\": \"slow-case\""));
    assert!(report.contains("\"timedOut\": true"));
}

#[test]
fn parity_preflight_help_lists_resource_checks() {
    let help = command_stdout(["parity-preflight", "--help"]);
    for expected in [
        "<MANIFEST>",
        "--root",
        "--whisperx-command",
        "--model-dir",
        "--require-expected",
        "--include-non-gating",
    ] {
        assert!(help.contains(expected), "help should contain `{expected}`");
    }
}

#[test]
fn parity_goldens_help_lists_generation_options() {
    let help = command_stdout(["parity-goldens", "--help"]);
    for expected in [
        "<MANIFEST>",
        "--root",
        "--whisperx-command",
        "--model-dir",
        "--model-cache-only",
        "--case",
        "--include-non-gating",
        "--overwrite",
        "--dry-run",
    ] {
        assert!(help.contains(expected), "help should contain `{expected}`");
    }
}

#[test]
fn parity_goldens_dry_run_prints_whisperx_command_without_writing() {
    let temp = tempfile::tempdir().expect("tempdir");
    let root = temp.path().join("smoke");
    let manifest = temp.path().join("fixtures.json");
    fs::write(
        &manifest,
        r#"{
          "fixtures": [
            {
              "name": "tiny-output-all-defaults",
              "input": "audio/input.wav",
              "nativeAsr": { "modelId": "tiny.en" },
              "alignment": { "enabled": true, "modelId": "facebook/wav2vec2-base-960h" },
              "whisperx": { "model": "tiny.en" },
              "language": "en",
              "expectedOutputs": [
                { "format": "txt", "path": "expected/whisperx-3.8.6/tiny-output-all-defaults.txt" },
                { "format": "srt", "path": "expected/whisperx-3.8.6/tiny-output-all-defaults.srt" }
              ]
            }
          ]
        }"#,
    )
    .expect("manifest");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("parity-goldens")
        .arg(&manifest)
        .arg("--root")
        .arg(&root)
        .arg("--whisperx-command")
        .arg("/bin/true")
        .arg("--case")
        .arg("tiny-output-all-defaults")
        .arg("--dry-run")
        .assert()
        .success()
        .stdout(predicate::str::contains("command: /bin/true"))
        .stdout(predicate::str::contains("--output_format all"))
        .stdout(predicate::str::contains("--return_char_alignments").not())
        .stdout(predicate::str::contains("tiny-output-all-defaults.txt"));

    assert!(
        !root.exists(),
        "dry run should not write generated directories"
    );
}

#[test]
fn parity_goldens_dry_run_emits_char_alignment_flag_only_when_requested() {
    let temp = tempfile::tempdir().expect("tempdir");
    let root = temp.path().join("smoke");
    let manifest = temp.path().join("fixtures.json");
    fs::write(
        &manifest,
        r#"{
          "fixtures": [
            {
              "name": "tiny-en-char-alignments",
              "input": "audio/input.wav",
              "expectedJson": "expected/tiny-en-char-alignments.whisperx.json",
              "nativeAsr": { "modelId": "tiny.en" },
              "alignment": {
                "enabled": true,
                "modelId": "facebook/wav2vec2-base-960h",
                "returnCharAlignments": true
              },
              "whisperx": { "model": "tiny.en" },
              "language": "en"
            }
          ]
        }"#,
    )
    .expect("manifest");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("parity-goldens")
        .arg(&manifest)
        .arg("--root")
        .arg(&root)
        .arg("--whisperx-command")
        .arg("/bin/true")
        .arg("--case")
        .arg("tiny-en-char-alignments")
        .arg("--dry-run")
        .assert()
        .success()
        .stdout(predicate::str::contains("--return_char_alignments"))
        .stdout(predicate::str::contains("--return_char_alignments true").not())
        .stdout(predicate::str::contains("--return_char_alignments false").not());
}

#[test]
fn parity_goldens_dry_run_passes_highlight_words_bool_value() {
    let temp = tempfile::tempdir().expect("tempdir");
    let root = temp.path().join("smoke");
    let manifest = temp.path().join("fixtures.json");
    fs::write(
        &manifest,
        r#"{
          "fixtures": [
            {
              "name": "tiny-output-subtitles-highlight",
              "input": "audio/input.wav",
              "nativeAsr": { "modelId": "tiny.en" },
              "alignment": { "enabled": true, "modelId": "facebook/wav2vec2-base-960h" },
              "whisperx": { "model": "tiny.en" },
              "language": "en",
              "output": {
                "formats": ["srt"],
                "basename": "tiny-output-subtitles-highlight",
                "subtitles": { "highlightWords": true }
              },
              "expectedOutputs": [
                { "format": "srt", "path": "expected/whisperx-3.8.6/tiny-output-subtitles-highlight.srt" }
              ]
            }
          ]
        }"#,
    )
    .expect("manifest");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("parity-goldens")
        .arg(&manifest)
        .arg("--root")
        .arg(&root)
        .arg("--whisperx-command")
        .arg("/bin/true")
        .arg("--case")
        .arg("tiny-output-subtitles-highlight")
        .arg("--dry-run")
        .assert()
        .success()
        .stdout(predicate::str::contains("--highlight_words True"))
        .stdout(predicate::str::contains("--highlight_words --").not());
}

#[test]
fn parity_goldens_dry_run_maps_translation_fixture_to_python_translate() {
    let temp = tempfile::tempdir().expect("tempdir");
    let root = temp.path().join("smoke");
    let manifest = temp.path().join("fixtures.json");
    fs::write(
        &manifest,
        r#"{
          "fixtures": [
            {
              "name": "small-de-translate-cache",
              "input": "audio/input-de.wav",
              "expectedJson": "expected/whisperx-3.8.6/small-de-translate-cache.json",
              "nativeAsr": { "task": "translate", "modelId": "small" },
              "translation": {
                "enabled": true,
                "modelId": "Helsinki-NLP/opus-mt-de-en",
                "modelCacheOnly": true,
                "sourceLanguage": "de",
                "targetLanguage": "en"
              },
              "alignment": { "enabled": true, "modelId": "facebook/wav2vec2-base-960h" },
              "whisperx": { "model": "small" },
              "language": "de"
            }
          ]
        }"#,
    )
    .expect("manifest");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("parity-goldens")
        .arg(&manifest)
        .arg("--root")
        .arg(&root)
        .arg("--whisperx-command")
        .arg("/bin/true")
        .arg("--case")
        .arg("small-de-translate-cache")
        .arg("--dry-run")
        .assert()
        .success()
        .stdout(predicate::str::contains("--task translate"))
        .stdout(predicate::str::contains("--model_cache_only True"))
        .stdout(predicate::str::contains("small-de-translate-cache.json"))
        .stdout(predicate::str::contains("--translation-model").not());

    assert!(
        !root.exists(),
        "dry run should not write generated directories"
    );
}

#[test]
fn parity_preflight_reads_smoke_root_from_dotenv() {
    let temp = tempfile::tempdir().expect("tempdir");
    let manifest = temp.path().join("fixtures.json");
    let smoke_root = temp.path().join("smoke-root");
    fs::create_dir_all(smoke_root.join("models")).expect("smoke root");
    fs::write(&manifest, r#"{"fixtures":[]}"#).expect("manifest");
    fs::write(temp.path().join(".env"), "SMOKE_ROOT=smoke-root\n").expect("dotenv");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .env_remove("SMOKE_ROOT")
        .current_dir(temp.path())
        .arg("parity-preflight")
        .arg(&manifest)
        .arg("--whisperx-command")
        .arg("/bin/true")
        .assert()
        .success()
        .stdout(predicate::str::contains("Parity preflight: passed"))
        .stdout(predicate::str::contains(
            smoke_root.to_string_lossy().as_ref(),
        ));
}

#[test]
fn parity_preflight_manifest_parse_errors_name_manifest() {
    let temp = tempfile::tempdir().expect("tempdir");
    let manifest = temp.path().join("fixtures.json");
    fs::write(&manifest, b"not json").expect("manifest");
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("parity-preflight")
        .arg(&manifest)
        .arg("--root")
        .arg(temp.path())
        .assert()
        .failure()
        .stderr(predicate::str::contains("failed to parse"))
        .stderr(predicate::str::contains(
            manifest.to_string_lossy().as_ref(),
        ));
}

#[test]
fn parity_preflight_requires_root_or_smoke_root() {
    let temp = tempfile::tempdir().expect("tempdir");
    let manifest = temp.path().join("fixtures.json");
    fs::write(&manifest, r#"{"fixtures":[]}"#).expect("manifest");
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .env_remove("SMOKE_ROOT")
        .current_dir(temp.path())
        .arg("parity-preflight")
        .arg(&manifest)
        .assert()
        .failure()
        .stderr(predicate::str::contains("SMOKE_ROOT"))
        .stderr(predicate::str::contains("--root"));
}

#[test]
fn parity_goldens_requires_root_or_smoke_root() {
    let temp = tempfile::tempdir().expect("tempdir");
    let manifest = temp.path().join("fixtures.json");
    fs::write(&manifest, r#"{"fixtures":[]}"#).expect("manifest");
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .env_remove("SMOKE_ROOT")
        .current_dir(temp.path())
        .arg("parity-goldens")
        .arg(&manifest)
        .assert()
        .failure()
        .stderr(predicate::str::contains("SMOKE_ROOT"))
        .stderr(predicate::str::contains("--root"));
}

#[test]
fn checked_in_asr_fixture_manifest_parses() {
    let fixture =
        Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/parity/asr-fixtures.json");
    let bytes = fs::read(&fixture).expect("fixture manifest");
    let parsed: native_whisperx::ParityFixtureSuite =
        serde_json::from_slice(&bytes).expect("valid manifest schema");
    assert_eq!(parsed.fixtures.len(), 12);
    assert_eq!(
        parsed
            .fixtures
            .iter()
            .filter(|fixture| fixture.gating)
            .count(),
        12
    );
    assert_eq!(
        parsed
            .fixtures
            .iter()
            .filter(|fixture| !fixture.gating)
            .count(),
        0
    );
    assert!(parsed
        .fixtures
        .iter()
        .any(|fixture| !fixture.expected_outputs.is_empty()));
    assert!(parsed
        .fixtures
        .iter()
        .filter(|fixture| !fixture.expected_outputs.is_empty())
        .any(|fixture| fixture.gating));
    assert!(parsed.fixtures.iter().any(|fixture| {
        fixture.name == "tiny-output-all-defaults"
            && fixture.gating
            && fixture.output.formats == vec![native_whisperx::OutputFormat::All]
            && fixture.expected_outputs.iter().any(|expected| {
                expected.format == native_whisperx::OutputFormat::Audacity && expected.gating
            })
            && fixture.expected_outputs.iter().any(|expected| {
                expected.format == native_whisperx::OutputFormat::Json
                    && expected.comparison == native_whisperx::OutputComparisonMode::JsonSemantic
                    && expected.gating
            })
            && !fixture
                .expected_outputs
                .iter()
                .any(|expected| expected.format == native_whisperx::OutputFormat::NativeJson)
    }));
    assert!(parsed.fixtures.iter().any(|fixture| {
        fixture.name == "tiny-output-subtitles-highlight"
            && fixture.gating
            && fixture.expected_outputs.len() == 4
            && fixture
                .expected_outputs
                .iter()
                .any(|expected| !expected.gating)
    }));
    assert!(parsed
        .fixtures
        .iter()
        .any(|fixture| fixture.name == "small-de-translate-cache"
            && fixture.gating
            && fixture.translation.enabled
            && fixture.translation.model_cache_only));
    assert!(parsed.fixtures.iter().any(|fixture| {
        fixture.name == "tiny-en-no-align-cache"
            && fixture.gating
            && fixture.comparison.segment_timing
            && fixture
                .required_diagnostics
                .iter()
                .any(|diagnostic| diagnostic == "asrModelSource=hugging-face-cache")
    }));
    assert!(parsed.fixtures.iter().any(|fixture| {
        fixture.name == "small-en-no-align-cache"
            && fixture.gating
            && fixture.comparison.segment_timing
            && fixture
                .required_diagnostics
                .iter()
                .any(|diagnostic| diagnostic == "asrModelSource=hugging-face-cache")
    }));
    assert!(parsed.fixtures.iter().any(|fixture| {
        fixture.name == "small-de-no-align-cache"
            && fixture.gating
            && !fixture.comparison.text
            && !fixture.comparison.segment_text
            && fixture
                .required_diagnostics
                .iter()
                .any(|diagnostic| diagnostic == "asrModelSource=hugging-face-cache")
    }));
    assert!(parsed.fixtures.iter().any(|fixture| {
        fixture.name == "tiny-en-alignment-alias-cache"
            && fixture.gating
            && fixture.comparison.segment_timing
            && fixture.comparison.word_timing
            && fixture
                .required_diagnostics
                .iter()
                .any(|diagnostic| diagnostic == "alignmentModelId=facebook/wav2vec2-base-960h")
            && fixture
                .required_diagnostics
                .iter()
                .any(|diagnostic| diagnostic == "alignmentModelSource=hugging-face-cache")
    }));
}

#[test]
fn checked_in_full_resource_fixture_manifest_parses() {
    let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../tests/parity/full-resource-fixtures.json");
    let bytes = fs::read(&fixture).expect("fixture manifest");
    let parsed: native_whisperx::ParityFixtureSuite =
        serde_json::from_slice(&bytes).expect("valid manifest schema");
    assert_eq!(parsed.fixtures.len(), 4);
    assert!(parsed.fixtures.iter().any(|fixture| {
        fixture.name == "silero-vad-tiny-en"
            && fixture.gating
            && fixture.vad.method == native_whisperx::VadMethod::Silero
            && fixture.comparison.vad_segment_count
            && fixture.comparison.vad_segment_timing
    }));
    assert!(parsed.fixtures.iter().any(|fixture| {
        fixture.name == "pyannote-vad-tiny-en"
            && fixture.gating
            && fixture.vad.method == native_whisperx::VadMethod::Pyannote
            && fixture.comparison.vad_segment_count
            && fixture.comparison.vad_segment_timing
    }));
    for fixture in parsed.fixtures.iter().filter(|fixture| {
        fixture.name == "diarization-two-speaker-pyannote-reference"
            || fixture.name == "diarization-speaker-embeddings-pyannote-reference"
    }) {
        assert_eq!(
            fixture.expected_target,
            native_whisperx::ExpectedTranscriptTarget::Whisperx
        );
        assert_eq!(fixture.timeout_seconds, Some(240));
        assert!(fixture
            .required_diagnostics
            .iter()
            .any(|diagnostic| diagnostic == "cuda=true"));
        assert!(fixture
            .required_diagnostics
            .iter()
            .any(|diagnostic| diagnostic == "diarizationSpeakerCount=2"));
    }
}

#[test]
fn checked_in_rust_native_bench_fixture_manifest_parses() {
    let fixture = Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("../../tests/parity/rust-native-bench-fixtures.json");
    let bytes = fs::read(&fixture).expect("fixture manifest");
    let raw: serde_json::Value = serde_json::from_slice(&bytes).expect("valid manifest json");
    let parsed: native_whisperx::ParityFixtureSuite =
        serde_json::from_slice(&bytes).expect("valid manifest schema");
    assert_eq!(parsed.fixtures.len(), 3);
    assert!(parsed.fixtures.iter().all(|fixture| !fixture.gating));
    assert!(parsed.fixtures.iter().all(|fixture| {
        fixture.native_asr.model_id == "large-v3-turbo"
            && fixture.native_asr.device == native_whisperx::DevicePreference::Cuda
            && fixture.native_asr.max_batch_size == Some(8)
            && fixture.vad.method == native_whisperx::VadMethod::Silero
            && fixture.alignment.enabled
            && fixture.alignment.model_id == "facebook/wav2vec2-base-960h"
            && fixture.whisperx.compute_type.is_none()
            && fixture
                .required_diagnostics
                .iter()
                .any(|diagnostic| diagnostic == "alignmentCuda=true")
            && fixture
                .required_diagnostics
                .iter()
                .any(|diagnostic| diagnostic == "alignmentDevice=cuda:0")
    }));
    assert!(raw["fixtures"]
        .as_array()
        .expect("fixtures")
        .iter()
        .all(|fixture| fixture["alignment"].get("device").is_none()
            && fixture["whisperx"].get("computeType").is_none()));
    let generated_clips = raw["metadata"]["generatedClips"]
        .as_array()
        .expect("generated clip metadata");
    assert_eq!(generated_clips.len(), 3);
    assert!(generated_clips
        .iter()
        .any(|clip| clip["durationSeconds"].as_u64() == Some(30)));
    assert!(generated_clips
        .iter()
        .any(|clip| clip["durationSeconds"].as_u64() == Some(180)));
    assert!(generated_clips
        .iter()
        .any(|clip| clip["durationSeconds"].as_u64() == Some(600)));
    assert!(parsed
        .fixtures
        .iter()
        .any(|fixture| fixture.name == "shrek-retold-30s-large-v3-turbo-cuda"));
    assert!(parsed
        .fixtures
        .iter()
        .any(|fixture| fixture.name == "shrek-retold-3m-large-v3-turbo-cuda"));
    assert!(parsed
        .fixtures
        .iter()
        .any(|fixture| fixture.name == "shrek-retold-10m-large-v3-turbo-cuda"));
}

#[test]
fn parity_matrix_uses_final_surface_statuses_only() {
    let matrix = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/parity-matrix.md");
    let matrix = fs::read_to_string(matrix).expect("parity matrix");
    let allowed = [
        "rust-native complete",
        "blocked",
        "reference-only",
        "intentionally unsupported",
    ];
    let mut checked_rows = 0;

    for line in matrix.lines() {
        if !line.starts_with("| ") || line.starts_with("| Area ") || line.starts_with("| ---") {
            continue;
        }
        let escaped = line.replace("\\|", "__PIPE__");
        let columns = escaped.split('|').map(str::trim).collect::<Vec<_>>();
        if columns.len() < 5 {
            continue;
        }
        let status = columns[3].trim_matches('`');
        if !allowed.contains(&status) {
            panic!("unexpected parity matrix status `{status}` in row `{line}`");
        }
        checked_rows += 1;
    }

    assert!(
        checked_rows >= 30,
        "expected CLI surface rows to be checked"
    );
}

#[test]
fn parity_fixture_manifest_accepts_gating_and_expected_outputs() {
    let parsed: native_whisperx::ParityFixtureSuite = serde_json::from_str(
        r#"{
          "fixtures": [
            {
              "name": "non-gating-output",
              "gating": false,
              "input": "audio/input.wav",
              "expectedOutputs": [
                {
                  "format": "json",
                  "path": "expected/output.json",
                  "comparison": "jsonSemantic"
                }
              ]
            }
          ]
        }"#,
    )
    .expect("manifest should parse");

    assert!(!parsed.fixtures[0].gating);
    assert_eq!(parsed.fixtures[0].expected_outputs.len(), 1);
}

#[test]
fn transcribe_rejects_highlight_without_alignment() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args(["input.wav", "--no_align", "--highlight_words"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("require alignment"));
}

#[test]
fn transcribe_rejects_max_line_width_without_alignment() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args(["input.wav", "--no_align", "--max_line_width", "42"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("require alignment"));
}

#[test]
fn transcribe_rejects_max_line_count_without_alignment() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args(["input.wav", "--no_align", "--max_line_count", "2"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("require alignment"));
}

#[test]
fn transcribe_rejects_native_translate_without_no_align() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args(["input.wav", "--task", "translate"])
        .assert()
        .failure()
        .stderr(predicate::str::contains(
            "native --task translate requires --translation-model or --translation-bundle",
        ));
}

#[test]
fn transcribe_rejects_native_speaker_embeddings() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args(["input.wav", "--diarize", "--speaker_embeddings"])
        .assert()
        .failure()
        .stderr(predicate::str::contains(
            "native speaker embeddings require --diarize-model pyannote/... and --diarization-model-bundle",
        ));
}

#[test]
fn transcribe_rejects_native_explicit_pyannote_diarize_model() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args([
            "input.wav",
            "--diarize",
            "--diarize-model",
            "pyannote/speaker-diarization-community-1",
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains(
            "native pyannote diarization requires --diarization-model-bundle",
        ));
}

#[test]
fn transcribe_rejects_native_diarization_bundle_without_pyannote_model() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args([
            "input.wav",
            "--diarization-model-bundle",
            "/models/pyannote-diarization",
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains(
            "native --diarization-model-bundle requires --diarize-model pyannote/...",
        ));
}

#[test]
fn transcribe_rejects_native_pyannote_before_audio_io() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args(["input.wav", "--vad_method", "pyannote"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("pyannote"));
}

#[cfg(not(feature = "silero-vad"))]
#[test]
fn transcribe_rejects_native_silero_without_feature_before_audio_io() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args(["input.wav", "--vad_method", "silero"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("silero-vad feature"));
}

#[test]
fn transcribe_rejects_basename_with_multiple_inputs() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args(["one.wav", "two.wav", "--basename", "fixed"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("multiple input files"));
}

#[cfg(unix)]
#[test]
fn transcribe_expands_relative_glob_inputs() {
    let fake = FakeWhisperx::new();
    let audio_dir = fake.root().join("audio");
    fs::create_dir_all(&audio_dir).expect("audio dir");
    fs::write(audio_dir.join("b.wav"), b"fake audio").expect("b wav");
    fs::write(audio_dir.join("a.wav"), b"fake audio").expect("a wav");

    let mut command = fake.command();
    command
        .current_dir(fake.root())
        .args([
            "transcribe",
            "audio/*.wav",
            "--provider",
            "external-whisperx",
            "--no-align",
            "--format",
            "json",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("\"source\": \"audio/a.wav\""))
        .stdout(predicate::str::contains("\"source\": \"audio/b.wav\""));

    assert!(audio_dir.join("a.json").is_file());
    assert!(audio_dir.join("b.json").is_file());
    let argv = fs::read_to_string(fake.argv_path()).expect("argv");
    assert!(argv.contains("audio/a.wav"));
    assert!(argv.contains("audio/b.wav"));
}

#[cfg(unix)]
#[test]
fn transcribe_expands_absolute_glob_inputs() {
    let fake = FakeWhisperx::new();
    let audio_dir = fake.root().join("absolute-audio");
    fs::create_dir_all(&audio_dir).expect("audio dir");
    let first = audio_dir.join("one.wav");
    let second = audio_dir.join("two.wav");
    fs::write(&first, b"fake audio").expect("one wav");
    fs::write(&second, b"fake audio").expect("two wav");
    let pattern = audio_dir.join("*.wav");

    let mut command = fake.command();
    command
        .current_dir(fake.root())
        .arg("transcribe")
        .arg(pattern)
        .args([
            "--provider",
            "external-whisperx",
            "--no-align",
            "--format",
            "json",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains(first.to_string_lossy().as_ref()))
        .stdout(predicate::str::contains(second.to_string_lossy().as_ref()));
}

#[test]
fn transcribe_fails_unmatched_glob() {
    let temp = tempfile::tempdir().expect("tempdir");
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .current_dir(temp.path())
        .args(["transcribe", "missing-*.wav", "--no-align"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("matched no input files"))
        .stderr(predicate::str::contains("missing-*.wav"));
}

#[test]
fn transcribe_rejects_glob_directory_match() {
    let temp = tempfile::tempdir().expect("tempdir");
    fs::create_dir_all(temp.path().join("audio-dir")).expect("audio dir");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .current_dir(temp.path())
        .args(["transcribe", "audio-*", "--no-align"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("matched non-file input"))
        .stderr(predicate::str::contains("audio-dir"));
}

#[test]
fn transcribe_rejects_basename_after_glob_expands_to_multiple_inputs() {
    let temp = tempfile::tempdir().expect("tempdir");
    fs::write(temp.path().join("one.wav"), b"fake audio").expect("one wav");
    fs::write(temp.path().join("two.wav"), b"fake audio").expect("two wav");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .current_dir(temp.path())
        .args(["transcribe", "*.wav", "--basename", "fixed"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("multiple input files"));
}

#[test]
fn transcribe_rejects_explicit_output_dir_collisions() {
    let temp = tempfile::tempdir().expect("tempdir");
    fs::create_dir_all(temp.path().join("day1")).expect("day1");
    fs::create_dir_all(temp.path().join("day2")).expect("day2");
    fs::write(temp.path().join("day1/audio.wav"), b"fake audio").expect("day1 audio");
    fs::write(temp.path().join("day2/audio.wav"), b"fake audio").expect("day2 audio");

    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .current_dir(temp.path())
        .args([
            "transcribe",
            "day1/audio.wav",
            "day2/audio.wav",
            "--output-dir",
            "out",
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains("output basename collision"))
        .stderr(predicate::str::contains("audio"))
        .stderr(predicate::str::contains("day1/audio.wav"))
        .stderr(predicate::str::contains("day2/audio.wav"));
}

#[cfg(unix)]
#[test]
fn transcribe_allows_same_stem_without_output_dir() {
    let fake = FakeWhisperx::new();
    let first_dir = fake.root().join("day1");
    let second_dir = fake.root().join("day2");
    fs::create_dir_all(&first_dir).expect("day1");
    fs::create_dir_all(&second_dir).expect("day2");
    fs::write(first_dir.join("audio.wav"), b"fake audio").expect("day1 audio");
    fs::write(second_dir.join("audio.wav"), b"fake audio").expect("day2 audio");

    let mut command = fake.command();
    command
        .current_dir(fake.root())
        .args([
            "transcribe",
            "day1/audio.wav",
            "day2/audio.wav",
            "--provider",
            "external-whisperx",
            "--no-align",
            "--format",
            "json",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("\"source\": \"day1/audio.wav\""))
        .stdout(predicate::str::contains("\"source\": \"day2/audio.wav\""));

    assert!(first_dir.join("audio.json").is_file());
    assert!(second_dir.join("audio.json").is_file());
}

#[test]
fn external_translate_help_parses_without_running_audio() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args([
            "input.wav",
            "--task",
            "translate",
            "--provider",
            "external-whisperx",
            "--help",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("--provider"));
}

#[test]
fn verbose_bool_forms_parse_before_help() {
    for args in [
        vec!["input.wav", "--verbose", "--help"],
        vec!["input.wav", "--verbose", "false", "--help"],
    ] {
        let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
        command
            .args(args)
            .assert()
            .success()
            .stdout(predicate::str::contains("--verbose"));
    }
}

#[cfg(unix)]
#[test]
fn external_whisperx_fake_command_forwards_args_and_imports_json() {
    use std::os::unix::fs::PermissionsExt;

    let temp = tempfile::tempdir().expect("tempdir");
    let fake = temp.path().join("whisperx");
    let argv_path = temp.path().join("argv.txt");
    let output_dir = temp.path().join("out");
    fs::write(
        &fake,
        r#"#!/usr/bin/env sh
set -eu
printf '%s\n' "$@" > "$NATIVE_WHISPERX_FAKE_ARGV"
out=""
prev=""
for arg in "$@"; do
  if [ "$prev" = "--output_dir" ]; then
    out="$arg"
  fi
  prev="$arg"
done
mkdir -p "$out"
cat > "$out/fake.json" <<'JSON'
{
  "language": "en",
  "text": "fake transcript text",
  "segments": [
    {
      "id": 0,
      "start": 0.0,
      "end": 1.0,
      "text": "fake transcript text",
      "words": [
        {"word": "fake", "start": 0.0, "end": 0.2}
      ]
    }
  ],
  "word_segments": [
    {"word": "fake", "start": 0.0, "end": 0.2}
  ]
}
JSON
"#,
    )
    .expect("write fake whisperx");
    let mut permissions = fs::metadata(&fake).expect("fake metadata").permissions();
    permissions.set_mode(0o755);
    fs::set_permissions(&fake, permissions).expect("chmod fake whisperx");

    let original_path = std::env::var_os("PATH").unwrap_or_default();
    let test_path = format!(
        "{}:{}",
        temp.path().display(),
        original_path.to_string_lossy()
    );
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .env("PATH", test_path)
        .env("NATIVE_WHISPERX_FAKE_ARGV", &argv_path)
        .args([
            "input.wav",
            "--provider",
            "external-whisperx",
            "--model",
            "small",
            "--language",
            "en",
            "--device",
            "cpu",
            "--batch_size",
            "8",
            "--compute_type",
            "int8",
            "--model_cache_only",
            "--vad_method",
            "silero",
            "--vad_onset",
            "0.5",
            "--vad_offset",
            "0.363",
            "--chunk_size",
            "20",
            "--beam_size",
            "5",
            "--diarize",
            "--hf_token",
            "fake-token",
            "--output_dir",
        ])
        .arg(&output_dir)
        .args(["--format", "json"])
        .assert()
        .success()
        .stdout(predicate::str::contains("fake transcript text"));

    let argv = fs::read_to_string(argv_path).expect("captured argv");
    for expected in [
        "input.wav",
        "--model\nsmall",
        "--language\nen",
        "--device\ncpu",
        "--batch_size\n8",
        "--compute_type\nint8",
        "--model_cache_only\nTrue",
        "--vad_method\nsilero",
        "--vad_onset\n0.5",
        "--vad_offset\n0.363",
        "--chunk_size\n20",
        "--beam_size\n5",
        "--diarize",
        "--diarize_model\npyannote/speaker-diarization-community-1",
        "--hf_token\nfake-token",
        "--output_format\njson",
        "--output_dir",
        output_dir.to_string_lossy().as_ref(),
    ] {
        assert!(argv.contains(expected), "argv should contain `{expected}`");
    }
}

#[test]
fn runtime_version_short_flag_works() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .arg("-P")
        .assert()
        .success()
        .stdout(predicate::str::contains("Rust runtime"));
}

#[test]
fn transcribe_no_align_alias_normalizes_to_disabled_alignment() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args(["input.wav", "--no_align", "--help"])
        .assert()
        .success()
        .stdout(predicate::str::contains("--no-align"))
        .stdout(predicate::str::contains("--whisper-bundle"));
}

#[test]
fn top_level_input_uses_transcribe_shape() {
    let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
    command
        .args(["input.wav", "--help"])
        .assert()
        .success()
        .stdout(predicate::str::contains("native-json"))
        .stdout(predicate::str::contains("--whisper-bundle"));
}

fn command_stdout<const N: usize>(args: [&str; N]) -> String {
    let output = Command::cargo_bin("native-whisperx")
        .expect("binary should build")
        .args(args)
        .output()
        .expect("command should run");
    assert!(
        output.status.success(),
        "command failed: {}",
        String::from_utf8_lossy(&output.stderr)
    );
    String::from_utf8(output.stdout).expect("stdout should be utf8")
}

#[cfg(unix)]
struct FakeWhisperx {
    temp: tempfile::TempDir,
    argv_path: PathBuf,
}

#[cfg(unix)]
impl FakeWhisperx {
    fn new() -> Self {
        use std::os::unix::fs::PermissionsExt;

        let temp = tempfile::tempdir().expect("tempdir");
        let fake = temp.path().join("whisperx");
        let argv_path = temp.path().join("argv.txt");
        fs::write(
            &fake,
            r#"#!/usr/bin/env sh
set -eu
printf '%s\n' "$@" >> "$NATIVE_WHISPERX_FAKE_ARGV"
audio="$1"
out=""
prev=""
for arg in "$@"; do
  if [ "$prev" = "--output_dir" ]; then
    out="$arg"
  fi
  prev="$arg"
done
mkdir -p "$out"
stem="$(basename "$audio")"
stem="${stem%.*}"
rm -f "$out"/*.json
cat > "$out/$stem.json" <<JSON
{
  "language": "en",
  "text": "fake transcript text for $audio",
  "segments": [
    {
      "id": 0,
      "start": 0.0,
      "end": 1.0,
      "text": "fake transcript text for $audio",
      "words": [
        {"word": "fake", "start": 0.0, "end": 0.2}
      ]
    }
  ],
  "word_segments": [
    {"word": "fake", "start": 0.0, "end": 0.2}
  ]
}
JSON
"#,
        )
        .expect("write fake whisperx");
        let mut permissions = fs::metadata(&fake).expect("fake metadata").permissions();
        permissions.set_mode(0o755);
        fs::set_permissions(&fake, permissions).expect("chmod fake whisperx");

        Self { temp, argv_path }
    }

    fn root(&self) -> &Path {
        self.temp.path()
    }

    fn argv_path(&self) -> &Path {
        &self.argv_path
    }

    fn command(&self) -> Command {
        let original_path = std::env::var_os("PATH").unwrap_or_default();
        let test_path = format!(
            "{}:{}",
            self.temp.path().display(),
            original_path.to_string_lossy()
        );
        let mut command = Command::cargo_bin("native-whisperx").expect("binary should build");
        command
            .env("PATH", test_path)
            .env("NATIVE_WHISPERX_FAKE_ARGV", &self.argv_path);
        command
    }
}

fn valid_speaker_library_json() -> &'static str {
    r#"{
      "version": 1,
      "embedding_model": {
        "family": "SpeechBrain",
        "name": "spkrec",
        "version": "1",
        "dimensions": 2
      },
      "profiles": [{
        "id": "speaker-a",
        "label": "Speaker A",
        "embeddings": [{
          "values": [1.0, 0.0],
          "model": {
            "family": "SpeechBrain",
            "name": "spkrec",
            "version": "1",
            "dimensions": 2
          },
          "sample_rate": 16000
        }],
        "metadata": {
          "note": "fixture"
        }
      }]
    }"#
}

fn two_profile_speaker_library_json() -> String {
    valid_speaker_library_json().replace(
        r#"{
        "id": "speaker-a",
        "label": "Speaker A",
        "embeddings": [{
          "values": [1.0, 0.0],
          "model": {
            "family": "SpeechBrain",
            "name": "spkrec",
            "version": "1",
            "dimensions": 2
          },
          "sample_rate": 16000
        }],
        "metadata": {
          "note": "fixture"
        }
      }"#,
        r#"{
        "id": "speaker-a",
        "label": "Speaker A",
        "embeddings": [{
          "values": [1.0, 0.0],
          "model": {
            "family": "SpeechBrain",
            "name": "spkrec",
            "version": "1",
            "dimensions": 2
          },
          "sample_rate": 16000
        }],
        "metadata": {
          "note": "fixture"
        }
      },
      {
        "id": "speaker-b",
        "label": "Speaker B",
        "embeddings": [{
          "values": [0.0, 1.0],
          "model": {
            "family": "SpeechBrain",
            "name": "spkrec",
            "version": "1",
            "dimensions": 2
          },
          "sample_rate": 16000
        }],
        "metadata": {
          "note": "second fixture"
        }
      }"#,
    )
}

fn draft_and_confirmed_speaker_library_json() -> String {
    valid_speaker_library_json().replace(
        r#"{
        "id": "speaker-a",
        "label": "Speaker A",
        "embeddings": [{
          "values": [1.0, 0.0],
          "model": {
            "family": "SpeechBrain",
            "name": "spkrec",
            "version": "1",
            "dimensions": 2
          },
          "sample_rate": 16000
        }],
        "metadata": {
          "note": "fixture"
        }
      }"#,
        r#"{
        "id": "speaker-a",
        "label": "Speaker A",
        "embeddings": [{
          "values": [1.0, 0.0],
          "model": {
            "family": "SpeechBrain",
            "name": "spkrec",
            "version": "1",
            "dimensions": 2
          },
          "sample_rate": 16000
        }],
        "metadata": {
          "note": "fixture"
        }
      },
      {
        "id": "draft-speaker-b",
        "label": "Draft Speaker B",
        "embeddings": [{
          "values": [0.0, 1.0],
          "model": {
            "family": "SpeechBrain",
            "name": "spkrec",
            "version": "1",
            "dimensions": 2
          },
          "sample_rate": 16000
        }],
        "metadata": {
          "status": "draft",
          "detectedLabel": "speaker_1"
        }
      }"#,
    )
}

fn extract_url(line: &str) -> String {
    let start = line.find("http://").expect("line should contain URL");
    line[start..].trim().to_string()
}

fn extract_session_token(html: &str) -> String {
    let marker = r#"const sessionToken = ""#;
    let start = html.find(marker).expect("session token marker") + marker.len();
    let rest = &html[start..];
    let end = rest.find('"').expect("session token end");
    rest[..end].to_string()
}

fn http_request(method: &str, url: &str) -> (u16, String) {
    http_request_with_body(method, url, &[], None)
}

fn http_request_with_body(
    method: &str,
    url: &str,
    headers: &[(&str, &str)],
    body: Option<&str>,
) -> (u16, String) {
    let without_scheme = url.strip_prefix("http://").expect("http URL");
    let (address, path) = match without_scheme.split_once('/') {
        Some((address, path)) => (address, format!("/{path}")),
        None => (without_scheme, "/".to_string()),
    };
    let mut stream = TcpStream::connect(address).expect("connect to server");
    write!(
        stream,
        "{method} {path} HTTP/1.1\r\nHost: {address}\r\nConnection: close\r\n"
    )
    .expect("request");
    for (name, value) in headers {
        write!(stream, "{name}: {value}\r\n").expect("header");
    }
    if let Some(body) = body {
        write!(
            stream,
            "Content-Type: application/json\r\nContent-Length: {}\r\n\r\n{body}",
            body.len()
        )
        .expect("body");
    } else {
        write!(stream, "\r\n").expect("headers end");
    }

    let mut response = String::new();
    stream.read_to_string(&mut response).expect("read response");
    let (head, body) = response.split_once("\r\n\r\n").expect("http response");
    let status = head
        .lines()
        .next()
        .and_then(|line| line.split_whitespace().nth(1))
        .expect("status code")
        .parse::<u16>()
        .expect("numeric status");
    (status, body.to_string())
}

struct ChildGuard(Child);

impl ChildGuard {
    fn stop(&mut self) {
        let _ = self.0.kill();
        let _ = self.0.wait();
    }
}

impl Drop for ChildGuard {
    fn drop(&mut self) {
        self.stop();
    }
}
