//! Integration tests for the model-compat lane of `mcptest run` (WOR-1486).
//!
//! A suite that declares a `model_compat:` block has its baseline and candidate
//! snapshots diffed offline during `mcptest run`, with the verdict folded into
//! the single report and exit code alongside the tool tests. This test writes a
//! self-contained suite (an offline `mcptest mock` server, one tool test so the
//! run reaches the post-drive lanes, plus a `model_compat:` block pointing at
//! copies of the bundled fixtures) and asserts the `model-compat/*` row and the
//! exit code, all key-free and offline.
//!
//! The bundled fixtures differ only by a trailing period in one response's text
//! (`"Transfer succeeded"` -> `"Transfer succeeded."`). The offline diff engine
//! classifies a text rephrase with no invariant violation as DRIFT, not FAIL,
//! so this lane reports non-breaking drift as advisory: the run still exits 0
//! and the `model-compat/...DRIFT` row appears in the report.

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

/// Baseline snapshot, a copy of the bundled
/// `tests/fixtures/model-compat/baseline.json`.
const BASELINE_JSON: &str = r#"{
  "schema": "https://mcptest.sh/schema/model-compat/v1.json",
  "version": 1,
  "model_id": "anthropic:claude-sonnet-4.5",
  "model_version": "20251022",
  "mcptest_version": "1.1.0",
  "server_version": "1.0.0",
  "server_config_hash": "sha256:0",
  "config_fingerprint": {
    "invariants": [
      "tool_called",
      "tool_order",
      "tool_args",
      "response_shape",
      "finish_reason"
    ],
    "variances": [
      "text_content",
      "field_order",
      "whitespace",
      "case",
      "additive_fields"
    ]
  },
  "assertions": [
    {
      "id": "tests/example.yaml::lookup::1",
      "content": [
        {
          "type": "text",
          "text": "Account 42 has balance 100"
        }
      ],
      "tool_calls": [],
      "finish_reason": "stop"
    },
    {
      "id": "tests/example.yaml::lookup::2",
      "content": [
        {
          "type": "text",
          "text": "Transfer succeeded"
        }
      ],
      "tool_calls": [],
      "finish_reason": "stop"
    }
  ],
  "invariants": []
}
"#;

/// Candidate snapshot, a copy of the bundled
/// `tests/fixtures/model-compat/candidate.json`. The only difference from the
/// baseline is the trailing period on the second response's text, a text
/// rephrase the engine classifies as non-breaking DRIFT.
const CANDIDATE_JSON: &str = r#"{
  "schema": "https://mcptest.sh/schema/model-compat/v1.json",
  "version": 1,
  "model_id": "anthropic:claude-sonnet-5.0",
  "model_version": "20260101",
  "mcptest_version": "1.1.0",
  "server_version": "1.0.0",
  "server_config_hash": "sha256:0",
  "config_fingerprint": {
    "invariants": [
      "tool_called",
      "tool_order",
      "tool_args",
      "response_shape",
      "finish_reason"
    ],
    "variances": [
      "text_content",
      "field_order",
      "whitespace",
      "case",
      "additive_fields"
    ]
  },
  "assertions": [
    {
      "id": "tests/example.yaml::lookup::1",
      "content": [
        {
          "type": "text",
          "text": "Account 42 has balance 100"
        }
      ],
      "tool_calls": [],
      "finish_reason": "stop"
    },
    {
      "id": "tests/example.yaml::lookup::2",
      "content": [
        {
          "type": "text",
          "text": "Transfer succeeded."
        }
      ],
      "tool_calls": [],
      "finish_reason": "stop"
    }
  ],
  "invariants": []
}
"#;

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 mock manifest exposing one `echo` tool, the live server the
/// ride-along tool test probes so selection is non-empty.
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 `model_compat:` block pointing at the two
/// snapshot copies written next to it.
fn write_suite(dir: &std::path::Path) -> std::path::PathBuf {
    let manifest = write_manifest(dir);
    std::fs::write(dir.join("baseline.json"), BASELINE_JSON).expect("write baseline fixture");
    std::fs::write(dir.join("candidate.json"), CANDIDATE_JSON).expect("write candidate fixture");
    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" }}
model_compat:
  baseline: baseline.json
  candidate: candidate.json
"#,
        bin = mcptest_bin(),
        manifest = manifest.display(),
    );
    let path = dir.join("suite.yml");
    std::fs::write(&path, suite).expect("write suite");
    path
}

/// The bundled fixtures differ only by a trailing period, which the diff engine
/// classifies as non-breaking DRIFT. The model-compat lane reports that drift
/// as an advisory row, so the run still exits zero: the tool test passes, the
/// `model-compat/...DRIFT` row appears, and the lane never gates CI.
#[test]
fn a_drifting_model_compat_block_folds_in_as_an_advisory_row_without_failing_the_run() {
    let dir = tempfile::tempdir().unwrap();
    let suite = write_suite(dir.path());
    mcptest()
        .args(["run", "--config", suite.to_str().unwrap()])
        .assert()
        .success()
        .stdout(predicate::str::contains("model-compat"))
        .stdout(predicate::str::contains("DRIFT"));
}
