//! Integration tests for the security scan lane of `mcptest run` (WOR-1483).
//!
//! A suite that opts in with `security: { scan: true }` has the deterministic
//! security scan run as a lane during `mcptest run`: the lane connects the
//! suite's servers, snapshots each `tools/list`, runs the deterministic scan,
//! and folds each finding into the single report and exit code alongside the
//! tool tests. These tests pair the scan with the built-in `evil` mock preset,
//! whose `read_file` tool carries a poisoned description that deterministically
//! fires SEC-001 (a High finding), plus one passing tool test so selection is
//! non-empty and the run reaches the post-drive lanes (WOR-1490). Everything is
//! offline and key-free.

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

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

/// Absolute path to the built `mcptest` binary, used as the mock server command
/// inside the generated suite.
fn mcptest_bin() -> &'static str {
    env!("CARGO_BIN_EXE_mcptest")
}

/// Write a suite that runs the built-in `evil` mock as its server, asserts one
/// passing tool test against its (poisoned) `read_file` tool so the run reaches
/// the lanes, and opts into the security scan with `scan`. The `read_file`
/// description carries a hidden model-directed instruction, so the deterministic
/// surface lane raises SEC-001 (High) when `scan` is on.
fn write_suite(dir: &std::path::Path, scan: bool) -> std::path::PathBuf {
    let suite = format!(
        r#"servers:
  evil:
    command: ['{bin}', 'mock', '--preset', 'evil']
tools:
  - name: read_file returns content
    server: evil
    tool: read_file
    args: {{ path: "/etc/hosts" }}
    expect:
      - target: result.content[0].text
        matcher: {{ icontains: "File contents" }}
security:
  scan: {scan}
"#,
        bin = mcptest_bin(),
        scan = scan,
    );
    let path = dir.join("suite.yml");
    std::fs::write(&path, suite).expect("write suite");
    path
}

/// With `scan: true`, the deterministic scan over the evil mock's tool surface
/// fires SEC-001 on the poisoned `read_file` description. That High finding
/// gates the run: the exit code is non-zero and the `security/SEC-001` row is
/// named on stdout, while the tool test still passes.
#[test]
fn the_security_scan_lane_gates_the_run_on_a_poisoned_tool_description() {
    let dir = tempfile::tempdir().unwrap();
    let suite = write_suite(dir.path(), true);
    mcptest()
        .args(["run", "--config", suite.to_str().unwrap()])
        .assert()
        .failure()
        .stdout(predicate::str::contains("security/SEC-001"));
}

/// With `scan: false`, the security lane is a no-op: the run sees only the tool
/// test, which passes, so the exit code is zero and no `security/` row appears.
/// This pins the opt-in: an existing suite without the flag is unchanged.
#[test]
fn scan_false_leaves_the_run_untouched() {
    let dir = tempfile::tempdir().unwrap();
    let suite = write_suite(dir.path(), false);
    mcptest()
        .args(["run", "--config", suite.to_str().unwrap()])
        .assert()
        .success()
        .stdout(predicate::str::contains("security/").not());
}
