"""Tests for ABAC condition evaluation integrated with the engine.

These test the full pipeline: conditions on policies evaluated during
authorize()/is_allowed(), including fail-safe behavior.
"""

import pytest

from pyauthz import (
    Condition,
    Effect,
    Policy,
    PyAuthz,
    Resource,
    Role,
    Subject,
)


@pytest.fixture
def rp():
    return PyAuthz()


# --- Owner check (cross-reference) ---


class TestOwnerCheck:
    def test_owner_can_edit(self, rp):
        rp.add_role(Role(name="editor", permissions=["documents:write"]))
        rp.add_policy(
            Policy(
                name="owner-edit",
                effect=Effect.ALLOW,
                roles=["editor"],
                permissions=["documents:write"],
                conditions=[
                    Condition(field="resource.owner_id", op="==", value="subject.id"),
                ],
            )
        )

        alice = Subject(id="alice", roles=["editor"])
        doc = Resource(id="doc-1", type="documents", owner_id="alice")
        assert rp.is_allowed(alice, "write", doc) is True

    def test_non_owner_denied(self, rp):
        rp.add_role(Role(name="editor", permissions=["documents:write"]))
        rp.add_policy(
            Policy(
                name="owner-edit",
                effect=Effect.ALLOW,
                roles=["editor"],
                permissions=["documents:write"],
                conditions=[
                    Condition(field="resource.owner_id", op="==", value="subject.id"),
                ],
            )
        )

        bob = Subject(id="bob", roles=["editor"])
        doc = Resource(id="doc-1", type="documents", owner_id="alice")
        assert rp.is_allowed(bob, "write", doc) is False


# --- Classification / deep attribute access ---


class TestClassificationAccess:
    def test_clearance_allows_access(self, rp):
        rp.add_role(Role(name="analyst", permissions=["documents:read"]))
        rp.add_policy(
            Policy(
                name="classified-docs",
                effect=Effect.ALLOW,
                roles=["analyst"],
                permissions=["documents:read"],
                conditions=[
                    Condition(
                        field="resource.attributes.classification", op="==", value="TOP_SECRET"
                    ),
                    Condition(
                        field="subject.attributes.clearance", op="in", value=["TS", "TS/SCI"]
                    ),
                ],
            )
        )

        analyst = Subject(id="alice", roles=["analyst"], attributes={"clearance": "TS"})
        doc = Resource(id="doc-1", type="documents", attributes={"classification": "TOP_SECRET"})
        assert rp.is_allowed(analyst, "read", doc) is True

    def test_insufficient_clearance_denied(self, rp):
        rp.add_role(Role(name="analyst", permissions=["documents:read"]))
        rp.add_policy(
            Policy(
                name="classified-docs",
                effect=Effect.ALLOW,
                roles=["analyst"],
                permissions=["documents:read"],
                conditions=[
                    Condition(
                        field="subject.attributes.clearance", op="in", value=["TS", "TS/SCI"]
                    ),
                ],
            )
        )

        analyst = Subject(id="bob", roles=["analyst"], attributes={"clearance": "SECRET"})
        doc = Resource(id="doc-1", type="documents", attributes={"classification": "TOP_SECRET"})
        assert rp.is_allowed(analyst, "read", doc) is False


# --- Time/environment-based policies ---


class TestEnvironmentPolicies:
    def test_business_hours_allowed(self, rp):
        rp.add_role(Role(name="contractor", permissions=["internal:read"]))
        rp.add_policy(
            Policy(
                name="business-hours",
                effect=Effect.ALLOW,
                roles=["contractor"],
                permissions=["internal:read"],
                conditions=[
                    Condition(field="environment.hour", op=">=", value=9),
                    Condition(field="environment.hour", op="<=", value=17),
                ],
            )
        )

        contractor = Subject(id="carol", roles=["contractor"])
        resource = Resource(id="r-1", type="internal")
        assert rp.is_allowed(contractor, "read", resource, hour=14) is True

    def test_outside_business_hours_denied(self, rp):
        rp.add_role(Role(name="contractor", permissions=["internal:read"]))
        rp.add_policy(
            Policy(
                name="business-hours",
                effect=Effect.ALLOW,
                roles=["contractor"],
                permissions=["internal:read"],
                conditions=[
                    Condition(field="environment.hour", op=">=", value=9),
                    Condition(field="environment.hour", op="<=", value=17),
                ],
            )
        )

        contractor = Subject(id="carol", roles=["contractor"])
        resource = Resource(id="r-1", type="internal")
        assert rp.is_allowed(contractor, "read", resource, hour=22) is False


# --- Explicit deny with conditions ---


class TestExplicitDenyWithConditions:
    def test_deny_policy_with_conditions(self, rp):
        """A DENY policy with conditions only denies when conditions match."""
        rp.add_role(Role(name="user", permissions=["documents:read"]))
        rp.add_policy(
            Policy(
                name="allow-read",
                effect=Effect.ALLOW,
                roles=["user"],
                permissions=["documents:read"],
            )
        )
        rp.add_policy(
            Policy(
                name="deny-classified",
                effect=Effect.DENY,
                roles=["user"],
                permissions=["documents:read"],
                conditions=[
                    Condition(
                        field="resource.attributes.classification", op="==", value="TOP_SECRET"
                    ),
                ],
            )
        )

        user = Subject(id="alice", roles=["user"])
        normal_doc = Resource(id="d1", type="documents", attributes={"classification": "PUBLIC"})
        secret_doc = Resource(
            id="d2", type="documents", attributes={"classification": "TOP_SECRET"}
        )

        # Normal doc: only ALLOW matches (DENY condition doesn't pass)
        assert rp.is_allowed(user, "read", normal_doc) is True
        # Secret doc: both match, deny-override kicks in
        assert rp.is_allowed(user, "read", secret_doc) is False


# --- Fail-safe: missing attributes → DENY ---


class TestFailSafeEngine:
    def test_missing_attribute_causes_deny(self, rp):
        """If a condition references a missing attribute, the policy doesn't match."""
        rp.add_role(Role(name="analyst", permissions=["documents:read"]))
        rp.add_policy(
            Policy(
                name="need-clearance",
                effect=Effect.ALLOW,
                roles=["analyst"],
                permissions=["documents:read"],
                conditions=[
                    Condition(field="subject.attributes.clearance", op="in", value=["TS"]),
                ],
            )
        )

        # Subject has no clearance attribute
        analyst = Subject(id="alice", roles=["analyst"])
        doc = Resource(id="doc-1", type="documents")
        assert rp.is_allowed(analyst, "read", doc) is False

    def test_missing_env_attribute_causes_deny(self, rp):
        rp.add_role(Role(name="user", permissions=["documents:read"]))
        rp.add_policy(
            Policy(
                name="need-hour",
                effect=Effect.ALLOW,
                roles=["user"],
                permissions=["documents:read"],
                conditions=[
                    Condition(field="environment.hour", op=">=", value=9),
                ],
            )
        )

        user = Subject(id="alice", roles=["user"])
        doc = Resource(id="doc-1", type="documents")
        # No hour in environment → condition fails → no match → deny
        assert rp.is_allowed(user, "read", doc) is False


# --- Combined: hierarchy + conditions ---


class TestHierarchyWithConditions:
    def test_inherited_role_triggers_policy_with_conditions(self, rp):
        """A policy targeting 'viewer' should match a user with 'editor' role
        (which inherits from viewer), and conditions should still be evaluated."""
        rp.add_role(Role(name="viewer", permissions=["documents:read"]))
        rp.add_role(Role(name="editor", permissions=["documents:write"], parents=["viewer"]))

        rp.add_policy(
            Policy(
                name="viewer-own-docs",
                effect=Effect.ALLOW,
                roles=["viewer"],
                permissions=["documents:read"],
                conditions=[
                    Condition(field="resource.owner_id", op="==", value="subject.id"),
                ],
            )
        )

        editor = Subject(id="alice", roles=["editor"])
        own_doc = Resource(id="d1", type="documents", owner_id="alice")
        other_doc = Resource(id="d2", type="documents", owner_id="bob")

        # Editor inherits viewer → policy matches, condition passes
        assert rp.is_allowed(editor, "read", own_doc) is True
        # Editor inherits viewer → policy matches, but condition fails
        assert rp.is_allowed(editor, "read", other_doc) is False
