"""
Tests for the import_roles management command and RoleSyncService.

Tests cover:
    - Creating roles from YAML with correct flags
    - Roles created manually are not removed when tool runs again
    - Changes to YAML update roles (creation, deactivation, permission changes)
    - Deactivated roles have permissions suspended but retain role bindings
    - Reactivation restores permissions automatically
    - Error handling for invalid YAML or missing permissions
"""

import os
import tempfile
from io import StringIO

import pytest
from django.core.management import call_command

from topdownrbac.models import Role
from topdownrbac.services.import_roles_as_yaml_service import RoleSyncService
from topdownrbac.tests.conftest import (
    assert_has_change,
    assert_has_delete,
    assert_has_view,
    assert_no_change,
    assert_no_delete,
    assert_no_view,
)


def _create_yaml_file(content):
    """Helper to create a temporary YAML file."""
    fd, path = tempfile.mkstemp(suffix=".yaml")
    with os.fdopen(fd, "w") as f:
        f.write(content)
    return path


@pytest.mark.django_db
class TestRoleSyncService:
    """Test the RoleSyncService directly."""

    def test_create_roles_from_yaml(self, perm_view, perm_change):
        """Roles are created from YAML with correct permissions."""
        yaml_content = """
roles:
  viewer:
    permissions:
      - view_restrictedobject
  editor:
    permissions:
      - view_restrictedobject
      - change_restrictedobject
"""
        path = _create_yaml_file(yaml_content)
        try:
            service = RoleSyncService(path)
            deleted, updates = service.sync()

            assert Role.objects.filter(name="viewer").exists()
            assert Role.objects.filter(name="editor").exists()

            viewer = Role.objects.get(name="viewer")
            editor = Role.objects.get(name="editor")

            assert perm_view in viewer.permissions.all()
            assert viewer.permissions.count() == 1

            assert perm_view in editor.permissions.all()
            assert perm_change in editor.permissions.all()
            assert editor.permissions.count() == 2
        finally:
            os.unlink(path)

    def test_roles_created_with_correct_flags(self, db):
        """Roles from YAML have externally_provisioned=True and editable=False."""
        yaml_content = """
roles:
  test_role:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content)
        try:
            service = RoleSyncService(path)
            service.sync()

            role = Role.objects.get(name="test_role")
            assert role.externally_provisioned is True
            assert role.editable is False
        finally:
            os.unlink(path)

    def test_manual_roles_not_removed_on_sync(self, perm_view):
        """Manually created roles are not removed when import_roles runs."""
        manual_role = Role.objects.create(name="manual_role", externally_provisioned=False)
        manual_role.permissions.add(perm_view)

        yaml_content = """
roles:
  yaml_role:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content)
        try:
            service = RoleSyncService(path)
            deleted, updates = service.sync()

            assert Role.objects.filter(name="manual_role").exists()
            assert Role.objects.filter(name="yaml_role").exists()
            assert len(deleted) == 0
        finally:
            os.unlink(path)

    def test_yaml_role_deactivated_when_removed_from_yaml(self, db):
        """Externally provisioned role is deactivated when removed from YAML."""
        yaml_content_v1 = """
roles:
  role_to_keep:
    permissions:
      - view_restrictedobject
  role_to_deactivate:
    permissions:
      - change_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v1)
        try:
            service = RoleSyncService(path)
            service.sync()

            assert Role.objects.filter(name="role_to_keep").exists()
            assert Role.objects.filter(name="role_to_deactivate").exists()
        finally:
            os.unlink(path)

        yaml_content_v2 = """
roles:
  role_to_keep:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v2)
        try:
            service = RoleSyncService(path)
            deactivated, updates = service.sync()

            # role still exists but is deactivated
            role = Role.objects.get(name="role_to_deactivate")
            assert role.is_active is False
            assert "role_to_deactivate" in deactivated
            assert Role.objects.get(name="role_to_keep").is_active is True
        finally:
            os.unlink(path)

    def test_permissions_added_on_yaml_change(self, perm_view, perm_change, perm_delete):
        """Adding permissions to YAML updates the role."""
        yaml_content_v1 = """
roles:
  evolving_role:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v1)
        try:
            service = RoleSyncService(path)
            service.sync()

            role = Role.objects.get(name="evolving_role")
            assert role.permissions.count() == 1
        finally:
            os.unlink(path)

        yaml_content_v2 = """
roles:
  evolving_role:
    permissions:
      - view_restrictedobject
      - change_restrictedobject
      - delete_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v2)
        try:
            service = RoleSyncService(path)
            deleted, updates = service.sync()

            role = Role.objects.get(name="evolving_role")
            assert role.permissions.count() == 3
            assert perm_view in role.permissions.all()
            assert perm_change in role.permissions.all()
            assert perm_delete in role.permissions.all()

            assert len(updates) == 1
            role_name, added, removed, created, reactivated = updates[0]
            assert role_name == "evolving_role"
            assert "change_restrictedobject" in added
            assert "delete_restrictedobject" in added
        finally:
            os.unlink(path)

    def test_permissions_removed_on_yaml_change(self, perm_view, perm_change, perm_delete):
        """Removing permissions from YAML updates the role."""
        yaml_content_v1 = """
roles:
  shrinking_role:
    permissions:
      - view_restrictedobject
      - change_restrictedobject
      - delete_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v1)
        try:
            service = RoleSyncService(path)
            service.sync()

            role = Role.objects.get(name="shrinking_role")
            assert role.permissions.count() == 3
        finally:
            os.unlink(path)

        yaml_content_v2 = """
roles:
  shrinking_role:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v2)
        try:
            service = RoleSyncService(path)
            deleted, updates = service.sync()

            role = Role.objects.get(name="shrinking_role")
            assert role.permissions.count() == 1
            assert perm_view in role.permissions.all()
            assert perm_change not in role.permissions.all()
            assert perm_delete not in role.permissions.all()

            assert len(updates) == 1
            role_name, added, removed, created, reactivated = updates[0]
            assert "change_restrictedobject" in removed
            assert "delete_restrictedobject" in removed
        finally:
            os.unlink(path)

    def test_no_changes_on_identical_sync(self, db):
        """Running sync twice with same YAML reports no changes."""
        yaml_content = """
roles:
  stable_role:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content)
        try:
            service = RoleSyncService(path)
            service.sync()

            service2 = RoleSyncService(path)
            deleted, updates = service2.sync()

            assert len(deleted) == 0
            assert len(updates) == 0
        finally:
            os.unlink(path)

    def test_error_on_missing_roles_key(self, db):
        """YAML without 'roles' key raises ValueError."""
        yaml_content = """
not_roles:
  some_role:
    permissions: []
"""
        path = _create_yaml_file(yaml_content)
        try:
            service = RoleSyncService(path)
            with pytest.raises(ValueError, match="roles"):
                service.sync()
        finally:
            os.unlink(path)

    def test_error_on_missing_permission(self, db):
        """YAML with non-existent permission raises ValueError."""
        yaml_content = """
roles:
  bad_role:
    permissions:
      - nonexistent_permission
"""
        path = _create_yaml_file(yaml_content)
        try:
            service = RoleSyncService(path)
            with pytest.raises(ValueError, match="nonexistent_permission"):
                service.sync()
        finally:
            os.unlink(path)

    def test_role_with_empty_permissions(self, db):
        """Role with no permissions is created correctly."""
        yaml_content = """
roles:
  empty_role:
    permissions: []
"""
        path = _create_yaml_file(yaml_content)
        try:
            service = RoleSyncService(path)
            service.sync()

            role = Role.objects.get(name="empty_role")
            assert role.permissions.count() == 0
            assert role.externally_provisioned is True
        finally:
            os.unlink(path)


@pytest.mark.django_db
class TestRoleSyncUserPermissionPropagation:
    """Test that YAML role changes propagate to UserPermissions for assigned users."""

    def test_user_gets_permissions_from_yaml_role(self, user1, user2, hierarchy):
        """User with RoleBinding to YAML role gets permissions."""
        from topdownrbac.models import RoleBinding

        obj1 = hierarchy["obj1"]
        yaml_content = """
roles:
  yaml_viewer:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content)
        try:
            service = RoleSyncService(path)
            service.sync()

            role = Role.objects.get(name="yaml_viewer")
            RoleBinding.objects.create(name="rb1", subject=user1, role=role, restricted_object=obj1)

            assert_has_view(user1, obj1)
            assert_no_change(user1, obj1)
            assert_no_view(user2, obj1)
        finally:
            os.unlink(path)

    def test_user_gains_permissions_when_yaml_adds_perms(self, user1, hierarchy):
        """User gains permissions when YAML role is updated to add permissions."""
        from topdownrbac.models import RoleBinding

        obj1 = hierarchy["obj1"]
        yaml_content_v1 = """
roles:
  evolving_role:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v1)
        try:
            service = RoleSyncService(path)
            service.sync()

            role = Role.objects.get(name="evolving_role")
            RoleBinding.objects.create(name="rb1", subject=user1, role=role, restricted_object=obj1)

            assert_has_view(user1, obj1)
            assert_no_change(user1, obj1)
            assert_no_delete(user1, obj1)
        finally:
            os.unlink(path)

        yaml_content_v2 = """
roles:
  evolving_role:
    permissions:
      - view_restrictedobject
      - change_restrictedobject
      - delete_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v2)
        try:
            service = RoleSyncService(path)
            service.sync()

            assert_has_view(user1, obj1)
            assert_has_change(user1, obj1)
            assert_has_delete(user1, obj1)
        finally:
            os.unlink(path)

    def test_user_loses_permissions_when_yaml_removes_perms(self, user1, hierarchy):
        """User loses permissions when YAML role is updated to remove permissions."""
        from topdownrbac.models import RoleBinding

        obj1 = hierarchy["obj1"]
        yaml_content_v1 = """
roles:
  shrinking_role:
    permissions:
      - view_restrictedobject
      - change_restrictedobject
      - delete_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v1)
        try:
            service = RoleSyncService(path)
            service.sync()

            role = Role.objects.get(name="shrinking_role")
            RoleBinding.objects.create(name="rb1", subject=user1, role=role, restricted_object=obj1)

            assert_has_view(user1, obj1)
            assert_has_change(user1, obj1)
            assert_has_delete(user1, obj1)
        finally:
            os.unlink(path)

        yaml_content_v2 = """
roles:
  shrinking_role:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v2)
        try:
            service = RoleSyncService(path)
            service.sync()

            assert_has_view(user1, obj1)
            assert_no_change(user1, obj1)
            assert_no_delete(user1, obj1)
        finally:
            os.unlink(path)

    def test_multiple_users_affected_by_yaml_change(self, user1, user2, hierarchy):
        """Multiple users with RoleBindings to same role are all affected by YAML change."""
        from topdownrbac.models import RoleBinding

        obj1 = hierarchy["obj1"]
        yaml_content_v1 = """
roles:
  shared_role:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v1)
        try:
            service = RoleSyncService(path)
            service.sync()

            role = Role.objects.get(name="shared_role")
            RoleBinding.objects.create(name="rb1", subject=user1, role=role, restricted_object=obj1)
            RoleBinding.objects.create(name="rb2", subject=user2, role=role, restricted_object=obj1)

            assert_has_view(user1, obj1)
            assert_has_view(user2, obj1)
            assert_no_change(user1, obj1)
            assert_no_change(user2, obj1)
        finally:
            os.unlink(path)

        yaml_content_v2 = """
roles:
  shared_role:
    permissions:
      - view_restrictedobject
      - change_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v2)
        try:
            service = RoleSyncService(path)
            service.sync()

            assert_has_view(user1, obj1)
            assert_has_view(user2, obj1)
            assert_has_change(user1, obj1)
            assert_has_change(user2, obj1)
        finally:
            os.unlink(path)

    def test_propagated_rolebinding_with_yaml_role(self, user1, hierarchy):
        """Propagated RoleBinding with YAML role propagates permissions to descendants."""
        from topdownrbac.models import RoleBinding

        obj1, obj2 = hierarchy["obj1"], hierarchy["obj2"]
        yaml_content = """
roles:
  propagated_role:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content)
        try:
            service = RoleSyncService(path)
            service.sync()

            role = Role.objects.get(name="propagated_role")
            RoleBinding.objects.create(
                name="rb1",
                subject=user1,
                role=role,
                restricted_object=obj1,
                propagate=True,
            )

            assert_has_view(user1, obj1)
            assert_has_view(user1, obj2)
        finally:
            os.unlink(path)

        yaml_content_v2 = """
roles:
  propagated_role:
    permissions:
      - view_restrictedobject
      - change_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v2)
        try:
            service = RoleSyncService(path)
            service.sync()

            assert_has_view(user1, obj1)
            assert_has_view(user1, obj2)
            assert_has_change(user1, obj1)
            assert_has_change(user1, obj2)
        finally:
            os.unlink(path)

    def test_group_members_affected_by_yaml_change(self, user1, user2, hierarchy):
        """Group members with RoleBinding to YAML role are affected by permission changes."""
        from topdownrbac.models import RoleBinding, UserGroupSubject

        obj1 = hierarchy["obj1"]
        group = UserGroupSubject.objects.create()
        group.add_user(user1)
        group.add_user(user2)

        yaml_content_v1 = """
roles:
  group_role:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v1)
        try:
            service = RoleSyncService(path)
            service.sync()

            role = Role.objects.get(name="group_role")
            RoleBinding.objects.create(name="group_rb", subject=group, role=role, restricted_object=obj1)

            assert_has_view(user1, obj1)
            assert_has_view(user2, obj1)
            assert_no_change(user1, obj1)
            assert_no_change(user2, obj1)
        finally:
            os.unlink(path)

        yaml_content_v2 = """
roles:
  group_role:
    permissions:
      - view_restrictedobject
      - change_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v2)
        try:
            service = RoleSyncService(path)
            service.sync()

            assert_has_view(user1, obj1)
            assert_has_view(user2, obj1)
            assert_has_change(user1, obj1)
            assert_has_change(user2, obj1)
        finally:
            os.unlink(path)

    def test_deactivation_suspends_user_permissions(self, user1, hierarchy):
        """Deactivating a role via YAML removal suspends all user permissions from its bindings."""
        from topdownrbac.models import RoleBinding

        obj1 = hierarchy["obj1"]
        yaml_content_v1 = """
roles:
  suspendable_role:
    permissions:
      - view_restrictedobject
      - change_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v1)
        try:
            service = RoleSyncService(path)
            service.sync()

            role = Role.objects.get(name="suspendable_role")
            RoleBinding.objects.create(name="rb1", subject=user1, role=role, restricted_object=obj1)

            assert_has_view(user1, obj1)
            assert_has_change(user1, obj1)
        finally:
            os.unlink(path)

        # remove role from yaml -> deactivation
        yaml_content_v2 = """
roles: {}
"""
        path = _create_yaml_file(yaml_content_v2)
        try:
            service = RoleSyncService(path)
            service.sync()

            # permissions suspended
            assert_no_view(user1, obj1)
            assert_no_change(user1, obj1)

            # but role and binding still exist
            role = Role.objects.get(name="suspendable_role")
            assert role.is_active is False
            assert role.rolebindings.count() == 1
        finally:
            os.unlink(path)

    def test_reactivation_restores_user_permissions(self, user1, hierarchy):
        """Re-adding a deactivated role to YAML restores all user permissions."""
        from topdownrbac.models import RoleBinding

        obj1 = hierarchy["obj1"]
        yaml_content = """
roles:
  toggle_role:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content)
        try:
            service = RoleSyncService(path)
            service.sync()

            role = Role.objects.get(name="toggle_role")
            RoleBinding.objects.create(name="rb1", subject=user1, role=role, restricted_object=obj1)
            assert_has_view(user1, obj1)
        finally:
            os.unlink(path)

        # deactivate
        path = _create_yaml_file("roles: {}")
        try:
            RoleSyncService(path).sync()
            assert_no_view(user1, obj1)
        finally:
            os.unlink(path)

        # reactivate
        path = _create_yaml_file(yaml_content)
        try:
            service = RoleSyncService(path)
            deactivated, updates = service.sync()

            assert_has_view(user1, obj1)
            assert len(deactivated) == 0
            # reactivated role appears in updates
            assert any(u[4] for u in updates)  # reactivated flag
        finally:
            os.unlink(path)

    def test_cannot_create_rolebinding_with_deactivated_role(self, user1, hierarchy):
        """Creating a RoleBinding with a deactivated role raises PermissionDenied."""
        from django.core.exceptions import PermissionDenied

        from topdownrbac.models import RoleBinding

        obj1 = hierarchy["obj1"]
        yaml_content = """
roles:
  temp_role:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content)
        try:
            RoleSyncService(path).sync()
        finally:
            os.unlink(path)

        # deactivate
        path = _create_yaml_file("roles: {}")
        try:
            RoleSyncService(path).sync()
        finally:
            os.unlink(path)

        role = Role.objects.get(name="temp_role")
        with pytest.raises(PermissionDenied):
            RoleBinding.objects.create(name="rb1", subject=user1, role=role, restricted_object=obj1)


@pytest.mark.django_db
class TestImportRolesManagementCommand:
    """Test the import_roles management command."""

    def test_command_runs_successfully(self, db):
        """Management command runs and outputs success message."""
        yaml_content = """
roles:
  cmd_test_role:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content)
        try:
            out = StringIO()
            call_command("import_roles", path, stdout=out)

            output = out.getvalue()
            assert "synced successfully" in output.lower()
            assert Role.objects.filter(name="cmd_test_role").exists()
        finally:
            os.unlink(path)

    def test_command_reports_created_roles(self, db):
        """Management command reports when roles are created."""
        yaml_content = """
roles:
  new_role:
    permissions:
      - view_restrictedobject
      - change_restrictedobject
"""
        path = _create_yaml_file(yaml_content)
        try:
            out = StringIO()
            call_command("import_roles", path, stdout=out)

            output = out.getvalue()
            assert "Created role new_role" in output
        finally:
            os.unlink(path)

    def test_command_reports_deactivated_roles(self, db):
        """Management command reports when roles are deactivated."""
        yaml_content_v1 = """
roles:
  to_be_deactivated:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v1)
        try:
            call_command("import_roles", path, stdout=StringIO())
        finally:
            os.unlink(path)

        yaml_content_v2 = """
roles: {}
"""
        path = _create_yaml_file(yaml_content_v2)
        try:
            out = StringIO()
            call_command("import_roles", path, stdout=out)

            output = out.getvalue()
            assert "Deactivated" in output
            assert "to_be_deactivated" in output
        finally:
            os.unlink(path)

    def test_command_reports_updated_roles(self, db):
        """Management command reports when existing roles have permissions changed."""
        yaml_content_v1 = """
roles:
  updatable_role:
    permissions:
      - view_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v1)
        try:
            call_command("import_roles", path, stdout=StringIO())
        finally:
            os.unlink(path)

        yaml_content_v2 = """
roles:
  updatable_role:
    permissions:
      - change_restrictedobject
      - delete_restrictedobject
"""
        path = _create_yaml_file(yaml_content_v2)
        try:
            out = StringIO()
            call_command("import_roles", path, stdout=out)

            output = out.getvalue()
            assert "Updated role updatable_role" in output
            assert "+ change_restrictedobject" in output
            assert "+ delete_restrictedobject" in output
            assert "- view_restrictedobject" in output
        finally:
            os.unlink(path)
