//! G3 — CLI↔HTTP↔MCP contract test.
//!
//! For each fixture tool, invoke via:
//!   Tier A — Registry directly (reference)
//!   Tier B — HTTP daemon (axum::serve on 127.0.0.1:0)
//!   Tier C — MCP server (handle_request in-process)
//!
//! Assert:
//!   1. All tiers that succeed produce equal normalised payloads
//!   2. The payload validates against the listing output schema
//!   3. All tiers that fail produce the same error code
//!
//! The normaliser strips volatile fields (trace_id, timestamp, …) so the
//! contract is on semantic equivalence, not byte-for-byte equality.

use contract_tests::{
    listing_output_schema, make_fixture_registry, normalise,
    tier_http, tier_mcp, tier_registry,
};
use jsonschema::Validator;
use provisioning_mcp_server::registry_server::McpServer;
use provisioning_core::Environment;
use provisioning_daemon::{AppState, http::router};
use serde_json::{Value, json};
use std::sync::{Arc, OnceLock};

// ── Test harness ──────────────────────────────────────────────────────────────

static CRYPTO: OnceLock<()> = OnceLock::new();

fn init_crypto() {
    CRYPTO.get_or_init(|| {
        let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
    });
}

struct Harness {
    env: Arc<Environment>,
    registry: Arc<provisioning_core::Registry>,
    mcp: McpServer,
    http_base: String,
    _daemon_task: tokio::task::JoinHandle<()>,
}

async fn boot() -> Harness {
    init_crypto();

    // Shared fixture registry for all tiers.
    let registry = Arc::new(make_fixture_registry());
    let env = Arc::new(Environment::default());

    // Tier B — bind daemon on ephemeral port.
    let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("bind");
    let addr = listener.local_addr().expect("local_addr");
    let http_base = format!("http://{addr}");

    // AppState owns its own Arc<Registry>; we build it from a fresh fixture
    // registry so the daemon and the reference tier see identical tool sets.
    let daemon_registry = make_fixture_registry();
    let app = AppState::new(daemon_registry, Environment::default());
    let app_router = router(app);
    let task = tokio::spawn(async move {
        axum::serve(listener, app_router).await.expect("daemon serve");
    });

    // Wait for the daemon to accept connections (fast — local TCP).
    for _ in 0..40 {
        if reqwest::get(format!("{}/health", http_base)).await.is_ok() {
            break;
        }
        tokio::time::sleep(std::time::Duration::from_millis(25)).await;
    }

    // Tier C — in-process MCP server with its own fixture registry.
    let mcp = McpServer::new(make_fixture_registry(), Environment::default(), "g3-test", "0");

    Harness { env, registry, mcp, http_base, _daemon_task: task }
}

fn validate_listing(v: &Value) {
    let schema = listing_output_schema();
    let validator = Validator::new(&schema).expect("compile schema");
    validator
        .validate(v)
        .unwrap_or_else(|errors| panic!("listing schema validation failed: {errors}"));
}

// ── Success contract ──────────────────────────────────────────────────────────

#[tokio::test]
async fn listing_tool_agrees_across_three_tiers() {
    let h = boot().await;
    let params = json!({});

    let a = tier_registry(&h.registry, h.env.clone(), "ct_listing", params.clone()).await;
    let b = tier_http(&h.http_base, "ct_listing", params.clone()).await;
    let c = tier_mcp(&h.mcp, "ct_listing", params.clone()).await;

    let a_payload = a.payload.expect("tier A registry must succeed");
    let b_payload = b.payload.expect("tier B http must succeed");
    let c_payload = c.payload.expect("tier C mcp must succeed");

    validate_listing(&a_payload);
    validate_listing(&b_payload);
    validate_listing(&c_payload);

    assert_eq!(normalise(&a_payload), normalise(&b_payload), "A↔B diverge");
    assert_eq!(normalise(&a_payload), normalise(&c_payload), "A↔C diverge");
    assert_eq!(normalise(&b_payload), normalise(&c_payload), "B↔C diverge");
}

#[tokio::test]
async fn echo_tool_agrees_across_three_tiers() {
    let h = boot().await;
    let params = json!({ "message": "contract" });

    let a = tier_registry(&h.registry, h.env.clone(), "ct_echo", params.clone()).await;
    let b = tier_http(&h.http_base, "ct_echo", params.clone()).await;
    let c = tier_mcp(&h.mcp, "ct_echo", params.clone()).await;

    let a_payload = a.payload.expect("A must succeed");
    let b_payload = b.payload.expect("B must succeed");
    let c_payload = c.payload.expect("C must succeed");

    validate_listing(&a_payload);
    validate_listing(&b_payload);
    validate_listing(&c_payload);

    assert_eq!(normalise(&a_payload), normalise(&b_payload));
    assert_eq!(normalise(&a_payload), normalise(&c_payload));

    // Exact content check — echo should round-trip the message
    assert_eq!(a_payload["items"][0]["message"], "contract");
    assert_eq!(b_payload["items"][0]["message"], "contract");
    assert_eq!(c_payload["items"][0]["message"], "contract");
}

// ── Failure contract ──────────────────────────────────────────────────────────

#[tokio::test]
async fn invalid_param_error_code_agrees_across_tiers() {
    let h = boot().await;
    let params = json!({}); // echo requires "message" → InvalidParam

    let a = tier_registry(&h.registry, h.env.clone(), "ct_echo", params.clone()).await;
    let b = tier_http(&h.http_base, "ct_echo", params.clone()).await;
    let c = tier_mcp(&h.mcp, "ct_echo", params.clone()).await;

    // All three must classify as InvalidParam → -32602
    assert_eq!(a.error_code, Some(-32602), "A: {:?}", a.error_message);
    assert_eq!(b.error_code, Some(-32602), "B: {:?}", b.error_message);
    assert_eq!(c.error_code, Some(-32602), "C: {:?}", c.error_message);
}

#[tokio::test]
async fn failing_tool_error_code_agrees_across_tiers() {
    let h = boot().await;
    let params = json!({});

    let a = tier_registry(&h.registry, h.env.clone(), "ct_fail", params.clone()).await;
    let b = tier_http(&h.http_base, "ct_fail", params.clone()).await;
    let c = tier_mcp(&h.mcp, "ct_fail", params.clone()).await;

    assert_eq!(a.error_code, Some(-32602));
    assert_eq!(b.error_code, Some(-32602));
    assert_eq!(c.error_code, Some(-32602));
}

// ── tools/list contract ───────────────────────────────────────────────────────

#[tokio::test]
async fn tools_list_count_agrees_across_surfaces() {
    let h = boot().await;

    // Registry baseline
    let reg_count = h.registry.len();

    // HTTP
    let http_body: Value = reqwest::get(format!("{}/api/v1/tools", h.http_base))
        .await
        .expect("http list")
        .json()
        .await
        .expect("parse");
    let http_count = http_body["tools"].as_array().expect("tools array").len();

    // MCP
    let rpc = json!({"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}});
    let mcp_resp = h.mcp.handle_request(rpc).await;
    let mcp_count = mcp_resp["result"]["tools"].as_array().expect("mcp tools").len();

    assert_eq!(reg_count, http_count, "HTTP count diverges from registry");
    assert_eq!(reg_count, mcp_count, "MCP count diverges from registry");
}
