//! Integration tests for `mcptest run --coverage` (WOR-1386).
//!
//! Per-run coverage records every dispatched tool call and scores it against
//! the connected server's `tools/list` catalog. These tests stand up a
//! deterministic `mcptest mock` stdio server with two tools, exercise one, and
//! assert the run reports 50% tool coverage and that `--coverage-threshold`
//! trips exit code 6 on a passing run.

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

/// Absolute path to the built `mcptest` binary, so the suite's mock-server
/// command does not depend on `mcptest` being on `PATH`.
fn mcptest_bin() -> String {
    assert_cmd::cargo::cargo_bin("mcptest")
        .to_string_lossy()
        .into_owned()
}

/// Write a two-tool mock manifest plus a suite that calls only the first tool,
/// and return the suite path. The server command uses the absolute binary path;
/// single-quoted scalars keep Windows backslashes literal.
fn write_suite(dir: &std::path::Path) -> std::path::PathBuf {
    let bin = mcptest_bin();
    let manifest = dir.join("server.yml");
    std::fs::write(
        &manifest,
        "mock_server:\n  name: catalog\n  tools:\n    - name: search\n      description: Search the catalog. Read-only.\n      input_schema: { type: object, additionalProperties: false, properties: {} }\n      response:\n        content:\n          - type: text\n            text: 'hit'\n    - name: fetch\n      description: Fetch a record. Read-only.\n      input_schema: { type: object, additionalProperties: false, properties: {} }\n      response:\n        content:\n          - type: text\n            text: 'doc'\n",
    )
    .unwrap();
    let suite = dir.join("suite.yml");
    std::fs::write(
        &suite,
        format!(
            "servers:\n  catalog:\n    command: ['{bin}', 'mock', '--tools-from', '{manifest}']\ntools:\n  - name: search returns a hit\n    server: catalog\n    tool: search\n    expect:\n      - target: result.content[0].text\n        matcher: {{ contains: hit }}\n",
            manifest = manifest.display(),
        ),
    )
    .unwrap();
    suite
}

#[test]
fn run_coverage_reports_partial_tool_coverage() {
    let dir = tempdir().unwrap();
    let suite = write_suite(dir.path());
    // One of the two declared tools is exercised, so tool coverage is 50%.
    mcptest()
        .arg("run")
        .arg("--config")
        .arg(&suite)
        .arg("--coverage")
        .assert()
        .success()
        .stdout(predicate::str::contains("tool coverage").and(predicate::str::contains("50%")));
}

#[test]
fn run_without_coverage_prints_no_coverage_line() {
    let dir = tempdir().unwrap();
    let suite = write_suite(dir.path());
    mcptest()
        .arg("run")
        .arg("--config")
        .arg(&suite)
        .assert()
        .success()
        .stdout(predicate::str::contains("coverage").not());
}

#[test]
fn run_coverage_threshold_trips_exit_six_on_a_passing_run() {
    let dir = tempdir().unwrap();
    let suite = write_suite(dir.path());
    // The run passes, but 50% tool coverage is below the 80% floor, so the
    // quality gate trips exit 6 (matching `mcptest coverage --threshold`).
    mcptest()
        .arg("run")
        .arg("--config")
        .arg(&suite)
        .arg("--coverage")
        .arg("--coverage-threshold")
        .arg("tools=80")
        .assert()
        .code(6)
        .stderr(predicate::str::contains("coverage threshold not met"));
}

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