//! Integration tests for `mcptest inspect`.
//!
//! Drives the interactive REPL by piping a command script into the binary's
//! stdin, the same way a scripted CI session would. The core test connects to
//! a stdio `mcptest mock` and exercises list/call/capabilities/discover/quit.
//! The bidirectional test connects to a tiny shell fixture that pushes a live
//! notification and a server-initiated `sampling/createMessage` request, and
//! asserts the REPL prints the notification and fulfills the request.

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

use assert_cmd::cargo::CommandCargoExt;
use tempfile::tempdir;

/// Run `mcptest inspect <target...>` with `script` on stdin, returning stdout.
fn run_inspect(target: &[&str], script: &str) -> String {
    let bin = std::process::Command::cargo_bin("mcptest").expect("binary builds");
    let bin_path = bin.get_program().to_owned();
    let mut args = vec!["inspect", "--"];
    args.extend_from_slice(target);
    let mut child = std::process::Command::new(&bin_path)
        .args(&args)
        .stdin(Stdio::piped())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .expect("spawn mcptest inspect");
    {
        let stdin = child.stdin.as_mut().expect("stdin handle");
        stdin.write_all(script.as_bytes()).expect("write script");
        stdin.flush().expect("flush stdin");
    }
    drop(child.stdin.take());
    let output = child.wait_with_output().expect("wait for inspect");
    assert!(
        output.status.success(),
        "inspect exited non-zero: {:?}\nstderr: {}",
        output.status,
        String::from_utf8_lossy(&output.stderr)
    );
    String::from_utf8(output.stdout).expect("utf-8 stdout")
}

fn mcptest_bin() -> String {
    std::process::Command::cargo_bin("mcptest")
        .expect("binary builds")
        .get_program()
        .to_string_lossy()
        .into_owned()
}

#[test]
fn repl_lists_and_calls_a_tool_over_stdio() {
    let dir = tempdir().unwrap();
    let manifest = dir.path().join("mock.yml");
    std::fs::write(
        &manifest,
        r#"mock_server:
  name: weather
  tools:
    - name: get_weather
      description: "Get the weather for a city."
      input_schema:
        type: object
        required: [city]
        properties:
          city: { type: string }
      response:
        content:
          - type: text
            text: "It is 72F in ${args.city}."
"#,
    )
    .unwrap();

    let suite_out = dir.path().join("suite.yaml");
    let bin = mcptest_bin();
    let manifest_str = manifest.to_string_lossy().into_owned();
    let suite_str = suite_out.to_string_lossy().into_owned();
    let script = format!(
        "tools\ncall get_weather {{\"city\":\"Denver\"}}\ncaps\ndiscover {suite_str}\nquit\n"
    );
    let stdout = run_inspect(&[&bin, "mock", "--tools-from", &manifest_str], &script);

    assert!(stdout.contains("Connected to weather"), "banner: {stdout}");
    assert!(stdout.contains("get_weather"), "tool listing: {stdout}");
    assert!(
        stdout.contains("It is 72F in Denver."),
        "tool call result: {stdout}"
    );
    assert!(
        stdout.contains("Protocol version:"),
        "capabilities view: {stdout}"
    );
    assert!(stdout.contains("wrote"), "discover scaffold: {stdout}");
    // The scaffolded suite landed on disk and names the discovered tool.
    let suite = std::fs::read_to_string(&suite_out).expect("scaffold written");
    assert!(suite.contains("get_weather"), "scaffold body: {suite}");
}

#[cfg(unix)]
#[test]
fn repl_prints_notifications_and_fulfills_sampling() {
    use std::os::unix::fs::PermissionsExt;

    let dir = tempdir().unwrap();
    let fixture = dir.path().join("bidi.sh");
    // A stdio server that, on initialize, pushes a log notification and a
    // server-initiated sampling request. Builtins only (no per-line forks).
    std::fs::write(
        &fixture,
        r##"#!/bin/sh
while IFS= read -r line; do
  case "$line" in
    *'"id":'*) tmp=${line#*'"id":'}; id=${tmp%%[!0-9]*} ;;
    *) id= ;;
  esac
  case "$line" in
    *'"method":"initialize"'*)
      printf '{"jsonrpc":"2.0","id":%s,"result":{"protocolVersion":"2025-03-26","serverInfo":{"name":"bidi","version":"0.0.1"},"capabilities":{}}}\n' "$id"
      printf '{"jsonrpc":"2.0","method":"notifications/message","params":{"level":"info","data":"hello from server"}}\n'
      printf '{"jsonrpc":"2.0","id":9001,"method":"sampling/createMessage","params":{"messages":[]}}\n'
      ;;
    *'"method":"notifications/initialized"'*) : ;;
    *'"method":"tools/list"'*)
      printf '{"jsonrpc":"2.0","id":%s,"result":{"tools":[]}}\n' "$id" ;;
    *'"method":'*)
      [ -n "$id" ] && printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32601,"message":"nope"}}\n' "$id" ;;
    *) : ;;
  esac
done
"##,
    )
    .unwrap();
    let mut perms = std::fs::metadata(&fixture).unwrap().permissions();
    perms.set_mode(0o755);
    std::fs::set_permissions(&fixture, perms).unwrap();

    let fixture_str = fixture.to_string_lossy().into_owned();
    // `tools` round-trips so the async notification + sampling request have
    // landed and printed before we quit; `notif` replays the log.
    let stdout = run_inspect(&[&fixture_str], "tools\nnotif\nquit\n");

    assert!(
        stdout.contains("notifications/message"),
        "live notification not surfaced: {stdout}"
    );
    assert!(
        stdout.contains("fulfilled sampling/createMessage"),
        "sampling request not fulfilled: {stdout}"
    );
}
