//! Integration tests for per-run artifact persistence (WOR-1255) and the
//! file-backed repro lines of the agent reporter (WOR-1256).
//!
//! Every `mcptest run` persists the redacted report and the full per-test
//! failure outputs under `.mcptest/runs/<run_id>`, keyed by the real run
//! id, so a truncated `actual` in the agent reporter can point at a
//! resolvable `mcptest://runs/...` resource instead of forcing a re-run.

use mcptest_mcp_server::{resources, workspace::Workspace};

/// Absolute path to the built `mcptest` binary under test.
fn mcptest_bin() -> String {
    assert_cmd::cargo::cargo_bin("mcptest")
        .to_string_lossy()
        .into_owned()
}

/// Write a mock manifest plus a one-test suite expecting `needle` in the
/// mock's `text` response. Returns the suite file name (relative to `dir`,
/// so repro lines stay executable from there).
fn write_failing_suite(
    dir: &std::path::Path,
    suite_name: &str,
    manifest_name: &str,
    text: &str,
    needle: &str,
) -> String {
    let bin = mcptest_bin();
    let manifest = dir.join(manifest_name);
    std::fs::write(
        &manifest,
        format!(
            "mock_server:\n  name: status-service\n  tools:\n    - name: get_status\n      description: Return status. Read-only.\n      input_schema: {{ type: object, additionalProperties: false, properties: {{}} }}\n      response:\n        content:\n          - type: text\n            text: '{text}'\n"
        ),
    )
    .unwrap();
    let suite = format!(
        "servers:\n  status:\n    command: ['{bin}', 'mock', '--tools-from', '{manifest}']\ntools:\n  - name: status check\n    server: status\n    tool: get_status\n    args: {{}}\n    expect:\n      - target: \"result.content[0].text\"\n        matcher: {{ contains: \"{needle}\" }}\n",
        manifest = manifest.display(),
    );
    std::fs::write(dir.join(suite_name), suite).unwrap();
    suite_name.to_string()
}

#[test]
fn large_failure_shows_truncated_preview_plus_uri_and_resource_returns_full_value() {
    let dir = tempfile::tempdir().unwrap();
    let big = "z".repeat(2000);
    let suite = write_failing_suite(dir.path(), "suite.yml", "server.yml", &big, "operational");

    let assert = assert_cmd::Command::cargo_bin("mcptest")
        .unwrap()
        .args([
            "run",
            &suite,
            "--run-id",
            "esc-hatch-1",
            "--reporter",
            "agent",
        ])
        .current_dir(dir.path())
        .assert()
        .code(1);
    let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();

    // The preview is clipped and the stable single full: line points at
    // the persisted output for the real run id.
    assert!(
        stdout.contains("...(+"),
        "expected a clip marker in:\n{stdout}"
    );
    assert!(
        stdout.contains("  full: mcptest://runs/esc-hatch-1/tests/status_check/output"),
        "expected the full-output URI in:\n{stdout}"
    );
    // The whole 2000-char blob must not be on the wire.
    assert!(!stdout.contains(&big));

    // The resource resolves to the full, untruncated value.
    let ws = Workspace::from_root(dir.path());
    let resolved =
        resources::read("mcptest://runs/esc-hatch-1/tests/status_check/output", &ws).unwrap();
    let text = resolved["contents"][0]["text"].as_str().unwrap();
    assert!(text.contains(&big));
}

#[test]
fn file_backed_repro_line_is_executable_verbatim_from_the_run_directory() {
    let dir = tempfile::tempdir().unwrap();
    let suite = write_failing_suite(
        dir.path(),
        "suite.yml",
        "server.yml",
        "status: degraded",
        "operational",
    );

    let assert = assert_cmd::Command::cargo_bin("mcptest")
        .unwrap()
        .args(["run", &suite, "--reporter", "agent"])
        .current_dir(dir.path())
        .assert()
        .code(1);
    let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
    let repro = stdout
        .lines()
        .find_map(|l| l.trim_start().strip_prefix("repro: "))
        .expect("a repro line in the agent output");
    assert_eq!(repro, "mcptest run suite.yml --filter \"status check\"");

    // Execute it exactly as printed (resolving `mcptest` to the built
    // binary); the single failing case must reproduce with exit 1.
    assert_cmd::Command::cargo_bin("mcptest")
        .unwrap()
        .args(["run", "suite.yml", "--filter", "status check"])
        .current_dir(dir.path())
        .assert()
        .code(1);
}

#[test]
fn two_concurrent_runs_persist_independent_artifacts() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path().to_path_buf();
    let suite_a = write_failing_suite(
        &root,
        "suite-a.yml",
        "server-a.yml",
        "marker-from-run-a",
        "missing-a",
    );
    let suite_b = write_failing_suite(
        &root,
        "suite-b.yml",
        "server-b.yml",
        "marker-from-run-b",
        "missing-b",
    );

    let run = |suite: String, run_id: &'static str, root: std::path::PathBuf| {
        std::thread::spawn(move || {
            assert_cmd::Command::cargo_bin("mcptest")
                .unwrap()
                .args(["run", &suite, "--run-id", run_id, "--reporter", "quiet"])
                .current_dir(&root)
                .assert()
                .code(1);
        })
    };
    let a = run(suite_a, "concurrent-run-a", root.clone());
    let b = run(suite_b, "concurrent-run-b", root.clone());
    a.join().unwrap();
    b.join().unwrap();

    // Both runs persisted, keyed by their own ids, with their own outputs.
    let store = mcptest_core::report::run_store::RunStore::for_workspace(&root);
    let out_a = store
        .read_test_output("concurrent-run-a", "status check")
        .expect("run a output persisted");
    let out_b = store
        .read_test_output("concurrent-run-b", "status check")
        .expect("run b output persisted");
    assert!(out_a.contains("marker-from-run-a"));
    assert!(!out_a.contains("marker-from-run-b"));
    assert!(out_b.contains("marker-from-run-b"));
    assert!(!out_b.contains("marker-from-run-a"));

    let runs = root.join(".mcptest").join("runs");
    assert!(runs.join("concurrent-run-a.json").exists());
    assert!(runs.join("concurrent-run-b.json").exists());
}

#[test]
fn run_json_artifact_carries_the_pinned_run_id() {
    let dir = tempfile::tempdir().unwrap();
    let suite = write_failing_suite(
        dir.path(),
        "suite.yml",
        "server.yml",
        "status: degraded",
        "degraded",
    );
    assert_cmd::Command::cargo_bin("mcptest")
        .unwrap()
        .args([
            "run",
            &suite,
            "--run-id",
            "pinned-id-1",
            "--reporter",
            "quiet",
        ])
        .current_dir(dir.path())
        .assert()
        .success();
    let body = std::fs::read_to_string(
        dir.path()
            .join(".mcptest")
            .join("runs")
            .join("pinned-id-1.json"),
    )
    .expect("run artifact persisted under the pinned id");
    let report: serde_json::Value = serde_json::from_str(&body).unwrap();
    assert_eq!(report["run_id"], "pinned-id-1");
    assert_eq!(report["summary"]["passed"], 1);
}

#[test]
fn persisted_artifacts_match_the_redacted_reporter_view_exactly() {
    let dir = tempfile::tempdir().unwrap();
    let suite = write_failing_suite(
        dir.path(),
        "suite.yml",
        "server.yml",
        "status: degraded",
        "operational",
    );
    // Capture the canonical wire artifact (the JSON reporter renders the
    // redacted view) and compare it to the persisted run JSON: the same
    // redaction must apply on write, no more and no less. The
    // token-shaped-value redaction itself is pinned by the run_store unit
    // tests; this guards the integration point.
    let assert = assert_cmd::Command::cargo_bin("mcptest")
        .unwrap()
        .args([
            "run",
            &suite,
            "--run-id",
            "parity-1",
            "--reporter",
            "json",
            "--output",
            "-",
        ])
        .current_dir(dir.path())
        .assert()
        .code(1);
    let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
    let wire: serde_json::Value = serde_json::from_str(&stdout).unwrap();
    let disk: serde_json::Value = serde_json::from_str(
        &std::fs::read_to_string(
            dir.path()
                .join(".mcptest")
                .join("runs")
                .join("parity-1.json"),
        )
        .unwrap(),
    )
    .unwrap();
    assert_eq!(wire, disk, "wire and disk views of the run drifted");
}
