//! Integration tests for health checks against the workspace's shared services.
//!
//! # Running
//!
//! Start the workspace services first:
//!
//! ```text
//! sunbeam ops compose up postgres keto
//! ```
//!
//! Then:
//!
//! ```text
//! cargo test -p sunbeam-g2v --test health_integration -- --nocapture --test-threads=1
//! ```
//!
//! Each test that depends on a real service uses a probe-then-skip pattern: if
//! the service isn't reachable the test prints a clear message and returns
//! without failing, so this file is safe to run in environments where the
//! compose stack isn't up.
//!
//! Defaults (from `.envrc`):
//! - `DATABASE_URL`   `postgres://sunbeam:sunbeam@localhost:5432/sunbeam`
//! - `KETO_GRPC_URL`  `http://localhost:4466`

use std::sync::Arc;
use sunbeam_g2v::health::{HealthCheck, HealthCheckResult, HealthRouter};
use sunbeam_g2v::testing::TestHarness;

// ============================================================================
// Probe helpers
// ============================================================================

async fn probe_pg() -> Option<sqlx::PgPool> {
    let url = std::env::var("DATABASE_URL")
        .unwrap_or_else(|_| "postgres://sunbeam:sunbeam@localhost:5432/sunbeam".to_string());
    match sqlx::PgPool::connect(&url).await {
        Ok(p) => Some(p),
        Err(e) => {
            eprintln!(
                "[health_integration] Postgres not reachable at {url}: {e}; skipping. \
                 Run `sunbeam ops compose up postgres` to enable these tests."
            );
            None
        }
    }
}

async fn probe_keto() -> Option<sunbeam_g2v::middleware::auth::KetoClient> {
    let grpc_endpoint = std::env::var("KETO_GRPC_URL")
        .unwrap_or_else(|_| "http://localhost:4466".to_string());
    let write_grpc_endpoint = std::env::var("KETO_WRITE_GRPC_URL")
        .unwrap_or_else(|_| "http://localhost:4467".to_string());

    let client = sunbeam_g2v::middleware::auth::KetoClient::new(
        sunbeam_g2v::middleware::auth::KetoConfig {
            grpc_endpoint: grpc_endpoint.clone(),
            write_grpc_endpoint,
        },
    );

    // A probe call — any result (allowed/denied/gRPC error) is fine as long as
    // there's no transport-level failure, which we treat as "not reachable".
    match tokio::time::timeout(
        std::time::Duration::from_secs(3),
        client.check_permission("__health__", "__probe__", "probe", "__probe__"),
    )
    .await
    {
        // Transport succeeded (even if Keto returned an error response).
        Ok(_) => Some(client),
        Err(_) => {
            eprintln!(
                "[health_integration] Keto not reachable at {grpc_endpoint} (timeout); skipping. \
                 Run `sunbeam ops compose up keto` to enable these tests."
            );
            None
        }
    }
}

// ============================================================================
// Hand-rolled failing check (used in the 503 harness test)
// ============================================================================

struct FailingCheck;

impl HealthCheck for FailingCheck {
    fn name(&self) -> &str {
        "failing"
    }

    fn check(
        &self,
    ) -> std::pin::Pin<
        Box<dyn std::future::Future<Output = sunbeam_g2v::error::ServiceResult<HealthCheckResult>> + Send + 'static>,
    > {
        Box::pin(async { Ok(HealthCheckResult::unhealthy("injected failure")) })
    }
}

// ============================================================================
// 1. DatabaseHealthCheck against workspace Postgres
// ============================================================================

#[tokio::test]
async fn test_database_health_check_against_workspace_postgres() {
    let Some(pool) = probe_pg().await else {
        return;
    };

    let check = sunbeam_g2v::health::DatabaseHealthCheck::new(pool);
    let result = check.check().await.expect("check returned Err");

    assert!(
        result.is_healthy(),
        "DatabaseHealthCheck should be healthy against workspace postgres; detail: {:?}",
        result.details
    );
}

// ============================================================================
// 2. KetoHealthCheck against workspace Keto
// ============================================================================

#[tokio::test]
async fn test_keto_health_check_against_workspace_keto() {
    let Some(client) = probe_keto().await else {
        return;
    };

    let check = sunbeam_g2v::health::KetoHealthCheck::new(client);
    let result = check.check().await.expect("check returned Err");

    assert!(
        result.is_healthy(),
        "KetoHealthCheck should be healthy against workspace keto; detail: {:?}",
        result.details
    );
}

// ============================================================================
// 3. HealthRouter /ready 200 when DB + Keto pass
// ============================================================================

#[tokio::test]
async fn test_health_router_ready_200_when_all_pass() {
    let Some(pool) = probe_pg().await else {
        return;
    };
    let Some(keto_client) = probe_keto().await else {
        return;
    };

    let health_router = HealthRouter::new()
        .with_check(Arc::new(
            sunbeam_g2v::health::DatabaseHealthCheck::new(pool),
        ))
        .with_check(Arc::new(
            sunbeam_g2v::health::KetoHealthCheck::new(keto_client),
        ))
        .into_axum_router();

    let mut harness = TestHarness::new().with_routes(health_router);
    harness.start().await.unwrap();

    let resp = harness.get("/health/ready").await.unwrap();
    assert_eq!(
        resp.status, 200,
        "/health/ready should return 200 when all checks pass; body: {}",
        resp.body_text()
    );

    let body: serde_json::Value = serde_json::from_slice(&resp.body)
        .expect("response should be valid JSON");
    assert_eq!(body["status"], "ok", "status field should be 'ok'");
    assert!(body["checks"].is_array(), "checks should be an array");

    let checks = body["checks"].as_array().unwrap();
    assert_eq!(checks.len(), 2, "should have 2 check entries");
    for entry in checks {
        assert_eq!(
            entry["healthy"], true,
            "all checks should be healthy; entry: {entry}"
        );
    }

    harness.stop().await.unwrap();
}

// ============================================================================
// 4. HealthRouter /ready 503 when one check fails (no real service needed)
// ============================================================================

#[tokio::test]
async fn test_health_router_ready_503_when_one_fails() {
    // This test doesn't need real services — it uses the hand-rolled FailingCheck.
    struct PassingCheck;
    impl HealthCheck for PassingCheck {
        fn name(&self) -> &str {
            "passing"
        }
        fn check(
            &self,
        ) -> std::pin::Pin<
            Box<
                dyn std::future::Future<
                        Output = sunbeam_g2v::error::ServiceResult<HealthCheckResult>,
                    > + Send
                    + 'static,
            >,
        > {
            Box::pin(async { Ok(HealthCheckResult::healthy()) })
        }
    }

    let health_router = HealthRouter::new()
        .with_check(Arc::new(PassingCheck))
        .with_check(Arc::new(FailingCheck))
        .into_axum_router();

    let mut harness = TestHarness::new().with_routes(health_router);
    harness.start().await.unwrap();

    let resp = harness.get("/health/ready").await.unwrap();
    assert_eq!(
        resp.status, 503,
        "/health/ready should return 503 when a check fails; body: {}",
        resp.body_text()
    );

    let body: serde_json::Value = serde_json::from_slice(&resp.body)
        .expect("response should be valid JSON");
    assert_eq!(body["status"], "degraded", "status should be 'degraded'");

    let checks = body["checks"].as_array().unwrap();
    let failing = checks
        .iter()
        .find(|e| e["name"] == "failing")
        .expect("should have a 'failing' entry");
    assert_eq!(failing["healthy"], false);
    assert_eq!(failing["detail"], "injected failure");

    harness.stop().await.unwrap();
}
