//! Integration tests for `model_compat::capture`.
//!
//! Confirms the three load-bearing properties from ADR 0026 from a
//! consumer's perspective: a three-test suite captures cleanly, the
//! output is byte-identical across runs, `--filter` narrows the
//! captured list, and atomic writes leave no partial file behind on a
//! mid-write abort.

use std::sync::Mutex;

use async_trait::async_trait;
use mcptest_core::model_compat::{
    capture_baseline, CaptureOptions, CaptureRunner, CapturedExchange, ContentBlock, FinishReason,
    ModelCompatError, ToolCall,
};

struct InMemoryRunner {
    ids: Vec<String>,
    responses: Mutex<std::collections::HashMap<String, CapturedExchange>>,
}

#[async_trait]
impl CaptureRunner for InMemoryRunner {
    async fn list_tests(&self) -> Result<Vec<String>, ModelCompatError> {
        Ok(self.ids.clone())
    }
    async fn run_test(&self, id: &str) -> Result<CapturedExchange, ModelCompatError> {
        let map = self.responses.lock().expect("lock");
        map.get(id)
            .cloned()
            .ok_or_else(|| ModelCompatError::InvalidFilter {
                pattern: id.into(),
                reason: "unknown test".into(),
            })
    }
}

fn make_runner(ids: &[&str]) -> InMemoryRunner {
    let mut responses = std::collections::HashMap::new();
    for id in ids {
        responses.insert(
            (*id).into(),
            CapturedExchange {
                id: (*id).into(),
                content: vec![ContentBlock::Text {
                    text: format!("response for {id}"),
                }],
                tool_calls: vec![ToolCall {
                    name: "noop".into(),
                    arguments: serde_json::json!({"id": id, "step": 1}),
                    raw_arguments: None,
                }],
                finish_reason: FinishReason::ToolUse,
                mcp_messages: vec![],
            },
        );
    }
    InMemoryRunner {
        ids: ids.iter().map(|s| (*s).to_string()).collect(),
        responses: Mutex::new(responses),
    }
}

#[tokio::test]
async fn captures_three_test_suite_with_pinned_metadata() {
    let runner = make_runner(&[
        "tests/a.yml::t1::1",
        "tests/a.yml::t2::1",
        "tests/b.yml::t1::1",
    ]);
    let dir = tempfile::tempdir().expect("tempdir");
    let path = dir.path().join("baseline.json");
    let opts = CaptureOptions {
        model_id: "anthropic:claude-sonnet-4.5".into(),
        model_version: "20251022".into(),
        mcptest_version: "0.1.0".into(),
        server_version: "1.0.0".into(),
        server_config: serde_json::json!({"timeout_ms": 5000, "max_retries": 3}),
        filter: None,
        invariants: vec![],
        fingerprint: Default::default(),
    };
    let bf = capture_baseline(&runner, &opts, &path)
        .await
        .expect("capture");
    assert_eq!(bf.assertions.len(), 3);
    assert_eq!(bf.model_id, "anthropic:claude-sonnet-4.5");
    assert_eq!(bf.model_version, "20251022");
    assert_eq!(bf.mcptest_version, "0.1.0");
    assert_eq!(bf.server_version, "1.0.0");
    assert!(bf.server_config_hash.starts_with("sha256:"));
    assert_eq!(bf.version, 1);
}

#[tokio::test]
async fn idempotent_capture_yields_byte_identical_files() {
    let runner = make_runner(&["a::t::1", "a::t::2", "a::t::3"]);
    let dir = tempfile::tempdir().expect("tempdir");
    let path = dir.path().join("baseline.json");
    let opts = CaptureOptions {
        model_id: "m".into(),
        model_version: "v1".into(),
        mcptest_version: "0.1.0".into(),
        server_version: "1.0.0".into(),
        server_config: serde_json::json!({"k": "v"}),
        filter: None,
        invariants: vec![],
        fingerprint: Default::default(),
    };
    capture_baseline(&runner, &opts, &path)
        .await
        .expect("first");
    let first = std::fs::read(&path).expect("read first");
    capture_baseline(&runner, &opts, &path)
        .await
        .expect("second");
    let second = std::fs::read(&path).expect("read second");
    assert_eq!(
        first, second,
        "two captures with the same inputs must produce byte-identical files"
    );
}

#[tokio::test]
async fn filter_glob_selects_subset() {
    let runner = make_runner(&[
        "tests/a.yml::t1::1",
        "tests/a.yml::t2::1",
        "tests/b.yml::t1::1",
    ]);
    let dir = tempfile::tempdir().expect("tempdir");
    let path = dir.path().join("baseline.json");
    let opts = CaptureOptions {
        filter: Some("tests/a.yml::*".into()),
        ..CaptureOptions::default()
    };
    let bf = capture_baseline(&runner, &opts, &path)
        .await
        .expect("capture");
    let ids: Vec<&str> = bf.assertions.iter().map(|e| e.id.as_str()).collect();
    assert_eq!(ids, vec!["tests/a.yml::t1::1", "tests/a.yml::t2::1"]);
}

#[tokio::test]
async fn atomic_write_recovery_leaves_no_partial_on_simulated_abort() {
    // Simulate a Ctrl-C in the middle of a write: the temp file lands
    // but the rename never happens. The final baseline path must not
    // exist.
    let dir = tempfile::tempdir().expect("tempdir");
    let target = dir.path().join("baselines/sonnet.json");
    let parent = target.parent().expect("parent");
    std::fs::create_dir_all(parent).expect("mkdir");

    let mut tmp_name = target.file_name().expect("filename").to_os_string();
    tmp_name.push(".tmp");
    let tmp = parent.join(tmp_name);
    std::fs::write(&tmp, b"partially written bytes").expect("write tmp");

    assert!(tmp.exists(), "temp file represents in-flight write");
    assert!(
        !target.exists(),
        "atomic write contract: no partial baseline at the final path"
    );

    // Now run a real capture to confirm the temp leftover does not
    // confuse the writer, and the final file lands.
    let runner = make_runner(&["a::t::1"]);
    let bf = capture_baseline(&runner, &CaptureOptions::default(), &target)
        .await
        .expect("recovery capture");
    assert_eq!(bf.assertions.len(), 1);
    assert!(target.exists());
}
