//! Integration tests for Highflame client using wiremock.

use std::time::Duration;

use futures_util::StreamExt as _;
use serde_json::{json, Value};
use wiremock::{
    matchers::{header, method, path},
    Mock, MockServer, ResponseTemplate,
};

use highflame::{
    ContentType, Decision, GuardRequest, Highflame, HighflameError, HighflameOptions, ToolContext,
};

// ── Fixtures ──────────────────────────────────────────────────────────────────

const API_KEY: &str = "test-key";
const SERVICE_KEY: &str = "hf_sk_test_key";

fn allow_body() -> Value {
    json!({
        "decision": "allow",
        "request_id": "req_test",
        "timestamp": "2024-01-01T00:00:00Z",
        "latency_ms": 5,
        "tiers_evaluated": ["fast"],
        "tiers_skipped": [],
        "detectors": [],
        "context": {},
        "signals": []
    })
}

fn deny_body() -> Value {
    json!({
        "decision": "deny",
        "policy_reason": "secrets detected",
        "request_id": "req_test",
        "timestamp": "2024-01-01T00:00:00Z",
        "latency_ms": 5,
        "tiers_evaluated": ["fast"],
        "tiers_skipped": ["standard"],
        "detectors": [],
        "context": {"contains_secrets": true},
        "signals": []
    })
}

fn problem_body(status: u16, title: &str, detail: &str) -> Value {
    json!({"status": status, "title": title, "detail": detail})
}

fn debug_policies_body() -> Value {
    json!({
        "product": "guardrails",
        "has_policies": true,
        "policy_count": 1,
        "policies": [
            {"policy_id": "policy-1", "policy_name": "Default Policy", "mode": "enforce", "category": "security", "rule_ids": ["rule-1"]}
        ]
    })
}

fn detectors_body() -> Value {
    json!({
        "detectors": [
            {"name": "secrets", "version": "1.0", "tier": "fast", "context_keys": [], "status": "healthy"},
            {"name": "keyword", "version": "1.0", "tier": "fast", "context_keys": [], "status": "healthy"}
        ],
        "count": 2
    })
}

fn token_body() -> Value {
    json!({
        "access_token": "eyJhbGciOiJSUzI1NiJ9.test.sig",
        "token_type": "Bearer",
        "expires_in": 900,
        "account_id": "acc_from_token",
        "gateway_id": "gw_123",
        "project_id": "proj_from_token"
    })
}

fn minimal_request() -> GuardRequest {
    GuardRequest {
        content: "hello".to_string(),
        content_type: ContentType::Prompt,
        action: "process_prompt".to_string(),
        ..Default::default()
    }
}

fn make_client(server: &MockServer) -> Highflame {
    Highflame::new(
        HighflameOptions::new(API_KEY)
            .base_url(server.uri())
            .max_retries(0),
    )
}

fn make_sk_client(server: &MockServer, token_server: &MockServer) -> Highflame {
    Highflame::new(
        HighflameOptions::new(SERVICE_KEY)
            .base_url(server.uri())
            .token_url(format!("{}/v1/auth/token", token_server.uri()))
            .max_retries(0),
    )
}

// ── guard().evaluate() ──────────────────────────────────────────────────────

#[tokio::test]
async fn test_guard_allow() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .respond_with(ResponseTemplate::new(200).set_body_json(allow_body()))
        .mount(&server)
        .await;

    let resp = make_client(&server)
        .guard()
        .evaluate(&minimal_request())
        .await
        .unwrap();
    assert_eq!(resp.decision, Decision::Allow);
    assert!(resp.allowed());
    assert_eq!(resp.latency_ms, 5);
}

#[tokio::test]
async fn test_guard_deny() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .respond_with(ResponseTemplate::new(200).set_body_json(deny_body()))
        .mount(&server)
        .await;

    let resp = make_client(&server)
        .guard()
        .evaluate(&GuardRequest {
            content: "api_key=abc".to_string(),
            content_type: ContentType::Prompt,
            action: "process_prompt".to_string(),
            ..Default::default()
        })
        .await
        .unwrap();
    assert_eq!(resp.decision, Decision::Deny);
    assert!(resp.denied());
    assert_eq!(resp.policy_reason.as_deref(), Some("secrets detected"));
}

#[tokio::test]
async fn test_guard_sends_auth_header() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .and(header(
            "authorization",
            format!("Bearer {API_KEY}").as_str(),
        ))
        .respond_with(ResponseTemplate::new(200).set_body_json(allow_body()))
        .expect(1)
        .mount(&server)
        .await;

    make_client(&server)
        .guard()
        .evaluate(&minimal_request())
        .await
        .unwrap();
}

#[tokio::test]
async fn test_guard_sends_tenant_headers() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .and(header("x-account-id", "acc_1"))
        .and(header("x-project-id", "proj_1"))
        .respond_with(ResponseTemplate::new(200).set_body_json(allow_body()))
        .expect(1)
        .mount(&server)
        .await;

    let client = Highflame::new(
        HighflameOptions::new(API_KEY)
            .base_url(server.uri())
            .max_retries(0)
            .account_id("acc_1")
            .project_id("proj_1"),
    );
    client.guard().evaluate(&minimal_request()).await.unwrap();
}

#[tokio::test]
async fn test_guard_400_returns_api_error() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .respond_with(ResponseTemplate::new(400).set_body_json(problem_body(
            400,
            "Bad Request",
            "content is required",
        )))
        .mount(&server)
        .await;

    let err = make_client(&server)
        .guard()
        .evaluate(&minimal_request())
        .await
        .unwrap_err();
    match err {
        HighflameError::Api {
            status,
            title,
            detail,
        } => {
            assert_eq!(status, 400);
            assert_eq!(title, "Bad Request");
            assert!(detail.contains("content is required"));
        }
        other => panic!("expected HighflameError::Api, got {other:?}"),
    }
}

#[tokio::test]
async fn test_guard_422_returns_api_error() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .respond_with(ResponseTemplate::new(422).set_body_json(problem_body(
            422,
            "Unprocessable Entity",
            "bad field",
        )))
        .mount(&server)
        .await;

    let err = make_client(&server)
        .guard()
        .evaluate(&minimal_request())
        .await
        .unwrap_err();
    assert!(err.is_api_error());
    assert_eq!(err.status(), Some(422));
}

#[tokio::test]
async fn test_guard_retries_on_503_and_succeeds() {
    let server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .respond_with(ResponseTemplate::new(503).set_body_json(problem_body(
            503,
            "Service Unavailable",
            "",
        )))
        .up_to_n_times(1)
        .mount(&server)
        .await;

    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .respond_with(ResponseTemplate::new(200).set_body_json(allow_body()))
        .mount(&server)
        .await;

    let client = Highflame::new(
        HighflameOptions::new(API_KEY)
            .base_url(server.uri())
            .max_retries(1)
            .timeout(Duration::from_secs(10)),
    );
    let resp = client.guard().evaluate(&minimal_request()).await.unwrap();
    assert_eq!(resp.decision, Decision::Allow);
}

// ── debug().policies() ──────────────────────────────────────────────────────

#[tokio::test]
async fn test_debug_policies_returns_response() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/v1/debug/policies"))
        .respond_with(ResponseTemplate::new(200).set_body_json(debug_policies_body()))
        .mount(&server)
        .await;

    let result = make_client(&server).debug().policies(None).await.unwrap();
    assert!(!result.policies.is_empty());
}

#[tokio::test]
async fn test_debug_policies_503_returns_api_error() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/v1/debug/policies"))
        .respond_with(ResponseTemplate::new(503).set_body_json(problem_body(
            503,
            "Service Unavailable",
            "db down",
        )))
        .mount(&server)
        .await;

    let err = make_client(&server)
        .debug()
        .policies(None)
        .await
        .unwrap_err();
    assert!(err.is_api_error());
    assert_eq!(err.status(), Some(503));
}

// ── detectors().list() ──────────────────────────────────────────────────────

#[tokio::test]
async fn test_list_detectors_returns_response() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/v1/detectors"))
        .respond_with(ResponseTemplate::new(200).set_body_json(detectors_body()))
        .mount(&server)
        .await;

    let result = make_client(&server).detectors().list().await.unwrap();
    assert_eq!(result.count, 2);
    assert_eq!(result.detectors[0].name, "secrets");
    assert_eq!(result.detectors[1].name, "keyword");
}

// ── guard().stream() ─────────────────────────────────────────────────────────

#[tokio::test]
async fn test_stream_yields_sse_events() {
    let sse_body = concat!(
        "event: detection\n",
        "data: {\"contains_secrets\": true}\n",
        "\n",
        "event: decision\n",
        "data: {\"decision\": \"deny\"}\n",
        "\n",
        "event: done\n",
        "data: {}\n",
        "\n",
    );

    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/guard/stream"))
        .respond_with(
            ResponseTemplate::new(200)
                .insert_header("content-type", "text/event-stream")
                .set_body_bytes(sse_body.as_bytes()),
        )
        .mount(&server)
        .await;

    let client = make_client(&server);
    let stream = client.guard().stream(&minimal_request()).await.unwrap();
    let events: Vec<_> = stream.collect::<Vec<_>>().await;

    assert_eq!(events.len(), 3);
    let ev0 = events[0].as_ref().unwrap();
    assert_eq!(ev0.r#type, "detection");
    assert_eq!(
        ev0.data.get("contains_secrets").and_then(|v| v.as_bool()),
        Some(true)
    );
    let ev1 = events[1].as_ref().unwrap();
    assert_eq!(ev1.r#type, "decision");
    assert_eq!(
        ev1.data.get("decision").and_then(|v| v.as_str()),
        Some("deny")
    );
    let ev2 = events[2].as_ref().unwrap();
    assert_eq!(ev2.r#type, "done");
}

#[tokio::test]
async fn test_stream_400_returns_api_error() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/guard/stream"))
        .respond_with(ResponseTemplate::new(400).set_body_json(problem_body(
            400,
            "Bad Request",
            "content is required",
        )))
        .mount(&server)
        .await;

    let req = minimal_request();
    match make_client(&server).guard().stream(&req).await {
        Err(err) => {
            assert!(err.is_api_error());
            assert_eq!(err.status(), Some(400));
        }
        Ok(_) => panic!("expected Err, got Ok"),
    }
}

// ── Request body serialisation ────────────────────────────────────────────────

#[tokio::test]
async fn test_request_body_excludes_none_fields() {
    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .respond_with(ResponseTemplate::new(200).set_body_json(allow_body()))
        .mount(&server)
        .await;

    make_client(&server)
        .guard()
        .evaluate(&minimal_request())
        .await
        .unwrap();

    let reqs = server.received_requests().await.unwrap();
    let body: Value = serde_json::from_slice(&reqs[0].body).unwrap();
    assert!(body.get("tool").is_none(), "tool must be absent");
    assert!(body.get("model").is_none(), "model must be absent");
    assert!(body.get("file").is_none(), "file must be absent");
    assert!(body.get("mcp").is_none(), "mcp must be absent");
    assert!(
        body.get("session_id").is_none(),
        "session_id must be absent"
    );
}

#[tokio::test]
async fn test_request_body_includes_tool_context() {
    use std::collections::HashMap;

    let server = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .respond_with(ResponseTemplate::new(200).set_body_json(allow_body()))
        .mount(&server)
        .await;

    let mut args = HashMap::new();
    args.insert("cmd".to_string(), json!("ls"));

    let req = GuardRequest {
        content: "shell ls".to_string(),
        content_type: ContentType::ToolCall,
        action: "call_tool".to_string(),
        tool: Some(ToolContext {
            name: "shell".to_string(),
            is_builtin: false,
            arguments: Some(args),
            ..Default::default()
        }),
        ..Default::default()
    };

    make_client(&server).guard().evaluate(&req).await.unwrap();

    let reqs = server.received_requests().await.unwrap();
    let body: Value = serde_json::from_slice(&reqs[0].body).unwrap();
    assert_eq!(body["tool"]["name"], "shell");
    assert_eq!(body["tool"]["arguments"]["cmd"], "ls");
}

// ── Service key flow ──────────────────────────────────────────────────────────

#[tokio::test]
async fn test_service_key_exchanges_token_on_first_call() {
    let server = MockServer::start().await;
    let token_server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/v1/auth/token"))
        .respond_with(ResponseTemplate::new(200).set_body_json(token_body()))
        .expect(1)
        .mount(&token_server)
        .await;

    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .and(header(
            "authorization",
            "Bearer eyJhbGciOiJSUzI1NiJ9.test.sig",
        ))
        .respond_with(ResponseTemplate::new(200).set_body_json(allow_body()))
        .expect(1)
        .mount(&server)
        .await;

    let resp = make_sk_client(&server, &token_server)
        .guard()
        .evaluate(&minimal_request())
        .await
        .unwrap();
    assert_eq!(resp.decision, Decision::Allow);
}

#[tokio::test]
async fn test_service_key_injects_tenant_headers_from_token() {
    let server = MockServer::start().await;
    let token_server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/v1/auth/token"))
        .respond_with(ResponseTemplate::new(200).set_body_json(token_body()))
        .mount(&token_server)
        .await;

    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .and(header("x-account-id", "acc_from_token"))
        .and(header("x-project-id", "proj_from_token"))
        .respond_with(ResponseTemplate::new(200).set_body_json(allow_body()))
        .expect(1)
        .mount(&server)
        .await;

    make_sk_client(&server, &token_server)
        .guard()
        .evaluate(&minimal_request())
        .await
        .unwrap();
}

#[tokio::test]
async fn test_service_key_caches_token_across_calls() {
    let server = MockServer::start().await;
    let token_server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/v1/auth/token"))
        .respond_with(ResponseTemplate::new(200).set_body_json(token_body()))
        .expect(1)
        .mount(&token_server)
        .await;

    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .respond_with(ResponseTemplate::new(200).set_body_json(allow_body()))
        .mount(&server)
        .await;

    let client = make_sk_client(&server, &token_server);
    let r1 = minimal_request();
    let r2 = minimal_request();
    let r3 = minimal_request();
    client.guard().evaluate(&r1).await.unwrap();
    client.guard().evaluate(&r2).await.unwrap();
    client.guard().evaluate(&r3).await.unwrap();
}

#[tokio::test]
async fn test_service_key_refreshes_expired_token() {
    let server = MockServer::start().await;
    let token_server = MockServer::start().await;

    let short_token = json!({
        "access_token": "eyJhbGciOiJSUzI1NiJ9.test.sig",
        "token_type": "Bearer",
        "expires_in": 61,
        "account_id": "acc_from_token",
        "gateway_id": "gw_123",
        "project_id": "proj_from_token"
    });

    Mock::given(method("POST"))
        .and(path("/v1/auth/token"))
        .respond_with(ResponseTemplate::new(200).set_body_json(short_token))
        .expect(2)
        .mount(&token_server)
        .await;

    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .respond_with(ResponseTemplate::new(200).set_body_json(allow_body()))
        .mount(&server)
        .await;

    let client = make_sk_client(&server, &token_server);
    client.guard().evaluate(&minimal_request()).await.unwrap();

    tokio::time::sleep(Duration::from_millis(1_100)).await;

    client.guard().evaluate(&minimal_request()).await.unwrap();
}

#[tokio::test]
async fn test_service_key_constructor_overrides_tenant_headers() {
    let server = MockServer::start().await;
    let token_server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/v1/auth/token"))
        .respond_with(ResponseTemplate::new(200).set_body_json(token_body()))
        .mount(&token_server)
        .await;

    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .and(header("x-account-id", "override_acc"))
        .and(header("x-project-id", "override_proj"))
        .respond_with(ResponseTemplate::new(200).set_body_json(allow_body()))
        .expect(1)
        .mount(&server)
        .await;

    let client = Highflame::new(
        HighflameOptions::new(SERVICE_KEY)
            .base_url(server.uri())
            .token_url(format!("{}/v1/auth/token", token_server.uri()))
            .max_retries(0)
            .account_id("override_acc")
            .project_id("override_proj"),
    );
    client.guard().evaluate(&minimal_request()).await.unwrap();
}

#[tokio::test]
async fn test_service_key_exchange_failure_returns_auth_error() {
    let server = MockServer::start().await;
    let token_server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/v1/auth/token"))
        .respond_with(ResponseTemplate::new(401).set_body_json(problem_body(
            401,
            "Unauthorized",
            "invalid key",
        )))
        .mount(&token_server)
        .await;

    let err = make_sk_client(&server, &token_server)
        .guard()
        .evaluate(&minimal_request())
        .await
        .unwrap_err();
    assert!(err.is_authentication_error());
    assert_eq!(err.status(), Some(401));
}

#[tokio::test]
async fn test_service_key_concurrent_refresh_deduplicates() {
    let server = MockServer::start().await;
    let token_server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/v1/auth/token"))
        .respond_with(
            ResponseTemplate::new(200)
                .set_body_json(token_body())
                .set_delay(Duration::from_millis(50)),
        )
        .expect(1)
        .mount(&token_server)
        .await;

    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .respond_with(ResponseTemplate::new(200).set_body_json(allow_body()))
        .mount(&server)
        .await;

    let client = make_sk_client(&server, &token_server);
    let req1 = minimal_request();
    let req2 = minimal_request();
    let req3 = minimal_request();
    let g1 = client.guard();
    let g2 = client.guard();
    let g3 = client.guard();
    let (r1, r2, r3) = tokio::join!(g1.evaluate(&req1), g2.evaluate(&req2), g3.evaluate(&req3),);
    assert!(r1.is_ok());
    assert!(r2.is_ok());
    assert!(r3.is_ok());
}

// ── default_headers ──────────────────────────────────────────────────────────

#[tokio::test]
async fn test_default_headers_sent_on_every_request() {
    let server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .and(header("x-custom-routing", "my-service"))
        .and(header("x-custom", "val"))
        .respond_with(ResponseTemplate::new(200).set_body_json(allow_body()))
        .expect(1)
        .mount(&server)
        .await;

    let mut headers = std::collections::HashMap::new();
    headers.insert("X-Custom-Routing".to_string(), "my-service".to_string());
    headers.insert("X-Custom".to_string(), "val".to_string());

    let client = Highflame::new(
        HighflameOptions::new(API_KEY)
            .base_url(server.uri())
            .max_retries(0)
            .default_headers(headers),
    );
    client.guard().evaluate(&minimal_request()).await.unwrap();
}

#[tokio::test]
async fn test_default_headers_through_token_refresh() {
    let server = MockServer::start().await;
    let token_server = MockServer::start().await;

    Mock::given(method("POST"))
        .and(path("/v1/auth/token"))
        .respond_with(ResponseTemplate::new(200).set_body_json(token_body()))
        .mount(&token_server)
        .await;

    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .and(header("x-custom-routing", "my-service"))
        .respond_with(ResponseTemplate::new(200).set_body_json(allow_body()))
        .expect(1)
        .mount(&server)
        .await;

    let mut headers = std::collections::HashMap::new();
    headers.insert("X-Custom-Routing".to_string(), "my-service".to_string());

    let client = Highflame::new(
        HighflameOptions::new(SERVICE_KEY)
            .base_url(server.uri())
            .token_url(format!("{}/v1/auth/token", token_server.uri()))
            .max_retries(0)
            .default_headers(headers),
    );
    client.guard().evaluate(&minimal_request()).await.unwrap();
}

#[tokio::test]
async fn test_default_headers_cannot_override_auth() {
    let server = MockServer::start().await;

    // Expect the SDK-managed Authorization header, not the rogue one.
    Mock::given(method("POST"))
        .and(path("/v1/guard"))
        .and(header(
            "authorization",
            format!("Bearer {API_KEY}").as_str(),
        ))
        .respond_with(ResponseTemplate::new(200).set_body_json(allow_body()))
        .expect(1)
        .mount(&server)
        .await;

    let mut headers = std::collections::HashMap::new();
    headers.insert("Authorization".to_string(), "Bearer rogue".to_string());

    let client = Highflame::new(
        HighflameOptions::new(API_KEY)
            .base_url(server.uri())
            .max_retries(0)
            .default_headers(headers),
    );
    client.guard().evaluate(&minimal_request()).await.unwrap();
}

// ── Error type helpers ────────────────────────────────────────────────────────

#[test]
fn test_error_api_helpers() {
    let err = HighflameError::Api {
        status: 403,
        title: "Forbidden".to_string(),
        detail: "not allowed".to_string(),
    };
    assert_eq!(err.status(), Some(403));
    assert!(err.is_api_error());
    assert!(!err.is_connection_error());
    assert!(!err.is_authentication_error());
    assert!(err.to_string().contains("403"));
    assert!(err.to_string().contains("Forbidden"));
}

#[test]
fn test_error_authentication_helpers() {
    let err = HighflameError::Authentication("bad token".to_string());
    assert_eq!(err.status(), Some(401));
    assert!(err.is_authentication_error());
    assert!(!err.is_api_error());
}

#[test]
fn test_error_rate_limit_helpers() {
    let err = HighflameError::RateLimit("too many requests".to_string());
    assert_eq!(err.status(), Some(429));
    assert!(err.is_rate_limit_error());
    assert!(!err.is_api_error());
}

#[test]
fn test_error_connection_helpers() {
    let err = HighflameError::ApiConnection("timeout".to_string());
    assert_eq!(err.status(), None);
    assert!(!err.is_api_error());
    assert!(err.is_connection_error());
}
