//! End-to-end tests for `mcptest record` (WOR-1264): a stdio MCP client
//! talks through the record proxy to a live `mcptest mock` target, the
//! traffic lands redacted in a cassette with its live target stamped in
//! `meta`, and the tape feeds the distill, validate, run chain with zero
//! edits.

#![cfg(unix)]

use std::io::{BufRead, BufReader, Read, Write};
use std::process::Stdio;

use assert_cmd::cargo::CommandCargoExt;
use assert_cmd::Command as AssertCommand;
use serde_json::Value;

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

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

/// Mock manifest with one echo tool, the live target the proxy records.
fn write_echo_manifest(dir: &std::path::Path) -> std::path::PathBuf {
    let path = dir.join("echo.yaml");
    std::fs::write(
        &path,
        r#"mock_server:
  name: echo
  tools:
    - name: echo
      description: "Echo"
      input_schema:
        type: object
      response:
        content:
          - type: text
            text: "hello from the live server"
"#,
    )
    .expect("write mock manifest");
    path
}

/// One recorded client session through the proxy: initialize, the
/// initialized notification, and a tools/call carrying a secret-named arg.
/// Returns (stdout response lines, stderr, exit success).
fn record_session(dir: &std::path::Path, target: &str, output: &str) -> (Vec<Value>, String, bool) {
    let mut child = mcptest_std()
        .current_dir(dir)
        .args(["record", "--target", target, "--output", output])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn mcptest record");
    let mut stdin = child.stdin.take().expect("stdin handle");
    let mut reader = BufReader::new(child.stdout.take().expect("stdout handle"));

    let lines = [
        r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test-client","version":"0"}}}"#,
        r#"{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}"#,
        r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"msg":"hi","api_key":"sk-test1234567890abcdef"}}}"#,
    ];
    for line in lines {
        stdin.write_all(line.as_bytes()).expect("write line");
        stdin.write_all(b"\n").expect("write newline");
    }
    stdin.flush().expect("flush stdin");

    // The notification has no response, so exactly two lines come back.
    let mut responses = Vec::new();
    for _ in 0..2 {
        let mut line = String::new();
        reader.read_line(&mut line).expect("read response line");
        responses.push(serde_json::from_str::<Value>(&line).expect("response parses"));
    }
    drop(stdin); // EOF ends the session
    let status = child.wait().expect("proxy exits");
    let mut stderr = String::new();
    child
        .stderr
        .take()
        .expect("stderr handle")
        .read_to_string(&mut stderr)
        .expect("read stderr");
    (responses, stderr, status.success())
}

#[test]
fn record_proxies_a_session_and_writes_a_redacted_stamped_cassette() {
    let dir = tempfile::tempdir().unwrap();
    let bin = env!("CARGO_BIN_EXE_mcptest");
    let manifest = write_echo_manifest(dir.path());
    let target = format!("{bin} mock --tools-from {}", manifest.display());

    let (responses, stderr, success) = record_session(dir.path(), &target, "tape.json");
    assert!(success, "record session must exit 0; stderr:\n{stderr}");

    // The client saw real responses from the live target, ids untouched.
    assert_eq!(responses[0]["id"], 1);
    assert_eq!(responses[0]["result"]["serverInfo"]["name"], "echo");
    assert_eq!(responses[1]["id"], 2);
    assert_eq!(
        responses[1]["result"]["content"][0]["text"],
        "hello from the live server"
    );

    // The startup banner carries the ready-to-paste client config and the
    // distill handoff.
    assert!(stderr.contains("mcpServers"), "stderr:\n{stderr}");
    assert!(stderr.contains("mcptest distill"), "stderr:\n{stderr}");

    // The tape holds both exchanges (the notification is not recorded),
    // remembers its live target, and never carries the secret to disk.
    let tape: Value =
        serde_json::from_str(&std::fs::read_to_string(dir.path().join("tape.json")).unwrap())
            .expect("cassette parses");
    let exchanges = tape["exchanges"].as_array().expect("exchanges list");
    assert_eq!(exchanges.len(), 2);
    assert_eq!(exchanges[0]["request"]["method"], "initialize");
    assert_eq!(exchanges[1]["request"]["method"], "tools/call");
    assert_eq!(tape["meta"]["source_command"], target);
    let api_key = exchanges[1]["request"]["params"]["arguments"]["api_key"]
        .as_str()
        .expect("api_key recorded as a string");
    assert!(
        api_key.starts_with("[REDACTED:"),
        "secret must be scrubbed before it lands on disk, got {api_key}"
    );
}

#[test]
fn recorded_tape_distills_validates_and_replays_green() {
    let dir = tempfile::tempdir().unwrap();
    let bin = env!("CARGO_BIN_EXE_mcptest");
    let manifest = write_echo_manifest(dir.path());
    let target = format!("{bin} mock --tools-from {}", manifest.display());

    let (_responses, _stderr, success) = record_session(dir.path(), &target, "tape.json");
    assert!(success, "record session must exit 0");

    mcptest_assert()
        .current_dir(dir.path())
        .args(["distill", "tape.json", "--output", "suite.yaml"])
        .assert()
        .success();
    mcptest_assert()
        .current_dir(dir.path())
        .args(["validate", "--config", "suite.yaml"])
        .assert()
        .success();
    mcptest_assert()
        .current_dir(dir.path())
        .env_remove("CI")
        .args(["run", "--config", "suite.yaml"])
        .assert()
        .success();
}
