//! End-to-end tests for `response_headers` / `response_headers_absent`
//! against a live streamable HTTP target (WOR-1287).
//!
//! Before WOR-1287 the run path did not capture per-response HTTP headers,
//! so a declared header check on a URL server failed with an interim
//! "capture has not landed" message. These tests stand up a wiremock
//! streamable HTTP MCP endpoint that stamps an `x-trace-id` header on every
//! response and pin the real behavior end to end: matching expectations
//! pass the run, a genuinely absent header passes the absent check, and a
//! forbidden header that the server does send fails the run with the field
//! name in the output.

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

/// Responder that echoes the request's JSON-RPC id and stamps the headers
/// the suite under test asserts on (`content-type` plus `x-trace-id`).
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() });
        // `set_body_raw` pins the content-type to exactly this value;
        // `set_body_string` would stamp its own and leave two headers.
        ResponseTemplate::new(200)
            .set_body_raw(serde_json::to_vec(&envelope).unwrap(), "application/json")
            .insert_header("x-trace-id", "trace-1")
    }
}

/// Mount `initialize` and `tools/call` so a single tool test can run.
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/call" }),
        ))
        .respond_with(reply(json!({
            "isError": false,
            "content": [{ "type": "text", "text": "pong" }]
        })))
        .mount(server)
        .await;
}

/// Render a suite whose single tool test runs the long-form `expect:`
/// mapping (caller provides the YAML lines under `    expect:`) against
/// the mock URL server.
fn write_suite(dir: &std::path::Path, url: &str, expect_block: &str) -> std::path::PathBuf {
    let suite = format!(
        r#"servers:
  remote:
    url: "{url}"
tools:
  - name: header contract
    server: remote
    tool: ping
    expect:
{expect_block}
"#
    );
    let path = dir.join("suite.yml");
    std::fs::write(&path, suite).expect("write suite");
    path
}

#[tokio::test(flavor = "multi_thread")]
async fn present_header_expectations_pass_against_a_live_http_target() {
    let server = MockServer::start().await;
    mount(&server).await;
    let dir = tempdir().unwrap();
    let suite = write_suite(
        dir.path(),
        &server.uri(),
        "      assertions:\n        - target: \"result.content[0].text\"\n          matcher: { contains: \"pong\" }\n      response_headers:\n        content-type: \"application/json\"\n        x-trace-id: { exists: true }\n",
    );
    mcptest()
        .args(["run", "--config"])
        .arg(&suite)
        .assert()
        .code(0)
        .stdout(predicate::str::contains("1 passed"));
}

#[tokio::test(flavor = "multi_thread")]
async fn absent_check_passes_when_the_header_is_genuinely_absent() {
    let server = MockServer::start().await;
    mount(&server).await;
    let dir = tempdir().unwrap();
    let suite = write_suite(
        dir.path(),
        &server.uri(),
        "      assertions:\n        - target: \"result.content[0].text\"\n          matcher: { contains: \"pong\" }\n      response_headers_absent: [set-cookie]\n",
    );
    mcptest()
        .args(["run", "--config"])
        .arg(&suite)
        .assert()
        .code(0)
        .stdout(predicate::str::contains("1 passed"));
}

#[tokio::test(flavor = "multi_thread")]
async fn absent_check_fails_when_the_server_sends_the_forbidden_header() {
    let server = MockServer::start().await;
    mount(&server).await;
    let dir = tempdir().unwrap();
    // The stub stamps `x-trace-id` on every response, so declaring it
    // forbidden must fail the run and name the field.
    let suite = write_suite(
        dir.path(),
        &server.uri(),
        "      assertions:\n        - target: \"result.content[0].text\"\n          matcher: { contains: \"pong\" }\n      response_headers_absent: [x-trace-id]\n",
    );
    mcptest()
        .args(["run", "--config"])
        .arg(&suite)
        .assert()
        .code(1)
        .stdout(
            predicate::str::contains("1 failed").and(predicate::str::contains("response_headers")),
        );
}
