"""Tests for Phase 11.C — object-level permissions, cache, new evaluator ops."""

from __future__ import annotations

import pytest

from simorgh.apps.iam import cache as iam_cache
from simorgh.apps.iam.engine import _evaluate_decision, is_allowed, is_allowed_object
from simorgh.apps.iam.evaluator import PolicyEvalError, evaluate
from simorgh.apps.iam.models import Policy
from simorgh.apps.workspaces.services import create_workspace
from simorgh.core.context import RequestContext

# ---------------------------------------------------------------------------
# Evaluator: new ops
# ---------------------------------------------------------------------------


def test_same_org_node_op():
    ctx = {"actor": {"org_id": 5}, "resource": {"org_id": 5}}
    assert evaluate(
        {"same_org_node": [{"attr": "actor.org_id"}, {"attr": "resource.org_id"}]},
        ctx,
    )
    ctx2 = {"actor": {"org_id": 5}, "resource": {"org_id": 7}}
    assert not evaluate(
        {"same_org_node": [{"attr": "actor.org_id"}, {"attr": "resource.org_id"}]},
        ctx2,
    )


def test_same_org_node_handles_missing():
    assert not evaluate(
        {"same_org_node": [{"attr": "missing"}, {"attr": "also.missing"}]},
        {},
    )


@pytest.mark.django_db
def test_subtree_contains_walks_parent_chain(acme_tree):
    root_id = acme_tree["root"].pk
    de_id = acme_tree["de"].pk  # root → eu → de
    ctx = {"a": root_id, "b": de_id}
    assert evaluate(
        {"subtree_contains": [{"attr": "a"}, {"attr": "b"}]},
        ctx,
    )
    # Reverse direction is false
    assert not evaluate(
        {"subtree_contains": [{"attr": "b"}, {"attr": "a"}]},
        ctx,
    )


def test_unknown_op_raises():
    with pytest.raises(PolicyEvalError):
        evaluate({"frobnicate": []}, {})


# ---------------------------------------------------------------------------
# IAM cache
# ---------------------------------------------------------------------------


def test_iam_cache_roundtrip():
    iam_cache.clear()
    key = iam_cache.cache_key(1, "tenants.tenant.view", None)
    assert iam_cache.get(key) is None
    iam_cache.set(key, True)
    assert iam_cache.get(key) is True
    iam_cache.clear()
    assert iam_cache.get(key) is None


@pytest.mark.django_db
def test_is_allowed_memoises(alice, alice_membership, tenant_acme, perms, monkeypatch):
    iam_cache.clear()
    ctx = RequestContext(
        actor=alice,
        tenant=tenant_acme,
        memberships=(alice_membership,),
        permissions=frozenset({"tenants.tenant.view"}),
    )

    calls = {"n": 0}
    real = _evaluate_decision

    def spy(ctx, action, resource, extra):
        calls["n"] += 1
        return real(ctx, action, resource, extra)

    monkeypatch.setattr("simorgh.apps.iam.engine._evaluate_decision", spy)

    assert is_allowed(ctx, "tenants.tenant.view") is True
    assert is_allowed(ctx, "tenants.tenant.view") is True
    assert is_allowed(ctx, "tenants.tenant.view") is True
    assert calls["n"] == 1  # ← memoised


# ---------------------------------------------------------------------------
# is_allowed_object — derives object_owner_id / object_tenant_id / org_node_id
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_is_allowed_object_injects_object_attrs(
    alice, alice_membership, tenant_acme, acme_tree, perms, role_viewer
):
    iam_cache.clear()
    ws = create_workspace(tenant_acme, slug="objperm", name="Obj")

    # Attach an ABAC policy that requires object_owner_id == actor.id.
    Policy.objects.create(
        role=role_viewer,
        permission=perms["tenants.tenant.view"],
        conditions={"==": [{"attr": "extra.object_owner_id"}, {"attr": "actor.id"}]},
        is_active=True,
    )

    ctx = RequestContext(
        actor=alice,
        tenant=tenant_acme,
        memberships=(alice_membership,),
        permissions=frozenset({"tenants.tenant.view"}),
    )

    # No owner attribute → object_owner_id is None → policy fails.
    assert is_allowed_object(ctx, "tenants.tenant.view", ws) is False


@pytest.mark.django_db
def test_is_allowed_object_owner_match_allows(
    alice, alice_membership, tenant_acme, acme_tree, perms, role_viewer
):
    iam_cache.clear()
    ws = create_workspace(tenant_acme, slug="ownmatch", name="Own")
    ws.owner_id = alice.pk  # synthesised attribute

    Policy.objects.create(
        role=role_viewer,
        permission=perms["tenants.tenant.view"],
        conditions={"==": [{"attr": "extra.object_owner_id"}, {"attr": "actor.id"}]},
        is_active=True,
    )

    ctx = RequestContext(
        actor=alice,
        tenant=tenant_acme,
        memberships=(alice_membership,),
        permissions=frozenset({"tenants.tenant.view"}),
    )

    assert is_allowed_object(ctx, "tenants.tenant.view", ws) is True


# ---------------------------------------------------------------------------
# HasObjectPermission DRF class
# ---------------------------------------------------------------------------


@pytest.mark.django_db
def test_has_object_permission_drf_class(
    alice, alice_membership, tenant_acme, acme_tree, perms, role_viewer
):
    from simorgh.apps.iam.permissions import HasObjectPermission
    from simorgh.core.context import use_request_context

    iam_cache.clear()
    ws = create_workspace(tenant_acme, slug="drf", name="DRF")

    ctx = RequestContext(
        actor=alice,
        tenant=tenant_acme,
        memberships=(alice_membership,),
        permissions=frozenset({"tenants.tenant.view"}),
    )

    class _View:
        required_object_permission = "tenants.tenant.view"

    perm = HasObjectPermission()
    with use_request_context(ctx):
        assert perm.has_permission(None, _View()) is True
        assert perm.has_object_permission(None, _View(), ws) is True

    class _UnauthView:
        required_object_permission = "tenants.tenant.view"

    empty_ctx = RequestContext(actor=None, tenant=None)
    with use_request_context(empty_ctx):
        assert perm.has_permission(None, _UnauthView()) is False
