//! Integration tests for `mcptest tools call` pipeline primitives.
//!
//! Stands up a wiremock streamable-HTTP MCP endpoint and exercises the
//! chaining flags end to end: JSONPath arg extraction from stdin, whole-args
//! from stdin, output projection, and the `--then` bind/then chained call.

use assert_cmd::Command;
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")
}

/// Mount a JSON-RPC responder that answers each method from a canned body.
/// The `tools/call` matcher inspects the requested tool name so a chained
/// call can return a different body than the first call.
async fn mount(server: &MockServer, init: Value, tool_responses: Vec<(&'static str, Value)>) {
    Mock::given(method("POST"))
        .and(wiremock::matchers::body_partial_json(
            json!({ "method": "initialize" }),
        ))
        .respond_with(reply(init))
        .mount(server)
        .await;
    for (tool_name, body) in tool_responses {
        let want = json!({ "method": "tools/call", "params": { "name": tool_name } });
        Mock::given(method("POST"))
            .and(wiremock::matchers::body_partial_json(want))
            .respond_with(reply(body))
            .mount(server)
            .await;
    }
}

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")
    }
}

fn init_body() -> Value {
    json!({
        "protocolVersion": "2025-06-18",
        "serverInfo": { "name": "fake", "version": "0.1" },
        "capabilities": {}
    })
}

#[tokio::test(flavor = "multi_thread")]
async fn tools_call_with_args_prints_result() {
    let server = MockServer::start().await;
    mount(
        &server,
        init_body(),
        vec![("search", json!({ "id": "u1", "name": "alice" }))],
    )
    .await;

    let assertion = mcptest()
        .args([
            "tools",
            "call",
            "search",
            "--url",
            &server.uri(),
            "--args",
            r#"{"query":"alice"}"#,
            "--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["id"], "u1");
}

#[tokio::test(flavor = "multi_thread")]
async fn tools_call_select_projects_output() {
    let server = MockServer::start().await;
    mount(
        &server,
        init_body(),
        vec![(
            "list_issues",
            json!({ "issues": [{ "number": 1 }, { "number": 2 }] }),
        )],
    )
    .await;

    let assertion = mcptest()
        .args([
            "tools",
            "call",
            "list_issues",
            "--url",
            &server.uri(),
            "--select",
            "$.issues[*].number",
            "--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, json!([1, 2]));
}

#[tokio::test(flavor = "multi_thread")]
async fn tools_call_arg_extracts_from_stdin() {
    let server = MockServer::start().await;
    mount(
        &server,
        init_body(),
        vec![("fetch_user", json!({ "ok": true }))],
    )
    .await;

    let assertion = mcptest()
        .args([
            "tools",
            "call",
            "fetch_user",
            "--url",
            &server.uri(),
            "--arg",
            "user_id=$.id",
            "--json",
        ])
        .write_stdin(r#"{"id":"abc"}"#)
        .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["ok"], true);
}

#[tokio::test(flavor = "multi_thread")]
async fn tools_call_args_from_stdin_reads_whole_object() {
    let server = MockServer::start().await;
    mount(
        &server,
        init_body(),
        vec![("fetch_user", json!({ "found": "yes" }))],
    )
    .await;

    let assertion = mcptest()
        .args([
            "tools",
            "call",
            "fetch_user",
            "--url",
            &server.uri(),
            "--args-from-stdin",
            "--json",
        ])
        .write_stdin(r#"{"user_id":"abc"}"#)
        .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["found"], "yes");
}

#[tokio::test(flavor = "multi_thread")]
async fn tools_call_then_chains_a_bound_value() {
    let server = MockServer::start().await;
    mount(
        &server,
        init_body(),
        vec![
            ("search", json!({ "id": ["u1"] })),
            ("fetch_user", json!({ "profile": "alice" })),
        ],
    )
    .await;

    let assertion = mcptest()
        .args([
            "tools",
            "call",
            "search",
            "--url",
            &server.uri(),
            "--args",
            r#"{"query":"alice"}"#,
            "--bind",
            "user_id=$.id[0]",
            "--then",
            "fetch_user",
            "--then-arg",
            "user_id=:user_id",
            "--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");
    // The final output is the chained fetch_user result.
    assert_eq!(parsed["profile"], "alice");
}

#[tokio::test(flavor = "current_thread")]
async fn tools_call_help_lists_chaining_flags() {
    let assertion = mcptest()
        .args(["tools", "call", "--help"])
        .assert()
        .success();
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
    for flag in ["--arg", "--bind", "--then", "--select", "--args-from-stdin"] {
        assert!(stdout.contains(flag), "help missing {flag}");
    }
}

#[tokio::test(flavor = "current_thread")]
async fn bare_tools_still_lists_catalog() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(wiremock::matchers::body_partial_json(
            json!({ "method": "initialize" }),
        ))
        .respond_with(reply(init_body()))
        .mount(&server)
        .await;
    Mock::given(method("POST"))
        .and(wiremock::matchers::body_partial_json(
            json!({ "method": "tools/list" }),
        ))
        .respond_with(reply(json!({ "tools": [{ "name": "alpha" }] })))
        .mount(&server)
        .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"), "bare tools list regressed");
}
