//! Integration tests for the compliance lane of `mcptest run` (WOR-1482).
//!
//! A suite that declares a `compliance:` block has its checks run 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
//! `mcptest mock` server, one tool test so the run reaches the post-drive
//! lanes, plus a `compliance:` block of raw protocol checks) and assert the
//! `compliance/<rule>` rows and the gated exit code, all key-free and offline.

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 suites.
fn mcptest_bin() -> &'static str {
    env!("CARGO_BIN_EXE_mcptest")
}

/// Write a mock manifest exposing one `echo` tool, the live server every
/// check and the ride-along tool test in this file probes.
fn write_manifest(dir: &std::path::Path) -> std::path::PathBuf {
    let manifest = r#"mock_server:
  name: echo
  tools:
    - name: echo
      description: "Echo a fixed line."
      input_schema:
        type: object
      response:
        isError: false
        content:
          - type: text
            text: "hello"
"#;
    let path = dir.join("echo.yaml");
    std::fs::write(&path, manifest).expect("write mock manifest");
    path
}

/// Render a suite with one offline tool test (so selection is non-empty and
/// the run reaches the lanes) plus a `compliance:` block. The block's
/// `tools/list` check carries the given expectation `schema:`, so the caller
/// chooses a passing (`type: array`) or failing (impossible) check.
fn write_suite(dir: &std::path::Path, tools_list_schema: &str) -> std::path::PathBuf {
    let manifest = write_manifest(dir);
    let suite = format!(
        r#"servers:
  mock:
    command: ['{bin}', 'mock', '--tools-from', '{manifest}']
tools:
  - name: echo round-trips
    server: mock
    tool: echo
    expect:
      - target: result.content[0].text
        matcher: {{ exact: "hello" }}
compliance:
  - name: PROTO-002 initialize returns capabilities
    server: mock
    check: "initialize"
    expect:
      - target: result.capabilities
        matcher:
          schema: {{ type: object }}
  - name: TOOL-001 tools/list responds
    server: mock
    check: "tools/list"
    expect:
      - target: result.tools
        matcher:
          schema: {tools_list_schema}
"#,
        bin = mcptest_bin(),
        manifest = manifest.display(),
    );
    let path = dir.join("suite.yml");
    std::fs::write(&path, suite).expect("write suite");
    path
}

/// Passing compliance checks fold in as passing rows: the run exits zero and
/// the summary counts the tool test plus both compliance checks as passed.
#[test]
fn a_passing_compliance_block_folds_into_the_run_as_passing_rows() {
    let dir = tempfile::tempdir().unwrap();
    let suite = write_suite(dir.path(), "{ type: array, minItems: 1 }");
    mcptest()
        .args(["run", "--config", suite.to_str().unwrap()])
        .assert()
        .success()
        // The tool test plus both compliance checks all pass.
        .stdout(predicate::str::contains("3 passed"))
        .stdout(predicate::str::contains("compliance/PROTO-002"))
        .stdout(predicate::str::contains("compliance/TOOL-001"));
}

/// A compliance check whose expectation cannot match fails the whole run: the
/// exit code is non-zero and the `compliance/<rule>` row is named on stdout so
/// the failure is attributable, while the tool test and the other check still
/// pass.
#[test]
fn a_failing_compliance_check_gates_the_run_and_names_its_row() {
    let dir = tempfile::tempdir().unwrap();
    // tools/list returns an array, so requiring an object can never match.
    let suite = write_suite(dir.path(), "{ type: object }");
    mcptest()
        .args(["run", "--config", suite.to_str().unwrap()])
        .assert()
        .failure()
        .stdout(predicate::str::contains("compliance/TOOL-001"))
        // The tool test and the initialize check pass; only the tools/list
        // compliance check fails.
        .stdout(predicate::str::contains("2 passed").and(predicate::str::contains("1 failed")));
}
