"""
Tests for permission propagation through RestrictedObject hierarchy.
Tests cover:
    - Propagated permissions on UserSubjects
    - Propagated permissions on UserGroupSubjects
    - Scoping of propagated permissions
    - Multiple bindings combining permissions
"""

import pytest

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


@pytest.mark.django_db
class TestUserPropagation:
    """Test permission propagation for individual UserSubjects."""

    def test_propagated_permissions_apply_to_all_descendants(self, user1, role1, hierarchy, perm_view, perm_change):
        """Propagated binding grants permissions on target and all descendants."""
        role1.add_permission(perm_view)
        role1.add_permission(perm_change)
        RoleBinding.objects.create(
            name="rb1",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        for obj in hierarchy["all_objects"]:
            assert_has_view_and_change(user1, obj)
        assert_no_delete(user1, hierarchy["obj1"])

    def test_propagated_permissions_when_perms_added_after_binding(
        self, user1, role1, hierarchy, perm_view, perm_change
    ):
        """Permissions added after binding also propagate to descendants."""
        RoleBinding.objects.create(
            name="rb1",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        role1.add_permission(perm_view)
        role1.add_permission(perm_change)
        for obj in hierarchy["all_objects"]:
            assert_has_view_and_change(user1, obj)

    def test_deleting_binding_removes_propagated_permissions(self, user1, role1, hierarchy, perm_view):
        """Deleting propagated binding revokes permissions on all descendants."""
        role1.add_permission(perm_view)
        rb = RoleBinding.objects.create(
            name="rb1",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        rb.delete()
        for obj in hierarchy["all_objects"]:
            assert_no_view(user1, obj)

    def test_propagation_scoped_to_subtree(self, user1, role1, hierarchy, perm_view):
        """Propagated permissions only apply to the bound object's subtree."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb1",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj2"],
            propagate=True,
        )
        for obj in hierarchy["obj2_subtree"]:
            assert_has_view(user1, obj)
        assert_no_view(user1, hierarchy["obj1"])
        assert_no_view(user1, hierarchy["obj3"])

    def test_multiple_bindings_combine_propagated_permissions(
        self, user1, role1, role2, hierarchy, perm_view, perm_change
    ):
        """Multiple propagated bindings combine their permissions."""
        role1.add_permission(perm_view)
        role2.add_permission(perm_change)
        RoleBinding.objects.create(
            name="rb1",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        RoleBinding.objects.create(
            name="rb2",
            subject=user1,
            role=role2,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        for obj in hierarchy["all_objects"]:
            assert_has_view_and_change(user1, obj)

    def test_deleting_one_binding_keeps_permissions_from_other(
        self, user1, role1, role2, hierarchy, perm_view, perm_change
    ):
        """Deleting one propagated binding preserves permissions from others."""
        role1.add_permission(perm_view)
        role2.add_permission(perm_change)
        rb1 = RoleBinding.objects.create(
            name="rb1",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        RoleBinding.objects.create(
            name="rb2",
            subject=user1,
            role=role2,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        rb1.delete()
        for obj in hierarchy["all_objects"]:
            assert_no_view(user1, obj)
            assert_has_change(user1, obj)


@pytest.mark.django_db
class TestGroupPropagation:
    """Test permission propagation for UserGroupSubjects."""

    def test_propagated_permissions_apply_to_all_group_members(
        self,
        user1,
        user2,
        user3,
        role1,
        hierarchy,
        group1,
        group2,
        perm_view,
        perm_change,
    ):
        """Propagated group binding grants permissions to all members on subtree."""
        role1.add_permission(perm_view)
        role1.add_permission(perm_change)
        RoleBinding.objects.create(
            name="rb1",
            subject=group1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        for user in [user1, user2]:
            for obj in hierarchy["all_objects"]:
                assert_has_view_and_change(user, obj)
        for obj in hierarchy["all_objects"]:
            assert_no_view(user3, obj)

    def test_adding_user_grants_propagated_permissions(self, user1, user3, role1, hierarchy, group1, perm_view):
        """New group member receives existing propagated permissions."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb1",
            subject=group1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        assert_no_view(user3, hierarchy["obj4"])
        group1.add_user(user3)
        for obj in hierarchy["all_objects"]:
            assert_has_view(user3, obj)

    def test_removing_user_revokes_propagated_permissions(self, user1, user2, role1, hierarchy, group1, perm_view):
        """Removing user from group revokes propagated permissions."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb1",
            subject=group1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        group1.remove_user(user2)
        for obj in hierarchy["all_objects"]:
            assert_has_view(user1, obj)
        for obj in hierarchy["all_objects"]:
            assert_no_view(user2, obj)

    def test_removing_perm_from_role_removes_propagated_permissions(
        self, user1, user2, role1, hierarchy, group1, perm_view, perm_change
    ):
        """Removing permission from role revokes it from all group members on subtree."""
        role1.add_permission(perm_view)
        role1.add_permission(perm_change)
        RoleBinding.objects.create(
            name="rb1",
            subject=group1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        role1.remove_permission(perm_view)
        for user in [user1, user2]:
            for obj in hierarchy["all_objects"]:
                assert_no_view(user, obj)
                assert_has_change(user, obj)

    def test_propagation_scoped_to_subtree(self, user1, user2, role1, hierarchy, group1, perm_view):
        """Propagated permissions only apply to the bound object's subtree."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb1",
            subject=group1,
            role=role1,
            restricted_object=hierarchy["obj2"],
            propagate=True,
        )
        for user in [user1, user2]:
            assert_no_view(user, hierarchy["obj1"])
            assert_no_view(user, hierarchy["obj3"])
            for obj in hierarchy["obj2_subtree"]:
                assert_has_view(user, obj)

    def test_propagation_and_direct_binding_combine(self, user1, role1, role2, hierarchy, group1, perm_view):
        """High-level propagation and low-level direct binding combine."""
        role1.add_permission(perm_view)
        role2.add_permission(perm_view)
        rb1 = RoleBinding.objects.create(
            name="rb1",
            subject=group1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        rb2 = RoleBinding.objects.create(
            name="rb2",
            subject=group1,
            role=role2,
            restricted_object=hierarchy["obj4"],
            propagate=False,
        )
        assert_has_view(user1, hierarchy["obj4"])
        rb1.delete()
        assert_has_view(user1, hierarchy["obj4"])
        rb2.delete()
        assert_no_view(user1, hierarchy["obj4"])

    def test_delete_parent_detaches_children(self, hierarchy):
        """Deleting a RestrictedObject detaches its children to root level."""
        obj2 = hierarchy["obj2"]
        obj4 = hierarchy["obj4"]
        obj5 = hierarchy["obj5"]

        obj2.delete()

        # Children should still exist but now be root nodes
        obj4.refresh_from_db()
        obj5.refresh_from_db()
        assert obj4.parent is None
        assert obj5.parent is None
        assert obj4.depth == 1
        assert obj5.depth == 1


@pytest.mark.django_db
class TestRemoveParentPermsFlatValues:
    """
    Test that _remove_parent_perms correctly uses values_list("pk", flat=True).
    """

    def test_remove_parent_perms_cleans_inherited_perms_from_descendants(self, user1, role1, hierarchy, perm_view):
        """
        Set up propagated permissions, then call _remove_parent_perms
        directly. All inherited perms on the node AND its descendants
        must be removed.
        """
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )

        for obj in hierarchy["obj2_subtree"]:
            assert_has_view(user1, obj)

        hierarchy["obj2"]._remove_parent_perms()

        for obj in hierarchy["obj2_subtree"]:
            assert_no_view(user1, obj)

    def test_remove_parent_perms_counts_correct_deletions(self, user1, role1, hierarchy, perm_view):
        """
        Verify that _remove_parent_perms deletes the expected number of
        UserPermission rows (not zero, which would happen if the
        target_ids list is malformed from flat=False).
        """
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )

        subtree_pks = [o.pk for o in hierarchy["obj2_subtree"]]
        perm_count_before = UserPermission.objects.filter(user=user1, restricted_object_id__in=subtree_pks).count()
        assert perm_count_before == 3

        hierarchy["obj2"]._remove_parent_perms()

        perm_count_after = UserPermission.objects.filter(user=user1, restricted_object_id__in=subtree_pks).count()
        assert perm_count_after == 0
