"""Completion-audit regressions for authorization-path mixing and queryset leaks."""
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.db import transaction
from django.test import TestCase

from apps.organization.models import Company, Region, ServiceCenter, Department, UserOrganizationAssignment
from apps.organization.assignment_services import create_assignment
from . import test_authorization as fixtures
from .authorization import authorized_queryset, is_authorized
from .models import Role, UserRoleAssignment
from .services import create_role_assignment


class AuthorizationAuditTests(TestCase):
    setUpTestData = classmethod(fixtures.AuthorizationTests.setUpTestData.__func__)
    grant = fixtures.AuthorizationTests.grant
    allowed = fixtures.AuthorizationTests.allowed
    models = (Company, Region, ServiceCenter, Department)

    def visible(self, model, permission=None, user=None):
        return set(authorized_queryset(user=user or self.user, permission=permission or self.permission,
                                       queryset=model.objects.all()))

    def assert_no_objects(self, permission=None, user=None):
        for model in self.models:
            with self.subTest(model=model.__name__):
                self.assertEqual(self.visible(model, permission, user), set())

    def test_reverse_and_comparable_scope_permission_mixing(self):
        cases = [
            ({"region": self.region, "service_center": self.center}, self.center,
             {"region": self.region, "service_center": self.sibling_center}, self.sibling_center),
            ({"region": self.region}, self.region, {"region": self.sibling_region}, self.sibling_region),
            ({"department": self.department}, self.department, {"department": self.other_department}, self.other_department),
            ({"region": self.region, "service_center": self.center, "department": self.department}, self.center,
             {"region": self.region, "service_center": self.sibling_center, "department": self.other_department}, self.sibling_center),
        ]
        for scope_a, target_a, scope_b, target_b in cases:
            for reverse in [False, True]:
                with self.subTest(scope=list(scope_a), reverse=reverse), transaction.atomic():
                    self.grant(role=self.role if reverse else self.change_role, **scope_a)
                    self.grant(role=self.change_role if reverse else self.role, **scope_b)
                    allowed, denied = (target_b, target_a) if reverse else (target_a, target_b)
                    permission = "organization.change_servicecenter"
                    self.assertTrue(self.allowed(allowed, permission=permission))
                    self.assertFalse(self.allowed(denied, permission=permission))
                    self.assertIn(allowed, self.visible(type(allowed), permission))
                    self.assertNotIn(denied, self.visible(type(denied), permission))
                    transaction.set_rollback(True)

    def test_cross_company_permission_scope_mixing_in_both_directions(self):
        for reverse in [False, True]:
            with self.subTest(reverse=reverse), transaction.atomic():
                self.grant(role=self.role if reverse else self.change_role)
                self.grant(company=self.foreign_company, role=self.change_role if reverse else self.role)
                permitted_company = self.foreign_company if reverse else self.company
                denied_company = self.company if reverse else self.foreign_company
                permission = "organization.change_servicecenter"
                for model in self.models:
                    expected = set(model.objects.filter(pk=permitted_company.pk) if model is Company
                                   else model.objects.filter(company=permitted_company))
                    denied = model.objects.filter(pk=denied_company.pk) if model is Company else model.objects.filter(company=denied_company)
                    self.assertEqual(self.visible(model, permission), expected)
                    for obj in denied:
                        self.assertFalse(self.allowed(obj, permission=permission))
                transaction.set_rollback(True)

    def check_inactive_mixing(self, layer):
        source, path = self.grant(role=self.change_role)
        self.grant(region=self.region, service_center=self.center)
        parent = {"role": self.change_role, "role_assignment": path, "organization_assignment": source}[layer]
        # Bypass cascades deliberately: the evaluator must check the path itself.
        type(parent).objects.filter(pk=parent.pk).update(is_active=False)
        self.assertTrue(self.allowed(self.center))
        self.assertFalse(self.allowed(self.center, permission="organization.change_servicecenter"))
        self.assert_no_objects(permission="organization.change_servicecenter")

    def test_inactive_role_cannot_supply_permission_to_active_scope(self):
        self.check_inactive_mixing("role")

    def test_inactive_role_assignment_cannot_supply_permission_to_active_scope(self):
        self.check_inactive_mixing("role_assignment")

    def test_inactive_organization_assignment_cannot_supply_permission_to_active_scope(self):
        self.check_inactive_mixing("organization_assignment")

    def test_queryset_multiple_roles_assignments_and_companies(self):
        source, _ = self.grant(region=self.region)
        create_role_assignment(user=self.user, role=self.change_role, organization_assignment=source)
        self.grant(department=self.department)
        self.grant(company=self.foreign_company)
        expected = {
            Company: {self.foreign_company},
            Region: {self.region, self.foreign_region},
            ServiceCenter: {self.center, self.sibling_center, self.foreign_center},
            Department: {self.department, self.foreign_department},
        }
        for model in self.models:
            self.assertEqual(self.visible(model), expected[model])
            for obj in model.objects.all():
                self.assertEqual(self.allowed(obj), obj in expected[model])

    def test_staff_direct_and_group_permissions_independently_grant_no_scope(self):
        for kind in ["staff", "direct", "group"]:
            with self.subTest(kind=kind), transaction.atomic():
                user = get_user_model().objects.get(pk=self.user.pk)
                if kind == "staff":
                    user.is_staff = True
                    user.save(update_fields=["is_staff"])
                elif kind == "direct":
                    user.user_permissions.add(self.view)
                else:
                    group = Group.objects.create(name="Audit native group")
                    group.permissions.add(self.view)
                    user.groups.add(group)
                if kind != "staff":
                    self.assertTrue(user.has_perm(self.permission))
                self.assert_no_objects(user=user)
                self.assertFalse(self.allowed(self.company, user=user))
                create_assignment(user=user, company=self.company)
                self.assert_no_objects(user=user)
                self.assertFalse(self.allowed(self.center, user=user))
                transaction.set_rollback(True)

    def test_queryset_user_states_and_deny_by_default_all_models(self):
        self.assert_no_objects()
        self.grant()
        for user in [self.user, self.superuser]:
            with transaction.atomic():
                if user is self.superuser:
                    for model in self.models:
                        self.assertEqual(self.visible(model, user=user), set(model.objects.all()))
                get_user_model().objects.filter(pk=user.pk).update(is_active=False)
                self.assert_no_objects(user=user)
                transaction.set_rollback(True)

    def test_queryset_malformed_unknown_permission_denies_all_models(self):
        self.grant()
        for permission in ["organization", "organization..view_company", "organization.missing", "missing.view_company"]:
            for user in [self.user, self.superuser]:
                with self.subTest(permission=permission, superuser=user.is_superuser):
                    self.assert_no_objects(permission=permission, user=user)
