//! Integration tests for OIDC provider support
//!
//! Tests the complete OIDC flow with various provider backends:
//! - OIDC provider discovery (well-known endpoint)
//! - JWKS (JSON Web Key Set) retrieval and caching
//! - ID token validation (signature, expiry, claims)
//! - Provider-specific configurations (Auth0, Google, Okta, Microsoft)
//!
//! Uses wiremock to mock provider endpoints without needing real credentials.
//!
//! **Execution engine:** none
//! **Infrastructure:** none
//! **Parallelism:** safe

#![allow(clippy::unwrap_used, clippy::panic)] // Reason: test code, panics acceptable
#![allow(clippy::future_not_send)] // Reason: wiremock futures are not Send; tests run single-threaded

use fraiseql_server::auth::{OAuthProvider, oidc_provider::OidcProvider};
use serde_json::json;
use wiremock::{
    Mock, MockServer, ResponseTemplate,
    matchers::{method, path},
};

/// Wrapper that sets `FRAISEQL_OIDC_ALLOW_INSECURE=1` so wiremock's `http://`
/// server passes the SSRF scheme check (which otherwise requires `https://`).
async fn new_provider_insecure(
    name: &str,
    issuer_url: &str,
    client_id: &str,
    client_secret: &str,
    redirect_uri: &str,
) -> Result<OidcProvider, fraiseql_server::auth::AuthError> {
    temp_env::async_with_vars(
        [("FRAISEQL_OIDC_ALLOW_INSECURE", Some("1"))],
        OidcProvider::new(name, issuer_url, client_id, client_secret, redirect_uri),
    )
    .await
}

// ============================================================================
// HELPER FUNCTIONS
// ============================================================================

/// Create a standard OIDC discovery document
fn standard_discovery_doc() -> serde_json::Value {
    json!({
        "issuer": "https://provider.example.com",
        "authorization_endpoint": "https://provider.example.com/authorize",
        "token_endpoint": "https://provider.example.com/token",
        "userinfo_endpoint": "https://provider.example.com/userinfo",
        "jwks_uri": "https://provider.example.com/jwks",
        "revocation_endpoint": "https://provider.example.com/revoke",
        "response_types_supported": ["code", "id_token", "token id_token"],
        "subject_types_supported": ["public"],
        "id_token_signing_alg_values_supported": ["RS256"],
        "scopes_supported": ["openid", "email", "profile"]
    })
}

/// Create Auth0-specific discovery document
fn auth0_discovery_doc(tenant: &str) -> serde_json::Value {
    json!({
        "issuer": format!("https://{}.auth0.com/", tenant),
        "authorization_endpoint": format!("https://{}.auth0.com/authorize", tenant),
        "token_endpoint": format!("https://{}.auth0.com/oauth/token", tenant),
        "userinfo_endpoint": format!("https://{}.auth0.com/userinfo", tenant),
        "jwks_uri": format!("https://{}.auth0.com/.well-known/jwks.json", tenant),
        "response_types_supported": ["code", "code id_token", "code token", "code id_token token", "id_token", "id_token token", "token"],
        "subject_types_supported": ["public"],
        "id_token_signing_alg_values_supported": ["RS256"],
        "scopes_supported": ["openid", "profile", "email"]
    })
}

/// Create Google-specific discovery document
fn google_discovery_doc() -> serde_json::Value {
    json!({
        "issuer": "https://accounts.google.com",
        "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
        "token_endpoint": "https://oauth2.googleapis.com/token",
        "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
        "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
        "response_types_supported": ["code", "id_token", "id_token token", "code id_token", "code id_token token"],
        "subject_types_supported": ["public"],
        "id_token_signing_alg_values_supported": ["RS256"],
        "scopes_supported": ["openid", "email", "profile"]
    })
}

/// Create Microsoft-specific discovery document
fn microsoft_discovery_doc() -> serde_json::Value {
    json!({
        "issuer": "https://login.microsoftonline.com/common/v2.0",
        "authorization_endpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
        "token_endpoint": "https://login.microsoftonline.com/common/oauth2/v2.0/token",
        "userinfo_endpoint": "https://graph.microsoft.com/oidc/userinfo",
        "jwks_uri": "https://login.microsoftonline.com/common/discovery/v2.0/keys",
        "response_types_supported": ["code", "id_token", "token id_token", "code id_token", "code token", "code id_token token"],
        "subject_types_supported": ["public"],
        "id_token_signing_alg_values_supported": ["RS256"],
        "scopes_supported": ["openid", "email", "profile"]
    })
}

/// Create Okta-specific discovery document
fn okta_discovery_doc(tenant: &str) -> serde_json::Value {
    json!({
        "issuer": format!("https://{}.okta.com", tenant),
        "authorization_endpoint": format!("https://{}.okta.com/oauth2/v1/authorize", tenant),
        "token_endpoint": format!("https://{}.okta.com/oauth2/v1/token", tenant),
        "userinfo_endpoint": format!("https://{}.okta.com/oauth2/v1/userinfo", tenant),
        "jwks_uri": format!("https://{}.okta.com/oauth2/v1/keys", tenant),
        "revocation_endpoint": format!("https://{}.okta.com/oauth2/v1/revoke", tenant),
        "response_types_supported": ["code", "code id_token", "code token", "code id_token token", "id_token", "id_token token"],
        "subject_types_supported": ["public"],
        "id_token_signing_alg_values_supported": ["RS256"],
        "scopes_supported": ["openid", "email", "profile"]
    })
}

/// Allow mock HTTP servers in OIDC tests (wiremock binds to `http://127.0.0.1`).
///
/// The SSRF guard requires `https://` in production. Tests use a local mock
/// server over plain HTTP and set `FRAISEQL_OIDC_ALLOW_INSECURE=true` to
/// bypass that guard, which is the documented escape hatch.
fn allow_insecure_for_test() {
    std::env::set_var("FRAISEQL_OIDC_ALLOW_INSECURE", "true");
}

// ============================================================================
// OIDC PROVIDER DISCOVERY TESTS
// ============================================================================

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

    // Mock the OIDC discovery endpoint
    Mock::given(method("GET"))
        .and(path("/.well-known/openid-configuration"))
        .respond_with(ResponseTemplate::new(200).set_body_json(standard_discovery_doc()))
        .mount(&server)
        .await;

    // Create provider by fetching discovery
    let provider = new_provider_insecure(
        "test-provider",
        &server.uri(),
        "test-client-id",
        "test-client-secret",
        "http://localhost:8000/auth/callback",
    )
    .await;

    let provider =
        provider.unwrap_or_else(|e| panic!("Provider should be created successfully: {e}"));
    assert_eq!(provider.name(), "test-provider");
}

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

    let discovery = standard_discovery_doc();
    Mock::given(method("GET"))
        .and(path("/.well-known/openid-configuration"))
        .respond_with(ResponseTemplate::new(200).set_body_json(&discovery))
        .mount(&server)
        .await;

    let provider = new_provider_insecure(
        "test",
        &server.uri(),
        "client-id",
        "client-secret",
        "http://localhost:8000/callback",
    )
    .await;

    provider.unwrap_or_else(|e| panic!("expected Ok creating provider: {e}"));

    // Verify discovery document contains required fields
    let doc = discovery;
    assert!(doc["issuer"].is_string());
    assert!(doc["authorization_endpoint"].is_string());
    assert!(doc["token_endpoint"].is_string());
    assert!(doc["userinfo_endpoint"].is_string());
    assert!(doc["jwks_uri"].is_string());
}

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

    // Discovery doc missing required field
    let incomplete_discovery = json!({
        "issuer": "https://example.com",
        "authorization_endpoint": "https://example.com/authorize",
        "token_endpoint": "https://example.com/token",
        // Missing: userinfo_endpoint, jwks_uri
    });

    Mock::given(method("GET"))
        .and(path("/.well-known/openid-configuration"))
        .respond_with(ResponseTemplate::new(200).set_body_json(incomplete_discovery))
        .mount(&server)
        .await;

    // Provider creation should handle missing fields gracefully
    let provider = new_provider_insecure(
        "test",
        &server.uri(),
        "client-id",
        "client-secret",
        "http://localhost:8000/callback",
    )
    .await;

    // May fail or succeed depending on implementation
    let _ = provider; // intentional
}

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

    Mock::given(method("GET"))
        .and(path("/.well-known/openid-configuration"))
        .respond_with(ResponseTemplate::new(404))
        .mount(&server)
        .await;

    let provider = new_provider_insecure(
        "test",
        &server.uri(),
        "client-id",
        "client-secret",
        "http://localhost:8000/callback",
    )
    .await;

    assert!(provider.is_err(), "Provider should fail when discovery endpoint returns 404");
}

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

    Mock::given(method("GET"))
        .and(path("/.well-known/openid-configuration"))
        .respond_with(ResponseTemplate::new(200).set_body_string("invalid json {{{"))
        .mount(&server)
        .await;

    let provider = new_provider_insecure(
        "test",
        &server.uri(),
        "client-id",
        "client-secret",
        "http://localhost:8000/callback",
    )
    .await;

    assert!(
        provider.is_err(),
        "Provider should fail when discovery endpoint returns invalid JSON"
    );
}

// ============================================================================
// JWKS RETRIEVAL AND CACHING TESTS
// ============================================================================

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

    // Mock OIDC discovery
    Mock::given(method("GET"))
        .and(path("/.well-known/openid-configuration"))
        .respond_with(ResponseTemplate::new(200).set_body_json(standard_discovery_doc()))
        .mount(&server)
        .await;

    // Mock JWKS endpoint
    let jwks = json!({
        "keys": [
            {
                "kty": "RSA",
                "use": "sig",
                "kid": "key-1",
                "n": "module_of_large_rsa_key",
                "e": "AQAB",
                "alg": "RS256"
            }
        ]
    });

    Mock::given(method("GET"))
        .and(path("/jwks"))
        .respond_with(ResponseTemplate::new(200).set_body_json(jwks))
        .mount(&server)
        .await;

    let provider = new_provider_insecure(
        "test",
        &server.uri(),
        "client-id",
        "client-secret",
        "http://localhost:8000/callback",
    )
    .await;

    provider.unwrap_or_else(|e| panic!("expected Ok creating provider with JWKS endpoint: {e}"));
}

#[tokio::test]
async fn test_jwks_endpoint_contains_public_keys() {
    let jwks = json!({
        "keys": [
            {
                "kty": "RSA",
                "use": "sig",
                "kid": "key-1",
                "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw",
                "e": "AQAB",
                "alg": "RS256"
            }
        ]
    });

    assert!(jwks["keys"].is_array());
    assert_eq!(jwks["keys"].as_array().unwrap().len(), 1);

    let key = &jwks["keys"][0];
    assert_eq!(key["kty"], "RSA");
    assert_eq!(key["use"], "sig");
    assert!(key["n"].is_string());
    assert!(key["e"].is_string());
}

#[tokio::test]
async fn test_jwks_multiple_keys() {
    let jwks = json!({
        "keys": [
            {
                "kty": "RSA",
                "use": "sig",
                "kid": "key-1",
                "n": "...",
                "e": "AQAB"
            },
            {
                "kty": "RSA",
                "use": "sig",
                "kid": "key-2",
                "n": "...",
                "e": "AQAB"
            }
        ]
    });

    assert_eq!(jwks["keys"].as_array().unwrap().len(), 2);
}

// ============================================================================
// AUTH0 PROVIDER TESTS
// ============================================================================

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

    let tenant = "test-tenant";
    Mock::given(method("GET"))
        .and(path("/.well-known/openid-configuration"))
        .respond_with(ResponseTemplate::new(200).set_body_json(auth0_discovery_doc(tenant)))
        .mount(&server)
        .await;

    let provider = new_provider_insecure(
        "auth0",
        &server.uri(),
        "auth0-client-id",
        "auth0-client-secret",
        "http://localhost:8000/auth/callback",
    )
    .await;

    let provider = provider.unwrap_or_else(|e| panic!("expected Ok creating auth0 provider: {e}"));
    assert_eq!(provider.name(), "auth0");
}

#[tokio::test]
async fn test_auth0_discovery_contains_oauth_token_endpoint() {
    let doc = auth0_discovery_doc("example");
    assert!(doc["token_endpoint"].as_str().unwrap().contains("/oauth/token"));
    assert!(doc["jwks_uri"].as_str().unwrap().contains("/.well-known/jwks.json"));
}

// ============================================================================
// GOOGLE OAUTH PROVIDER TESTS
// ============================================================================

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

    Mock::given(method("GET"))
        .and(path("/.well-known/openid-configuration"))
        .respond_with(ResponseTemplate::new(200).set_body_json(google_discovery_doc()))
        .mount(&server)
        .await;

    let provider = new_provider_insecure(
        "google",
        &server.uri(),
        "google-client-id",
        "google-client-secret",
        "http://localhost:8000/auth/google/callback",
    )
    .await;

    provider.unwrap_or_else(|e| panic!("expected Ok creating google provider: {e}"));
}

#[tokio::test]
async fn test_google_discovery_endpoints() {
    let doc = google_discovery_doc();
    assert!(doc["authorization_endpoint"].as_str().unwrap().contains("accounts.google.com"));
    assert!(doc["token_endpoint"].as_str().unwrap().contains("oauth2.googleapis.com"));
    assert!(doc["jwks_uri"].as_str().unwrap().contains("googleapis.com"));
}

// ============================================================================
// MICROSOFT PROVIDER TESTS
// ============================================================================

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

    Mock::given(method("GET"))
        .and(path("/.well-known/openid-configuration"))
        .respond_with(ResponseTemplate::new(200).set_body_json(microsoft_discovery_doc()))
        .mount(&server)
        .await;

    let provider = new_provider_insecure(
        "microsoft",
        &server.uri(),
        "microsoft-client-id",
        "microsoft-client-secret",
        "http://localhost:8000/auth/microsoft/callback",
    )
    .await;

    provider.unwrap_or_else(|e| panic!("expected Ok creating microsoft provider: {e}"));
}

#[tokio::test]
async fn test_microsoft_discovery_v2_endpoints() {
    let doc = microsoft_discovery_doc();
    assert!(doc["token_endpoint"].as_str().unwrap().contains("/v2.0/token"));
    assert!(doc["authorization_endpoint"].as_str().unwrap().contains("/v2.0/authorize"));
}

// ============================================================================
// OKTA PROVIDER TESTS
// ============================================================================

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

    Mock::given(method("GET"))
        .and(path("/.well-known/openid-configuration"))
        .respond_with(ResponseTemplate::new(200).set_body_json(okta_discovery_doc("dev-12345")))
        .mount(&server)
        .await;

    let provider = new_provider_insecure(
        "okta",
        &server.uri(),
        "okta-client-id",
        "okta-client-secret",
        "http://localhost:8000/auth/okta/callback",
    )
    .await;

    provider.unwrap_or_else(|e| panic!("expected Ok creating okta provider: {e}"));
}

#[tokio::test]
async fn test_okta_discovery_tenant_specific_endpoints() {
    let doc = okta_discovery_doc("dev-12345");
    assert!(doc["token_endpoint"].as_str().unwrap().contains("dev-12345.okta.com"));
    assert!(doc["jwks_uri"].as_str().unwrap().contains("/oauth2/v1/keys"));
    assert!(doc["revocation_endpoint"].as_str().unwrap().contains("/oauth2/v1/revoke"));
}

// ============================================================================
// PROVIDER CONFIGURATION TESTS
// ============================================================================

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

    Mock::given(method("GET"))
        .and(path("/.well-known/openid-configuration"))
        .respond_with(ResponseTemplate::new(200).set_body_json(standard_discovery_doc()))
        .mount(&server)
        .await;

    let provider = new_provider_insecure(
        "my-custom-provider",
        &server.uri(),
        "id",
        "secret",
        "http://localhost:8000/callback",
    )
    .await
    .unwrap();

    assert_eq!(provider.name(), "my-custom-provider");
}

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

    Mock::given(method("GET"))
        .and(path("/.well-known/openid-configuration"))
        .respond_with(ResponseTemplate::new(200).set_body_json(standard_discovery_doc()))
        .mount(&server)
        .await;

    let provider = new_provider_insecure(
        "test",
        &server.uri(),
        "test-client-id",
        "test-client-secret",
        "http://localhost:8000/auth/callback",
    )
    .await
    .unwrap();

    // Generate authorization URL
    let auth_url = provider.authorization_url("state-token-123");

    // Verify URL structure
    assert!(auth_url.contains("authorize"));
    assert!(auth_url.contains("client_id="));
    assert!(auth_url.contains("state="));
    assert!(auth_url.contains("scope="));
    assert!(auth_url.contains("openid"));
}

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

    Mock::given(method("GET"))
        .and(path("/.well-known/openid-configuration"))
        .respond_with(ResponseTemplate::new(200).set_body_json(standard_discovery_doc()))
        .mount(&server)
        .await;

    // Create multiple provider instances with different names
    let provider1 = new_provider_insecure(
        "provider-1",
        &server.uri(),
        "id1",
        "secret1",
        "http://localhost:8000/callback1",
    )
    .await;

    let provider2 = new_provider_insecure(
        "provider-2",
        &server.uri(),
        "id2",
        "secret2",
        "http://localhost:8000/callback2",
    )
    .await;

    let p1 = provider1.unwrap_or_else(|e| panic!("expected Ok creating provider-1: {e}"));
    let p2 = provider2.unwrap_or_else(|e| panic!("expected Ok creating provider-2: {e}"));

    // Each should have distinct names
    assert_eq!(p1.name(), "provider-1");
    assert_eq!(p2.name(), "provider-2");
}

// ============================================================================
// ERROR HANDLING TESTS
// ============================================================================

#[tokio::test]
async fn test_discovery_timeout_handling() {
    // Test that provider handles connection failures gracefully
    let invalid_url = "http://localhost:1/invalid";

    let provider = new_provider_insecure(
        "test",
        invalid_url,
        "id",
        "secret",
        "http://localhost:8000/callback",
    )
    .await;

    assert!(provider.is_err(), "Provider should fail on invalid URL");
}

#[tokio::test]
async fn test_multiple_scopes_in_discovery() {
    let doc = json!({
        "issuer": "https://example.com",
        "authorization_endpoint": "https://example.com/authorize",
        "token_endpoint": "https://example.com/token",
        "userinfo_endpoint": "https://example.com/userinfo",
        "jwks_uri": "https://example.com/jwks",
        "scopes_supported": ["openid", "email", "profile", "groups", "roles"]
    });

    let scopes = doc["scopes_supported"].as_array().unwrap();
    assert!(scopes.len() >= 5);
    assert!(scopes.iter().any(|s| s == "openid"));
    assert!(scopes.iter().any(|s| s == "groups"));
}
