//! Integration tests for the imperative introspection subcommands.
//!
//! Each subcommand connects to an MCP server, runs the matching spec method,
//! and prints the result. The tests stand up a wiremock-backed streamable
//! HTTP endpoint that answers JSON-RPC envelopes from canned bodies.

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

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

/// Build a mock that answers each POST with the response that matches the
/// requested method. The wiremock layer matches on every POST and uses a
/// dynamic responder to inspect the body.
async fn mount_jsonrpc(server: &MockServer, responses: Vec<(&'static str, Value)>) {
    for (method_name, payload) in responses {
        let body = payload.clone();
        Mock::given(method("POST"))
            .and(wiremock::matchers::body_partial_json(
                json!({ "method": method_name }),
            ))
            .respond_with(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(server)
            .await;
    }
}

#[tokio::test(flavor = "current_thread")]
async fn tools_help_lists_url_and_format_flags() {
    // Run synchronously; this test does not need the tokio runtime, but the
    // other tests in this file are async so the file shares the attribute.
    let assertion = mcptest().args(["tools", "--help"]).assert().success();
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
    assert!(stdout.contains("--url"), "help missing --url");
    assert!(stdout.contains("--format"), "help missing --format");
}

#[tokio::test(flavor = "current_thread")]
async fn tools_list_prints_pretty_catalog() {
    let server = MockServer::start().await;
    mount_jsonrpc(
        &server,
        vec![
            (
                "initialize",
                json!({
                    "protocolVersion": "2025-06-18",
                    "serverInfo": { "name": "fake", "version": "0.1" },
                    "capabilities": {}
                }),
            ),
            (
                "tools/list",
                json!({
                    "tools": [
                        { "name": "alpha", "description": "first" },
                        { "name": "beta", "description": "second" }
                    ]
                }),
            ),
        ],
    )
    .await;

    let assertion = mcptest()
        .args(["tools", "--url", &server.uri()])
        .assert()
        .success();
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
    assert!(stdout.contains("alpha"), "stdout missing alpha");
    assert!(stdout.contains("beta"), "stdout missing beta");
    assert!(stdout.contains("NAME"), "stdout missing header");
}

#[tokio::test(flavor = "current_thread")]
async fn tools_list_json_format_emits_valid_json() {
    let server = MockServer::start().await;
    mount_jsonrpc(
        &server,
        vec![
            (
                "initialize",
                json!({
                    "protocolVersion": "2025-06-18",
                    "serverInfo": { "name": "fake", "version": "0.1" },
                    "capabilities": {}
                }),
            ),
            (
                "tools/list",
                json!({
                    "tools": [ { "name": "only" } ]
                }),
            ),
        ],
    )
    .await;

    let assertion = mcptest()
        .args(["tools", "--url", &server.uri(), "--format", "json"])
        .assert()
        .success();
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
    let parsed: Value = serde_json::from_str(stdout.trim()).expect("valid json");
    assert_eq!(parsed["tools"][0]["name"], "only");
}

#[tokio::test(flavor = "current_thread")]
async fn resources_list_prints_uri_column() {
    let server = MockServer::start().await;
    mount_jsonrpc(
        &server,
        vec![
            (
                "initialize",
                json!({
                    "protocolVersion": "2025-06-18",
                    "serverInfo": { "name": "fake", "version": "0.1" },
                    "capabilities": {}
                }),
            ),
            (
                "resources/list",
                json!({
                    "resources": [
                        { "uri": "file:///readme.md", "name": "README", "mimeType": "text/markdown" }
                    ]
                }),
            ),
        ],
    )
    .await;

    let assertion = mcptest()
        .args(["resources", "--url", &server.uri()])
        .assert()
        .success();
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
    assert!(stdout.contains("file:///readme.md"));
    assert!(stdout.contains("README"));
}

#[tokio::test(flavor = "current_thread")]
async fn prompts_list_prints_argument_names() {
    let server = MockServer::start().await;
    mount_jsonrpc(
        &server,
        vec![
            (
                "initialize",
                json!({
                    "protocolVersion": "2025-06-18",
                    "serverInfo": { "name": "fake", "version": "0.1" },
                    "capabilities": {}
                }),
            ),
            (
                "prompts/list",
                json!({
                    "prompts": [
                        {
                            "name": "code_review",
                            "arguments": [ { "name": "language" }, { "name": "file_path" } ]
                        }
                    ]
                }),
            ),
        ],
    )
    .await;

    let assertion = mcptest()
        .args(["prompts", "--url", &server.uri()])
        .assert()
        .success();
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
    assert!(stdout.contains("code_review"));
    assert!(stdout.contains("language"));
    assert!(stdout.contains("file_path"));
}

#[tokio::test(flavor = "current_thread")]
async fn capabilities_prints_server_info_and_protocol_version() {
    let server = MockServer::start().await;
    mount_jsonrpc(
        &server,
        vec![(
            "initialize",
            json!({
                "protocolVersion": "2025-06-18",
                "serverInfo": { "name": "demo-server", "version": "1.2.3" },
                "capabilities": {
                    "tools": { "listChanged": true },
                    "resources": { "subscribe": true }
                }
            }),
        )],
    )
    .await;

    let assertion = mcptest()
        .args(["capabilities", "--url", &server.uri()])
        .assert()
        .success();
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
    assert!(stdout.contains("demo-server"));
    assert!(stdout.contains("1.2.3"));
    assert!(stdout.contains("2025-06-18"));
    assert!(stdout.contains("tools"));
}

#[tokio::test(flavor = "current_thread")]
async fn tools_against_unreachable_server_exits_nonzero() {
    // Port 1 has no listener, so the connection is refused immediately. We
    // deliberately avoid starting a MockServer and dropping it to free its
    // ephemeral port: under parallel test execution (observed on macOS) the
    // OS reused that freed port for a sibling test's live MockServer, so the
    // "unreachable" URL occasionally connected and the command exited 0.
    mcptest()
        .args(["tools", "--url", "http://127.0.0.1:1"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("mcptest"));
}
