//! Integration tests for the WOR-1267 authoring control surface:
//! YAML `skip:`/`only:`, the `re:` filter prefix, and `mcptest run
//! --explain`.
//!
//! Execution-path tests run against a cassette server so they need no
//! network and carry a built-in execution detector: the cassette records
//! only the `echo` tool, so any test that calls an unrecorded tool FAILS
//! if (and only if) it actually executes. `--explain` tests use a stdio
//! server whose command does not exist, so any accidental spawn fails
//! the run loudly.

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

fn mcptest() -> Command {
    let mut cmd = Command::cargo_bin("mcptest").expect("binary builds");
    // The CI refusal for `only:` keys off CI=true; clear it so these
    // tests behave the same locally and on CI runners.
    cmd.env_remove("CI");
    cmd
}

/// A cassette with an `initialize` handshake and one recorded `tools/call`
/// for `echo`. Calls to any other tool fail with a replay error, which is
/// how these tests detect (un)wanted execution.
const CASSETTE: &str = r#"{
  "version": "1",
  "exchanges": [
    {
      "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" } } }
    },
    {
      "request": { "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "echo", "arguments": { "msg": "hi" } } },
      "response": { "jsonrpc": "2.0", "id": 2, "result": { "content": [ { "type": "text", "text": "replayed: hi" } ] } }
    }
  ]
}"#;

/// Write the cassette plus a suite body (with `{cassette}` interpolated)
/// into a tempdir and return the suite path.
fn write_suite(dir: &std::path::Path, suite_body: &str) -> std::path::PathBuf {
    let cassette = dir.join("cassette.json");
    std::fs::write(&cassette, CASSETTE).unwrap();
    let cfg = dir.join("mcptest.yaml");
    std::fs::write(
        &cfg,
        suite_body.replace("{cassette}", &cassette.display().to_string()),
    )
    .unwrap();
    cfg
}

/// One passing `echo` test. The second test calls the unrecorded
/// `delete` tool, so it fails if it ever executes.
const PASSING_AND_PARKED: &str = r#"servers:
  recorded:
    cassette: '{cassette}'
tools:
  - name: echo replays
    server: recorded
    tool: echo
    args: { msg: hi }
    expect:
      - target: result.content[0].text
        matcher: { exact: "replayed: hi" }
  - name: parked delete
    server: recorded
    tool: delete
    skip: "waiting on upstream bug 123"
    expect:
      - target: result.isError
        matcher: { exact: false }
"#;

// ----- skip -------------------------------------------------------------

#[test]
fn skipped_test_reports_skip_with_reason_and_does_not_execute() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_suite(dir.path(), PASSING_AND_PARKED);

    // The parked test calls a tool the cassette never recorded, so the
    // run can only succeed if the skip really prevented execution.
    mcptest()
        .args(["run", "--config", cfg.to_str().unwrap()])
        .assert()
        .success()
        .stdout(predicate::str::contains("[SKIP] parked delete"))
        .stdout(predicate::str::contains(
            "skipped: waiting on upstream bug 123",
        ))
        .stdout(predicate::str::contains("1 passed"))
        .stdout(predicate::str::contains("1 skipped"));
}

#[test]
fn skip_requires_a_non_empty_reason_string() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_suite(
        dir.path(),
        r#"servers:
  recorded:
    cassette: '{cassette}'
tools:
  - name: parked
    server: recorded
    tool: echo
    skip: true
"#,
    );

    // A bare boolean skip is rejected at load time (config error, exit 2).
    mcptest()
        .args(["run", "--config", cfg.to_str().unwrap()])
        .assert()
        .code(2);
}

// ----- only --------------------------------------------------------------

/// The focused test passes via the cassette; the unmarked one calls an
/// unrecorded tool, so it fails if `only:` does not exclude it.
const FOCUSED_SUITE: &str = r#"servers:
  recorded:
    cassette: '{cassette}'
tools:
  - name: focused echo
    server: recorded
    tool: echo
    args: { msg: hi }
    only: true
    expect:
      - target: result.content[0].text
        matcher: { exact: "replayed: hi" }
  - name: unmarked delete
    server: recorded
    tool: delete
    expect:
      - target: result.isError
        matcher: { exact: false }
"#;

#[test]
fn only_runs_just_the_marked_test_and_prints_a_loud_warning() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_suite(dir.path(), FOCUSED_SUITE);

    mcptest()
        .args(["run", "--config", cfg.to_str().unwrap()])
        .assert()
        .success()
        .stderr(predicate::str::contains("WARNING: only: true is set"))
        .stdout(predicate::str::contains("[PASS] focused echo"))
        .stdout(predicate::str::contains("[SKIP] unmarked delete"))
        .stdout(predicate::str::contains("skipped: not in only set"))
        .stdout(predicate::str::contains("1 passed"))
        .stdout(predicate::str::contains("1 skipped"));
}

#[test]
fn only_under_ci_fails_with_an_actionable_config_error() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_suite(dir.path(), FOCUSED_SUITE);

    mcptest()
        .env("CI", "true")
        .args(["run", "--config", cfg.to_str().unwrap()])
        .assert()
        .code(2)
        .stderr(predicate::str::contains("only: true"))
        .stderr(predicate::str::contains("CI"))
        .stderr(predicate::str::contains("remove the `only:` marker"));
}

// ----- re: filter ---------------------------------------------------------

/// `create_issue` passes via the cassette; `recreate_db` calls an
/// unrecorded tool, so it fails whenever the filter keeps it. The two
/// tests use separate server entries (same cassette file) so the failing
/// replay cannot disturb the passing test's transport.
const CREATE_RECREATE_SUITE: &str = r#"servers:
  recorded:
    cassette: '{cassette}'
  recorded_b:
    cassette: '{cassette}'
tools:
  - name: create_issue
    server: recorded
    tool: echo
    args: { msg: hi }
    expect:
      - target: result.content[0].text
        matcher: { exact: "replayed: hi" }
  - name: recreate_db
    server: recorded_b
    tool: delete
    expect:
      - target: result.isError
        matcher: { exact: false }
"#;

#[test]
fn re_prefix_filter_matches_anchored_so_recreate_is_excluded() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_suite(dir.path(), CREATE_RECREATE_SUITE);

    // Anchored regex keeps only create_issue; recreate_db never runs, so
    // the run passes.
    mcptest()
        .args([
            "run",
            "--config",
            cfg.to_str().unwrap(),
            "--filter",
            "re:^create_",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains("[PASS] create_issue"))
        .stdout(predicate::str::contains("1 passed"));
}

#[test]
fn plain_filter_keeps_substring_semantics_and_matches_both() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_suite(dir.path(), CREATE_RECREATE_SUITE);

    // Substring matching keeps recreate_db too; its unrecorded tool call
    // fails, which proves the plain form still matches anywhere.
    mcptest()
        .args([
            "run",
            "--config",
            cfg.to_str().unwrap(),
            "--filter",
            "create_",
        ])
        .assert()
        .failure()
        .stdout(predicate::str::contains("1 passed"))
        .stdout(predicate::str::contains("1 failed"));
}

#[test]
fn invalid_re_filter_exits_2_with_an_actionable_error() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_suite(dir.path(), CREATE_RECREATE_SUITE);

    mcptest()
        .args([
            "run",
            "--config",
            cfg.to_str().unwrap(),
            "--filter",
            "re:^create_(",
        ])
        .assert()
        .code(2)
        .stderr(predicate::str::contains("invalid --filter regex"));
}

// ----- --explain -----------------------------------------------------------

/// Suite whose stdio server command does not exist: any spawn attempt
/// fails the run loudly, so a passing `--explain` proves nothing executed.
const EXPLAIN_SUITE: &str = r#"servers:
  ghost:
    command: ["/nonexistent/mcptest-explain-must-not-spawn"]
tools:
  - name: create_issue
    server: ghost
    tool: create_issue
    args: { title: "hello" }
    expect:
      - target: result.isError
        matcher: { exact: false }
      - target: result.content[0].text
        matcher: { regex: "^ok" }
agents:
  - name: triage agent
    model: claude-sonnet-4-5
    servers: [ghost]
    prompt: "triage the open issues"
    max_turns: 3
    time_budget: "30s"
    expect:
      - target: tools.called
        matcher: { contains: ["create_issue"] }
"#;

#[test]
fn explain_lists_tests_assertions_and_budgets_without_executing() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    std::fs::write(&cfg, EXPLAIN_SUITE).unwrap();

    mcptest()
        .args(["run", "--config", cfg.to_str().unwrap(), "--explain"])
        .assert()
        .success()
        .stdout(predicate::str::contains("dry run"))
        .stdout(predicate::str::contains("RUN create_issue"))
        .stdout(predicate::str::contains(
            "tool create_issue on server ghost",
        ))
        .stdout(predicate::str::contains(
            "expect result.isError exact false",
        ))
        .stdout(predicate::str::contains(
            "expect result.content[0].text regex \"^ok\"",
        ))
        .stdout(predicate::str::contains("RUN triage agent"))
        .stdout(predicate::str::contains(
            "budgets: max_turns=3, max_tokens=1024, runs=1, time_budget=30s",
        ))
        .stdout(predicate::str::contains("2 test(s) would run, 0 skipped"));
}

#[test]
fn explain_honors_filter_and_reflects_skip_and_only() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = dir.path().join("mcptest.yaml");
    std::fs::write(
        &cfg,
        r#"servers:
  ghost:
    command: ["/nonexistent/mcptest-explain-must-not-spawn"]
tools:
  - name: focused
    server: ghost
    tool: a
    only: true
    expect:
      - target: result.isError
        matcher: { exact: false }
  - name: parked
    server: ghost
    tool: b
    skip: "blocked on WOR upstream"
  - name: unmarked
    server: ghost
    tool: c
"#,
    )
    .unwrap();

    mcptest()
        .args(["run", "--config", cfg.to_str().unwrap(), "--explain"])
        .assert()
        .success()
        .stdout(predicate::str::contains("only: true is active"))
        .stdout(predicate::str::contains("RUN focused"))
        .stdout(predicate::str::contains(
            "SKIP parked (blocked on WOR upstream)",
        ))
        .stdout(predicate::str::contains("SKIP unmarked (not in only set)"))
        .stdout(predicate::str::contains("1 test(s) would run, 2 skipped"));

    // --filter composes: narrow to the parked test only.
    mcptest()
        .args([
            "run",
            "--config",
            cfg.to_str().unwrap(),
            "--explain",
            "--filter",
            "parked",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains(
            "SKIP parked (blocked on WOR upstream)",
        ))
        .stdout(predicate::str::contains("0 test(s) would run, 1 skipped"));
}

// ----- --print-config -------------------------------------------------------

#[test]
fn print_config_shows_skip_reasons_and_only_state() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_suite(dir.path(), FOCUSED_SUITE);

    mcptest()
        .args(["run", "--config", cfg.to_str().unwrap(), "--print-config"])
        .assert()
        .success()
        .stdout(predicate::str::contains("only: ACTIVE"))
        .stdout(predicate::str::contains("tests skipped (1):"))
        .stdout(predicate::str::contains("unmarked delete: not in only set"));
}

#[test]
fn collect_only_lists_skipped_tests_with_their_reason() {
    let dir = tempfile::tempdir().unwrap();
    let cfg = write_suite(dir.path(), PASSING_AND_PARKED);

    mcptest()
        .args(["run", "--config", cfg.to_str().unwrap(), "--collect-only"])
        .assert()
        .success()
        .stdout(predicate::str::contains("echo replays"))
        .stdout(predicate::str::contains(
            "parked delete (skipped: waiting on upstream bug 123)",
        ))
        .stdout(predicate::str::contains("2 test(s) discovered (1 skipped)"));
}
