//! Integration tests for the `run_tool_test` verb of `mcptest mcp-server`.
//!
//! WOR-1255 / WOR-1256 / WOR-1257: the verb persists the inline suite under
//! `.mcptest/inline/<run_id>.yml` before running, spawns `mcptest run` once
//! for the whole suite, and returns per-test results plus structured
//! failures whose repro lines are executable verbatim from the workspace
//! root and whose clipped values point at the full-output resource.

use std::io::Cursor;

use serde_json::{json, Value};

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

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

/// A write-enabled server rooted at `dir`, with runs and cassettes under
/// `dir/.mcptest` (the same layout the spawned engine persists into).
fn server(dir: &std::path::Path) -> Server {
    Server::new(
        Workspace::from_root(dir),
        Capabilities {
            enable_writes: true,
            binary_override: Some(mcptest_bin()),
        },
    )
}

/// Drive one `run_tool_test` call through the JSON-RPC loop and return the
/// parsed JSON payload.
fn call_run_tool_test(server: &Server, suite: &str) -> Value {
    let req = json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": { "name": "run_tool_test", "arguments": { "suite": suite } }
    });
    let mut buf = Vec::new();
    server
        .serve(Cursor::new(format!("{req}\n")), &mut buf)
        .unwrap();
    let response: Value = serde_json::from_str(String::from_utf8(buf).unwrap().trim()).unwrap();
    let text = response["result"]["content"][0]["text"]
        .as_str()
        .unwrap_or_else(|| panic!("run_tool_test returned no text payload: {response}"));
    serde_json::from_str(text).unwrap()
}

/// Write a mock-server manifest whose `get_status` tool returns `text`,
/// and return the suite header that spawns it. Single-quoted scalars keep
/// Windows backslashes literal.
fn suite_header(dir: &std::path::Path, manifest_name: &str, text: &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();
    format!(
        "servers:\n  status:\n    command: ['{bin}', 'mock', '--tools-from', '{manifest}']\n",
        manifest = manifest.display(),
    )
}

/// One tool test entry asserting the mock's text contains `needle`.
fn test_entry(name: &str, needle: &str) -> String {
    format!(
        "  - name: {name}\n    server: status\n    tool: get_status\n    args: {{}}\n    expect:\n      - target: \"result.content[0].text\"\n        matcher: {{ contains: \"{needle}\" }}\n"
    )
}

/// Split a printed repro line into argv tokens, honoring double quotes,
/// so the test can execute it exactly as printed.
fn shell_split(line: &str) -> Vec<String> {
    let mut out = Vec::new();
    let mut cur = String::new();
    let mut in_quotes = false;
    for c in line.chars() {
        match c {
            '"' => in_quotes = !in_quotes,
            ' ' if !in_quotes => {
                if !cur.is_empty() {
                    out.push(std::mem::take(&mut cur));
                }
            }
            _ => cur.push(c),
        }
    }
    if !cur.is_empty() {
        out.push(cur);
    }
    out
}

#[test]
fn a_twenty_test_inline_suite_returns_twenty_results_from_one_invocation() {
    let dir = tempfile::tempdir().unwrap();
    let srv = server(dir.path());
    let mut suite = suite_header(dir.path(), "server.yml", "status: degraded");
    suite.push_str("tools:\n");
    for i in 0..20 {
        suite.push_str(&test_entry(&format!("case {i:02}"), "degraded"));
    }

    let started = std::time::Instant::now();
    let result = call_run_tool_test(&srv, &suite);
    let elapsed = started.elapsed();

    assert_eq!(result["verdict"], "pass");
    assert_eq!(result["total"], 20);
    let results = result["results"].as_array().unwrap();
    assert_eq!(results.len(), 20, "expected one result per test");
    for r in results {
        assert_eq!(r["verdict"], "pass");
        assert!(r["name"].as_str().unwrap().starts_with("case "));
        assert!(r["duration_ms"].is_u64());
    }
    // One engine invocation: a single run id across the call, and exactly
    // one run artifact persisted for it.
    let run_id = result["run_id"].as_str().unwrap();
    assert!(!run_id.is_empty());
    let run_jsons: Vec<_> = std::fs::read_dir(dir.path().join(".mcptest").join("runs"))
        .unwrap()
        .flatten()
        .filter(|e| e.path().extension().is_some_and(|x| x == "json"))
        .collect();
    assert_eq!(run_jsons.len(), 1, "one spawn must persist one run");
    assert!(run_jsons[0]
        .path()
        .file_name()
        .unwrap()
        .to_string_lossy()
        .contains(run_id));
    // Not an assertion (machines differ), but keep the signal visible in
    // `--nocapture` runs: 20 verdicts from a single spawn.
    eprintln!("20-test single-call wall clock: {elapsed:?}");
}

#[test]
fn inline_failure_repro_lines_are_executable_verbatim_from_workspace_root() {
    let dir = tempfile::tempdir().unwrap();
    let srv = server(dir.path());
    let mut suite = suite_header(dir.path(), "server.yml", "status: degraded");
    suite.push_str("tools:\n");
    suite.push_str(&test_entry("status check", "operational"));

    let result = call_run_tool_test(&srv, &suite);
    assert_eq!(result["verdict"], "fail");
    let failures = result["failures"].as_array().unwrap();
    assert_eq!(failures.len(), 1);
    let repro = failures[0]["repro"].as_str().unwrap();
    let run_id = result["run_id"].as_str().unwrap();
    assert_eq!(
        repro,
        format!("mcptest run .mcptest/inline/{run_id}.yml --filter \"status check\"")
    );
    // The persisted inline suite is the one we sent.
    let inline = dir.path().join(".mcptest").join("inline");
    let persisted = std::fs::read_to_string(inline.join(format!("{run_id}.yml"))).unwrap();
    assert_eq!(persisted, suite);

    // Execute the repro exactly as printed (resolving `mcptest` to the
    // built binary) from the workspace root; the failure must reproduce.
    let argv = shell_split(repro);
    assert_eq!(argv[0], "mcptest");
    assert_cmd::Command::cargo_bin("mcptest")
        .unwrap()
        .args(&argv[1..])
        .current_dir(dir.path())
        .assert()
        .code(1);
}

#[test]
fn clipped_actual_carries_a_resolvable_full_output_uri() {
    let dir = tempfile::tempdir().unwrap();
    let srv = server(dir.path());
    // A response far past the 240-char field budget forces the clip.
    let big = "x".repeat(2000);
    let mut suite = suite_header(dir.path(), "server.yml", &big);
    suite.push_str("tools:\n");
    suite.push_str(&test_entry("status check", "operational"));

    let result = call_run_tool_test(&srv, &suite);
    assert_eq!(result["verdict"], "fail");
    let failure = &result["failures"][0];
    assert!(
        failure["assert"].as_str().unwrap().contains("...(+"),
        "expected a clipped preview, got: {failure}"
    );
    let run_id = result["run_id"].as_str().unwrap();
    let uri = failure["full"].as_str().unwrap();
    assert_eq!(
        uri,
        format!("mcptest://runs/{run_id}/tests/status_check/output")
    );

    // The URI resolves through the resources surface to the full value.
    let ws = Workspace::from_root(dir.path());
    let resolved = resources::read(uri, &ws).unwrap();
    let text = resolved["contents"][0]["text"].as_str().unwrap();
    assert!(text.contains(&big), "full output must be untruncated");
}

#[test]
fn two_concurrent_inline_runs_persist_distinct_suites_and_repros() {
    let dir = tempfile::tempdir().unwrap();
    let root = dir.path().to_path_buf();
    // Two distinct failing suites against the same workspace root.
    let suite_a = {
        let mut s = suite_header(&root, "server-a.yml", "marker-alpha");
        s.push_str("tools:\n");
        s.push_str(&test_entry("alpha case", "missing-from-alpha"));
        s
    };
    let suite_b = {
        let mut s = suite_header(&root, "server-b.yml", "marker-beta");
        s.push_str("tools:\n");
        s.push_str(&test_entry("beta case", "missing-from-beta"));
        s
    };

    let spawn = |suite: String, root: std::path::PathBuf| {
        std::thread::spawn(move || {
            let srv = server(&root);
            call_run_tool_test(&srv, &suite)
        })
    };
    let a = spawn(suite_a.clone(), root.clone());
    let b = spawn(suite_b.clone(), root.clone());
    let result_a = a.join().unwrap();
    let result_b = b.join().unwrap();

    let id_a = result_a["run_id"].as_str().unwrap();
    let id_b = result_b["run_id"].as_str().unwrap();
    assert_ne!(id_a, id_b, "concurrent runs must not share a run id");

    // Each run persisted its own inline suite, unclobbered.
    let inline = root.join(".mcptest").join("inline");
    let persisted_a = std::fs::read_to_string(inline.join(format!("{id_a}.yml"))).unwrap();
    let persisted_b = std::fs::read_to_string(inline.join(format!("{id_b}.yml"))).unwrap();
    assert_eq!(persisted_a, suite_a);
    assert_eq!(persisted_b, suite_b);

    // And each repro references its own persisted suite.
    let repro_a = result_a["failures"][0]["repro"].as_str().unwrap();
    let repro_b = result_b["failures"][0]["repro"].as_str().unwrap();
    assert!(repro_a.contains(&format!(".mcptest/inline/{id_a}.yml")));
    assert!(repro_b.contains(&format!(".mcptest/inline/{id_b}.yml")));

    // Both runs' artifacts exist side by side.
    let runs = root.join(".mcptest").join("runs");
    assert!(runs.join(format!("{id_a}.json")).exists());
    assert!(runs.join(format!("{id_b}.json")).exists());
}
