"""
Tests for UserSubject permissions without propagation.
"""

from unittest.mock import patch

import pytest
from django.db.models import ProtectedError

from topdownrbac.models import (
    RestrictedObject,
    RoleBinding,
    UserGroupSubject,
    UserPermission,
)
from topdownrbac.tests.conftest import (
    assert_has_view,
    assert_has_view_and_change,
    assert_no_delete,
    assert_no_view,
)


@pytest.mark.django_db
class TestUserSubjectPermission:
    """Test permission assignment for individual UserSubjects."""

    def test_permissions_granted_when_role_has_perms_before_binding(
        self, user1, user2, role1, obj1, perm_view, perm_change
    ):
        """Permissions are granted when role already has permissions at binding time."""
        role1.add_permission(perm_view)
        role1.add_permission(perm_change)
        RoleBinding.objects.create(name="rb1", subject=user1, role=role1, restricted_object=obj1)
        assert_has_view_and_change(user1, obj1)
        assert_no_delete(user1, obj1)
        assert_no_view(user2, obj1)

    def test_permissions_granted_when_perms_added_after_binding(
        self, user1, user2, role1, obj1, perm_view, perm_change
    ):
        """Permissions are granted when added to role after binding exists."""
        RoleBinding.objects.create(name="rb1", subject=user1, role=role1, restricted_object=obj1)
        role1.add_permission(perm_view)
        role1.add_permission(perm_change)
        assert_has_view_and_change(user1, obj1)
        assert_no_delete(user1, obj1)
        assert_no_view(user2, obj1)

    def test_permissions_removed_when_perm_removed_from_role(self, user1, role1, obj1, perm_view):
        """Removing a permission from a role revokes it from bound users."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(name="rb1", subject=user1, role=role1, restricted_object=obj1)
        assert_has_view(user1, obj1)
        role1.remove_permission(perm_view)
        assert_no_view(user1, obj1)

    def test_permissions_removed_when_rolebinding_deleted(self, user1, role1, obj1, perm_view):
        """Deleting a RoleBinding revokes all associated permissions."""
        role1.add_permission(perm_view)
        rb = RoleBinding.objects.create(name="rb1", subject=user1, role=role1, restricted_object=obj1)
        assert_has_view(user1, obj1)
        rb.delete()
        assert_no_view(user1, obj1)

    def test_permissions_scoped_to_specific_object(self, user1, role1, obj1, obj2, perm_view):
        """Permissions only apply to the RestrictedObject in the binding."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(name="rb1", subject=user1, role=role1, restricted_object=obj1)
        assert_has_view(user1, obj1)
        assert_no_view(user1, obj2)

    def test_multiple_roles_combine_permissions(self, user1, role1, role2, obj1, perm_view, perm_change):
        """Multiple role bindings on same user/object combine their permissions."""
        role1.add_permission(perm_view)
        role2.add_permission(perm_change)
        RoleBinding.objects.create(name="rb1", subject=user1, role=role1, restricted_object=obj1)
        RoleBinding.objects.create(name="rb2", subject=user1, role=role2, restricted_object=obj1)
        assert_has_view_and_change(user1, obj1)

    def test_deleting_one_binding_preserves_permissions_from_other(self, user1, role1, obj1, perm_view):
        """Deleting one RoleBinding keeps permissions from remaining bindings."""
        role1.add_permission(perm_view)
        rb1 = RoleBinding.objects.create(name="rb1", subject=user1, role=role1, restricted_object=obj1)
        RoleBinding.objects.create(name="rb2", subject=user1, role=role1, restricted_object=obj1)
        rb1.delete()
        assert_has_view(user1, obj1)

    def test_cannot_delete_role_with_active_bindings(self, user1, role1, obj1, perm_view):
        """Role with active RoleBindings cannot be deleted (ProtectedError)."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(name="rb1", subject=user1, role=role1, restricted_object=obj1)
        with pytest.raises(ProtectedError):
            role1.delete()


@pytest.mark.django_db
class TestOverlappingBindingsSourceFiltering:
    """
    When two RoleBindings grant the same permission to the same user on the
    same object, removing one binding (or one perm from a role) must only
    delete the UserPermissions sourced from *that* binding, leaving the other
    binding's permissions intact.
    """

    def test_removing_perm_from_one_role_preserves_other_bindings_perm(self, user1, obj1, role1, role2, perm_view):
        """
        Two roles both grant view. Removing view from role1 must not remove
        the UserPermission created by role2's binding.
        """
        role1.add_permission(perm_view)
        role2.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb_a",
            subject=user1,
            role=role1,
            restricted_object=obj1,
        )
        rb_b = RoleBinding.objects.create(
            name="rb_b",
            subject=user1,
            role=role2,
            restricted_object=obj1,
        )
        assert (
            UserPermission.objects.filter(
                user=user1,
                perm=perm_view,
                restricted_object=obj1,
            ).count()
            == 2
        )

        role1.remove_permission(perm_view)

        assert_has_view(user1, obj1)
        assert UserPermission.objects.filter(
            user=user1,
            perm=perm_view,
            restricted_object=obj1,
            source=rb_b,
        ).exists()

    def test_removing_user_from_group_preserves_direct_binding_perms(self, user1, obj1, role1, role2, perm_view):
        """
        User has perms from a direct binding AND from a group binding.
        Removing user from the group must only remove the group-sourced
        UserPermissions.
        """
        role1.add_permission(perm_view)
        role2.add_permission(perm_view)
        group = UserGroupSubject.objects.create()
        group.add_user(user1)

        rb_direct = RoleBinding.objects.create(
            name="rb_direct",
            subject=user1,
            role=role1,
            restricted_object=obj1,
        )
        rb_group = RoleBinding.objects.create(
            name="rb_group",
            subject=group,
            role=role2,
            restricted_object=obj1,
        )

        assert (
            UserPermission.objects.filter(
                user=user1,
                perm=perm_view,
                restricted_object=obj1,
            ).count()
            == 2
        )

        group.remove_user(user1)

        assert_has_view(user1, obj1)
        assert UserPermission.objects.filter(
            user=user1,
            perm=perm_view,
            source=rb_direct,
        ).exists()
        assert not UserPermission.objects.filter(
            user=user1,
            perm=perm_view,
            source=rb_group,
        ).exists()


@pytest.mark.django_db
class TestAddSinglePermExistingPermsFilter:
    """
    Test that _add_single_perm_to_subject correctly deduplicates existing
    UserPermission rows using its existing_perms filter query.
    """

    def test_add_single_perm_idempotent_no_duplicates(self, user1, role1, obj1, perm_view):
        """
        Calling _add_single_perm_to_subject twice for the same perm must
        not create duplicates.
        """
        role1.add_permission(perm_view)
        rb = RoleBinding.objects.create(name="rb", subject=user1, role=role1, restricted_object=obj1)

        count_after_create = UserPermission.objects.filter(
            user=user1, perm=perm_view, restricted_object=obj1, source=rb
        ).count()
        assert count_after_create == 1

        rb._add_single_perm_to_subject(perm_view)

        count_after_second = UserPermission.objects.filter(
            user=user1, perm=perm_view, restricted_object=obj1, source=rb
        ).count()
        assert count_after_second == 1

    def test_add_single_perm_idempotent_with_propagation(self, user1, role1, hierarchy, perm_view):
        """
        With propagation, calling _add_single_perm_to_subject again must
        not create duplicates on any descendant.
        """
        role1.add_permission(perm_view)
        rb = RoleBinding.objects.create(
            name="rb",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )

        count_before = UserPermission.objects.filter(user=user1, source=rb).count()
        assert count_before == 5  # obj1 + 4 descendants

        rb._add_single_perm_to_subject(perm_view)

        count_after = UserPermission.objects.filter(user=user1, source=rb).count()
        assert count_after == 5

    def test_add_single_perm_idempotent_for_group(self, user1, user2, role1, obj1, perm_view):
        """
        For a group binding, calling _add_single_perm_to_subject again must
        not create duplicates for any group member.
        """
        group = UserGroupSubject.objects.create()
        group.add_user(user1)
        group.add_user(user2)

        role1.add_permission(perm_view)
        rb = RoleBinding.objects.create(name="rb", subject=group, role=role1, restricted_object=obj1)

        count_before = UserPermission.objects.filter(perm=perm_view, restricted_object=obj1, source=rb).count()
        assert count_before == 2  # one per user

        rb._add_single_perm_to_subject(perm_view)

        count_after = UserPermission.objects.filter(perm=perm_view, restricted_object=obj1, source=rb).count()
        assert count_after == 2

    def test_add_single_perm_for_specific_restricted_object(self, user1, role1, hierarchy, perm_view):
        """
        When restricted_object argument is passed, only that object gets
        the permission -- not the full subtree from propagation.
        """
        role1.add_permission(perm_view)
        rb = RoleBinding.objects.create(
            name="rb",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )

        UserPermission.objects.filter(source=rb).delete()

        rb._add_single_perm_to_subject(perm_view, hierarchy["obj4"])

        assert UserPermission.objects.filter(user=user1, source=rb, restricted_object=hierarchy["obj4"]).exists()
        assert not UserPermission.objects.filter(user=user1, source=rb, restricted_object=hierarchy["obj1"]).exists()
        assert not UserPermission.objects.filter(user=user1, source=rb, restricted_object=hierarchy["obj2"]).exists()


@pytest.mark.django_db
class TestAddAllPermsForOneRestrictedObject:
    """
    Test that _add_all_perms_for_one_restricted_object passes the
    restricted_object argument correctly and only targets that object.
    """

    def test_add_all_perms_targets_only_specified_object(self, user1, role1, hierarchy, perm_view, perm_change):
        """
        _add_all_perms_for_one_restricted_object must create permissions
        only on the specified restricted_object, not on the entire subtree.
        """
        role1.add_permission(perm_view)
        role1.add_permission(perm_change)
        rb = RoleBinding.objects.create(
            name="rb",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )

        UserPermission.objects.filter(source=rb).delete()

        rb._add_all_perms_for_one_restricted_object(hierarchy["obj3"])

        obj3_perms = UserPermission.objects.filter(user=user1, source=rb, restricted_object=hierarchy["obj3"])
        assert obj3_perms.count() == 2

        other_perms = UserPermission.objects.filter(source=rb).exclude(restricted_object=hierarchy["obj3"])
        assert other_perms.count() == 0

    def test_add_all_perms_for_one_object_with_mock(self, user1, role1, obj1, perm_view, perm_change):
        """
        Verify that _add_all_perms_for_one_restricted_object passes the
        restricted_object arg correctly to _add_single_perm_to_subject.
        """
        role1.add_permission(perm_view)
        role1.add_permission(perm_change)
        rb = RoleBinding.objects.create(name="rb", subject=user1, role=role1, restricted_object=obj1)

        new_obj = RestrictedObject.objects.create(parent=obj1)

        with patch.object(rb, "_add_single_perm_to_subject") as mock_add:
            rb._add_all_perms_for_one_restricted_object(new_obj)

            assert mock_add.call_count == 2
            for c in mock_add.call_args_list:
                assert c[0][1] is new_obj


@pytest.mark.django_db
class TestWhoHasPerm:
    """Test RestrictedObject.who_has_perm() returns correct users."""

    def test_returns_user_with_direct_binding(self, user1, role1, obj1, perm_view):
        """User with a direct role binding granting the perm is returned."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(name="rb", subject=user1, role=role1, restricted_object=obj1)
        result = set(obj1.who_has_perm("view_restrictedobject"))
        assert result == {user1}

    def test_returns_empty_when_no_permissions(self, obj1):
        """Returns empty queryset when no one has the perm on the object."""
        result = obj1.who_has_perm("view_restrictedobject")
        assert result.count() == 0

    def test_does_not_return_user_without_perm(self, user1, user2, role1, obj1, perm_view):
        """User without the perm on the object is not returned."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(name="rb", subject=user1, role=role1, restricted_object=obj1)
        result = set(obj1.who_has_perm("view_restrictedobject"))
        assert user2 not in result

    def test_returns_multiple_users(self, user1, user2, role1, obj1, perm_view):
        """All users with the perm are returned."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(name="rb1", subject=user1, role=role1, restricted_object=obj1)
        RoleBinding.objects.create(name="rb2", subject=user2, role=role1, restricted_object=obj1)
        result = set(obj1.who_has_perm("view_restrictedobject"))
        assert result == {user1, user2}

    def test_scoped_to_specific_object(self, user1, role1, obj1, obj2, perm_view):
        """Perm on obj1 does not cause the user to appear in obj2.who_has_perm()."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(name="rb", subject=user1, role=role1, restricted_object=obj1)
        assert obj2.who_has_perm("view_restrictedobject").count() == 0

    def test_filters_by_perm_codename(self, user1, role1, obj1, perm_view, perm_change):
        """Only the requested perm codename is considered."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(name="rb", subject=user1, role=role1, restricted_object=obj1)
        assert obj1.who_has_perm("view_restrictedobject").count() == 1
        assert obj1.who_has_perm("change_restrictedobject").count() == 0

    def test_no_duplicate_users_with_multiple_bindings(self, user1, role1, role2, obj1, perm_view):
        """User appearing in multiple bindings is returned only once."""
        role1.add_permission(perm_view)
        role2.add_permission(perm_view)
        RoleBinding.objects.create(name="rb1", subject=user1, role=role1, restricted_object=obj1)
        RoleBinding.objects.create(name="rb2", subject=user1, role=role2, restricted_object=obj1)
        result = list(obj1.who_has_perm("view_restrictedobject"))
        assert len(result) == 1
        assert result[0] == user1

    def test_includes_users_from_group_binding(self, user1, user2, role1, obj1, group1, perm_view):
        """Users who have the perm via a group binding are returned."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(name="rb_group", subject=group1, role=role1, restricted_object=obj1)
        result = set(obj1.who_has_perm("view_restrictedobject"))
        assert result == {user1, user2}

    def test_includes_users_from_propagated_binding(self, user1, role1, hierarchy, perm_view):
        """Users who have the perm via propagation from an ancestor are returned."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        result = set(hierarchy["obj4"].who_has_perm("view_restrictedobject"))
        assert result == {user1}

    def test_returns_queryset(self, obj1):
        """who_has_perm returns a Django QuerySet."""
        from django.db.models import QuerySet

        result = obj1.who_has_perm("view_restrictedobject")
        assert isinstance(result, QuerySet)
