//! End-to-end `cassette:` server source.
//!
//! A server declared with `cassette: <path>` replays recorded exchanges with
//! no network. These tests write a cassette plus a suite to a tempdir and run
//! `mcptest run` against it: a matching tool call replays and passes, and a
//! call the cassette never recorded fails with a clear replay error rather
//! than hanging.

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

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

/// A cassette with an `initialize` handshake and one recorded `tools/call`.
const CASSETTE: &str = r#"{
  "version": "1",
  "exchanges": [
    {
      "request": { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {} },
      "response": { "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-03-26", "capabilities": {}, "serverInfo": { "name": "recorded", "version": "0.1" } } }
    },
    {
      "request": { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "echo", "arguments": { "msg": "hi" } } },
      "response": { "jsonrpc": "2.0", "id": 2, "result": { "content": [ { "type": "text", "text": "replayed: hi" } ] } }
    }
  ]
}"#;

fn write_cassette(dir: &std::path::Path) -> std::path::PathBuf {
    let path = dir.join("cassette.json");
    std::fs::write(&path, CASSETTE).unwrap();
    path
}

#[test]
fn cassette_server_replays_a_recorded_tool_call() {
    let dir = tempfile::tempdir().unwrap();
    let cassette = write_cassette(dir.path());
    let cfg = dir.path().join("mcptest.yaml");
    std::fs::write(
        &cfg,
        format!(
            r#"servers:
  recorded:
    cassette: '{}'
tools:
  - name: echo replays from cassette
    server: recorded
    tool: echo
    args: {{ msg: hi }}
    expect:
      - target: result.content[0].text
        matcher: {{ exact: "replayed: hi" }}
"#,
            cassette.display()
        ),
    )
    .unwrap();

    mcptest()
        .args(["run", "--config", cfg.to_str().unwrap()])
        .assert()
        .success()
        .stdout(predicate::str::contains("1 passed"));
}

#[test]
fn cassette_server_fails_a_call_it_never_recorded() {
    let dir = tempfile::tempdir().unwrap();
    let cassette = write_cassette(dir.path());
    let cfg = dir.path().join("mcptest.yaml");
    // The cassette recorded `echo`; ask for `delete`, which it never saw.
    std::fs::write(
        &cfg,
        format!(
            r#"servers:
  recorded:
    cassette: '{}'
tools:
  - name: unrecorded call fails
    server: recorded
    tool: delete
    args: {{ id: 1 }}
    expect:
      - target: result.content[0].text
        matcher: {{ exact: "replayed: hi" }}
"#,
            cassette.display()
        ),
    )
    .unwrap();

    mcptest()
        .args(["run", "--config", cfg.to_str().unwrap()])
        .assert()
        .failure();
}

/// A cassette with the handshake plus two distinct recorded `tools/call`s.
/// Used by the WOR-1283 content-matching tests below.
const TWO_CALL_CASSETTE: &str = r#"{
  "version": "1",
  "exchanges": [
    {
      "request": { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {} },
      "response": { "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-03-26", "capabilities": {}, "serverInfo": { "name": "recorded", "version": "0.1" } } }
    },
    {
      "request": { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "echo", "arguments": { "msg": "first" } } },
      "response": { "jsonrpc": "2.0", "id": 2, "result": { "content": [ { "type": "text", "text": "replayed: first" } ] } }
    },
    {
      "request": { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "echo", "arguments": { "msg": "second" } } },
      "response": { "jsonrpc": "2.0", "id": 3, "result": { "content": [ { "type": "text", "text": "replayed: second" } ] } }
    }
  ]
}"#;

#[test]
fn cassette_replays_a_call_recorded_later_in_the_tape() {
    // WOR-1283: replay matches by content, not by recorded position, so a
    // suite may exercise only the second recorded call. The old cursor
    // failed this with a mismatch at the first recorded tools/call.
    let dir = tempfile::tempdir().unwrap();
    let cassette = dir.path().join("cassette.json");
    std::fs::write(&cassette, TWO_CALL_CASSETTE).unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    std::fs::write(
        &cfg,
        format!(
            r#"servers:
  recorded:
    cassette: '{}'
tools:
  - name: second recording replays on its own
    server: recorded
    tool: echo
    args: {{ msg: second }}
    expect:
      - target: result.content[0].text
        matcher: {{ exact: "replayed: second" }}
"#,
            cassette.display()
        ),
    )
    .unwrap();

    mcptest()
        .args(["run", "--config", cfg.to_str().unwrap()])
        .assert()
        .success()
        .stdout(predicate::str::contains("1 passed"));
}

#[test]
fn unrecorded_call_fails_alone_without_breaking_a_recorded_sibling() {
    // WOR-1283: an unmatched request answers with an error but the replay
    // task keeps serving, so the sibling test that does hold a recording
    // still passes regardless of which test reaches the transport first.
    let dir = tempfile::tempdir().unwrap();
    let cassette = dir.path().join("cassette.json");
    std::fs::write(&cassette, TWO_CALL_CASSETTE).unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    std::fs::write(
        &cfg,
        format!(
            r#"run_options:
  parallel: 1
servers:
  recorded:
    cassette: '{}'
tools:
  - name: unrecorded call fails
    server: recorded
    tool: delete
    args: {{ id: 1 }}
    expect:
      - target: result.content[0].text
        matcher: {{ exact: "never recorded" }}
  - name: recorded sibling still replays
    server: recorded
    tool: echo
    args: {{ msg: first }}
    expect:
      - target: result.content[0].text
        matcher: {{ exact: "replayed: first" }}
"#,
            cassette.display()
        ),
    )
    .unwrap();

    mcptest()
        .args(["run", "--config", cfg.to_str().unwrap()])
        .assert()
        .failure()
        .stdout(predicate::str::contains("1 passed").and(predicate::str::contains("1 failed")));
}

#[test]
fn cassette_server_with_url_replays_without_touching_the_target() {
    // WOR-1279: `url:` next to `cassette:` declares the refresh target for
    // `--update-cassettes`; a plain run still replays the recording and
    // never reaches for the URL (which here does not resolve at all).
    let dir = tempfile::tempdir().unwrap();
    let cassette = write_cassette(dir.path());
    let cfg = dir.path().join("mcptest.yaml");
    std::fs::write(
        &cfg,
        format!(
            r#"servers:
  recorded:
    cassette: '{}'
    url: "http://mcptest-wor1279.invalid/mcp"
tools:
  - name: replays offline
    server: recorded
    tool: echo
    args: {{ msg: hi }}
    expect:
      - target: result.content[0].text
        matcher: {{ exact: "replayed: hi" }}
"#,
            cassette.display()
        ),
    )
    .unwrap();

    mcptest()
        .args(["run", "--config", cfg.to_str().unwrap()])
        .assert()
        .code(0)
        .stdout(predicate::str::contains("1 passed"));
}

#[test]
fn cassette_server_rejects_both_refresh_targets() {
    // `command:` and `url:` together is still ambiguous, with or without
    // the cassette.
    let dir = tempfile::tempdir().unwrap();
    let cassette = write_cassette(dir.path());
    let cfg = dir.path().join("mcptest.yaml");
    std::fs::write(
        &cfg,
        format!(
            r#"servers:
  bad:
    cassette: '{}'
    command: ["./srv"]
    url: "https://example.com/mcp"
tools:
  - name: never runs
    server: bad
    tool: echo
    expect:
      - target: result
        matcher: {{ exact: ok }}
"#,
            cassette.display()
        ),
    )
    .unwrap();

    mcptest()
        .args(["run", "--config", cfg.to_str().unwrap()])
        .assert()
        .code(2);
}
