//! Integration tests for `mcptest mock --tools-from <PATH>` and
//! the `--fault` flag.
//!
//! The mock spawns a stdio MCP server seeded from a YAML manifest or a
//! `tools/list` JSON baseline. These tests pipe synthetic JSON-RPC traffic
//! into the binary and assert that the responses match the spec shape and
//! the declared catalog.
//!
//! Fault tests only exercise `recover-after`, whose recovered call returns
//! promptly. `hang`/`wedged` deliberately block (bounded at ten minutes), so
//! they are covered at the library level, not end-to-end here.

use std::io::Write;
use std::process::Stdio;

use assert_cmd::cargo::CommandCargoExt;
use assert_cmd::Command as AssertCommand;
use predicates::prelude::*;
use serde_json::Value;
use tempfile::tempdir;

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

fn run_mock_with_lines(manifest_path: &str, lines: &[&str]) -> Vec<Value> {
    run_mock_with_args(manifest_path, &[], lines)
}

fn run_mock_with_args(manifest_path: &str, extra: &[&str], lines: &[&str]) -> Vec<Value> {
    let mut args = vec!["mock", "--tools-from", manifest_path];
    args.extend_from_slice(extra);
    let mut child = mcptest_std()
        .args(&args)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn mcptest mock");
    {
        let stdin = child.stdin.as_mut().expect("stdin handle");
        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");
    }
    // Close stdin to signal end of input. Drop the writer half by
    // taking the field and letting it fall out of scope.
    drop(child.stdin.take());
    let output = child.wait_with_output().expect("wait for mcptest mock");
    let stdout = String::from_utf8(output.stdout).expect("utf-8 stdout");
    stdout
        .lines()
        .filter(|l| !l.trim().is_empty())
        .map(|l| serde_json::from_str::<Value>(l).expect("parse response"))
        .collect()
}

fn write_yaml(dir: &std::path::Path, body: &str) -> std::path::PathBuf {
    let path = dir.join("mock.yml");
    std::fs::write(&path, body).expect("write manifest");
    path
}

#[test]
fn mock_help_lists_tools_from_flag() {
    let assertion = mcptest_assert().args(["mock", "--help"]).assert().success();
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
    assert!(stdout.contains("--tools-from"), "help missing --tools-from");
}

#[test]
fn mock_initialize_returns_server_info() {
    let dir = tempdir().unwrap();
    let path = write_yaml(
        dir.path(),
        r#"
mock_server:
  name: "demo"
  tools:
    - name: ping
      response:
        content:
          - type: text
            text: pong
"#,
    );
    let lines = run_mock_with_lines(
        path.to_str().unwrap(),
        &[r#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"#],
    );
    assert_eq!(lines.len(), 1);
    assert_eq!(lines[0]["result"]["serverInfo"]["name"], "demo");
}

#[test]
fn mock_tools_list_returns_declared_catalog() {
    let dir = tempdir().unwrap();
    let path = write_yaml(
        dir.path(),
        r#"
mock_server:
  name: "catalog-test"
  tools:
    - name: alpha
      description: "first"
    - name: beta
      description: "second"
"#,
    );
    let lines = run_mock_with_lines(
        path.to_str().unwrap(),
        &[
            r#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"#,
            r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#,
        ],
    );
    let tools = lines[1]["result"]["tools"].as_array().unwrap();
    let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
    assert_eq!(names, vec!["alpha", "beta"]);
}

#[test]
fn mock_tools_call_returns_canned_response_with_interpolation() {
    let dir = tempdir().unwrap();
    let path = write_yaml(
        dir.path(),
        r#"
mock_server:
  name: "interp"
  tools:
    - name: echo
      input_schema:
        type: object
        properties:
          msg: { type: string }
        required: [msg]
      response:
        content:
          - type: text
            text: "echo: ${args.msg}"
"#,
    );
    let lines = run_mock_with_lines(
        path.to_str().unwrap(),
        &[
            r#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"#,
            r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"echo","arguments":{"msg":"hi"}}}"#,
        ],
    );
    assert_eq!(lines.len(), 2);
    let text = lines[1]["result"]["content"][0]["text"].as_str().unwrap();
    assert_eq!(text, "echo: hi");
}

#[test]
fn mock_tools_call_for_unknown_tool_returns_minus_32601() {
    let dir = tempdir().unwrap();
    let path = write_yaml(
        dir.path(),
        r#"
mock_server:
  name: "errs"
  tools:
    - name: only
"#,
    );
    let lines = run_mock_with_lines(
        path.to_str().unwrap(),
        &[
            r#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"#,
            r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"missing","arguments":{}}}"#,
        ],
    );
    assert_eq!(lines[1]["error"]["code"], -32601);
}

#[test]
fn mock_tools_call_missing_required_arg_returns_minus_32602() {
    let dir = tempdir().unwrap();
    let path = write_yaml(
        dir.path(),
        r#"
mock_server:
  name: "errs"
  tools:
    - name: needs_msg
      input_schema:
        type: object
        properties:
          msg: { type: string }
        required: [msg]
"#,
    );
    let lines = run_mock_with_lines(
        path.to_str().unwrap(),
        &[
            r#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"#,
            r#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"needs_msg","arguments":{}}}"#,
        ],
    );
    assert_eq!(lines[1]["error"]["code"], -32602);
}

#[test]
fn mock_accepts_baseline_json_input_format() {
    let dir = tempdir().unwrap();
    let path = dir.path().join("baseline.json");
    std::fs::write(
        &path,
        r#"{ "tools": [ { "name": "search", "description": "Search the web" } ] }"#,
    )
    .unwrap();
    let lines = run_mock_with_lines(
        path.to_str().unwrap(),
        &[
            r#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"#,
            r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#,
        ],
    );
    let tools = lines[1]["result"]["tools"].as_array().unwrap();
    assert_eq!(tools[0]["name"], "search");
}

#[test]
fn mock_missing_manifest_exits_one_with_clear_error() {
    mcptest_assert()
        .args(["mock", "--tools-from", "/nonexistent/file.yml"])
        .assert()
        .failure()
        .stderr(predicate::str::contains("failed to read"));
}

#[test]
fn mock_help_lists_fault_flag() {
    let assertion = mcptest_assert().args(["mock", "--help"]).assert().success();
    let stdout = String::from_utf8(assertion.get_output().stdout.clone()).unwrap();
    assert!(stdout.contains("--fault"), "help missing --fault");
}

#[test]
fn mock_rejects_unknown_fault_kind() {
    let dir = tempdir().unwrap();
    let path = write_yaml(
        dir.path(),
        r#"
mock_server:
  name: "x"
  tools:
    - name: t
"#,
    );
    mcptest_assert()
        .args([
            "mock",
            "--tools-from",
            path.to_str().unwrap(),
            "--fault",
            "explode",
        ])
        .assert()
        .failure()
        .stderr(predicate::str::contains("invalid --fault"));
}

#[test]
fn mock_slow_fault_still_returns_the_canned_response() {
    // A short slow delay keeps this end-to-end run fast while proving the
    // flag plumbs through to a real (delayed) response. A `hang`/`wedged`
    // fault would block for the full ten-minute cap, and `recover-after`
    // must pass through that hang first, so both stay library-level (see
    // crates/mcptest-mcp-server/tests/server_smoke.rs).
    let dir = tempdir().unwrap();
    let path = write_yaml(
        dir.path(),
        r#"
mock_server:
  name: "slow"
  tools:
    - name: echo
      response:
        content:
          - type: text
            text: "back"
"#,
    );
    let call =
        r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"echo","arguments":{}}}"#;
    let lines = run_mock_with_args(path.to_str().unwrap(), &["--fault", "slow:50"], &[call]);
    assert_eq!(lines[0]["result"]["content"][0]["text"], "back");
}
