import uuid
from unittest.mock import patch

from django.contrib import admin
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction
from django.db.models.deletion import ProtectedError
from django.test import TestCase
from django.urls import reverse

from .assignments import UserOrganizationAssignment as Assignment
from .assignment_services import create_assignment, deactivate_assignment, reactivate_assignment, set_primary_assignment
from .models import Company, Region, ServiceCenter, Department
from . import services
from .test_lifecycle import make_tree


class AssignmentTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.user = get_user_model().objects.create_user(username="assigned")
        cls.other_user = get_user_model().objects.create_user(username="other-assigned")
        cls.company, cls.region, cls.center, cls.department = make_tree("CCARE")
        cls.other_company, cls.other_region, cls.other_center, cls.other_department = make_tree("OTHER")
        cls.destination = Region.objects.create(company=cls.company, code="DEST", name="Destination")

    def assignment(self, **kwargs):
        values = dict(user=self.user, company=self.company)
        values.update(kwargs)
        return create_assignment(**values)

    def assert_db_rejects(self, operation):
        with self.assertRaises(IntegrityError), transaction.atomic():
            operation()

    def test_basics_uuid_timestamps_relationships_defaults(self):
        obj = self.assignment()
        obj.refresh_from_db()
        self.assertIsInstance(obj.pk, uuid.UUID)
        self.assertIsNotNone(obj.created_at)
        self.assertIsNotNone(obj.updated_at)
        self.assertEqual(obj.user, self.user)
        self.assertEqual(obj.company, self.company)
        self.assertIsNone(obj.region)
        self.assertIsNone(obj.service_center)
        self.assertIsNone(obj.department)
        self.assertTrue(obj.is_active)
        self.assertFalse(obj.is_primary)

    def test_optional_scopes_and_multiple_assignments(self):
        scopes = [{}, {"region": self.region}, {"department": self.department},
                  {"region": self.region, "department": self.department},
                  {"region": self.region, "service_center": self.center},
                  {"region": self.region, "service_center": self.center, "department": self.department}]
        for scope in scopes:
            self.assignment(**scope)
        self.assertEqual(Assignment.objects.active().for_user(self.user).count(), 6)

    def test_company_required(self):
        with self.assertRaises(ValidationError):
            self.assignment(company=None)

    def test_cross_company_region_rejected(self):
        with self.assertRaises(ValidationError):
            self.assignment(region=self.other_region)

    def test_cross_company_center_rejected(self):
        with self.assertRaises(ValidationError):
            self.assignment(region=self.region, service_center=self.other_center)

    def test_cross_company_department_rejected(self):
        with self.assertRaises(ValidationError):
            self.assignment(department=self.other_department)

    def test_center_requires_explicit_region(self):
        with self.assertRaises(ValidationError):
            self.assignment(service_center=self.center)
        self.assert_db_rejects(lambda: Assignment.objects.bulk_create([
            Assignment(user=self.user, company=self.company, service_center=self.center)]))

    def test_center_region_mismatch_rejected(self):
        with self.assertRaises(ValidationError):
            self.assignment(region=self.destination, service_center=self.center)

    def test_center_parent_company_checked_despite_bulk_corruption(self):
        Region.objects.filter(pk=self.region.pk).update(company=self.other_company, code="MOVED")
        with self.assertRaises(ValidationError):
            self.assignment(region=self.region, service_center=self.center)

    def test_active_assignment_inactive_company_rejected(self):
        services.deactivate_company(company=self.company)
        with self.assertRaises(ValidationError):
            self.assignment()

    def test_active_assignment_inactive_region_rejected(self):
        services.deactivate_region(region=self.region)
        with self.assertRaises(ValidationError):
            self.assignment(region=self.region)

    def test_active_assignment_inactive_center_rejected(self):
        services.deactivate_service_center(service_center=self.center)
        with self.assertRaises(ValidationError):
            self.assignment(region=self.region, service_center=self.center)

    def test_active_assignment_inactive_department_rejected(self):
        services.deactivate_department(department=self.department)
        with self.assertRaises(ValidationError):
            self.assignment(department=self.department)

    def test_inactive_history_allows_inactive_units(self):
        services.deactivate_company(company=self.company)
        obj = self.assignment(region=self.region, service_center=self.center, department=self.department, is_active=False)
        self.assertFalse(obj.is_active)

    def test_primary_must_be_active_model_and_database(self):
        with self.assertRaises(ValidationError):
            self.assignment(is_primary=True, is_active=False)
        self.assert_db_rejects(lambda: Assignment.objects.bulk_create([
            Assignment(user=self.user, company=self.company, is_primary=True, is_active=False)]))

    def test_one_primary_per_user_across_companies(self):
        self.assignment(is_primary=True)
        with self.assertRaises(ValidationError):
            self.assignment(company=self.other_company, is_primary=True)
        self.assert_db_rejects(lambda: Assignment.objects.bulk_create([
            Assignment(user=self.user, company=self.other_company, is_primary=True)]))

    def test_different_users_can_have_primaries(self):
        self.assignment(is_primary=True)
        self.assignment(user=self.other_user, is_primary=True)
        self.assertEqual(Assignment.objects.filter(is_primary=True).count(), 2)

    def test_safe_primary_switch_across_companies(self):
        first = self.assignment(is_primary=True)
        second = self.assignment(company=self.other_company)
        result = set_primary_assignment(assignment=second)
        first.refresh_from_db()
        self.assertFalse(first.is_primary)
        self.assertTrue(first.is_active)
        self.assertTrue(result.is_primary)

    def test_failed_primary_switch_rolls_back_demotion(self):
        first = self.assignment(is_primary=True)
        second = self.assignment(region=self.region, is_active=False)
        with self.assertRaises(ValidationError):
            set_primary_assignment(assignment=second)
        first.refresh_from_db()
        self.assertTrue(first.is_primary)

    def test_duplicate_active_nullable_scopes_model_and_database(self):
        scopes = [{}, {"region": self.region}, {"department": self.department},
                  {"region": self.region, "department": self.department},
                  {"region": self.region, "service_center": self.center},
                  {"region": self.region, "service_center": self.center, "department": self.department}]
        for scope in scopes:
            with self.subTest(scope=list(scope)):
                self.assignment(**scope)
                with self.assertRaises(ValidationError):
                    self.assignment(**scope)
                self.assert_db_rejects(lambda: Assignment.objects.bulk_create([
                    Assignment(user=self.user, company=self.company, **scope)]))

    def test_different_centers_and_departments_allowed(self):
        center = ServiceCenter.objects.create(company=self.company, region=self.region, code="TWO", name="Two", center_type="OWN")
        department = Department.objects.create(company=self.company, code="TWO", name="Two")
        self.assignment(region=self.region, service_center=self.center, department=self.department)
        self.assignment(region=self.region, service_center=center, department=self.department)
        self.assignment(region=self.region, service_center=self.center, department=department)
        self.assertEqual(Assignment.objects.count(), 3)

    def test_inactive_duplicate_history_allowed(self):
        active = self.assignment()
        self.assignment(is_active=False)
        self.assignment(is_active=False)
        inactive = self.assignment(is_active=False)
        with self.assertRaises(ValidationError):
            reactivate_assignment(assignment=inactive)
        self.assertTrue(Assignment.objects.get(pk=active.pk).is_active)

    def test_deactivation_clears_primary_no_replacement(self):
        first = self.assignment(is_primary=True)
        second = self.assignment(region=self.region)
        result = deactivate_assignment(assignment=first)
        self.assertFalse(result.is_active)
        self.assertFalse(result.is_primary)
        second.refresh_from_db()
        self.assertFalse(second.is_primary)
        self.assertEqual(Assignment.objects.count(), 2)

    def test_explicit_reactivation_nonprimary(self):
        obj = self.assignment(is_primary=True)
        obj = deactivate_assignment(assignment=obj)
        obj = reactivate_assignment(assignment=obj)
        self.assertTrue(obj.is_active)
        self.assertFalse(obj.is_primary)

    def test_user_deactivation_leaves_history_and_organization_unchanged(self):
        obj = self.assignment(is_primary=True)
        self.user.is_active = False
        self.user.save(update_fields=["is_active"])
        obj.refresh_from_db()
        self.company.refresh_from_db()
        self.assertTrue(obj.is_active)
        self.assertTrue(self.company.is_active)

    def test_user_transfer_rejected(self):
        obj = self.assignment()
        obj.user = self.other_user
        with self.assertRaises(ValidationError):
            obj.save()

    def test_partial_write_checks_persisted_scope(self):
        obj = self.assignment(region=self.region, service_center=self.center)
        obj.region = self.destination
        obj.service_center = None  # Not persisted in this partial save.
        with self.assertRaises(ValidationError):
            obj.save(update_fields=["region"])
        obj.refresh_from_db()
        self.assertEqual(obj.region, self.region)

    def test_partial_deactivation_cannot_leave_primary(self):
        obj = self.assignment(is_primary=True)
        obj.is_primary = False  # Not part of this partial write.
        obj.is_active = False
        with self.assertRaises(ValidationError):
            obj.save(update_fields=["is_active"])

    def test_parent_deletion_protected_by_history(self):
        obj = self.assignment(region=self.region, service_center=self.center, department=self.department, is_active=False)
        for parent in [self.user, self.company, self.region, self.center, self.department]:
            with self.subTest(model=type(parent).__name__), self.assertRaises(ProtectedError):
                parent.delete()
        self.assertTrue(Assignment.objects.filter(pk=obj.pk).exists())

    def test_query_helpers(self):
        self.assignment()
        self.assignment(region=self.region, is_active=False)
        self.assignment(user=self.other_user)
        self.assertEqual(Assignment.objects.for_user(self.user).count(), 2)
        self.assertEqual(Assignment.objects.for_user(self.user).active().count(), 1)

    def test_representation_does_not_query(self):
        obj = self.assignment()
        with self.assertNumQueries(0):
            self.assertIn(str(obj.pk), str(obj))


class AssignmentLifecycleTests(TestCase):
    setUpTestData = classmethod(AssignmentTests.setUpTestData.__func__)
    assignment = AssignmentTests.assignment

    def test_company_cascade_and_reactivation_preserve_ended_assignments(self):
        obj = self.assignment(region=self.region, service_center=self.center, department=self.department, is_primary=True)
        unrelated = self.assignment(company=self.other_company)
        services.deactivate_company(company=self.company)
        obj.refresh_from_db()
        unrelated.refresh_from_db()
        self.assertFalse(obj.is_active)
        self.assertFalse(obj.is_primary)
        self.assertTrue(unrelated.is_active)
        services.reactivate_company(company=self.company)
        services.reactivate_region(region=self.region)
        services.reactivate_service_center(service_center=self.center)
        services.reactivate_department(department=self.department)
        obj.refresh_from_db()
        self.assertFalse(obj.is_active)

    def test_region_cascade_only_related_assignments(self):
        region_only = self.assignment(region=self.region)
        center = self.assignment(region=self.region, service_center=self.center, is_primary=True)
        company_only = self.assignment()
        department = self.assignment(department=self.department)
        sibling = self.assignment(region=self.destination)
        services.deactivate_region(region=self.region)
        for obj in [region_only, center]:
            obj.refresh_from_db()
            self.assertFalse(obj.is_active)
            self.assertFalse(obj.is_primary)
        for obj in [company_only, department, sibling]:
            obj.refresh_from_db()
            self.assertTrue(obj.is_active)

    def test_leaf_cascades_are_scoped(self):
        center = self.assignment(region=self.region, service_center=self.center)
        department = self.assignment(department=self.department, is_primary=True)
        region = self.assignment(region=self.region)
        services.deactivate_service_center(service_center=self.center)
        center.refresh_from_db()
        department.refresh_from_db()
        self.assertFalse(center.is_active)
        self.assertTrue(department.is_active)
        services.deactivate_department(department=self.department)
        department.refresh_from_db()
        region.refresh_from_db()
        self.assertFalse(department.is_active)
        self.assertFalse(department.is_primary)
        self.assertTrue(region.is_active)

    def test_direct_deactivation_cannot_leave_active_assignments(self):
        for parent, scope in [(self.company, {}), (self.region, {"region": self.region}),
                              (self.center, {"region": self.region, "service_center": self.center}),
                              (self.department, {"department": self.department})]:
            self.assignment(**scope)
            parent.is_active = False
            with self.subTest(model=type(parent).__name__), self.assertRaises(ValidationError):
                parent.save()

    def test_company_cascade_rolls_back_assignments_with_organization(self):
        obj = self.assignment(is_primary=True)
        before = obj.updated_at
        with patch.object(Company, "save", side_effect=RuntimeError("simulated failure")):
            with self.assertRaises(RuntimeError):
                services.deactivate_company(company=self.company)
        obj.refresh_from_db()
        self.center.refresh_from_db()
        self.assertTrue(obj.is_active)
        self.assertTrue(obj.is_primary)
        self.assertEqual(obj.updated_at, before)
        self.assertTrue(self.center.is_active)

    def test_region_and_leaf_cascades_roll_back_assignments(self):
        obj = self.assignment(region=self.region, service_center=self.center, department=self.department, is_primary=True)
        for model, operation, kwargs in [
            (Region, services.deactivate_region, {"region": self.region}),
            (ServiceCenter, services.deactivate_service_center, {"service_center": self.center}),
            (Department, services.deactivate_department, {"department": self.department}),
        ]:
            with patch.object(model, "save", side_effect=RuntimeError("simulated failure")):
                with self.assertRaises(RuntimeError):
                    operation(**kwargs)
            obj.refresh_from_db()
            self.assertTrue(obj.is_active)
            self.assertTrue(obj.is_primary)

    def test_center_move_with_active_or_historical_assignment_rejected(self):
        obj = self.assignment(region=self.region, service_center=self.center)
        for inactive in [False, True]:
            if inactive:
                deactivate_assignment(assignment=obj)
            with self.assertRaises(ValidationError):
                services.move_service_center(service_center=self.center, destination_region=self.destination)
        self.center.refresh_from_db()
        self.assertEqual(self.center.region, self.region)


class AssignmentAdminTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.user = get_user_model().objects.create_superuser(username="assignment-admin", password="test-only-password")
        cls.company, cls.region, cls.center, cls.department = make_tree("CCARE")
        cls.other_company, cls.other_region, _, _ = make_tree("OTHER")

    def setUp(self):
        self.client.force_login(self.user)

    def test_registration_and_list_page(self):
        self.assertIn(Assignment, admin.site._registry)
        create_assignment(user=self.user, company=self.company)
        self.assertEqual(self.client.get(reverse("admin:organization_userorganizationassignment_changelist")).status_code, 200)

    def test_admin_rejects_cross_company_scope(self):
        response = self.client.post(reverse("admin:organization_userorganizationassignment_add"), {
            "user": self.user.pk, "company": self.company.pk, "region": self.other_region.pk,
            "is_active": "on", "_save": "Save",
        })
        self.assertContains(response, "must belong to the assignment company")
        self.assertEqual(Assignment.objects.count(), 0)

    def test_admin_creates_valid_scope(self):
        response = self.client.post(reverse("admin:organization_userorganizationassignment_add"), {
            "user": self.user.pk, "company": self.company.pk, "region": self.region.pk,
            "service_center": self.center.pk, "department": self.department.pk,
            "is_active": "on", "_save": "Save",
        })
        self.assertEqual(response.status_code, 302)
        self.assertEqual(Assignment.objects.count(), 1)

    def test_admin_primary_switch_and_deactivation(self):
        first = create_assignment(user=self.user, company=self.company, is_primary=True)
        second = create_assignment(user=self.user, company=self.company, region=self.region)
        url = reverse("admin:organization_userorganizationassignment_changelist")
        response = self.client.post(url, {"action": "make_primary", "_selected_action": [str(second.pk)]})
        self.assertEqual(response.status_code, 302)
        first.refresh_from_db()
        second.refresh_from_db()
        self.assertFalse(first.is_primary)
        self.assertTrue(second.is_primary)
        self.client.post(url, {"action": "deactivate_selected", "_selected_action": [str(second.pk)]})
        second.refresh_from_db()
        self.assertFalse(second.is_primary)
        self.assertFalse(second.is_active)

    def test_admin_cannot_delete_history(self):
        obj = create_assignment(user=self.user, company=self.company)
        response = self.client.post(reverse("admin:organization_userorganizationassignment_delete", args=[obj.pk]), {"post": "yes"})
        self.assertEqual(response.status_code, 403)
        self.assertTrue(Assignment.objects.filter(pk=obj.pk).exists())
