//! Integration tests for `mcptest scenario run`.
//!
//! Drives the binary over the shipped scenario-world example (deterministic
//! cassette replay, no live model) and over a synthetic failing scenario,
//! asserting the exit code (the CI gate) and the human-readable summary.

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

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

/// Workspace root, so example paths resolve regardless of the test's CWD
/// (assert_cmd runs from the package directory, not the workspace root).
fn workspace_root() -> std::path::PathBuf {
    std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .ancestors()
        .nth(2)
        .expect("crates/mcptest is two levels below the workspace root")
        .to_path_buf()
}

/// The benign and adversarial example scenarios both pass and exit zero.
#[test]
fn the_shipped_example_passes_offline() {
    mcptest()
        .current_dir(workspace_root())
        .args(["scenario", "run", "examples/scenario-world/suite.yml"])
        .assert()
        .success()
        .stdout(predicate::str::contains("PASS restock the widget shelf"))
        .stdout(predicate::str::contains(
            "PASS refuse an unsafe inventory wipe",
        ))
        .stdout(predicate::str::contains("escalations=2 refusals=1"));
}

/// `--name` selects a single scenario and `--json` emits the structured report.
#[test]
fn name_filter_and_json_output() {
    mcptest()
        .current_dir(workspace_root())
        .args([
            "scenario",
            "run",
            "examples/scenario-world/suite.yml",
            "--name",
            "restock the widget shelf",
            "--json",
        ])
        .assert()
        .success()
        .stdout(predicate::str::contains(
            "\"name\": \"restock the widget shelf\"",
        ))
        .stdout(predicate::str::contains("\"final_state\""))
        .stdout(predicate::str::contains("refuse an unsafe inventory wipe").not());
}

/// A scenario whose expected final state does not match the replayed
/// trajectory fails and exits non-zero, naming the mismatch.
#[test]
fn an_expected_state_mismatch_fails_the_gate() {
    let dir = tempdir().unwrap();
    let cassettes = dir.path().join("cassettes");
    std::fs::create_dir_all(&cassettes).unwrap();
    std::fs::write(
        cassettes.join("c.json"),
        r#"{
          "schema_version": "v1",
          "agent_name": "s",
          "model": "stub",
          "user_prompt": "go",
          "recorded_at": "2026-06-21T00:00:00Z",
          "trace": {
            "tool_calls": [{ "name": "add_widget", "server": "inv", "args": {}, "hop_index": 0 }],
            "tool_results": [{ "call_index": 0, "server": "inv", "is_error": false, "result": {} }],
            "final_response": "done",
            "conversation": {
              "tokens": { "prompt": 1, "completion": 1, "total": 2 },
              "duration_ms": 1, "message_count": 1, "stop_reason": "completed", "turns": []
            }
          }
        }"#,
    )
    .unwrap();
    let suite = dir.path().join("suite.yml");
    std::fs::write(
        &suite,
        r#"scenarios:
  - name: off by one
    cassette: cassettes/c.json
    seed:
      widgets: 3
    transitions:
      - tool: add_widget
        effect:
          widgets: { inc: 1 }
    expect_state:
      widgets: 9
"#,
    )
    .unwrap();

    mcptest()
        .args(["scenario", "run", suite.to_str().unwrap()])
        .assert()
        .failure()
        .stdout(predicate::str::contains("FAIL off by one"))
        .stdout(predicate::str::contains(
            "state `widgets` expected 9 but world held 4",
        ));
}
