mod common;

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

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

#[tokio::test]
async fn test_keto_tuples_list() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/relation-tuples"))
        .and(query_param("namespace", "Role"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "relation_tuples": [
                {
                    "namespace": "Role",
                    "object": "namespace1:role-a",
                    "relation": "members",
                    "subject_id": "user:alice"
                }
            ],
            "next_page_token": ""
        })))
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local keto = require("assay.ory.keto")
        local k = keto.client("{}")
        local result = k.tuples:list({{ namespace = "Role" }})
        assert.eq(#result.relation_tuples, 1)
        assert.eq(result.relation_tuples[1].object, "namespace1:role-a")
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_keto_permissions_check_allowed() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/relation-tuples/check"))
        .respond_with(
            ResponseTemplate::new(200).set_body_json(serde_json::json!({ "allowed": true })),
        )
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local keto = require("assay.ory.keto")
        local k = keto.client("{}")
        local ok = k.permissions:check("apps", "cc", "admin", "user:alice")
        assert.eq(ok, true)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_keto_permissions_check_denied() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/relation-tuples/check"))
        .respond_with(
            ResponseTemplate::new(200).set_body_json(serde_json::json!({ "allowed": false })),
        )
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local keto = require("assay.ory.keto")
        local k = keto.client("{}")
        local ok = k.permissions:check("apps", "cc", "admin", "user:bob")
        assert.eq(ok, false)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_keto_roles_user_roles() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/relation-tuples"))
        .and(query_param("namespace", "Role"))
        .and(query_param("relation", "members"))
        .and(query_param("subject_id", "user:alice"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "relation_tuples": [
                {
                    "namespace": "Role",
                    "object": "namespace1:role-a",
                    "relation": "members",
                    "subject_id": "user:alice"
                },
                {
                    "namespace": "Role",
                    "object": "namespace2:role-a",
                    "relation": "members",
                    "subject_id": "user:alice"
                }
            ]
        })))
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local keto = require("assay.ory.keto")
        local k = keto.client("{}")
        local roles = k.roles:user_roles("alice")
        assert.eq(#roles, 2)
        assert.eq(roles[1].object, "namespace1:role-a")
        assert.eq(roles[2].object, "namespace2:role-a")
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_keto_roles_has_any() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/relation-tuples"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "relation_tuples": [
                {
                    "namespace": "Role",
                    "object": "namespace2:role-b",
                    "relation": "members",
                    "subject_id": "user:bob"
                }
            ]
        })))
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local keto = require("assay.ory.keto")
        local k = keto.client("{}")
        local has_admin = k.roles:has_any("bob", {{"namespace1:role-a", "namespace2:role-a"}})
        assert.eq(has_admin, false)
        local has_op = k.roles:has_any("bob", {{"namespace2:role-b"}})
        assert.eq(has_op, true)
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_keto_permissions_expand() {
    let server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/relation-tuples/expand"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "type": "union",
            "tuple": {
                "namespace": "Role",
                "object": "namespace1:role-a",
                "relation": "members"
            },
            "children": []
        })))
        .mount(&server)
        .await;

    let script = format!(
        r#"
        local keto = require("assay.ory.keto")
        local k = keto.client("{}")
        local tree = k.permissions:expand("Role", "namespace1:role-a", "members")
        assert.eq(tree.type, "union")
        "#,
        server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_keto_tuples_create() {
    let read_server = MockServer::start().await;
    let write_server = MockServer::start().await;
    Mock::given(method("PUT"))
        .and(path("/admin/relation-tuples"))
        .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({})))
        .mount(&write_server)
        .await;

    let script = format!(
        r#"
        local keto = require("assay.ory.keto")
        local k = keto.client("{}", {{ write_url = "{}" }})
        k.tuples:create({{
          namespace = "Role",
          object = "namespace2:role-c",
          relation = "members",
          subject_id = "user:carol",
        }})
        "#,
        read_server.uri(),
        write_server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local keto = require("assay.ory.keto")
        local k = keto.client("{}")
        local ok, err = pcall(function()
          k.tuples:create({{ namespace = "Role", object = "x", relation = "members", subject_id = "user:y" }})
        end)
        assert.eq(ok, false)
        assert.contains(tostring(err), "write_url not configured")
        "#,
        read_server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_keto_tuples_upsert_created_when_absent() {
    // Empty list response -> upsert creates exactly one tuple.
    let read_server = MockServer::start().await;
    let write_server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/relation-tuples"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "relation_tuples": [],
            "next_page_token": ""
        })))
        .mount(&read_server)
        .await;
    Mock::given(method("PUT"))
        .and(path("/admin/relation-tuples"))
        .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({})))
        .mount(&write_server)
        .await;

    let script = format!(
        r#"
        local keto = require("assay.ory.keto")
        local k = keto.client("{}", {{ write_url = "{}" }})
        local action, dups = k.tuples:upsert({{
          namespace = "Role",
          object = "namespace1:role-a",
          relation = "members",
          subject_id = "user:alice",
        }})
        assert.eq(action, "created")
        assert.eq(dups, 0)
        "#,
        read_server.uri(),
        write_server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_keto_tuples_upsert_noop_when_one_exists() {
    // List returns exactly one matching tuple -> upsert is a no-op
    // (no PUT, no DELETE issued).
    let read_server = MockServer::start().await;
    let write_server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/relation-tuples"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "relation_tuples": [
                {
                    "namespace": "Role",
                    "object": "namespace1:role-a",
                    "relation": "members",
                    "subject_id": "user:alice"
                }
            ],
            "next_page_token": ""
        })))
        .mount(&read_server)
        .await;
    // No write mocks mounted — if upsert tries to call PUT or DELETE,
    // wiremock returns 404 and the upsert errors. That's the assertion
    // shape we want: "noop" really means no write side-effect.

    let script = format!(
        r#"
        local keto = require("assay.ory.keto")
        local k = keto.client("{}", {{ write_url = "{}" }})
        local action, dups = k.tuples:upsert({{
          namespace = "Role",
          object = "namespace1:role-a",
          relation = "members",
          subject_id = "user:alice",
        }})
        assert.eq(action, "noop")
        assert.eq(dups, 0)
        "#,
        read_server.uri(),
        write_server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_keto_tuples_upsert_repairs_duplicates() {
    // List returns 4 identical tuples -> upsert deletes them all and
    // creates exactly one. Returns ("repaired", 3) — the 3 surplus
    // tuples that were removed (the kept tuple is excluded from the
    // count).
    let read_server = MockServer::start().await;
    let write_server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/relation-tuples"))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "relation_tuples": [
                {"namespace": "Role", "object": "x", "relation": "members", "subject_id": "user:dup"},
                {"namespace": "Role", "object": "x", "relation": "members", "subject_id": "user:dup"},
                {"namespace": "Role", "object": "x", "relation": "members", "subject_id": "user:dup"},
                {"namespace": "Role", "object": "x", "relation": "members", "subject_id": "user:dup"}
            ],
            "next_page_token": ""
        })))
        .mount(&read_server)
        .await;
    Mock::given(method("DELETE"))
        .and(path("/admin/relation-tuples"))
        .respond_with(ResponseTemplate::new(204))
        .mount(&write_server)
        .await;
    Mock::given(method("PUT"))
        .and(path("/admin/relation-tuples"))
        .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({})))
        .mount(&write_server)
        .await;

    let script = format!(
        r#"
        local keto = require("assay.ory.keto")
        local k = keto.client("{}", {{ write_url = "{}" }})
        local action, dups = k.tuples:upsert({{
          namespace = "Role",
          object = "x",
          relation = "members",
          subject_id = "user:dup",
        }})
        assert.eq(action, "repaired")
        assert.eq(dups, 3)
        "#,
        read_server.uri(),
        write_server.uri()
    );
    run_lua(&script).await.unwrap();
}

#[tokio::test]
async fn test_keto_tuples_upsert_with_subject_set() {
    // Keto's relation-tuple filters use dotted notation
    // (subject_set.namespace), not underscored. This test asserts the
    // client emits the correct query-param keys for subject_set tuples
    // — Keto's DELETE endpoint rejects unknown keys with HTTP 400 and
    // would break upsert's repair path on dup'd subject_set tuples
    // (e.g. parent edges in HRBAC seeds).
    let read_server = MockServer::start().await;
    let write_server = MockServer::start().await;
    Mock::given(method("GET"))
        .and(path("/relation-tuples"))
        .and(query_param("namespace", "managementplane"))
        .and(query_param("object", "na"))
        .and(query_param("relation", "parent"))
        .and(query_param("subject_set.namespace", "organization"))
        .and(query_param("subject_set.object", "simons"))
        .and(query_param("subject_set.relation", ""))
        .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
            "relation_tuples": [
                {"namespace": "managementplane", "object": "na", "relation": "parent",
                 "subject_set": {"namespace": "organization", "object": "simons", "relation": ""}},
                {"namespace": "managementplane", "object": "na", "relation": "parent",
                 "subject_set": {"namespace": "organization", "object": "simons", "relation": ""}}
            ],
            "next_page_token": ""
        })))
        .mount(&read_server)
        .await;
    Mock::given(method("DELETE"))
        .and(path("/admin/relation-tuples"))
        .and(query_param("subject_set.namespace", "organization"))
        .and(query_param("subject_set.object", "simons"))
        .respond_with(ResponseTemplate::new(204))
        .mount(&write_server)
        .await;
    Mock::given(method("PUT"))
        .and(path("/admin/relation-tuples"))
        .respond_with(ResponseTemplate::new(201).set_body_json(serde_json::json!({})))
        .mount(&write_server)
        .await;

    let script = format!(
        r#"
        local keto = require("assay.ory.keto")
        local k = keto.client("{}", {{ write_url = "{}" }})
        local action, dups = k.tuples:upsert({{
          namespace = "managementplane",
          object = "na",
          relation = "parent",
          subject_set = {{ namespace = "organization", object = "simons", relation = "" }},
        }})
        assert.eq(action, "repaired")
        assert.eq(dups, 1)
        "#,
        read_server.uri(),
        write_server.uri()
    );
    run_lua(&script).await.unwrap();
}

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

    let script = format!(
        r#"
        local keto = require("assay.ory.keto")
        local k = keto.client("{}")
        local ok, err = pcall(function()
          k.tuples:upsert({{ namespace = "Role", object = "x", relation = "members", subject_id = "user:y" }})
        end)
        assert.eq(ok, false)
        assert.contains(tostring(err), "write_url not configured")
        "#,
        read_server.uri()
    );
    run_lua(&script).await.unwrap();
}
