//! WOR-1416: `mcptest fault-coverage` reports runtime fault family coverage.
//!
//! The command loads the embedded fault taxonomy, detects the probes a suite
//! exercises (plus any `--probe` tokens), and marks each family covered,
//! partial, uncovered, or not applicable. These tests write tiny suites to a
//! tempdir and assert the rendered verdicts and the threshold gate.

use assert_cmd::Command;
use predicates::prelude::*;

fn mcptest() -> Command {
    Command::cargo_bin("mcptest").expect("binary builds")
}

/// A server-only suite with a `compliance:` block, so PROTO is exercised and
/// PROVIDER (agent-only) is not applicable.
fn write_compliance_suite(dir: &std::path::Path) -> std::path::PathBuf {
    let cfg = dir.join("mcptest.yaml");
    std::fs::write(
        &cfg,
        r#"servers:
  api:
    command: ["mcptest", "mock", "--tools-from", "./m.yml"]
compliance:
  spec_version: "v2025-06-18"
  tests:
    - name: "CONF-001 initialize advertises capabilities"
      server: api
      check: "initialize"
"#,
    )
    .unwrap();
    cfg
}

#[test]
fn reports_family_coverage_for_a_compliance_suite() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_compliance_suite(dir.path());

    mcptest()
        .args(["fault-coverage", cfg.to_str().unwrap()])
        .assert()
        .success()
        .stdout(
            predicate::str::contains("Runtime fault family coverage")
                // compliance block exercises the PROTO uninitialized leaf.
                .and(predicate::str::contains("Protocol interaction"))
                // a server-only suite cannot exercise provider-integration.
                .and(predicate::str::contains("Model-provider integration"))
                .and(predicate::str::contains("n/a")),
        );
}

#[test]
fn json_format_emits_the_report_object() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_compliance_suite(dir.path());

    let out = mcptest()
        .args(["fault-coverage", cfg.to_str().unwrap(), "--format", "json"])
        .assert()
        .success()
        .get_output()
        .stdout
        .clone();
    let value: serde_json::Value = serde_json::from_slice(&out).expect("stdout is JSON");
    assert!(value.get("families").and_then(|f| f.as_array()).is_some());
    assert!(value.get("coverage_pct").and_then(|p| p.as_f64()).is_some());
}

#[test]
fn extra_probe_tokens_raise_coverage() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_compliance_suite(dir.path());

    // With probes for negative-path and fuzz folded in, the PROTO family flips
    // from partial to covered (all three of its leaves exercised).
    mcptest()
        .args([
            "fault-coverage",
            cfg.to_str().unwrap(),
            "--probe",
            "negative-path",
            "--probe",
            "fuzz",
        ])
        .assert()
        .success()
        .stdout(
            predicate::str::contains("covered   Protocol interaction")
                .or(predicate::str::contains("covered Protocol interaction")),
        );
}

#[test]
fn threshold_gate_fails_when_coverage_is_too_low() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_compliance_suite(dir.path());

    // A bare compliance suite covers only a fraction of the taxonomy, so a 90%
    // floor must fail with the coverage-floor exit code.
    mcptest()
        .args(["fault-coverage", cfg.to_str().unwrap(), "--threshold", "90"])
        .assert()
        .code(6)
        .stderr(predicate::str::contains("below the 90% threshold"));
}

#[test]
fn unknown_probe_token_is_rejected() {
    mcptest()
        .args(["fault-coverage", "--no-suite", "--probe", "made-up"])
        .assert()
        .code(2)
        .stderr(predicate::str::contains("unknown probe token `made-up`"));
}
