//! Integration tests for `mcptest run --output` and `--reporter`.
//!
//! `--reporter` picks the format, `--output` picks the sink (a file, or
//! stdout when omitted or `-`), mirroring `mcptest report`. Before this wiring
//! both flags parsed but did nothing on the run path. These tests stand up a
//! wiremock streamable-HTTP MCP endpoint, drive a real run against it, and
//! assert each format reaches both a file and stdout, and that a json file is
//! round-trippable through `mcptest report`.

use assert_cmd::Command;
use predicates::prelude::*;
use serde_json::{json, Value};
use tempfile::tempdir;
use wiremock::matchers::method;
use wiremock::{Mock, MockServer, Request, ResponseTemplate};

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

fn reply(body: Value) -> impl Fn(&Request) -> ResponseTemplate {
    move |req: &Request| {
        let parsed: Value = serde_json::from_slice(&req.body).unwrap_or(Value::Null);
        let id = parsed.get("id").cloned().unwrap_or(Value::Null);
        let envelope = json!({ "jsonrpc": "2.0", "id": id, "result": body.clone() });
        ResponseTemplate::new(200)
            .set_body_string(serde_json::to_string(&envelope).unwrap())
            .insert_header("content-type", "application/json")
    }
}

/// Mount `initialize` and `tools/list` so a `tool_quality:` check can run
/// without a `tools/call`. The catalog is well documented so the run passes.
async fn mount(server: &MockServer) {
    Mock::given(method("POST"))
        .and(wiremock::matchers::body_partial_json(
            json!({ "method": "initialize" }),
        ))
        .respond_with(reply(json!({
            "protocolVersion": "2025-06-18",
            "serverInfo": { "name": "fake", "version": "0.1" },
            "capabilities": { "tools": {} }
        })))
        .mount(server)
        .await;
    Mock::given(method("POST"))
        .and(wiremock::matchers::body_partial_json(
            json!({ "method": "tools/list" }),
        ))
        .respond_with(reply(json!({ "tools": [
            {
                "name": "search",
                "description": "Search the corpus and return the top matching documents with scores.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "query": { "type": "string", "description": "The search query string to match against documents." }
                    }
                },
                "outputSchema": { "type": "object" },
                "annotations": {}
            }
        ] })))
        .mount(server)
        .await;
}

fn write_suite(dir: &std::path::Path, url: &str) -> std::path::PathBuf {
    let cfg = dir.join("suite.yml");
    let body = format!(
        "servers:\n  remote:\n    url: \"{url}\"\ntool_quality:\n  - name: catalog quality\n    server: remote\n"
    );
    std::fs::write(&cfg, body).unwrap();
    cfg
}

#[tokio::test(flavor = "multi_thread")]
async fn run_reporter_json_output_file_is_readable_by_mcptest_report() {
    let server = MockServer::start().await;
    mount(&server).await;
    let dir = tempdir().unwrap();
    let cfg = write_suite(dir.path(), &server.uri());
    let out = dir.path().join("run.json");

    // `--reporter json --output PATH`: the json format goes to the file (the
    // canonical, round-trippable artifact). `--reporter` picks format,
    // `--output` picks the sink.
    mcptest()
        .args([
            "run",
            "--config",
            cfg.to_str().unwrap(),
            "--reporter",
            "json",
            "--output",
            out.to_str().unwrap(),
        ])
        .assert()
        .success();

    let body = std::fs::read_to_string(&out).expect("--output wrote a file");
    let report: Value = serde_json::from_str(&body).expect("output is valid JSON");
    assert_eq!(report["summary"]["total"], 1);
    assert_eq!(report["summary"]["passed"], 1);
    assert_eq!(report["summary"]["failed"], 0);

    // And `mcptest report` re-renders it without complaint.
    mcptest()
        .args(["report", out.to_str().unwrap(), "--format", "junit"])
        .assert()
        .success()
        .stdout(predicate::str::contains("<testsuites"));
}

#[tokio::test(flavor = "multi_thread")]
async fn run_reporter_sarif_writes_sarif_to_a_file() {
    let server = MockServer::start().await;
    mount(&server).await;
    let dir = tempdir().unwrap();
    let cfg = write_suite(dir.path(), &server.uri());
    let out = dir.path().join("run.sarif");

    mcptest()
        .args([
            "run",
            "--config",
            cfg.to_str().unwrap(),
            "--reporter",
            "sarif",
            "--output",
            out.to_str().unwrap(),
        ])
        .assert()
        .success();

    let body = std::fs::read_to_string(&out).expect("--output wrote a sarif file");
    let sarif: Value = serde_json::from_str(&body).expect("sarif file is valid JSON");
    assert_eq!(sarif["version"], "2.1.0");
}

#[tokio::test(flavor = "multi_thread")]
async fn run_reporter_tap_renders_tap_to_stdout() {
    let server = MockServer::start().await;
    mount(&server).await;
    let dir = tempdir().unwrap();
    let cfg = write_suite(dir.path(), &server.uri());

    mcptest()
        .args([
            "--reporter",
            "tap",
            "run",
            "--config",
            cfg.to_str().unwrap(),
        ])
        .assert()
        .success()
        .stdout(predicate::str::starts_with("TAP version 14\n1..1\n"))
        .stdout(predicate::str::contains("ok 1 - "));
}

#[tokio::test(flavor = "multi_thread")]
async fn run_reporter_json_renders_the_report_to_stdout() {
    let server = MockServer::start().await;
    mount(&server).await;
    let dir = tempdir().unwrap();
    let cfg = write_suite(dir.path(), &server.uri());

    let assert = mcptest()
        .args([
            "--reporter",
            "json",
            "run",
            "--config",
            cfg.to_str().unwrap(),
        ])
        .assert()
        .success();
    let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
    let report: Value = serde_json::from_str(&stdout).expect("stdout is a JSON report");
    assert_eq!(report["summary"]["passed"], 1);
}

#[tokio::test(flavor = "multi_thread")]
async fn run_output_dash_writes_report_json_to_stdout() {
    let server = MockServer::start().await;
    mount(&server).await;
    let dir = tempdir().unwrap();
    let cfg = write_suite(dir.path(), &server.uri());

    let assert = mcptest()
        .args([
            "run",
            "--config",
            cfg.to_str().unwrap(),
            "--reporter",
            "json",
            "--output",
            "-",
        ])
        .assert()
        .success();
    let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
    let report: Value = serde_json::from_str(&stdout).expect("--output - emits JSON to stdout");
    assert_eq!(report["summary"]["total"], 1);
}

#[tokio::test(flavor = "multi_thread")]
async fn run_default_pretty_lists_each_test_on_stdout() {
    let server = MockServer::start().await;
    mount(&server).await;
    let dir = tempdir().unwrap();
    let cfg = write_suite(dir.path(), &server.uri());

    // No --reporter and no --output: the default pretty reporter now lists each
    // test and a Summary line on stdout, not just a one-line count.
    mcptest()
        .args(["run", "--config", cfg.to_str().unwrap()])
        .assert()
        .success()
        .stdout(predicate::str::starts_with("mcptest"))
        .stdout(predicate::str::contains("catalog quality"))
        .stdout(predicate::str::contains("Summary:"))
        .stdout(predicate::str::contains("1 passed"));
}

#[tokio::test(flavor = "multi_thread")]
async fn run_reporter_minimal_prints_one_line_summary() {
    let server = MockServer::start().await;
    mount(&server).await;
    let dir = tempdir().unwrap();
    let cfg = write_suite(dir.path(), &server.uri());

    // --reporter minimal restores the terse one-line summary (the legacy
    // default) on stdout instead of the per-test pretty listing.
    mcptest()
        .args([
            "--reporter",
            "minimal",
            "run",
            "--config",
            cfg.to_str().unwrap(),
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("ran 1 tests:"))
        .stdout(predicate::str::contains("1 passed"));
}
