//! Cassette record/replay/verify integration tests.
//!
//! Three end-to-end flows exercised here without spawning real MCP
//! servers (the runner state machine that drives record-mode is still
//! gated on ):
//!
//! 1. Build a synthetic 3-exchange cassette, persist it as YAML, and
//!    confirm a strict round trip through the on-disk format.
//! 2. Verify the cassette against an "actual" stream that mutates one
//!    leaf value: the verifier surfaces the delta with the right JSON
//!    Pointer.
//! 3. Replay the cassette against a sequence of live envelopes (no
//!    server involved); every exchange resolves to a recorded response
//!    under the semantic strategy.

use mcptest_core::cassette::{
    replay, replay_with_strategy, verify_exchange, Cassette, Exchange, ReplayOutcome,
    ReplayStrategy,
};
use mcptest_core::protocol::{JsonRpcId, JsonRpcMessage};
use serde_json::json;
use std::fs;
use tempfile::TempDir;

fn three_exchange_cassette() -> Cassette {
    let mut c = Cassette::new();
    c.meta
        .insert("recorded_by".into(), "mcptest-test/0.0.1".into());
    c.push(Exchange {
        request: JsonRpcMessage::request(
            JsonRpcId::Number(1),
            "initialize",
            json!({"protocolVersion": "2025-06-18"}),
        ),
        response: JsonRpcMessage::response(
            JsonRpcId::Number(1),
            json!({"protocolVersion": "2025-06-18", "capabilities": {}}),
        ),
    });
    c.push(Exchange {
        request: JsonRpcMessage::request(JsonRpcId::Number(2), "tools/list", json!({})),
        response: JsonRpcMessage::response(
            JsonRpcId::Number(2),
            json!({"tools": [{"name": "search"}]}),
        ),
    });
    c.push(Exchange {
        request: JsonRpcMessage::request(
            JsonRpcId::Number(3),
            "tools/call",
            json!({"name": "search", "arguments": {"q": "mcp"}}),
        ),
        response: JsonRpcMessage::response(
            JsonRpcId::Number(3),
            json!({"content": [{"type": "text", "text": "hit"}], "id": "11111111-2222-4222-8222-555555555555"}),
        ),
    });
    c
}

#[test]
fn cassette_writes_to_yaml_and_round_trips_through_disk() {
    let tmp = TempDir::new().unwrap();
    let path = tmp.path().join("session.yml");
    let original = three_exchange_cassette();

    let s = serde_yaml::to_string(&original).expect("serialize yaml");
    fs::write(&path, s).expect("write");
    let body = fs::read_to_string(&path).expect("read");
    let reloaded: Cassette = serde_yaml::from_str(&body).expect("deserialize yaml");

    assert_eq!(original, reloaded);
    assert_eq!(original.exchanges.len(), 3);
}

#[test]
fn semantic_replay_walks_live_request_sequence_through_cassette() {
    let cassette = three_exchange_cassette();
    let live = [
        JsonRpcMessage::request(
            JsonRpcId::Number(100),
            "initialize",
            json!({"protocolVersion": "2025-06-18"}),
        ),
        JsonRpcMessage::request(JsonRpcId::Number(101), "tools/list", json!({})),
        JsonRpcMessage::request(
            JsonRpcId::Number(102),
            "tools/call",
            json!({"name": "search", "arguments": {"q": "mcp"}}),
        ),
    ];
    for (i, req) in live.iter().enumerate() {
        match replay(&cassette, i, req) {
            ReplayOutcome::Match(_) => {}
            other => panic!("exchange {i} did not match: {other:?}"),
        }
    }
    // Past the end, the cassette signals exhaustion.
    let extra = JsonRpcMessage::request(JsonRpcId::Number(999), "tools/list", json!({}));
    assert_eq!(replay(&cassette, 3, &extra), ReplayOutcome::Exhausted);
}

#[test]
fn structural_replay_accepts_different_argument_values() {
    let cassette = three_exchange_cassette();
    // Different `q` value but identical shape: the structural strategy
    // should still match.
    let live = JsonRpcMessage::request(
        JsonRpcId::Number(50),
        "tools/call",
        json!({"name": "search", "arguments": {"q": "completely-different-query"}}),
    );
    match replay_with_strategy(&cassette, 2, &live, ReplayStrategy::Structural) {
        ReplayOutcome::Match(_) => {}
        other => panic!("structural strategy should have matched: {other:?}"),
    }
}

#[test]
fn strict_replay_rejects_different_argument_values() {
    let cassette = three_exchange_cassette();
    let live = JsonRpcMessage::request(
        JsonRpcId::Number(50),
        "tools/call",
        json!({"name": "search", "arguments": {"q": "different"}}),
    );
    match replay_with_strategy(&cassette, 2, &live, ReplayStrategy::Strict) {
        ReplayOutcome::Mismatch { .. } => {}
        other => panic!("strict strategy must reject value drift: {other:?}"),
    }
}

#[test]
fn verify_reports_no_delta_when_response_only_drifts_in_volatile_fields() {
    let cassette = three_exchange_cassette();
    // Simulate the live server returning a different UUID at the same
    // path. Under the semantic strategy the verifier should be silent.
    // Both UUIDs match the v4 regex (variant byte must be 8/9/a/b).
    let mut actual = cassette.exchanges[2].response.clone();
    actual.result = Some(json!({
        "content": [{"type": "text", "text": "hit"}],
        "id": "aaaaaaaa-bbbb-4ccc-bddd-eeeeeeeeeeee",
    }));
    let deltas = verify_exchange(
        &cassette.exchanges[2].response,
        &actual,
        ReplayStrategy::Semantic,
    );
    assert!(
        deltas.is_empty(),
        "semantic strategy should ignore volatile UUID: {deltas:?}"
    );
}

#[test]
fn verify_surfaces_real_response_drift() {
    let cassette = three_exchange_cassette();
    let mut actual = cassette.exchanges[1].response.clone();
    actual.result = Some(json!({"tools": [{"name": "search"}, {"name": "ping"}]}));
    let deltas = verify_exchange(
        &cassette.exchanges[1].response,
        &actual,
        ReplayStrategy::Strict,
    );
    assert!(!deltas.is_empty(), "added tool should surface as a delta");
    assert!(
        deltas.iter().any(|d| d.path.contains("tools")),
        "delta should point at /result/tools: {deltas:?}"
    );
}

#[test]
fn verify_surfaces_method_drift_under_every_strategy() {
    let recorded = JsonRpcMessage::request(JsonRpcId::Number(1), "tools/list", json!({}));
    let actual = JsonRpcMessage::request(JsonRpcId::Number(1), "tools/call", json!({}));
    for s in [
        ReplayStrategy::Strict,
        ReplayStrategy::Semantic,
        ReplayStrategy::Structural,
    ] {
        let deltas = verify_exchange(&recorded, &actual, s);
        assert!(
            deltas.iter().any(|d| d.path == "/method"),
            "{s:?} missed method drift"
        );
    }
}

#[test]
fn cassette_schema_validates_persisted_yaml_form() {
    // Load the published JSON Schema and check the cassette serializes
    // to a payload that the schema accepts. Catches schema drift the
    // moment the in-memory format changes.
    let schema_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("..")
        .join("..")
        .join("schemas")
        .join("cassette")
        .join("v1.json");
    let schema_body = std::fs::read_to_string(&schema_path).expect("schema present");
    let schema_value: serde_json::Value =
        serde_json::from_str(&schema_body).expect("schema valid json");
    let validator = jsonschema::validator_for(&schema_value).expect("schema compiles");

    let cassette = three_exchange_cassette();
    // Convert through JSON since the schema describes the canonical
    // on-disk JSON shape (YAML uses the same model via serde).
    let json_value = serde_json::to_value(&cassette).expect("cassette to json");
    let errors: Vec<_> = validator.iter_errors(&json_value).collect();
    assert!(
        errors.is_empty(),
        "cassette must validate against published schema: {errors:?}"
    );
}
