//! Integration tests for the `get_coverage` verb's `suggest` option
//! (WOR-1261).
//!
//! The verb reads the latest run from the workspace run store. With
//! `{"suggest": true}` it adds a `suggestions` block to its response:
//! the same draft tests `mcptest coverage --suggest` would emit, built by
//! the shared library builder from the run's coverage block, its catalog,
//! and the test names already present in that run.

use std::collections::{BTreeMap, BTreeSet};
use std::io::Cursor;

use serde_json::{json, Value};

use mcptest_core::coverage::{
    build_suggestions, CoverageReport, CoverageTracker, Observation, SuggestionContext,
};
use mcptest_mcp_server::{workspace::Workspace, Capabilities, Server};

/// A read-only server rooted at `dir` (get_coverage needs no write gate).
fn server(dir: &std::path::Path) -> Server {
    Server::new(Workspace::from_root(dir), Capabilities::default())
}

/// Drive one `get_coverage` call through the JSON-RPC loop and return the
/// parsed text payload.
fn call_get_coverage(server: &Server, arguments: Value) -> Value {
    let req = json!({
        "jsonrpc": "2.0",
        "id": 1,
        "method": "tools/call",
        "params": { "name": "get_coverage", "arguments": arguments }
    });
    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!("get_coverage returned no text payload: {response}"));
    serde_json::from_str(text).unwrap()
}

/// The catalog the fixture run was measured against: `alpha` covered,
/// `beta` (one required arg) not.
fn fixture_catalog() -> Vec<Value> {
    vec![
        json!({ "name": "alpha", "inputSchema": { "type": "object", "properties": {} } }),
        json!({
            "name": "beta",
            "inputSchema": {
                "type": "object",
                "properties": { "msg": { "type": "string" } },
                "required": ["msg"]
            }
        }),
    ]
}

/// A coverage report for a run that called `alpha` once and `beta` never.
fn fixture_report() -> CoverageReport {
    let mut tracker = CoverageTracker::new();
    tracker.record(&Observation {
        tool: "alpha".into(),
        args: json!({}),
        error_path: None,
        transport: "stdio".into(),
    });
    tracker.into_report(
        &["alpha".to_owned(), "beta".to_owned()],
        &[],
        &["stdio".to_owned()],
        Some(1),
    )
}

/// Persist one run JSON under `<dir>/.mcptest/runs` so the verb sees it
/// as the latest run.
fn write_run(dir: &std::path::Path, run: &Value) {
    let runs = dir.join(".mcptest").join("runs");
    std::fs::create_dir_all(&runs).unwrap();
    std::fs::write(
        runs.join("r1.json"),
        serde_json::to_string_pretty(run).unwrap(),
    )
    .unwrap();
}

#[test]
fn suggest_true_returns_the_same_drafts_the_library_builds_for_the_latest_run() {
    let dir = tempfile::tempdir().unwrap();
    let report = fixture_report();
    let run = json!({
        "run_id": "r1",
        "summary": { "total": 1, "passed": 1, "failed": 0 },
        "tests": [ { "name": "alpha responds", "status": "pass" } ],
        "coverage": serde_json::to_value(&report).unwrap(),
        "catalog": fixture_catalog(),
    });
    write_run(dir.path(), &run);

    let payload = call_get_coverage(&server(dir.path()), json!({ "suggest": true }));

    // The coverage block still rides along, unchanged.
    assert_eq!(payload["coverage"]["tool_coverage_pct"], 50.0);

    // The suggestions match the library builder byte for byte: one
    // happy-path draft for the uncovered `beta`, with no name collision
    // against the run's test names.
    let existing: BTreeSet<String> = ["alpha responds".to_string()].into_iter().collect();
    let hints = BTreeMap::new();
    let expected = build_suggestions(
        &report,
        &fixture_catalog(),
        &SuggestionContext {
            server_name: "server",
            existing_test_names: &existing,
            error_path_tools: &hints,
        },
    )
    .to_json();
    assert_eq!(payload["suggestions"], expected);

    let items = payload["suggestions"]["items"].as_array().unwrap();
    assert_eq!(items.len(), 1);
    assert_eq!(items[0]["kind"], "tool");
    assert_eq!(items[0]["tool"], "beta");
    let yaml = payload["suggestions"]["yaml"].as_str().unwrap();
    assert!(yaml.contains("# suggested by mcptest coverage"));
    assert!(yaml.contains("msg: \"sample\""));
}

#[test]
fn without_suggest_the_response_shape_is_unchanged() {
    let dir = tempfile::tempdir().unwrap();
    let run = json!({
        "run_id": "r1",
        "summary": { "total": 1, "passed": 1, "failed": 0 },
        "tests": [ { "name": "alpha responds", "status": "pass" } ],
        "coverage": serde_json::to_value(fixture_report()).unwrap(),
        "catalog": fixture_catalog(),
    });
    write_run(dir.path(), &run);

    let payload = call_get_coverage(&server(dir.path()), json!({}));
    // The legacy shape: the coverage block itself, with no suggestions
    // wrapper.
    assert!(payload.get("suggestions").is_none());
    assert_eq!(payload["tool_coverage_pct"], 50.0);
}

#[test]
fn suggest_against_a_run_without_a_coverage_block_returns_an_honest_note() {
    let dir = tempfile::tempdir().unwrap();
    let run = json!({
        "run_id": "r1",
        "summary": { "total": 1, "passed": 1, "failed": 0 },
        "tests": [ { "name": "alpha responds", "status": "pass" } ],
    });
    write_run(dir.path(), &run);

    let payload = call_get_coverage(&server(dir.path()), json!({ "suggest": true }));
    let suggestions = &payload["suggestions"];
    assert_eq!(suggestions["items"].as_array().unwrap().len(), 0);
    assert_eq!(suggestions["yaml"], "");
    assert!(suggestions["note"]
        .as_str()
        .unwrap()
        .contains("no structured coverage block"));
}

#[test]
fn get_coverage_descriptor_documents_the_suggest_option() {
    let tools = mcptest_mcp_server::tools::catalog(&Capabilities::default());
    let descriptor = tools
        .iter()
        .find(|t| t["name"] == "get_coverage")
        .expect("get_coverage in the catalog");
    assert!(descriptor["description"]
        .as_str()
        .unwrap()
        .contains("suggest"));
    assert_eq!(
        descriptor["inputSchema"]["properties"]["suggest"]["type"],
        "boolean"
    );
}
