mod common;

use common::run_lua;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

fn oidc_discovery_json(issuer: &str) -> serde_json::Value {
    serde_json::json!({
        "issuer": issuer,
        "authorization_endpoint": format!("{issuer}/auth"),
        "token_endpoint": format!("{issuer}/token"),
        "jwks_uri": format!("{issuer}/keys"),
        "userinfo_endpoint": format!("{issuer}/userinfo"),
        "revocation_endpoint": format!("{issuer}/revoke"),
        "scopes_supported": ["openid", "profile", "email", "groups", "offline_access"],
        "response_types_supported": ["code", "id_token", "token"],
        "grant_types_supported": ["authorization_code", "refresh_token", "client_credentials"],
        "subject_types_supported": ["public"],
        "id_token_signing_alg_values_supported": ["RS256"]
    })
}

async fn mount_discovery(server: &MockServer) {
    let issuer = server.uri();
    Mock::given(method("GET"))
        .and(path("/.well-known/openid-configuration"))
        .respond_with(ResponseTemplate::new(200).set_body_json(oidc_discovery_json(&issuer)))
        .mount(server)
        .await;
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local config = c.discovery:config()
        assert.eq(config.issuer, "{}")
        assert.eq(config.authorization_endpoint, "{}/auth")
        assert.eq(config.token_endpoint, "{}/token")
        assert.eq(config.jwks_uri, "{}/keys")
        assert.eq(config.userinfo_endpoint, "{}/userinfo")
        "#,
        server.uri(),
        server.uri(),
        server.uri(),
        server.uri(),
        server.uri(),
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    Mock::given(method("GET"))
        .and(path("/keys"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "keys": [{
                "kty": "RSA",
                "alg": "RS256",
                "use": "sig",
                "kid": "test-key-id",
                "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM",
                "e": "AQAB"
            }]
        })))
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local jwks = c.discovery:jwks()
        assert.eq(#jwks.keys, 1)
        assert.eq(jwks.keys[1].kty, "RSA")
        assert.eq(jwks.keys[1].kid, "test-key-id")
        assert.eq(jwks.keys[1].alg, "RS256")
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local iss = c.discovery:issuer()
        assert.eq(iss, "{}")
        "#,
        server.uri(),
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_dex_health_success() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/healthz"))
        .respond_with(ResponseTemplate::new(200).set_body_string("ok"))
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local ok = c.health:check()
        assert.eq(ok, true)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_dex_health_failure() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/healthz"))
        .respond_with(ResponseTemplate::new(503).set_body_string("unhealthy"))
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local ok = c.health:check()
        assert.eq(ok, false)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_dex_ready() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/healthz"))
        .respond_with(ResponseTemplate::new(200).set_body_string("ok"))
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local ok = c.health:ready()
        assert.eq(ok, true)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        assert.eq(c.discovery:has_endpoint("authorization_endpoint"), true)
        assert.eq(c.discovery:has_endpoint("token_endpoint"), true)
        assert.eq(c.discovery:has_endpoint("userinfo_endpoint"), true)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        assert.eq(c.discovery:has_endpoint("device_authorization_endpoint"), false)
        assert.eq(c.discovery:has_endpoint("nonexistent_endpoint"), false)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local scopes = c.scopes:list()
        assert.eq(#scopes, 5)
        assert.eq(scopes[1], "openid")
        assert.eq(scopes[2], "profile")
        assert.eq(scopes[3], "email")
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        assert.eq(c.scopes:supports("openid"), true)
        assert.eq(c.scopes:supports("email"), true)
        assert.eq(c.scopes:supports("offline_access"), true)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        assert.eq(c.scopes:supports("phone"), false)
        assert.eq(c.scopes:supports("address"), false)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local types = c.grants:response_types()
        assert.eq(#types, 3)
        assert.eq(types[1], "code")
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local types = c.grants:list()
        assert.eq(#types, 3)
        assert.eq(types[1], "authorization_code")
        assert.eq(types[2], "refresh_token")
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        assert.eq(c.grants:supports("authorization_code"), true)
        assert.eq(c.grants:supports("refresh_token"), true)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        assert.eq(c.grants:supports("implicit"), false)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local result = c:validate_config()
        assert.eq(result.ok, true)
        assert.eq(#result.errors, 0)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_dex_validate_config_invalid_missing_fields() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/.well-known/openid-configuration"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "issuer": server.uri(),
            "scopes_supported": ["openid"]
        })))
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local result = c:validate_config()
        assert.eq(result.ok, false)
        assert.eq(#result.errors, 3)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_dex_validate_config_issuer_mismatch() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/.well-known/openid-configuration"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "issuer": "https://wrong-issuer.example.com",
            "authorization_endpoint": format!("{}/auth", server.uri()),
            "token_endpoint": format!("{}/token", server.uri()),
            "jwks_uri": format!("{}/keys", server.uri())
        })))
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local result = c:validate_config()
        assert.eq(result.ok, false)
        assert.eq(#result.errors, 1)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_dex_admin_version_success() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/api/v1/version"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "server": "dex",
            "version": "2.39.0"
        })))
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local ver = c:admin_version()
        assert.eq(ver.server, "dex")
        assert.eq(ver.version, "2.39.0")
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_dex_admin_version_unavailable() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/api/v1/version"))
        .respond_with(ResponseTemplate::new(404))
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local ver = c:admin_version()
        assert.eq(ver, nil)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

// ===== Sub-object client pattern tests =====

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local config = c.discovery:config()
        assert.eq(config.issuer, "{}")
        assert.eq(config.authorization_endpoint, "{}/auth")
        "#,
        server.uri(),
        server.uri(),
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    Mock::given(method("GET"))
        .and(path("/keys"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "keys": [{
                "kty": "RSA",
                "alg": "RS256",
                "use": "sig",
                "kid": "client-test-key",
                "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM",
                "e": "AQAB"
            }]
        })))
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local jwks = c.discovery:jwks()
        assert.eq(#jwks.keys, 1)
        assert.eq(jwks.keys[1].kid, "client-test-key")
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local iss = c.discovery:issuer()
        assert.eq(iss, "{}")
        "#,
        server.uri(),
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_dex_client_health_check() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/healthz"))
        .respond_with(ResponseTemplate::new(200).set_body_string("ok"))
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        assert.eq(c.health:check(), true)
        assert.eq(c.health:ready(), true)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local scopes = c.scopes:list()
        assert.eq(#scopes, 5)
        assert.eq(c.scopes:supports("openid"), true)
        assert.eq(c.scopes:supports("phone"), false)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local grants = c.grants:list()
        assert.eq(#grants, 3)
        assert.eq(c.grants:supports("authorization_code"), true)
        assert.eq(c.grants:supports("implicit"), false)
        local rt = c.grants:response_types()
        assert.eq(#rt, 3)
        assert.eq(rt[1], "code")
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local result = c:validate_config()
        assert.eq(result.ok, true)
        assert.eq(#result.errors, 0)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_dex_client_admin_version() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/api/v1/version"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "server": "dex",
            "version": "2.39.0"
        })))
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        local ver = c:admin_version()
        assert.eq(ver.server, "dex")
        assert.eq(ver.version, "2.39.0")
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local dex = require("assay.dex")
        local c = dex.client("{}")
        assert.eq(c.discovery:has_endpoint("authorization_endpoint"), true)
        assert.eq(c.discovery:has_endpoint("nonexistent_endpoint"), false)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}
