//! WOR-1283: deterministic multi-call cassette replay under the parallel
//! runner, plus the `run_options.parallel` wiring.
//!
//! A cassette holding several distinct calls used to replay through a
//! sequential cursor while the tests sharing it ran in parallel, so the
//! run failed intermittently (measured 7 of 30 locally). Replay now
//! matches requests by content, so the same suite must pass every time.
//! The parallelism tests assert the engine's resolved worker count via
//! its debug log line rather than timing, so they stay deterministic.

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

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

/// Build a server-cassette JSON document from `(tool, args, text)` triples,
/// prefixed with the initialize handshake the replay transport expects.
/// Mirrors the fixture pattern in `cli_distill.rs`.
fn cassette_json(calls: &[(&str, serde_json::Value, &str)]) -> String {
    let mut exchanges = vec![serde_json::json!({
        "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" } } }
    })];
    for (i, (tool, args, text)) in calls.iter().enumerate() {
        let id = i as i64 + 2;
        exchanges.push(serde_json::json!({
            "request": { "jsonrpc": "2.0", "id": id, "method": "tools/call",
                         "params": { "name": tool, "arguments": args } },
            "response": { "jsonrpc": "2.0", "id": id, "result": {
                "isError": false,
                "content": [{ "type": "text", "text": text }] } }
        }));
    }
    serde_json::json!({ "version": "1", "exchanges": exchanges }).to_string()
}

/// Write the shared three-distinct-call fixture: a cassette plus a
/// three-test suite that replays it. Tests are declared in reverse
/// recorded order so position-based replay cannot pass by accident.
/// `run_options_block` lands verbatim at the top of the YAML.
fn write_three_call_fixture(dir: &std::path::Path, run_options_block: &str) -> std::path::PathBuf {
    let cassette = dir.join("cassette.json");
    std::fs::write(
        &cassette,
        cassette_json(&[
            ("echo", serde_json::json!({ "msg": "one" }), "reply one"),
            ("echo", serde_json::json!({ "msg": "two" }), "reply two"),
            ("echo", serde_json::json!({ "msg": "three" }), "reply three"),
        ]),
    )
    .expect("write cassette");
    let cfg = dir.join("mcptest.yaml");
    std::fs::write(
        &cfg,
        format!(
            r#"{run_options_block}servers:
  recorded:
    cassette: './cassette.json'
tools:
  - name: replays third call
    server: recorded
    tool: echo
    args: {{ msg: three }}
    expect:
      - target: result.content[0].text
        matcher: {{ exact: "reply three" }}
  - name: replays second call
    server: recorded
    tool: echo
    args: {{ msg: two }}
    expect:
      - target: result.content[0].text
        matcher: {{ exact: "reply two" }}
  - name: replays first call
    server: recorded
    tool: echo
    args: {{ msg: one }}
    expect:
      - target: result.content[0].text
        matcher: {{ exact: "reply one" }}
"#
        ),
    )
    .expect("write suite");
    cfg
}

#[test]
fn three_distinct_call_cassette_replays_green_fifteen_times_in_parallel() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_three_call_fixture(dir.path(), "");
    for _round in 0..15 {
        mcptest()
            .current_dir(dir.path())
            .args(["run", "--config", cfg.to_str().unwrap()])
            .assert()
            .success()
            .stdout(predicate::str::contains("3 passed").and(predicate::str::contains("0 failed")));
    }
}

#[test]
fn suite_run_options_parallel_one_reaches_the_engine() {
    // Not a timing test: the engine logs the resolved worker count at
    // debug level, so the wiring is observable deterministically.
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_three_call_fixture(dir.path(), "run_options:\n  parallel: 1\n");
    mcptest()
        .current_dir(dir.path())
        .args([
            "run",
            "--config",
            cfg.to_str().unwrap(),
            "--log-level",
            "mcptest_core::runner=debug",
        ])
        .assert()
        .success()
        .stderr(predicate::str::contains(
            "parallelism resolved to 1 workers",
        ));
}

#[test]
fn suite_run_options_parallel_false_serializes_the_run() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_three_call_fixture(dir.path(), "run_options:\n  parallel: false\n");
    mcptest()
        .current_dir(dir.path())
        .args([
            "run",
            "--config",
            cfg.to_str().unwrap(),
            "--log-level",
            "mcptest_core::runner=debug",
        ])
        .assert()
        .success()
        .stderr(predicate::str::contains(
            "parallelism resolved to 1 workers",
        ));
}

#[test]
fn cli_parallel_flag_overrides_suite_run_options() {
    // YAML pins one worker; the explicit CLI flag wins with three.
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_three_call_fixture(dir.path(), "run_options:\n  parallel: 1\n");
    mcptest()
        .current_dir(dir.path())
        .args([
            "run",
            "--config",
            cfg.to_str().unwrap(),
            "--parallel",
            "3",
            "--log-level",
            "mcptest_core::runner=debug",
        ])
        .assert()
        .success()
        .stderr(predicate::str::contains(
            "parallelism resolved to 3 workers",
        ));
}
