//! Integration coverage for the cross-server trust-boundary and
//! capability-negotiation integrity lanes (PROTOAMP, AttestMCP).
//!
//! These exercise the public `mcptest_core::security` surface the way a CLI
//! caller would: build a run, feed it the observed cross-server flows and the
//! captured negotiation trace, and assert the deterministic verdict. Every case
//! is offline and zero-LLM, so the determinism guarantee (ADR 0040) holds.

use mcptest_core::security::{
    capability_negotiation_findings, cross_server_trust_findings, extract_cross_server_flows,
    run_deterministic, CrossServerFlow, NegotiationTrace, RequestCapabilityClaim,
    SecurityRunOptions, SecuritySeverity, TrustBoundaryAssertion,
};
use serde_json::json;

fn crafted_cross_server_flow() -> CrossServerFlow {
    // Server alpha fetches an untrusted page; server beta runs the fetched text
    // as a SQL query verbatim. A's output becomes B's input with no boundary.
    CrossServerFlow {
        producer_server: "alpha".into(),
        producer_tool: "fetch_page".into(),
        output: json!("DROP TABLE users; -- injected by the page"),
        consumer_server: "beta".into(),
        consumer_tool: "run_sql".into(),
        input: json!({ "query": "DROP TABLE users; -- injected by the page" }),
        sanitizers: vec![],
    }
}

#[test]
fn crafted_unmodified_cross_server_flow_fails_the_propagation_check() {
    let flows = vec![crafted_cross_server_flow()];
    let found = cross_server_trust_findings(&flows, &TrustBoundaryAssertion::default());
    assert_eq!(found.len(), 1);
    assert_eq!(found[0].rule_id, "SEC-060");
    assert_eq!(found[0].severity, SecuritySeverity::High);
}

#[test]
fn properly_bounded_cross_server_flow_passes_the_propagation_check() {
    let flows = vec![crafted_cross_server_flow()];
    // The suite asserts the alpha->beta boundary is reviewed and enforced.
    let assertion = TrustBoundaryAssertion {
        allowed_flows: vec![("alpha".into(), "beta".into())],
        ..Default::default()
    };
    let found = cross_server_trust_findings(&flows, &assertion);
    assert!(found.is_empty());
}

#[test]
fn sanitized_cross_server_flow_passes_the_propagation_check() {
    let mut flow = crafted_cross_server_flow();
    flow.sanitizers = vec!["sql_escape".into()];
    let assertion = TrustBoundaryAssertion {
        required_sanitizers: vec!["sql_escape".into()],
        ..Default::default()
    };
    let found = cross_server_trust_findings(&[flow], &assertion);
    assert!(found.is_empty());
}

#[test]
fn widened_capability_sequence_fails_the_negotiation_integrity_check() {
    let trace = NegotiationTrace {
        target: "2026-07-28".into(),
        negotiated: vec!["tools".into(), "resources".into()],
        requests: vec![
            RequestCapabilityClaim {
                request_id: "req-2".into(),
                capabilities: vec!["tools".into()],
            },
            RequestCapabilityClaim {
                // Silently claims a capability the client never negotiated.
                request_id: "req-3".into(),
                capabilities: vec!["tools".into(), "sampling".into()],
            },
        ],
    };
    let found = capability_negotiation_findings(&trace);
    assert_eq!(found.len(), 1);
    assert_eq!(found[0].rule_id, "SEC-061");
    assert_eq!(found[0].definition, "req-3");
    assert!(found[0].evidence.contains("sampling"));
}

#[test]
fn consistent_capability_sequence_passes_the_negotiation_integrity_check() {
    let trace = NegotiationTrace {
        target: "2026-07-28".into(),
        negotiated: vec!["tools".into(), "resources".into(), "sampling".into()],
        requests: vec![
            RequestCapabilityClaim {
                request_id: "req-2".into(),
                capabilities: vec!["tools".into()],
            },
            RequestCapabilityClaim {
                request_id: "req-3".into(),
                capabilities: vec!["tools".into(), "resources".into()],
            },
        ],
    };
    let found = capability_negotiation_findings(&trace);
    assert!(found.is_empty());
}

#[test]
fn both_lanes_gate_through_the_unified_run() {
    let snapshot = json!({ "tools": [{ "name": "run_sql" }] });
    let options = SecurityRunOptions {
        cross_server_flows: vec![crafted_cross_server_flow()],
        negotiation: Some(NegotiationTrace {
            target: "2026-07-28".into(),
            negotiated: vec!["tools".into()],
            requests: vec![RequestCapabilityClaim {
                request_id: "req-2".into(),
                capabilities: vec!["tools".into(), "elicitation".into()],
            }],
        }),
        ..Default::default()
    };
    let run = run_deterministic(&snapshot, &options);
    // Both deterministic findings land in the one gating report.
    assert!(run.report.findings.iter().any(|f| f.rule_id == "SEC-060"));
    assert!(run.report.findings.iter().any(|f| f.rule_id == "SEC-061"));
    // The verdict gates at high on the strength of either finding.
    assert!(run.fails_at(SecuritySeverity::High));
}

#[test]
fn flows_extracted_from_a_trace_envelope_drive_the_propagation_check() {
    // The shape `ConversationTrace::to_envelope` produces: alpha fetches a page,
    // then beta runs that page text as a query verbatim. The extractor sees one
    // cross-server flow and the detector fires SEC-060 on the default boundary.
    let envelope = json!({
        "tool_calls": [
            { "name": "fetch_page", "server": "alpha", "args": { "url": "x" } },
            { "name": "run_sql", "server": "beta", "args": { "query": "DROP TABLE users" } },
        ],
        "tool_results": [
            { "call_index": 0, "server": "alpha", "is_error": false, "result": "DROP TABLE users" },
        ],
    });
    let flows = extract_cross_server_flows(&envelope);
    assert_eq!(flows.len(), 1);
    let found = cross_server_trust_findings(&flows, &TrustBoundaryAssertion::default());
    assert_eq!(found.len(), 1);
    assert_eq!(found[0].rule_id, "SEC-060");

    // Allow-listing the alpha->beta pair clears the same extracted flow.
    let assertion = TrustBoundaryAssertion {
        allowed_flows: vec![("alpha".into(), "beta".into())],
        ..Default::default()
    };
    assert!(cross_server_trust_findings(&flows, &assertion).is_empty());
}
