//! End-to-end `run_options.restart_policy` respawn behavior (WOR-1323).
//!
//! A stdio fixture server appends its PID to a spawn log every time it
//! starts, so counting log lines counts processes. The suite runs two tool
//! tests against it: `per_test` must spawn a fresh process for each test,
//! `per_file` restarts once at run start, and the default policy reuses
//! one process for the whole run.
//!
//! POSIX shell fixture, so the file is Unix-gated like the other
//! spawn-heavy integration tests.

#![cfg(unix)]

use std::os::unix::fs::PermissionsExt;
use std::path::Path;

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

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

/// Write the ping fixture server: log `$$` to the spawn log named by the
/// first argument, then serve initialize / tools/list / tools/call for one
/// `ping` tool over line-delimited JSON-RPC. Modeled on
/// `examples/transform/echo-server.sh`.
fn write_ping_server(dir: &Path) -> std::path::PathBuf {
    let body = r#"#!/bin/sh
# Stdio MCP fixture for the restart-policy tests (WOR-1323). Appends the
# process id to the spawn log at startup so the test can count spawns.
echo "$$" >> "$1"
while IFS= read -r line; do
  id=$(printf '%s' "$line" | sed -n 's/.*"id":[[:space:]]*\([0-9]*\).*/\1/p')
  if printf '%s' "$line" | grep -q '"method":"initialize"'; then
    printf '{"jsonrpc":"2.0","id":%s,"result":{"protocolVersion":"2025-03-26","serverInfo":{"name":"ping-fixture","version":"0.0.1"},"capabilities":{}}}\n' "$id"
  elif printf '%s' "$line" | grep -q '"method":"notifications/initialized"'; then
    :
  elif printf '%s' "$line" | grep -q '"method":"tools/list"'; then
    printf '{"jsonrpc":"2.0","id":%s,"result":{"tools":[{"name":"ping","description":"Reply pong","inputSchema":{"type":"object","properties":{}}}]}}\n' "$id"
  elif printf '%s' "$line" | grep -q '"method":"tools/call"'; then
    printf '{"jsonrpc":"2.0","id":%s,"result":{"content":[{"type":"text","text":"pong"}],"isError":false}}\n' "$id"
  elif [ -n "$id" ]; then
    printf '{"jsonrpc":"2.0","id":%s,"error":{"code":-32601,"message":"method not found"}}\n' "$id"
  fi
done
"#;
    let path = dir.join("ping-server.sh");
    std::fs::write(&path, body).expect("write fixture server");
    let mut perms = std::fs::metadata(&path).expect("meta").permissions();
    perms.set_mode(0o755);
    std::fs::set_permissions(&path, perms).expect("chmod");
    path
}

/// A two-test suite against the ping fixture, with an optional
/// `run_options:` block prepended.
fn write_suite(dir: &Path, run_options: &str) -> (std::path::PathBuf, std::path::PathBuf) {
    let server = write_ping_server(dir);
    let log = dir.join("spawn.log");
    let yaml = format!(
        r#"{run_options}servers:
  ping:
    command: ['{server}', '{log}']
tools:
  - name: first ping
    server: ping
    tool: ping
    args: {{}}
    expect:
      - target: result.content[0].text
        matcher: {{ exact: "pong" }}
  - name: second ping
    server: ping
    tool: ping
    args: {{}}
    expect:
      - target: result.content[0].text
        matcher: {{ exact: "pong" }}
"#,
        server = server.display(),
        log = log.display(),
    );
    let config = dir.join("mcptest.yaml");
    std::fs::write(&config, yaml).expect("write suite");
    (config, log)
}

/// PID lines from the spawn log, asserting they parse as integers.
fn spawn_pids(log: &Path) -> Vec<u64> {
    std::fs::read_to_string(log)
        .expect("spawn log exists")
        .lines()
        .map(|l| l.trim().parse::<u64>().expect("pid line"))
        .collect()
}

fn assert_all_distinct(pids: &[u64]) {
    let unique: std::collections::BTreeSet<u64> = pids.iter().copied().collect();
    assert_eq!(unique.len(), pids.len(), "expected distinct PIDs: {pids:?}");
}

#[test]
fn per_test_policy_spawns_a_fresh_server_process_for_each_test() {
    let dir = tempfile::tempdir().unwrap();
    let (config, log) = write_suite(dir.path(), "run_options:\n  restart_policy: per_test\n");

    mcptest()
        .args(["run", "--config"])
        .arg(&config)
        .assert()
        .code(0)
        .stdout(predicate::str::contains("2 passed"));

    // Three spawns: the pre-run connect, the RunStart respawn before the
    // first test, and the between-tests respawn before the second.
    let pids = spawn_pids(&log);
    assert_eq!(pids.len(), 3, "expected 3 spawns, got {pids:?}");
    assert_all_distinct(&pids);
}

#[test]
fn default_policy_reuses_one_server_process() {
    let dir = tempfile::tempdir().unwrap();
    let (config, log) = write_suite(dir.path(), "");

    mcptest()
        .args(["run", "--config"])
        .arg(&config)
        .assert()
        .code(0)
        .stdout(predicate::str::contains("2 passed"));

    let pids = spawn_pids(&log);
    assert_eq!(pids.len(), 1, "per_run reuses one process: {pids:?}");
}

#[test]
fn per_file_policy_restarts_once_at_run_start() {
    let dir = tempfile::tempdir().unwrap();
    let (config, log) = write_suite(dir.path(), "run_options:\n  restart_policy: per_file\n");

    mcptest()
        .args(["run", "--config"])
        .arg(&config)
        .assert()
        .code(0)
        .stdout(predicate::str::contains("2 passed"));

    // Two spawns: the pre-run connect plus the RunStart respawn. Both
    // tests come from one document, so they classify as the same file and
    // the between-tests boundary keeps the process.
    let pids = spawn_pids(&log);
    assert_eq!(pids.len(), 2, "expected 2 spawns, got {pids:?}");
    assert_all_distinct(&pids);
}
