// Created: 2026-04-14 by Constructor Tech
use async_trait::async_trait;

use super::*;
use crate::constraints::{Constraint, InPredicate, Predicate};
use crate::models::{EvaluationResponse, EvaluationResponseContext};
use modkit_security::pep_properties;

fn uuid(s: &str) -> Uuid {
    Uuid::parse_str(s).expect("valid test UUID")
}

const TENANT: &str = "11111111-1111-1111-1111-111111111111";
const SUBJECT: &str = "22222222-2222-2222-2222-222222222222";
const RESOURCE: &str = "33333333-3333-3333-3333-333333333333";

/// Mock that mirrors the real `static-authz-plugin` behaviour:
/// 1. Use `tenant_context.root_id` if present (explicit PEP override).
/// 2. Fall back to `subject.properties["tenant_id"]` (PDP self-resolution).
/// 3. Nil UUID → deny rather than grant unrestricted access.
/// 4. No tenant at all → deny.
struct AllowAllMock;

#[async_trait]
impl AuthZResolverClient for AllowAllMock {
    async fn evaluate(
        &self,
        req: EvaluationRequest,
    ) -> Result<EvaluationResponse, AuthZResolverError> {
        // Resolve tenant: explicit context first, then subject fallback.
        let tenant_id = req
            .context
            .tenant_context
            .as_ref()
            .and_then(|tc| tc.root_id)
            .or_else(|| {
                req.subject
                    .properties
                    .get("tenant_id")
                    .and_then(|v| v.as_str())
                    .and_then(|s| Uuid::parse_str(s).ok())
            });

        let Some(tid) = tenant_id else {
            return Ok(EvaluationResponse {
                decision: false,
                context: EvaluationResponseContext::default(),
            });
        };

        if tid == Uuid::default() {
            return Ok(EvaluationResponse {
                decision: false,
                context: EvaluationResponseContext::default(),
            });
        }

        Ok(EvaluationResponse {
            decision: true,
            context: EvaluationResponseContext {
                constraints: vec![Constraint {
                    predicates: vec![Predicate::In(InPredicate::new(
                        pep_properties::OWNER_TENANT_ID,
                        [tid],
                    ))],
                }],
                ..Default::default()
            },
        })
    }
}

/// Mock that always returns `decision=false` with an optional deny reason.
struct DenyMock {
    deny_reason: Option<crate::models::DenyReason>,
}

impl DenyMock {
    fn new() -> Self {
        Self { deny_reason: None }
    }

    fn with_reason(error_code: &str, details: Option<&str>) -> Self {
        Self {
            deny_reason: Some(crate::models::DenyReason {
                error_code: error_code.to_owned(),
                details: details.map(ToOwned::to_owned),
            }),
        }
    }
}

#[async_trait]
impl AuthZResolverClient for DenyMock {
    async fn evaluate(
        &self,
        _req: EvaluationRequest,
    ) -> Result<EvaluationResponse, AuthZResolverError> {
        Ok(EvaluationResponse {
            decision: false,
            context: EvaluationResponseContext {
                deny_reason: self.deny_reason.clone(),
                ..Default::default()
            },
        })
    }
}

/// Mock that always returns an RPC error.
struct FailMock;

#[async_trait]
impl AuthZResolverClient for FailMock {
    async fn evaluate(
        &self,
        _req: EvaluationRequest,
    ) -> Result<EvaluationResponse, AuthZResolverError> {
        Err(AuthZResolverError::Internal("boom".to_owned()))
    }
}

fn test_ctx() -> SecurityContext {
    SecurityContext::builder()
        .subject_id(uuid(SUBJECT))
        .subject_tenant_id(uuid(TENANT))
        .build()
        .unwrap()
}

const TEST_RESOURCE: ResourceType = ResourceType {
    name: "gts.x.core.users.user.v1~",
    supported_properties: &[pep_properties::OWNER_TENANT_ID, pep_properties::RESOURCE_ID],
};

fn enforcer(mock: impl AuthZResolverClient + 'static) -> PolicyEnforcer {
    PolicyEnforcer::new(Arc::new(mock))
}

// ── build_request ────────────────────────────────────────────────

#[test]
fn build_request_populates_fields() {
    let e = enforcer(AllowAllMock);
    let ctx = test_ctx();
    let req = e.build_request(&ctx, &TEST_RESOURCE, "get", Some(uuid(RESOURCE)), true);

    assert_eq!(req.resource.resource_type, "gts.x.core.users.user.v1~");
    assert_eq!(req.action.name, "get");
    assert_eq!(req.resource.id, Some(uuid(RESOURCE)));
    assert!(req.context.require_constraints);
    // No explicit context_tenant_id → tenant_context is None (PDP decides)
    assert!(req.context.tenant_context.is_none());
}

#[test]
fn build_request_with_overrides_tenant() {
    let custom_tenant = uuid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
    let e = enforcer(AllowAllMock);
    let ctx = test_ctx();
    let req = e.build_request_with(
        &ctx,
        &TEST_RESOURCE,
        "list",
        None,
        false,
        &AccessRequest::new().context_tenant_id(custom_tenant),
    );

    assert_eq!(
        req.context
            .tenant_context
            .as_ref()
            .and_then(|tc| tc.root_id),
        Some(custom_tenant),
    );
    assert!(!req.context.require_constraints);
}

// ── access_scope ─────────────────────────────────────────────────

#[tokio::test]
async fn access_scope_pdp_resolves_tenant_from_subject() {
    let e = enforcer(AllowAllMock);
    let ctx = test_ctx();
    // access_scope() passes default() — PDP resolves tenant from
    // subject.properties["tenant_id"] via its own fallback logic.
    let scope = e
        .access_scope(&ctx, &TEST_RESOURCE, "get", Some(uuid(RESOURCE)))
        .await
        .expect("PDP should resolve tenant from subject properties");

    assert_eq!(
        scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
        &[uuid(TENANT)]
    );
}

#[tokio::test]
async fn access_scope_with_explicit_tenant_returns_scope() {
    let e = enforcer(AllowAllMock);
    let ctx = test_ctx();
    let scope = e
        .access_scope_with(
            &ctx,
            &TEST_RESOURCE,
            "get",
            Some(uuid(RESOURCE)),
            &AccessRequest::new().context_tenant_id(uuid(TENANT)),
        )
        .await
        .expect("should succeed");

    assert_eq!(
        scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
        &[uuid(TENANT)]
    );
}

#[tokio::test]
async fn access_scope_with_for_create() {
    let e = enforcer(AllowAllMock);
    let ctx = test_ctx();
    let scope = e
        .access_scope_with(
            &ctx,
            &TEST_RESOURCE,
            "create",
            None,
            &AccessRequest::new()
                .context_tenant_id(uuid(TENANT))
                .tenant_mode(TenantMode::RootOnly),
        )
        .await
        .expect("should succeed");

    assert_eq!(
        scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
        &[uuid(TENANT)]
    );
}

#[tokio::test]
async fn access_scope_denied_returns_denied_error() {
    let e = enforcer(DenyMock::new());
    let ctx = test_ctx();
    let result = e.access_scope(&ctx, &TEST_RESOURCE, "get", None).await;

    assert!(matches!(
        result,
        Err(EnforcerError::Denied { deny_reason: None })
    ));
}

#[tokio::test]
async fn access_scope_denied_with_reason() {
    let e = enforcer(DenyMock::with_reason(
        "INSUFFICIENT_PERMISSIONS",
        Some("Missing admin role"),
    ));
    let ctx = test_ctx();
    let result = e.access_scope(&ctx, &TEST_RESOURCE, "get", None).await;

    match result {
        Err(EnforcerError::Denied { deny_reason }) => {
            let reason = deny_reason.expect("should have deny_reason");
            assert_eq!(reason.error_code, "INSUFFICIENT_PERMISSIONS");
            assert_eq!(reason.details.as_deref(), Some("Missing admin role"));
        }
        other => panic!("Expected Denied with reason, got: {other:?}"),
    }
}

#[tokio::test]
async fn access_scope_evaluation_failure() {
    let e = enforcer(FailMock);
    let ctx = test_ctx();
    let result = e.access_scope(&ctx, &TEST_RESOURCE, "get", None).await;

    assert!(matches!(result, Err(EnforcerError::EvaluationFailed(_))));
}

#[tokio::test]
async fn access_scope_anonymous_denied_by_pdp() {
    let e = enforcer(AllowAllMock);
    let ctx = SecurityContext::anonymous();
    // anonymous() has nil UUID as subject_tenant_id → PDP resolves it
    // from subject.properties["tenant_id"], sees nil UUID, and denies.
    let result = e.access_scope(&ctx, &TEST_RESOURCE, "list", None).await;

    assert!(matches!(
        result,
        Err(EnforcerError::Denied { deny_reason: None })
    ));
}

// ── builder methods ──────────────────────────────────────────────

#[test]
fn with_capabilities() {
    let e = enforcer(AllowAllMock).with_capabilities(vec![Capability::TenantHierarchy]);

    assert_eq!(e.capabilities, vec![Capability::TenantHierarchy]);
}

#[test]
fn debug_impl() {
    let e = enforcer(AllowAllMock);
    let dbg = format!("{e:?}");
    assert!(dbg.contains("PolicyEnforcer"));
}

// ── AccessRequest builder ─────────────────────────────────────────

#[test]
fn access_request_default_is_empty() {
    let req = AccessRequest::new();
    assert!(req.resource_properties.is_empty());
    assert!(req.tenant_context.is_none());
}

#[test]
fn access_request_builder_chain() {
    let tid = uuid(TENANT);
    let req = AccessRequest::new()
        .resource_property(pep_properties::OWNER_TENANT_ID, tid)
        .context_tenant_id(tid)
        .tenant_mode(TenantMode::RootOnly)
        .barrier_mode(BarrierMode::Ignore)
        .tenant_status(vec!["active".to_owned()]);

    assert_eq!(req.resource_properties.len(), 1);
    let tc = req.tenant_context.as_ref().expect("tenant context");
    assert_eq!(tc.root_id, Some(tid));
    assert_eq!(tc.mode, TenantMode::RootOnly);
    assert_eq!(tc.barrier_mode, BarrierMode::Ignore);
    assert_eq!(tc.tenant_status, Some(vec!["active".to_owned()]));
}

#[test]
fn access_request_tenant_context_setter() {
    let tid = uuid(TENANT);
    let req = AccessRequest::new().tenant_context(TenantContext {
        mode: TenantMode::RootOnly,
        root_id: Some(tid),
        ..Default::default()
    });

    let tc = req.tenant_context.as_ref().expect("tenant context");
    assert_eq!(tc.root_id, Some(tid));
    assert_eq!(tc.mode, TenantMode::RootOnly);
    assert_eq!(tc.barrier_mode, BarrierMode::Respect);
}

#[test]
fn access_request_resource_properties_replaces() {
    let mut props = HashMap::new();
    props.insert("a".to_owned(), serde_json::json!("1"));
    props.insert("b".to_owned(), serde_json::json!("2"));

    let req = AccessRequest::new()
        .resource_property("old_key", serde_json::json!("old"))
        .resource_properties(props);

    assert_eq!(req.resource_properties.len(), 2);
    assert!(!req.resource_properties.contains_key("old_key"));
}

#[test]
fn into_property_value_implementations() {
    let uuid_val = uuid(TENANT);
    let req = AccessRequest::new()
        .resource_property("uuid_prop", uuid_val)
        .resource_property("uuid_ref_prop", uuid_val)
        .resource_property("string_prop", "test".to_owned())
        .resource_property("str_prop", "test")
        .resource_property("int_prop", 42i64)
        .resource_property("json_prop", serde_json::json!({"key": "value"}));

    assert_eq!(req.resource_properties.len(), 6);
    assert_eq!(
        req.resource_properties.get("uuid_prop"),
        Some(&serde_json::json!(uuid_val.to_string())),
    );
    assert_eq!(
        req.resource_properties.get("string_prop"),
        Some(&serde_json::json!("test")),
    );
    assert_eq!(
        req.resource_properties.get("int_prop"),
        Some(&serde_json::json!(42)),
    );
}

// ── build_request_with ────────────────────────────────────────────

#[test]
fn build_request_with_applies_resource_properties() {
    let e = enforcer(AllowAllMock);
    let ctx = test_ctx();
    let tid = uuid(TENANT);
    let req = e.build_request_with(
        &ctx,
        &TEST_RESOURCE,
        "create",
        None,
        false,
        &AccessRequest::new().resource_property(pep_properties::OWNER_TENANT_ID, tid),
    );

    assert_eq!(
        req.resource.properties.get(pep_properties::OWNER_TENANT_ID),
        Some(&serde_json::json!(tid.to_string())),
    );
}

#[test]
fn build_request_with_applies_tenant_mode_and_barrier() {
    let e = enforcer(AllowAllMock);
    let ctx = test_ctx();
    let req = e.build_request_with(
        &ctx,
        &TEST_RESOURCE,
        "list",
        None,
        true,
        &AccessRequest::new()
            .tenant_mode(TenantMode::RootOnly)
            .barrier_mode(BarrierMode::Ignore)
            .tenant_status(vec!["active".to_owned()]),
    );

    let tc = req.context.tenant_context.as_ref().expect("tenant context");
    assert_eq!(tc.mode, TenantMode::RootOnly);
    assert_eq!(tc.barrier_mode, BarrierMode::Ignore);
    assert_eq!(tc.tenant_status, Some(vec!["active".to_owned()]));
}

#[test]
fn build_request_with_default_has_no_tenant_context() {
    let e = enforcer(AllowAllMock);
    let ctx = test_ctx();
    let req = e.build_request_with(
        &ctx,
        &TEST_RESOURCE,
        "get",
        None,
        true,
        &AccessRequest::default(),
    );

    // No explicit context_tenant_id → tenant_context is None (PDP decides)
    assert!(req.context.tenant_context.is_none());
}

// ── access_scope_with ─────────────────────────────────────────────

#[tokio::test]
async fn access_scope_with_custom_tenant() {
    let custom_tenant = uuid("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa");
    let e = enforcer(AllowAllMock);
    let ctx = test_ctx();
    let scope = e
        .access_scope_with(
            &ctx,
            &TEST_RESOURCE,
            "list",
            None,
            &AccessRequest::new().context_tenant_id(custom_tenant),
        )
        .await
        .expect("should succeed");

    assert_eq!(
        scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
        &[custom_tenant]
    );
}

#[tokio::test]
async fn access_scope_with_resource_properties() {
    let e = enforcer(AllowAllMock);
    let ctx = test_ctx();
    let scope = e
        .access_scope_with(
            &ctx,
            &TEST_RESOURCE,
            "get",
            None,
            &AccessRequest::new()
                .resource_property(
                    pep_properties::OWNER_TENANT_ID,
                    serde_json::json!(uuid(TENANT).to_string()),
                )
                .context_tenant_id(uuid(TENANT))
                .tenant_mode(TenantMode::RootOnly),
        )
        .await
        .expect("should succeed");

    assert_eq!(
        scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
        &[uuid(TENANT)]
    );
}

// ── request builder internals ────────────────────────────────────

#[test]
fn builds_request_with_all_fields() {
    const USERS_RESOURCE: ResourceType = ResourceType {
        name: "gts.x.core.users.user.v1~",
        supported_properties: &[pep_properties::OWNER_TENANT_ID],
    };

    let context_tenant_id = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
    let subject_id = Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap();
    let subject_tenant_id = Uuid::parse_str("33333333-3333-3333-3333-333333333333").unwrap();
    let resource_id = Uuid::parse_str("44444444-4444-4444-4444-444444444444").unwrap();

    let ctx = SecurityContext::builder()
        .subject_id(subject_id)
        .subject_tenant_id(subject_tenant_id)
        .subject_type("user")
        .token_scopes(vec!["admin".to_owned()])
        .bearer_token("test-token".to_owned())
        .build()
        .unwrap();

    let e = PolicyEnforcer::new(Arc::new(AllowAllMock))
        .with_capabilities(vec![Capability::TenantHierarchy]);

    let access_req = AccessRequest::new().tenant_context(TenantContext {
        root_id: Some(context_tenant_id),
        ..Default::default()
    });

    let request = e.build_request_with(
        &ctx,
        &USERS_RESOURCE,
        "get",
        Some(resource_id),
        true,
        &access_req,
    );

    assert_eq!(request.subject.id, subject_id);
    assert_eq!(
        request.subject.properties.get("tenant_id").unwrap(),
        &serde_json::Value::String(subject_tenant_id.to_string())
    );
    assert_eq!(request.subject.subject_type.as_deref(), Some("user"));
    assert_eq!(request.action.name, "get");
    assert_eq!(request.resource.resource_type, "gts.x.core.users.user.v1~");
    assert_eq!(request.resource.id, Some(resource_id));
    assert!(request.context.require_constraints);
    assert_eq!(
        request.context.tenant_context.as_ref().unwrap().root_id,
        Some(context_tenant_id)
    );
    assert_eq!(request.context.token_scopes, vec!["admin"]);
    assert_eq!(
        request.context.capabilities,
        vec![Capability::TenantHierarchy]
    );
    assert!(request.context.bearer_token.is_some());
    assert_eq!(
        request.context.supported_properties,
        vec![pep_properties::OWNER_TENANT_ID]
    );
}

#[test]
fn builds_request_without_tenant_context() {
    let ctx = SecurityContext::anonymous();

    let e = enforcer(AllowAllMock);

    let request = e.build_request_with(
        &ctx,
        &TEST_RESOURCE,
        "create",
        None,
        false,
        &AccessRequest::default(),
    );

    // No explicit context_tenant_id → tenant_context is None (PDP decides)
    assert!(request.context.tenant_context.is_none());
    assert!(!request.context.require_constraints);
    assert_eq!(request.resource.id, None);
    assert!(request.context.capabilities.is_empty());
    assert!(request.context.bearer_token.is_none());
}

#[test]
fn applies_resource_properties() {
    let tenant_id = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
    let ctx = SecurityContext::builder()
        .subject_id(Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap())
        .subject_tenant_id(tenant_id)
        .build()
        .unwrap();

    let e = enforcer(AllowAllMock);
    let access_req = AccessRequest::new()
        .resource_property(
            pep_properties::OWNER_TENANT_ID,
            serde_json::Value::String(tenant_id.to_string()),
        )
        .context_tenant_id(tenant_id);

    let request = e.build_request_with(&ctx, &TEST_RESOURCE, "create", None, false, &access_req);

    assert_eq!(
        request
            .resource
            .properties
            .get(pep_properties::OWNER_TENANT_ID),
        Some(&serde_json::Value::String(tenant_id.to_string())),
    );
}

#[test]
fn applies_tenant_mode_and_barrier_mode() {
    let tenant_id = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
    let ctx = SecurityContext::builder()
        .subject_id(Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap())
        .subject_tenant_id(tenant_id)
        .build()
        .unwrap();

    let e = enforcer(AllowAllMock);
    let access_req = AccessRequest::new().tenant_context(TenantContext {
        mode: TenantMode::RootOnly,
        root_id: Some(tenant_id),
        barrier_mode: BarrierMode::Ignore,
        tenant_status: Some(vec!["active".to_owned()]),
    });

    let request = e.build_request_with(&ctx, &TEST_RESOURCE, "list", None, true, &access_req);

    let tc = request.context.tenant_context.as_ref().unwrap();
    assert_eq!(tc.mode, TenantMode::RootOnly);
    assert_eq!(tc.barrier_mode, BarrierMode::Ignore);
    assert_eq!(tc.tenant_status, Some(vec!["active".to_owned()]));
}

#[test]
fn default_request_has_no_tenant_context_pdp_decides() {
    let subject_tenant = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
    let ctx = SecurityContext::builder()
        .subject_id(Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap())
        .subject_tenant_id(subject_tenant)
        .build()
        .unwrap();

    let e = enforcer(AllowAllMock);

    // No tenant_context provided — PEP passes None, PDP decides
    let request = e.build_request_with(
        &ctx,
        &TEST_RESOURCE,
        "list",
        None,
        true,
        &AccessRequest::default(),
    );

    // tenant_context is None — PDP will resolve from subject.properties
    assert!(request.context.tenant_context.is_none());
    // But subject.properties["tenant_id"] is still populated for the PDP
    assert_eq!(
        request.subject.properties.get("tenant_id"),
        Some(&serde_json::json!(subject_tenant.to_string())),
    );
}

#[test]
fn explicit_root_id_overrides_subject_tenant() {
    let subject_tenant = Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap();
    let explicit_tenant = Uuid::parse_str("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa").unwrap();
    let ctx = SecurityContext::builder()
        .subject_id(Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap())
        .subject_tenant_id(subject_tenant)
        .build()
        .unwrap();

    let e = enforcer(AllowAllMock);
    let access_req = AccessRequest::new().context_tenant_id(explicit_tenant);

    let request = e.build_request_with(&ctx, &TEST_RESOURCE, "get", None, true, &access_req);

    let tc = request.context.tenant_context.as_ref().unwrap();
    assert_eq!(tc.root_id, Some(explicit_tenant));
}
