"""
Tests for permission changes when RestrictedObject hierarchy changes.
Tests cover:
    - Permissions when new objects are created under propagated bindings
    - Permissions when objects are moved (change_parent)
    - Permissions when objects are detached from hierarchy
    - Parent FK persistence after change_parent / remove_parent
"""

import pytest

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


@pytest.mark.django_db
class TestObjectCreationInHierarchy:
    """Test that new objects inherit propagated permissions."""

    def test_new_child_inherits_propagated_permissions(
        self, user1, user2, role1, hierarchy, group1, perm_view, perm_change
    ):
        """New child object automatically inherits propagated permissions."""
        role1.add_permission(perm_view)
        role1.add_permission(perm_change)
        RoleBinding.objects.create(
            name="rb_user",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        RoleBinding.objects.create(
            name="rb_group",
            subject=group1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )

        new_obj = RestrictedObject.objects.create(parent=hierarchy["obj5"])
        assert_has_view_and_change(user1, new_obj)
        for user in [user1, user2]:
            assert_has_view_and_change(user, new_obj)

    def test_new_deep_child_inherits_all_ancestor_permissions(
        self, user1, user2, role1, hierarchy, group1, perm_view, perm_change
    ):
        """New deeply nested object inherits all propagated permissions from ancestors."""
        role1.add_permission(perm_view)
        role1.add_permission(perm_change)
        RoleBinding.objects.create(
            name="rb_user",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        RoleBinding.objects.create(
            name="rb_group",
            subject=group1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )

        level1 = RestrictedObject.objects.create(parent=hierarchy["obj5"])
        level2 = RestrictedObject.objects.create(parent=level1)
        for obj in [level1, level2]:
            assert_has_view_and_change(user1, obj)
            for user in [user1, user2]:
                assert_has_view_and_change(user, obj)


@pytest.mark.django_db
class TestParentChange:
    """Test permission updates when object's parent changes."""

    def test_moving_object_loses_old_permissions_gains_new(
        self, user1, user2, role1, role2, hierarchy, group1, perm_view, perm_change, perm_delete
    ):
        """Moving object to new parent updates permissions accordingly."""
        obj_alt = RestrictedObject.objects.create()
        role2.add_permission(perm_delete)
        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,
        )
        RoleBinding.objects.create(
            name="rb_alt",
            subject=group1,
            role=role2,
            restricted_object=obj_alt,
            propagate=True,
        )

        assert_has_view_and_change(user1, hierarchy["obj4"])
        assert_no_delete(user1, hierarchy["obj4"])
        hierarchy["obj4"].change_parent(obj_alt)
        assert_no_view(user1, hierarchy["obj4"])
        assert_no_change(user1, hierarchy["obj4"])
        assert_has_delete(user1, hierarchy["obj4"])

    def test_detaching_object_removes_propagated_permissions(self, user1, role1, hierarchy, perm_view, perm_change):
        """Moving object to root (no parent) removes inherited permissions."""
        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,
        )

        assert_has_view_and_change(user1, hierarchy["obj4"])
        hierarchy["obj4"].change_parent(None)
        assert_no_view(user1, hierarchy["obj4"])
        assert_no_change(user1, hierarchy["obj4"])

    def test_moving_subtree_updates_all_descendants(
        self, user1, user2, role1, role2, hierarchy, group1, perm_view, perm_change, perm_delete
    ):
        """Moving a subtree updates permissions for all descendants."""
        obj_alt = RestrictedObject.objects.create()
        role2.add_permission(perm_delete)
        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,
        )
        RoleBinding.objects.create(
            name="rb_alt",
            subject=group1,
            role=role2,
            restricted_object=obj_alt,
            propagate=True,
        )

        deep_obj = RestrictedObject.objects.create(parent=hierarchy["obj4"])
        assert_has_view(user1, deep_obj)
        hierarchy["obj4"].change_parent(obj_alt)
        assert_no_view(user1, deep_obj)
        assert_has_delete(user1, deep_obj)

    def test_moving_subtree_children_gain_new_parent_perms(
        self, user1, role1, role2, hierarchy, perm_view, perm_delete
    ):
        """All children of moved subtree gain the new parent's propagated permissions."""
        new_root = RestrictedObject.objects.create()
        role1.add_permission(perm_view)
        role2.add_permission(perm_delete)
        RoleBinding.objects.create(
            name="rb_old",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        RoleBinding.objects.create(
            name="rb_new",
            subject=user1,
            role=role2,
            restricted_object=new_root,
            propagate=True,
        )

        assert_has_view(user1, hierarchy["obj4"])
        assert_has_view(user1, hierarchy["obj5"])
        assert_no_delete(user1, hierarchy["obj4"])

        hierarchy["obj2"].change_parent(new_root)

        for obj in [hierarchy["obj2"], hierarchy["obj4"], hierarchy["obj5"]]:
            assert_no_view(user1, obj)
            assert_has_delete(user1, obj)

    def test_move_inherits_from_entire_new_ancestor_chain(self, user1, role1, role2, hierarchy, perm_view, perm_change):
        """Moved node inherits propagated perms from all ancestors, not just immediate parent."""
        new_root = RestrictedObject.objects.create()
        mid_node = RestrictedObject.objects.create(parent=new_root)
        role1.add_permission(perm_view)
        role2.add_permission(perm_change)
        RoleBinding.objects.create(
            name="rb_root",
            subject=user1,
            role=role1,
            restricted_object=new_root,
            propagate=True,
        )
        RoleBinding.objects.create(
            name="rb_mid",
            subject=user1,
            role=role2,
            restricted_object=mid_node,
            propagate=True,
        )

        hierarchy["obj4"].change_parent(mid_node)

        assert_has_view(user1, hierarchy["obj4"])
        assert_has_change(user1, hierarchy["obj4"])

    def test_move_between_siblings_keeps_common_ancestor_perms(
        self, user1, role1, role2, hierarchy, perm_view, perm_change
    ):
        """Moving to a sibling branch keeps root-inherited perms, loses old branch perms."""
        role1.add_permission(perm_view)
        role2.add_permission(perm_change)
        RoleBinding.objects.create(
            name="rb_root",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )
        RoleBinding.objects.create(
            name="rb_branch",
            subject=user1,
            role=role2,
            restricted_object=hierarchy["obj2"],
            propagate=True,
        )

        assert_has_view(user1, hierarchy["obj4"])
        assert_has_change(user1, hierarchy["obj4"])

        hierarchy["obj4"].change_parent(hierarchy["obj3"])

        assert_has_view(user1, hierarchy["obj4"])
        assert_no_change(user1, hierarchy["obj4"])


@pytest.mark.django_db
class TestChangeParentTreeStructure:
    """Test that change_parent correctly updates treebeard structure and parent FK."""

    def test_detach_makes_node_a_root(self, user1, role1, hierarchy, perm_view):
        """After change_parent(None), node must be a treebeard root with no parent."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )

        obj4 = hierarchy["obj4"]
        obj4.change_parent(None)
        obj4.refresh_from_db()

        assert obj4.parent is None
        assert obj4.parent_id is None
        assert obj4.is_root()
        assert obj4.get_parent() is None

    def test_move_to_new_parent_updates_parent_fk(self, user1, role1, hierarchy, perm_view):
        """After change_parent(new_parent), the parent FK must point to the new parent."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )

        alt_root = RestrictedObject.objects.create()
        obj4 = hierarchy["obj4"]
        obj4.change_parent(alt_root)
        obj4.refresh_from_db()

        assert obj4.parent_id == alt_root.pk
        assert obj4.parent == alt_root

    def test_move_to_new_parent_saves_parent_field(self, user1, role1, hierarchy, perm_view):
        """After change_parent, the parent FK must be persisted in the DB."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )

        alt_root = RestrictedObject.objects.create()
        obj4 = hierarchy["obj4"]
        obj4.change_parent(alt_root)

        obj4_from_db = RestrictedObject.objects.get(pk=obj4.pk)
        assert obj4_from_db.parent_id == alt_root.pk


@pytest.mark.django_db
class TestRemoveParentPermsPreservesDirectPerms:
    """
    Direct (non-inherited) permissions on a moved object must survive
    a change_parent call, while inherited permissions are removed.
    """

    def test_direct_perms_preserved_after_change_parent(
        self,
        user1,
        role1,
        role2,
        hierarchy,
        perm_view,
        perm_change,
    ):
        """
        User has propagated view from obj1 AND a direct change binding on
        obj4. After moving obj4 to a new root, the direct change perm must
        survive while the inherited view perm is removed.
        """
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb_prop",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )

        role2.add_permission(perm_change)
        RoleBinding.objects.create(
            name="rb_direct",
            subject=user1,
            role=role2,
            restricted_object=hierarchy["obj4"],
            propagate=False,
        )

        assert_has_view(user1, hierarchy["obj4"])
        assert_has_change(user1, hierarchy["obj4"])

        hierarchy["obj4"].change_parent(None)

        assert_no_view(user1, hierarchy["obj4"])
        assert_has_change(user1, hierarchy["obj4"])

    def test_direct_perms_on_descendant_preserved_after_subtree_move(
        self,
        user1,
        role1,
        role2,
        hierarchy,
        perm_view,
        perm_delete,
    ):
        """
        Propagated view from obj1 on whole tree. Direct delete on obj5.
        Move obj2 subtree (contains obj5) to new root.
        Direct delete on obj5 must survive.
        """
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb_prop",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )

        role2.add_permission(perm_delete)
        RoleBinding.objects.create(
            name="rb_direct_obj5",
            subject=user1,
            role=role2,
            restricted_object=hierarchy["obj5"],
            propagate=False,
        )

        assert_has_view(user1, hierarchy["obj5"])
        assert_has_delete(user1, hierarchy["obj5"])

        hierarchy["obj2"].change_parent(None)

        assert_no_view(user1, hierarchy["obj5"])
        assert_has_delete(user1, hierarchy["obj5"])


@pytest.mark.django_db
class TestAddPermsForOneRestrictedObject:
    """
    When a new child is created under a propagated binding, only that
    specific child should get permissions — not the entire subtree again.
    """

    def test_new_child_gets_perms_only_on_itself(
        self,
        user1,
        role1,
        hierarchy,
        perm_view,
    ):
        """
        Create a propagated binding on obj2. Count UserPermissions.
        Add a new child under obj2. Only 1 new UserPermission should be
        created (for the new child), not N (for the whole subtree).
        """
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj2"],
            propagate=True,
        )

        count_before = UserPermission.objects.filter(user=user1).count()

        new_child = RestrictedObject.objects.create(parent=hierarchy["obj2"])
        count_after = UserPermission.objects.filter(user=user1).count()

        assert count_after == count_before + 1
        assert_has_view(user1, new_child)

    def test_new_child_does_not_duplicate_existing_perms(
        self,
        user1,
        role1,
        hierarchy,
        perm_view,
    ):
        """
        Adding a new child must not re-create perms that already exist
        on existing descendants.
        """
        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

        new_child = RestrictedObject.objects.create(parent=hierarchy["obj5"])

        count_after = UserPermission.objects.filter(
            user=user1,
            source=rb,
        ).count()
        assert count_after == 6
        assert_has_view(user1, new_child)


@pytest.mark.django_db
class TestChangeParentMoveArgs:
    """
    Test that change_parent(None) uses the correct treebeard move position.
    Mutants change self.move(self.get_root(), "last-sibling") to
    self.move(self.get_root(), None) or drop the position arg.
    """

    def test_change_parent_none_makes_proper_root(self, hierarchy):
        """
        After change_parent(None), the node must be a proper treebeard root.
        If the move position arg is wrong, treebeard will either error or
        produce an incorrect tree structure.
        """
        obj2 = hierarchy["obj2"]
        roots_before = RestrictedObject.get_root_nodes().count()

        obj2.change_parent(None)
        obj2.refresh_from_db()

        roots_after = RestrictedObject.get_root_nodes().count()
        assert roots_after == roots_before + 1
        assert obj2.is_root()
        assert obj2.depth == 1


@pytest.mark.django_db
class TestRemoveParent:
    """Test that remove_parent() correctly detaches a node from its parent."""

    def test_remove_parent_makes_node_a_root(self, hierarchy):
        """After remove_parent(), the node must be a treebeard root with no parent."""
        obj4 = hierarchy["obj4"]
        obj4.remove_parent()
        obj4.refresh_from_db()

        assert obj4.parent is None
        assert obj4.parent_id is None
        assert obj4.is_root()
        assert obj4.get_parent() is None

    def test_remove_parent_removes_inherited_permissions(
        self,
        user1,
        role1,
        hierarchy,
        perm_view,
    ):
        """remove_parent() removes inherited permissions from the detached node."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb_prop",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )

        assert_has_view(user1, hierarchy["obj4"])

        hierarchy["obj4"].remove_parent()

        assert_no_view(user1, hierarchy["obj4"])

    def test_remove_parent_removes_inherited_permissions_from_descendants(
        self,
        user1,
        role1,
        hierarchy,
        perm_view,
    ):
        """remove_parent() removes inherited permissions from descendants too."""
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb_prop",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )

        assert_has_view(user1, hierarchy["obj4"])
        assert_has_view(user1, hierarchy["obj5"])

        hierarchy["obj2"].remove_parent()

        assert_no_view(user1, hierarchy["obj4"])
        assert_no_view(user1, hierarchy["obj5"])

    def test_remove_parent_preserves_direct_permissions(
        self,
        user1,
        role1,
        role2,
        hierarchy,
        perm_view,
        perm_change,
    ):
        """
        Direct (non-inherited) permissions on the detached object must survive
        remove_parent(), while inherited permissions are removed.
        """
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb_prop",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )

        role2.add_permission(perm_change)
        RoleBinding.objects.create(
            name="rb_direct",
            subject=user1,
            role=role2,
            restricted_object=hierarchy["obj4"],
            propagate=False,
        )

        assert_has_view(user1, hierarchy["obj4"])
        assert_has_change(user1, hierarchy["obj4"])

        hierarchy["obj4"].remove_parent()

        assert_no_view(user1, hierarchy["obj4"])
        assert_has_change(user1, hierarchy["obj4"])

    def test_remove_parent_preserves_direct_permissions_on_descendants(
        self,
        user1,
        role1,
        role2,
        hierarchy,
        perm_view,
        perm_delete,
    ):
        """
        Direct permissions on a descendant of the moved subtree must survive
        remove_parent() on the subtree root.
        """
        role1.add_permission(perm_view)
        RoleBinding.objects.create(
            name="rb_prop",
            subject=user1,
            role=role1,
            restricted_object=hierarchy["obj1"],
            propagate=True,
        )

        role2.add_permission(perm_delete)
        RoleBinding.objects.create(
            name="rb_direct_obj5",
            subject=user1,
            role=role2,
            restricted_object=hierarchy["obj5"],
            propagate=False,
        )

        assert_has_view(user1, hierarchy["obj5"])
        assert_has_delete(user1, hierarchy["obj5"])

        hierarchy["obj2"].remove_parent()

        assert_no_view(user1, hierarchy["obj5"])
        assert_has_delete(user1, hierarchy["obj5"])

    def test_remove_parent_on_root_is_noop_for_structure(self, hierarchy):
        """Calling remove_parent() on a root node should leave it as a root."""
        obj1 = hierarchy["obj1"]
        assert obj1.is_root()

        obj1.remove_parent()
        obj1.refresh_from_db()

        assert obj1.is_root()
        assert obj1.parent is None


@pytest.mark.django_db
class TestChangeParentSaveUpdateFields:
    """
    Test that change_parent uses update_fields=["parent"] to avoid
    corrupting treebeard paths when saving the parent FK.
    """

    def test_change_parent_only_saves_parent_field(self, hierarchy):
        """
        After change_parent, the parent FK must be persisted without
        corrupting the treebeard path set by move().
        """
        obj4 = hierarchy["obj4"]
        alt_root = RestrictedObject.objects.create()

        obj4.change_parent(alt_root)
        obj4_from_db = RestrictedObject.objects.get(pk=obj4.pk)

        assert obj4_from_db.parent_id == alt_root.pk
        assert obj4_from_db.depth == 2
        assert not obj4_from_db.is_root()
        assert obj4_from_db.get_parent().pk == alt_root.pk
