//! Integration tests for the scenarios lane of `mcptest run` (WOR-1485).
//!
//! A suite that declares a `scenarios:` block has its scenarios replayed as a
//! lane during `mcptest run`, folded into the single report and exit code
//! alongside the tool tests. These tests write a self-contained suite (an
//! offline cassette-server tool test plus a scenario over an agent-trace
//! cassette) so the run reaches the post-drive lanes, and assert the scenario
//! row and the gated exit code.

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

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

/// A cassette-server tape (the `version`/`exchanges` replay format) with the
/// handshake plus one recorded `tools/call`, so the suite has one selectable,
/// offline tool test. Without a selectable test the run short-circuits before
/// the lanes ever execute, so the scenario needs this companion test to ride
/// along.
const SERVER_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" } ] } }
    }
  ]
}"#;

/// An agent-trace cassette (the scenario replay format) that records one
/// `add_widget` call, taking the seeded `widgets` count from 3 to 4.
const SCENARIO_CASSETTE: &str = 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": []
    }
  }
}"#;

/// Write both cassettes into a fresh tempdir and return its path plus the suite
/// path. `expected_widgets` sets the scenario's `expect_state`, so the caller
/// chooses a passing (4) or failing (other) scenario.
fn write_suite(expected_widgets: u32) -> (tempfile::TempDir, std::path::PathBuf) {
    let dir = tempfile::tempdir().unwrap();
    std::fs::write(dir.path().join("server.json"), SERVER_CASSETTE).unwrap();
    std::fs::write(dir.path().join("scenario.json"), SCENARIO_CASSETTE).unwrap();
    let suite = dir.path().join("suite.yml");
    std::fs::write(
        &suite,
        format!(
            r#"servers:
  recorded:
    cassette: server.json
tools:
  - name: echo replays from cassette
    server: recorded
    tool: echo
    args: {{ msg: hi }}
    expect:
      - target: result.content[0].text
        matcher: {{ exact: "replayed: hi" }}
scenarios:
  - name: add one widget
    cassette: scenario.json
    seed:
      widgets: 3
    transitions:
      - tool: add_widget
        effect:
          widgets: {{ inc: 1 }}
    expect_state:
      widgets: {expected_widgets}
"#
        ),
    )
    .unwrap();
    (dir, suite)
}

/// A passing scenario folds in as a passing row: the run exits zero and the
/// summary counts both the tool test and the scenario as passed.
#[test]
fn a_passing_scenario_folds_into_the_run_as_a_passing_row() {
    let (_dir, suite) = write_suite(4);
    mcptest()
        .args(["run", "--config", suite.to_str().unwrap()])
        .assert()
        .success()
        .stdout(predicate::str::contains("2 passed"));
}

/// A scenario whose `expect_state` does not match the replayed trajectory fails
/// the whole run: the exit code is non-zero and the `scenario/<name>` row is
/// named on stdout so the failure is attributable.
#[test]
fn a_failing_scenario_gates_the_run_and_names_its_row() {
    // The cassette adds one widget (3 -> 4), but the suite expects 9.
    let (_dir, suite) = write_suite(9);
    mcptest()
        .args(["run", "--config", suite.to_str().unwrap()])
        .assert()
        .failure()
        .stdout(predicate::str::contains("scenario/add one widget"))
        // The tool test still passes; only the scenario lane fails.
        .stdout(predicate::str::contains("1 passed").and(predicate::str::contains("1 failed")));
}
