mod common;

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

#[tokio::test]
async fn test_hydra_require() {
    let script = r#"
        local hydra = require("assay.ory.hydra")
        assert.not_nil(hydra)
        assert.not_nil(hydra.client)
    "#;
    run_lua(script).await.unwrap();
}

#[tokio::test]
async fn test_hydra_clients_list() {
    let admin = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/admin/clients"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!([
            { "client_id": "example-app", "client_name": "Example App" },
            { "client_id": "another-app", "client_name": "Another App" }
        ])))
        .mount(&admin)
        .await;

    let script = format!(
        r#"
        local hydra = require("assay.ory.hydra")
        local h = hydra.client({{ admin_url = "{}" }})
        local clients = h.clients:list()
        assert.eq(#clients, 2)
        assert.eq(clients[1].client_id, "example-app")
        "#,
        admin.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_hydra_clients_update() {
    let admin = MockServer::start().await;
    Mock::given(method("PUT"))
        .and(path("/admin/clients/example-app"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "client_id": "example-app",
            "client_name": "Example App",
            "token_endpoint_auth_method": "client_secret_post"
        })))
        .mount(&admin)
        .await;

    let script = format!(
        r#"
        local hydra = require("assay.ory.hydra")
        local h = hydra.client({{ admin_url = "{}" }})
        local client = h.clients:update("example-app", {{
          client_name = "Example App",
          client_secret = "secret",
          grant_types = {{"authorization_code", "refresh_token"}},
          response_types = {{"code"}},
          scope = "openid profile email",
          redirect_uris = {{"https://app.example.com/auth/callback"}},
          token_endpoint_auth_method = "client_secret_post",
        }})
        assert.eq(client.client_id, "example-app")
        "#,
        admin.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_hydra_oauth2_authorize_url() {
    let script = r#"
        local hydra = require("assay.ory.hydra")
        local h = hydra.client({ public_url = "https://hydra.example.com" })
        local url = h.oauth2:authorize_url("example-app", {
          redirect_uri = "https://app.example.com/auth/callback",
          scope = "openid profile email",
          state = "xyz",
        })
        assert.contains(url, "https://hydra.example.com/oauth2/auth?")
        assert.contains(url, "client_id=example-app")
        assert.contains(url, "response_type=code")
        assert.contains(url, "state=xyz")
    "#;
    run_lua(script).await.unwrap();
}

#[tokio::test]
async fn test_hydra_oauth2_exchange_code() {
    let public = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/oauth2/token"))
        .and(body_string_contains("grant_type=authorization_code"))
        .and(body_string_contains("code=abc"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "access_token": "access.jwt",
            "id_token": "id.jwt",
            "refresh_token": "refresh.opaque",
            "token_type": "bearer",
            "expires_in": 3600
        })))
        .mount(&public)
        .await;

    let script = format!(
        r#"
        local hydra = require("assay.ory.hydra")
        local h = hydra.client({{ public_url = "{}" }})
        local tokens = h.oauth2:exchange_code({{
          code = "abc",
          redirect_uri = "https://app.example.com/auth/callback",
          client_id = "example-app",
          client_secret = "secret",
        }})
        assert.eq(tokens.access_token, "access.jwt")
        assert.eq(tokens.id_token, "id.jwt")
        "#,
        public.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_hydra_login_accept() {
    let admin = MockServer::start().await;
    Mock::given(method("PUT"))
        .and(path("/admin/oauth2/auth/requests/login/accept"))
        .and(query_param("login_challenge", "abc123"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "redirect_to": "https://hydra.example.com/oauth2/auth?client_id=example-app&..."
        })))
        .mount(&admin)
        .await;

    let script = format!(
        r#"
        local hydra = require("assay.ory.hydra")
        local h = hydra.client({{ admin_url = "{}" }})
        local result = h.login:accept("abc123", "user:alice", {{ remember = true, remember_for = 86400 }})
        assert.contains(result.redirect_to, "hydra.example.com")
        "#,
        admin.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_hydra_consent_accept_with_claims() {
    let admin = MockServer::start().await;
    Mock::given(method("PUT"))
        .and(path("/admin/oauth2/auth/requests/consent/accept"))
        .and(query_param("consent_challenge", "xyz789"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "redirect_to": "https://app.example.com/auth/callback?code=..."
        })))
        .mount(&admin)
        .await;

    let script = format!(
        r#"
        local hydra = require("assay.ory.hydra")
        local h = hydra.client({{ admin_url = "{}" }})
        local result = h.consent:accept("xyz789", {{
          grant_scope = {{"openid", "profile", "email"}},
          session = {{
            id_token = {{
              sub = "user:alice",
              email = "alice@example.com",
              role = "admin",
            }},
          }},
        }})
        assert.contains(result.redirect_to, "app.example.com")
        "#,
        admin.uri()
    );
    run_lua(&script).await.unwrap();
}

// Regression: when the caller doesn't pass grant_access_token_audience,
// the Lua default of `or {}` used to serialise as `{}` (JSON object),
// which Hydra rejects with `cannot unmarshal object into Go struct field
// AcceptOAuth2ConsentRequest.grant_access_token_audience of type
// sqlxx.StringSliceJSONFormat`. Wrap default in json.array() so it
// serialises as `[]`.
#[tokio::test]
async fn test_hydra_consent_accept_empty_audience_is_json_array() {
    let admin = MockServer::start().await;
    Mock::given(method("PUT"))
        .and(path("/admin/oauth2/auth/requests/consent/accept"))
        .and(query_param("consent_challenge", "xyz789"))
        .and(body_string_contains("\"grant_access_token_audience\":[]"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "redirect_to": "https://app.example.com/auth/callback?code=..."
        })))
        .mount(&admin)
        .await;

    let script = format!(
        r#"
        local hydra = require("assay.ory.hydra")
        local h = hydra.client({{ admin_url = "{}" }})
        -- No grant_access_token_audience passed → must serialise as [].
        local result = h.consent:accept("xyz789", {{
          grant_scope = {{"openid"}},
          session = {{ id_token = {{ sub = "user:alice" }} }},
        }})
        assert.contains(result.redirect_to, "app.example.com")
        "#,
        admin.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_hydra_logout_get() {
    let admin = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/admin/oauth2/auth/requests/logout"))
        .and(query_param("logout_challenge", "logout-abc"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "request_url": "https://hydra.example.com/oauth2/sessions/logout",
            "rp_initiated": true,
            "sid": "session-xyz",
            "subject": "user:alice",
            "client": { "client_id": "example-app" }
        })))
        .mount(&admin)
        .await;

    let script = format!(
        r#"
        local hydra = require("assay.ory.hydra")
        local h = hydra.client({{ admin_url = "{}" }})
        local req = h.logout:get("logout-abc")
        assert.eq(req.subject, "user:alice")
        assert.eq(req.rp_initiated, true)
        assert.eq(req.client.client_id, "example-app")
        "#,
        admin.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_hydra_logout_accept() {
    let admin = MockServer::start().await;
    Mock::given(method("PUT"))
        .and(path("/admin/oauth2/auth/requests/logout/accept"))
        .and(query_param("logout_challenge", "logout-abc"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "redirect_to": "https://app.example.com/auth/login"
        })))
        .mount(&admin)
        .await;

    let script = format!(
        r#"
        local hydra = require("assay.ory.hydra")
        local h = hydra.client({{ admin_url = "{}" }})
        local result = h.logout:accept("logout-abc")
        assert.contains(result.redirect_to, "app.example.com")
        "#,
        admin.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_hydra_logout_reject() {
    let admin = MockServer::start().await;
    Mock::given(method("PUT"))
        .and(path("/admin/oauth2/auth/requests/logout/reject"))
        .and(query_param("logout_challenge", "logout-abc"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
        .mount(&admin)
        .await;

    let script = format!(
        r#"
        local hydra = require("assay.ory.hydra")
        local h = hydra.client({{ admin_url = "{}" }})
        h.logout:reject("logout-abc")
        "#,
        admin.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_hydra_oauth2_introspect() {
    let admin = MockServer::start().await;
    Mock::given(method("POST"))
        .and(path("/admin/oauth2/introspect"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "active": true,
            "sub": "user:alice",
            "scope": "openid profile email"
        })))
        .mount(&admin)
        .await;

    let script = format!(
        r#"
        local hydra = require("assay.ory.hydra")
        local h = hydra.client({{ admin_url = "{}" }})
        local info = h.oauth2:introspect("access.jwt")
        assert.eq(info.active, true)
        assert.eq(info.sub, "user:alice")
        "#,
        admin.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_hydra_discovery_openid_config() {
    let public = 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://hydra.example.com",
            "authorization_endpoint": "https://hydra.example.com/oauth2/auth",
            "token_endpoint": "https://hydra.example.com/oauth2/token"
        })))
        .mount(&public)
        .await;

    let script = format!(
        r#"
        local hydra = require("assay.ory.hydra")
        local h = hydra.client({{ public_url = "{}" }})
        local wk = h.discovery:openid_config()
        assert.contains(wk.issuer, "hydra.example.com")
        "#,
        public.uri()
    );
    run_lua(&script).await.unwrap();
}
