//! Integration tests for `model_compat::invariants`.
//!
//! Confirms that an invariant declared on a baseline surfaces as a
//! FAIL entry in the diff regardless of how the surface diff alone
//! would have classified the pair.

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

fn baseline_with(items: Vec<CapturedExchange>, invariants: Vec<Invariant>) -> BaselineFile {
    BaselineFile {
        schema: "https://mcptest.sh/schema/model-compat/v1.json".into(),
        version: 1,
        model_id: "m".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: items,
        invariants,
    }
}

fn exchange_with_tools(id: &str, tools: Vec<ToolCall>) -> CapturedExchange {
    CapturedExchange {
        id: id.into(),
        content: vec![ContentBlock::Text { text: "ok".into() }],
        tool_calls: tools,
        finish_reason: FinishReason::ToolUse,
        mcp_messages: vec![],
    }
}

#[test]
fn drift_surface_with_invariant_violation_classifies_as_fail() {
    // Surface diff alone would classify as DRIFT (whitespace-only
    // text change) but the candidate is missing a required tool call.
    // The invariant must escalate the verdict to FAIL.
    let inv = Invariant {
        name: "needs-lookup".into(),
        kind: InvariantKind::ToolCalled,
        condition: InvariantCondition::Tool {
            tool: "lookup_account".into(),
        },
    };
    let mut baseline_ex = exchange_with_tools(
        "t::a::1",
        vec![ToolCall {
            name: "lookup_account".into(),
            arguments: serde_json::json!({"id": "1"}),
            raw_arguments: None,
        }],
    );
    baseline_ex.content = vec![ContentBlock::Text {
        text: "Looking up the account.".into(),
    }];
    let mut candidate_ex = exchange_with_tools("t::a::1", vec![]);
    candidate_ex.content = vec![ContentBlock::Text {
        text: "Looking up  the account.".into(), // extra space
    }];

    let a = baseline_with(vec![baseline_ex], vec![inv.clone()]);
    let b = baseline_with(vec![candidate_ex], vec![inv]);

    let diff = diff_baselines(&a, &b).expect("diff");
    assert!(
        diff.entries
            .iter()
            .any(|e| matches!(e.category, ChangeCategory::Invariant)
                && matches!(e.severity, Severity::Fail)),
        "invariant violation must surface as FAIL: {:#?}",
        diff.entries
    );
    assert!(diff.summary.fail >= 1);
}

#[test]
fn passing_invariant_does_not_inflate_severity() {
    let inv = Invariant {
        name: "needs-lookup".into(),
        kind: InvariantKind::ToolCalled,
        condition: InvariantCondition::Tool {
            tool: "lookup_account".into(),
        },
    };
    let baseline_ex = exchange_with_tools(
        "t::a::1",
        vec![ToolCall {
            name: "lookup_account".into(),
            arguments: serde_json::json!({"id": "1"}),
            raw_arguments: None,
        }],
    );
    let candidate_ex = baseline_ex.clone();
    let a = baseline_with(vec![baseline_ex], vec![inv.clone()]);
    let b = baseline_with(vec![candidate_ex], vec![inv]);
    let diff = diff_baselines(&a, &b).expect("diff");
    assert_eq!(diff.summary.fail, 0);
    assert!(diff.invariant_results.iter().all(|(_, r)| r.passed));
}

#[test]
fn arg_present_invariant_violation_surfaces_as_fail() {
    let inv = Invariant {
        name: "lookup-id".into(),
        kind: InvariantKind::ArgPresent,
        condition: InvariantCondition::Arg {
            tool: "lookup_account".into(),
            arg: "id".into(),
        },
    };
    let baseline_ex = exchange_with_tools(
        "t::a::1",
        vec![ToolCall {
            name: "lookup_account".into(),
            arguments: serde_json::json!({"id": "1"}),
            raw_arguments: None,
        }],
    );
    let candidate_ex = exchange_with_tools(
        "t::a::1",
        vec![ToolCall {
            name: "lookup_account".into(),
            arguments: serde_json::json!({}),
            raw_arguments: None,
        }],
    );
    let a = baseline_with(vec![baseline_ex], vec![inv.clone()]);
    let b = baseline_with(vec![candidate_ex], vec![inv]);
    let diff = diff_baselines(&a, &b).expect("diff");
    assert!(diff
        .entries
        .iter()
        .any(|e| matches!(e.category, ChangeCategory::Invariant)
            && matches!(e.severity, Severity::Fail)));
}

#[test]
fn latency_invariant_violation_surfaces_as_fail_in_capturedrun() {
    use mcptest_core::model_compat::{check_invariants, CapturedRun};
    let inv = Invariant {
        name: "p95".into(),
        kind: InvariantKind::LatencyUnderMs,
        condition: InvariantCondition::Latency { ms: 50 },
    };
    let exchange = exchange_with_tools("t::a::1", vec![]);
    let run = CapturedRun::new(exchange, 250);
    let r = check_invariants(&[inv], &run);
    assert!(!r[0].passed);
    assert!(r[0].detail.contains("250"));
}
