"""
Tests for error cases and validation in RBAC models.
Tests cover:
    - RestrictedObject: hierarchy cycle prevention, unauthenticated access
    - Role: immutability protection
    - RoleBinding: field immutability after creation
"""

import pytest
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import PermissionDenied, ValidationError
from treebeard.exceptions import InvalidMoveToDescendant

from topdownrbac.models import (
    RestrictedObject,
    Role,
    RoleBinding,
    Subject,
)
from topdownrbac.tests.conftest import assert_has_change, assert_has_delete, assert_has_view


@pytest.mark.django_db
class TestRestrictedObjectError:
    """Test error cases for RestrictedObject model."""

    def test_cannot_move_to_self(self, hierarchy):
        """change_parent rejects moving a node to itself."""
        with pytest.raises(InvalidMoveToDescendant):
            hierarchy["obj1"].change_parent(hierarchy["obj1"])

    def test_cannot_move_to_descendant(self, hierarchy):
        """change_parent rejects moving a node to a descendant."""
        with pytest.raises(InvalidMoveToDescendant):
            hierarchy["obj1"].change_parent(hierarchy["obj4"])

    def test_cannot_move_to_child(self, hierarchy):
        """change_parent rejects moving a node to its direct child."""
        with pytest.raises(InvalidMoveToDescendant):
            hierarchy["obj1"].change_parent(hierarchy["obj2"])

    def test_get_queryset_requires_authentication(self):
        """get_queryset_for_user raises PermissionDenied for anonymous users."""
        anonymous = AnonymousUser()
        with pytest.raises(PermissionDenied, match="must be logged in"):
            RestrictedObject.get_queryset_for_user(anonymous, "view_restrictedobject")


@pytest.mark.django_db
class TestRoleImmutability:
    """Test error cases for immutable roles."""

    def test_cannot_add_permission_to_immutable_role(self, immutable_role, perm_view):
        """Adding permission to immutable role raises PermissionDenied."""
        with pytest.raises(PermissionDenied, match="cannot be edited"):
            immutable_role.add_permission(perm_view)

    def test_cannot_add_many_permissions_to_immutable_role(self, immutable_role, perm_view, perm_change):
        """Adding multiple permissions to immutable role raises PermissionDenied."""
        with pytest.raises(PermissionDenied, match="cannot be edited"):
            immutable_role.add_many_permissions([perm_view, perm_change])

    def test_cannot_remove_permission_from_immutable_role(self, editable_role, perm_view):
        """Removing permission from immutable role raises PermissionDenied."""
        editable_role.add_permission(perm_view)
        editable_role.editable = False
        editable_role.save()
        with pytest.raises(PermissionDenied, match="cannot be edited"):
            editable_role.remove_permission(perm_view)

    def test_cannot_delete_immutable_role(self, immutable_role):
        """Deleting immutable role raises PermissionDenied."""
        with pytest.raises(PermissionDenied, match="cannot be deleted"):
            immutable_role.delete()

    def test_can_add_permission_to_editable_role(self, editable_role, perm_view):
        """Adding permission to editable role succeeds."""
        result = editable_role.add_permission(perm_view)
        assert result is True
        assert perm_view in editable_role.permissions.all()

    def test_can_delete_editable_role(self, editable_role):
        """Deleting editable role succeeds."""
        role_id = editable_role.pk
        editable_role.delete()
        assert not Role.objects.filter(pk=role_id).exists()

    def test_duplicate_permission_returns_false(self, editable_role, perm_view):
        """Adding duplicate permission returns False."""
        editable_role.add_permission(perm_view)
        result = editable_role.add_permission(perm_view)
        assert result is False

    def test_toggle_immutability(self, editable_role, perm_view):
        """Role can be toggled between mutable and immutable."""
        editable_role.set_immutable()
        assert editable_role.editable is False
        with pytest.raises(PermissionDenied):
            editable_role.add_permission(perm_view)
        editable_role.set_editable()
        assert editable_role.editable is True
        editable_role.add_permission(perm_view)
        assert perm_view in editable_role.permissions.all()

    def test_add_many_permissions_to_editable_role(self, editable_role, perm_view, perm_change):
        """Adding multiple permissions to editable role succeeds."""
        result = editable_role.add_many_permissions([perm_view, perm_change])
        assert result == ["view_restrictedobject", "change_restrictedobject"]
        assert perm_view in editable_role.permissions.all()
        assert perm_change in editable_role.permissions.all()

    def test_add_many_permissions_partial_duplicates(self, editable_role, perm_view, perm_change):
        """Adding permissions where some already exist still adds new ones."""
        editable_role.add_permission(perm_view)
        result = editable_role.add_many_permissions([perm_view, perm_change])
        assert result == ["change_restrictedobject"]
        assert perm_view in editable_role.permissions.all()
        assert perm_change in editable_role.permissions.all()

    def test_add_many_permissions_all_duplicates(self, editable_role, perm_view, perm_change):
        """Adding permissions that all already exist returns False."""
        editable_role.add_permission(perm_view)
        editable_role.add_permission(perm_change)
        result = editable_role.add_many_permissions([perm_view, perm_change])
        assert result == []


@pytest.mark.django_db
class TestRolePermissionsWithBindings:
    """Test add_many_permissions with RoleBindings for users and groups."""

    def test_add_many_permissions_propagates_to_single_user(self, user1, role1, obj1, perm_view, perm_change):
        """add_many_permissions creates UserPermissions for single user binding."""
        RoleBinding.objects.create(
            name="user_binding",
            subject=user1,
            role=role1,
            restricted_object=obj1,
        )
        role1.add_many_permissions([perm_view, perm_change])

        assert_has_view(user1, obj1)
        assert_has_change(user1, obj1)

    def test_add_many_permissions_propagates_to_group_members(
        self, user1, user2, group1, role1, obj1, perm_view, perm_change
    ):
        """add_many_permissions creates UserPermissions for all group members."""
        RoleBinding.objects.create(
            name="group_binding",
            subject=group1,
            role=role1,
            restricted_object=obj1,
        )
        role1.add_many_permissions([perm_view, perm_change])

        for user in [user1, user2]:
            assert_has_view(user, obj1)
            assert_has_change(user, obj1)

    def test_add_many_permissions_to_user_and_group_bindings(
        self, user1, user2, user3, group1, role1, obj1, perm_view, perm_change, perm_delete
    ):
        """add_many_permissions works when both user and group bindings exist."""
        RoleBinding.objects.create(
            name="user_binding",
            subject=user3,
            role=role1,
            restricted_object=obj1,
        )
        RoleBinding.objects.create(
            name="group_binding",
            subject=group1,
            role=role1,
            restricted_object=obj1,
        )
        role1.add_many_permissions([perm_view, perm_change, perm_delete])

        for user in [user1, user2, user3]:
            assert_has_view(user, obj1)
            assert_has_change(user, obj1)
            assert_has_delete(user, obj1)


@pytest.mark.django_db
class TestRoleBindingImmutability:
    """Test that RoleBinding fields cannot be changed after creation."""

    def test_cannot_change_role_after_creation(self, user1, role1, role2, obj1):
        """Changing role raises ValidationError."""
        rb = RoleBinding.objects.create(name="rb", subject=user1, role=role1, restricted_object=obj1)
        rb.role = role2
        with pytest.raises(ValidationError, match="(?i)cannot change"):
            rb.save()

    def test_cannot_change_subject_after_creation(self, user1, user2, role1, obj1):
        """Changing subject raises ValidationError."""
        rb = RoleBinding.objects.create(name="rb", subject=user1, role=role1, restricted_object=obj1)
        rb.subject = user2
        with pytest.raises(ValidationError, match="(?i)cannot change"):
            rb.save()

    def test_cannot_change_restricted_object_after_creation(self, user1, role1, obj1, obj2):
        """Changing restricted_object raises ValidationError."""
        rb = RoleBinding.objects.create(name="rb", subject=user1, role=role1, restricted_object=obj1)
        rb.restricted_object = obj2
        with pytest.raises(ValidationError, match="(?i)cannot change"):
            rb.save()

    def test_cannot_change_propagate_after_creation(self, user1, role1, obj1):
        """Changing propagate raises ValidationError."""
        rb = RoleBinding.objects.create(
            name="rb",
            subject=user1,
            role=role1,
            restricted_object=obj1,
            propagate=False,
        )
        rb.propagate = True
        with pytest.raises(ValidationError, match="(?i)cannot change"):
            rb.save()

    def test_can_change_name_after_creation(self, user1, role1, obj1):
        """Changing only name is allowed."""
        rb = RoleBinding.objects.create(
            name="old_name",
            subject=user1,
            role=role1,
            restricted_object=obj1,
        )
        rb.name = "new_name"
        rb.save()
        rb.refresh_from_db()
        assert rb.name == "new_name"


@pytest.mark.django_db
class TestSubjectBaseClass:
    """Test that Subject's polymorphic dispatch methods raise NotImplementedError."""

    def test_get_subject_type_raises_not_implemented(self):
        """Calling _get_subject_type() on a bare Subject raises NotImplementedError."""
        subject = Subject.objects.create()
        with pytest.raises(NotImplementedError):
            subject._get_subject_type()

    def test_add_perms_raises_not_implemented(self):
        """Calling add_perms() on a bare Subject raises NotImplementedError."""
        subject = Subject.objects.create()
        with pytest.raises(NotImplementedError):
            subject.add_perms(None, None, None)

    def test_get_user_subjects_raises_not_implemented(self):
        """Calling _get_user_subjects() on a bare Subject raises NotImplementedError."""
        subject = Subject.objects.create()
        with pytest.raises(NotImplementedError):
            subject._get_user_subjects()


@pytest.mark.django_db
class TestRestrictedObjectStr:
    """Test RestrictedObject.__str__ returns the pk as string."""

    def test_str_returns_pk_as_string(self):
        """__str__() must return str(restricted_object_id)."""
        obj = RestrictedObject.objects.create()
        assert str(obj) == str(obj.restricted_object_id)
        assert str(obj) != str(None)
