//! Integration tests for `mcptest compliance run --official` (WOR-1307).
//!
//! The official `@modelcontextprotocol/conformance` suite is a Node.js
//! subprocess. These tests stub it with a shell script that emits the
//! JSON shape the bridge parses (mirroring the fakes in
//! `mcptest-core/src/compliance/official.rs`), so no Node install or
//! network is needed. Unix-only because the stub is a shell script.

#![cfg(unix)]

use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};

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

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

/// Minimal rule registry: one MUST the official suite maps onto
/// (PROTO-001 <- initialize-handshake) and one MAY the native results
/// cover, so both sources land in an applicable section.
fn write_registry(dir: &Path) -> PathBuf {
    let path = dir.join("registry.yml");
    let yaml = r#"
version: 1
rules:
  - id: PROTO-001
    name: Initialize handshake
    category: PROTO
    severity: MUST
    applies_to: [stdio]
    spec_versions: [v2024-11-05]
    description: server answers initialize
    references: []
    section: lifecycle
    applicability: "true"
    spec_citation: spec
  - id: PROTO-010
    name: Optional client info
    category: PROTO
    severity: MAY
    applies_to: [stdio]
    spec_versions: [v2024-11-05]
    description: server echoes client info
    references: []
    section: lifecycle
    applicability: "true"
    spec_citation: spec
"#;
    fs::write(&path, yaml).expect("write registry");
    path
}

/// Pre-computed native results so the run needs no live server.
fn write_native_results(dir: &Path) -> PathBuf {
    let path = dir.join("results.json");
    let body = r#"[
      {"rule_id":"PROTO-010","passed":true,"source":"local","message":null,"duration_ms":1}
    ]"#;
    fs::write(&path, body).expect("write results");
    path
}

/// Stub conformance binary: answers `--version`, otherwise emits the
/// upstream JSON report. `initialize-handshake` exercises the known
/// scenario map (-> PROTO-001); `custom-extra` exercises the synthesized
/// OFFICIAL-* fallback id.
fn write_stub_conformance(dir: &Path) -> PathBuf {
    let path = dir.join("conformance-stub.sh");
    let script = r#"#!/bin/sh
if [ "$1" = "--version" ]; then
  echo "stub 1.0.0"
  exit 0
fi
printf '%s' '{"scenarios":[{"scenario_id":"initialize-handshake","passed":true,"duration_ms":12},{"scenario_id":"custom-extra","passed":true,"duration_ms":3}],"tool_version":"9.9.9"}'
"#;
    fs::write(&path, script).expect("write stub");
    let mut perms = fs::metadata(&path).expect("stat stub").permissions();
    perms.set_mode(0o755);
    fs::set_permissions(&path, perms).expect("chmod stub");
    path
}

#[test]
fn official_flag_folds_mapped_rule_ids_into_the_report() {
    let dir = tempfile::tempdir().expect("tempdir");
    let registry = write_registry(dir.path());
    let results = write_native_results(dir.path());
    let stub = write_stub_conformance(dir.path());

    mcptest()
        .args([
            "compliance",
            "run",
            "--results-from",
            results.to_str().expect("utf8"),
            "--registry",
            registry.to_str().expect("utf8"),
            "--format",
            "json",
            "--official",
            "--official-cmd",
            stub.to_str().expect("utf8"),
            "--server-url",
            "http://localhost:59999",
        ])
        .assert()
        .success()
        // The known upstream scenario maps to our stable rule id.
        .stdout(predicate::str::contains("PROTO-001"))
        // Folded results carry their provenance for reporters.
        .stdout(predicate::str::contains("\"source\": \"official\""))
        // Unknown upstream scenarios still appear via the fallback id.
        .stdout(predicate::str::contains("OFFICIAL-CUSTOM-EXTRA"))
        .stderr(predicate::str::contains(
            "folded 2 official conformance scenario(s)",
        ));
}

#[test]
fn official_with_missing_binary_reports_node_install_hint() {
    let dir = tempfile::tempdir().expect("tempdir");
    let registry = write_registry(dir.path());
    let results = write_native_results(dir.path());

    mcptest()
        .args([
            "compliance",
            "run",
            "--results-from",
            results.to_str().expect("utf8"),
            "--registry",
            registry.to_str().expect("utf8"),
            "--official",
            "--official-cmd",
            "/definitely/not/installed/conformance-xyz123",
            "--server-url",
            "http://localhost:59999",
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains("was not found on PATH"))
        .stderr(predicate::str::contains("Install Node.js"));
}

#[test]
fn official_without_server_url_is_rejected_with_guidance() {
    let dir = tempfile::tempdir().expect("tempdir");
    let registry = write_registry(dir.path());
    let results = write_native_results(dir.path());
    let stub = write_stub_conformance(dir.path());

    mcptest()
        .args([
            "compliance",
            "run",
            "--results-from",
            results.to_str().expect("utf8"),
            "--registry",
            registry.to_str().expect("utf8"),
            "--official",
            "--official-cmd",
            stub.to_str().expect("utf8"),
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains("--server-url"));
}

#[test]
fn official_cmd_without_the_official_flag_is_rejected_by_clap() {
    mcptest()
        .args([
            "compliance",
            "run",
            "--results-from",
            "ignored.json",
            "--official-cmd",
            "/bin/true",
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains("--official"));
}

#[test]
fn run_without_official_flag_emits_no_official_results() {
    let dir = tempfile::tempdir().expect("tempdir");
    let registry = write_registry(dir.path());
    // Cover every registry rule locally so the run exits 0 without any
    // help from the official suite.
    let results = dir.path().join("results.json");
    fs::write(
        &results,
        r#"[
          {"rule_id":"PROTO-001","passed":true,"source":"local","message":null,"duration_ms":1},
          {"rule_id":"PROTO-010","passed":true,"source":"local","message":null,"duration_ms":1}
        ]"#,
    )
    .expect("write results");

    mcptest()
        .args([
            "compliance",
            "run",
            "--results-from",
            results.to_str().expect("utf8"),
            "--registry",
            registry.to_str().expect("utf8"),
            "--format",
            "json",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("\"source\": \"official\"").not())
        .stderr(predicate::str::contains("folded").not());
}
