"""Tests for role hierarchy with parent inheritance and cycle detection."""

import pytest

from pyauthz import (
    Effect,
    Policy,
    PyAuthz,
    Resource,
    Role,
    Subject,
)
from pyauthz.core.errors import ConfigError


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


@pytest.fixture
def doc():
    return Resource(id="doc-1", type="documents")


# --- Role hierarchy: permission inheritance ---


class TestRoleInheritance:
    def test_child_inherits_parent_permissions(self, rp, doc):
        """Editor inherits viewer's documents:read permission."""
        rp.add_role(Role(name="viewer", permissions=["documents:read"]))
        rp.add_role(Role(name="editor", permissions=["documents:write"], parents=["viewer"]))

        editor = Subject(id="alice", roles=["editor"])
        # Editor can write (own permission)
        assert rp.is_allowed(editor, "write", doc) is True
        # Editor can read (inherited from viewer)
        assert rp.is_allowed(editor, "read", doc) is True

    def test_multi_level_inheritance(self, rp, doc):
        """Admin inherits from editor, which inherits from viewer."""
        rp.add_role(Role(name="viewer", permissions=["documents:read"]))
        rp.add_role(Role(name="editor", permissions=["documents:write"], parents=["viewer"]))
        rp.add_role(Role(name="admin", permissions=["documents:delete"], parents=["editor"]))

        admin = Subject(id="alice", roles=["admin"])
        assert rp.is_allowed(admin, "read", doc) is True  # from viewer
        assert rp.is_allowed(admin, "write", doc) is True  # from editor
        assert rp.is_allowed(admin, "delete", doc) is True  # own permission

    def test_multiple_parents(self, rp, doc):
        """A role can inherit from multiple parents."""
        rp.add_role(Role(name="reader", permissions=["documents:read"]))
        rp.add_role(Role(name="writer", permissions=["documents:write"]))
        rp.add_role(Role(name="contributor", permissions=[], parents=["reader", "writer"]))

        contributor = Subject(id="alice", roles=["contributor"])
        assert rp.is_allowed(contributor, "read", doc) is True
        assert rp.is_allowed(contributor, "write", doc) is True

    def test_parent_without_own_permissions(self, rp, doc):
        """A role can exist purely to group other roles."""
        rp.add_role(Role(name="viewer", permissions=["documents:read"]))
        rp.add_role(Role(name="base", permissions=[], parents=["viewer"]))

        user = Subject(id="alice", roles=["base"])
        assert rp.is_allowed(user, "read", doc) is True

    def test_hierarchy_with_explicit_policies(self, rp, doc):
        """Role hierarchy also works when explicit policies reference roles."""
        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="allow-viewers",
                effect=Effect.ALLOW,
                roles=["viewer"],
                permissions=["documents:read"],
            )
        )

        # Editor inherits viewer, so the policy that requires "viewer" should match
        editor = Subject(id="alice", roles=["editor"])
        assert rp.is_allowed(editor, "read", doc) is True

    def test_nonexistent_parent_is_ignored(self, rp, doc):
        """If a parent role doesn't exist, it's silently ignored during evaluation."""
        rp.add_role(Role(name="editor", permissions=["documents:write"], parents=["nonexistent"]))

        editor = Subject(id="alice", roles=["editor"])
        # Own permission still works
        assert rp.is_allowed(editor, "write", doc) is True
        # But no inherited permissions from nonexistent parent
        assert rp.is_allowed(editor, "read", doc) is False


# --- Cycle detection ---


class TestCycleDetection:
    def test_self_referencing_role_detected(self, rp):
        """A role that lists itself as a parent is a cycle."""
        with pytest.raises(ConfigError, match="Cycle detected"):
            rp.add_role(Role(name="a", parents=["a"]))

    def test_two_role_cycle_detected(self, rp):
        """A -> B -> A cycle is detected."""
        rp.add_role(Role(name="a", parents=["b"]))
        with pytest.raises(ConfigError, match="Cycle detected"):
            rp.add_role(Role(name="b", parents=["a"]))

    def test_three_role_cycle_detected(self, rp):
        """A -> B -> C -> A cycle is detected."""
        rp.add_role(Role(name="a", parents=["b"]))
        rp.add_role(Role(name="b", parents=["c"]))
        with pytest.raises(ConfigError, match="Cycle detected"):
            rp.add_role(Role(name="c", parents=["a"]))

    def test_no_cycle_with_diamond(self, rp):
        """Diamond inheritance (A->B, A->C, B->D, C->D) is NOT a cycle."""
        rp.add_role(Role(name="d", permissions=["documents:read"]))
        rp.add_role(Role(name="b", parents=["d"]))
        rp.add_role(Role(name="c", parents=["d"]))
        rp.add_role(Role(name="a", parents=["b", "c"]))  # Should not raise

    def test_replace_roles_detects_cycle(self, rp):
        """replace_roles() should also catch cycles."""
        with pytest.raises(ConfigError, match="Cycle detected"):
            rp.replace_roles(
                [
                    Role(name="a", parents=["b"]),
                    Role(name="b", parents=["a"]),
                ]
            )

    def test_replace_roles_no_cycle(self, rp, doc):
        """replace_roles() with valid hierarchy works."""
        rp.replace_roles(
            [
                Role(name="viewer", permissions=["documents:read"]),
                Role(name="editor", permissions=["documents:write"], parents=["viewer"]),
            ]
        )
        editor = Subject(id="alice", roles=["editor"])
        assert rp.is_allowed(editor, "read", doc) is True

    def test_role_without_parents_skips_cycle_check(self, rp):
        """Roles with no parents don't trigger cycle detection."""
        rp.add_role(Role(name="simple", permissions=["*:*"]))
        # No error — just verifying the code path
