//! Integration tests for `model_compat::diff`.
//!
//! Loads each of the 18 fixtures under `tests/fixtures/model-compat/`,
//! converts the `baseline.json` / `candidate.json` pair into the
//! library's typed shape, and asserts the diff engine produces the
//! classification pinned in `expected.yaml`.

use std::path::{Path, PathBuf};

use mcptest_core::model_compat::{
    diff_baselines, BaselineFile, CapturedExchange, ConfigFingerprint, ContentBlock, FinishReason,
    Severity, ToolCall,
};

fn fixtures_root() -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("..")
        .join("..")
        .join("tests")
        .join("fixtures")
        .join("model-compat")
}

#[derive(serde::Deserialize)]
struct Expected {
    classification: String,
    #[serde(default)]
    invariants_violated: Vec<String>,
    #[serde(default)]
    drift_kind: Option<String>,
}

/// Helper shape that captures a tool call's `arguments` in two passes:
/// once as a `serde_json::Value` (BTreeMap-backed, used for structural
/// comparison) and once as a `&RawValue` (source bytes, used by the
/// diff engine for key-reorder drift detection).
#[derive(serde::Deserialize)]
struct RawToolCall<'a> {
    #[serde(default)]
    name: String,
    #[serde(borrow)]
    arguments: Option<&'a serde_json::value::RawValue>,
}

#[derive(serde::Deserialize)]
struct RawExchange<'a> {
    #[serde(default)]
    content: Vec<serde_json::Value>,
    #[serde(default, borrow)]
    tool_calls: Vec<RawToolCall<'a>>,
    #[serde(default)]
    finish_reason: Option<String>,
}

fn load_exchange(path: &Path) -> CapturedExchange {
    let raw = std::fs::read_to_string(path).expect("read fixture json");
    let parsed: RawExchange<'_> = serde_json::from_str(&raw).expect("parse fixture json");

    let content = parsed
        .content
        .iter()
        .map(|block| match block.get("text").and_then(|t| t.as_str()) {
            Some(text) => ContentBlock::Text { text: text.into() },
            None => ContentBlock::Other,
        })
        .collect();

    let tool_calls = parsed
        .tool_calls
        .into_iter()
        .map(|c| {
            let raw_args = c.arguments.map(|r| r.get().to_owned());
            let arguments = raw_args
                .as_deref()
                .map(|s| serde_json::from_str(s).expect("re-parse arguments"))
                .unwrap_or(serde_json::Value::Null);
            ToolCall {
                name: c.name,
                arguments,
                raw_arguments: raw_args,
            }
        })
        .collect();

    let finish_reason = match parsed.finish_reason.as_deref() {
        Some("stop") => FinishReason::Stop,
        Some("tool_use") => FinishReason::ToolUse,
        Some("max_tokens") => FinishReason::MaxTokens,
        _ => FinishReason::Other,
    };

    CapturedExchange {
        id: path
            .parent()
            .and_then(|p| p.file_name())
            .map(|n| format!("{}::default::1", n.to_string_lossy()))
            .unwrap_or_else(|| "fixture::default::1".into()),
        content,
        tool_calls,
        finish_reason,
        mcp_messages: vec![],
    }
}

fn baseline_with(model: &str, ex: CapturedExchange) -> BaselineFile {
    BaselineFile {
        schema: "https://mcptest.sh/schema/model-compat/v1.json".into(),
        version: 1,
        model_id: model.into(),
        model_version: "x".into(),
        mcptest_version: "0.0.0".into(),
        server_version: "0.0.0".into(),
        server_config_hash: "sha256:0".into(),
        config_fingerprint: ConfigFingerprint::default(),
        assertions: vec![ex],
        invariants: vec![],
    }
}

fn list_scenarios() -> Vec<PathBuf> {
    let mut out: Vec<PathBuf> = std::fs::read_dir(fixtures_root())
        .expect("read fixtures dir")
        .filter_map(|e| e.ok())
        .map(|e| e.path())
        .filter(|p| p.is_dir())
        .collect();
    out.sort();
    out
}

fn dominant_severity(entries: &[mcptest_core::model_compat::DiffEntry]) -> Severity {
    if entries.iter().any(|e| matches!(e.severity, Severity::Fail)) {
        Severity::Fail
    } else if entries
        .iter()
        .any(|e| matches!(e.severity, Severity::Drift))
    {
        Severity::Drift
    } else {
        Severity::Pass
    }
}

#[test]
fn every_fixture_dominant_severity_matches_expected_classification() {
    for scenario in list_scenarios() {
        let baseline_ex = load_exchange(&scenario.join("baseline.json"));
        let candidate_ex = load_exchange(&scenario.join("candidate.json"));
        let raw_expected =
            std::fs::read_to_string(scenario.join("expected.yaml")).expect("read expected");
        let expected: Expected = serde_yaml::from_str(&raw_expected).expect("parse expected");

        let a = baseline_with("baseline", baseline_ex);
        let b = baseline_with("candidate", candidate_ex);
        let diff = diff_baselines(&a, &b).expect("diff runs");

        let actual = dominant_severity(&diff.entries);
        let want = match expected.classification.as_str() {
            "PASS" => Severity::Pass,
            "DRIFT" => Severity::Drift,
            "FAIL" => Severity::Fail,
            other => panic!("unknown classification {other}"),
        };

        assert_eq!(
            actual,
            want,
            "fixture {} expected {:?}, got {:?} (entries: {:#?}, invariants_violated: {:?}, drift_kind: {:?})",
            scenario.display(),
            want,
            actual,
            diff.entries,
            expected.invariants_violated,
            expected.drift_kind
        );
    }
}
