from concurrent.futures import ThreadPoolExecutor
from threading import Event
from time import monotonic, sleep
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 connection, connections, transaction
from django.test import TestCase, TransactionTestCase
from django.urls import reverse

from .models import Company, Department, Region, ServiceCenter
from . import services


def make_tree(code):
    company = Company.objects.create(code=code, name=code)
    region = Region.objects.create(company=company, code="REGION", name="Region")
    center = ServiceCenter.objects.create(company=company, region=region, code="CENTER", name="Center", center_type="OWN")
    department = Department.objects.create(company=company, code="DEPT", name="Department")
    return company, region, center, department


class LifecycleTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.company, cls.region, cls.center, cls.department = make_tree("CCARE")
        cls.other_tree = make_tree("OTHER")
        cls.destination = Region.objects.create(company=cls.company, code="DEST", name="Destination")

    def assert_active(self, objects, active):
        for obj in objects:
            obj.refresh_from_db()
            self.assertEqual(obj.is_active, active)

    def test_active_children_rejected_under_inactive_company(self):
        services.deactivate_company(company=self.company)
        for model, kwargs in [(Region, {}), (Department, {}), (ServiceCenter, {"region": self.region, "center_type": "OWN"})]:
            with self.subTest(model=model.__name__), self.assertRaises(ValidationError):
                model.objects.create(company=self.company, code="NEW", name="New", **kwargs)

    def test_inactive_children_allowed_under_inactive_parents(self):
        services.deactivate_company(company=self.company)
        for model, kwargs in [(Region, {}), (Department, {}), (ServiceCenter, {"region": self.region, "center_type": "OWN"})]:
            obj = model.objects.create(company=self.company, code="NEW", name="New", is_active=False, **kwargs)
            self.assertFalse(obj.is_active)

    def test_active_center_rejected_under_inactive_region(self):
        services.deactivate_region(region=self.region)
        with self.assertRaises(ValidationError):
            ServiceCenter.objects.create(company=self.company, region=self.region, code="NEW", name="New", center_type="OWN")

    def test_company_deactivation_cascades_only_its_tree(self):
        services.deactivate_company(company=self.company)
        self.assert_active([self.company, self.region, self.center, self.department, self.destination], False)
        self.assert_active(self.other_tree, True)

    def test_region_deactivation_is_scoped(self):
        services.deactivate_region(region=self.region)
        self.assert_active([self.region, self.center], False)
        self.assert_active([self.company, self.department, self.destination, *self.other_tree], True)

    def test_leaf_deactivations_are_local(self):
        services.deactivate_department(department=self.department)
        self.assert_active([self.department], False)
        self.assert_active([self.company, self.region, self.center], True)
        services.deactivate_service_center(service_center=self.center)
        self.assert_active([self.center], False)
        self.assert_active([self.company, self.region], True)

    def test_direct_parent_deactivation_rejects_without_cascade(self):
        for parent in [self.company, self.region]:
            parent.is_active = False
            with self.assertRaises(ValidationError):
                parent.save(update_fields=["is_active"])
        self.assert_active([self.company, self.region, self.center, self.department], True)

    def test_direct_parent_deactivation_without_active_children_allowed(self):
        empty = Company.objects.create(code="EMPTY", name="Empty")
        empty.is_active = False
        empty.save()
        self.assert_active([empty], False)

    def test_company_reactivation_does_not_reopen_children(self):
        services.deactivate_company(company=self.company)
        services.reactivate_company(company=self.company)
        self.assert_active([self.company], True)
        self.assert_active([self.region, self.center, self.department, self.destination], False)

    def test_children_cannot_reactivate_under_inactive_company(self):
        services.deactivate_company(company=self.company)
        for operation, kwargs in [(services.reactivate_region, {"region": self.region}),
                                  (services.reactivate_department, {"department": self.department}),
                                  (services.reactivate_service_center, {"service_center": self.center})]:
            with self.subTest(operation=operation.__name__), self.assertRaises(ValidationError):
                operation(**kwargs)

    def test_deliberate_top_down_reactivation(self):
        services.deactivate_company(company=self.company)
        services.reactivate_company(company=self.company)
        with self.assertRaises(ValidationError):
            services.reactivate_service_center(service_center=self.center)
        services.reactivate_region(region=self.region)
        self.assert_active([self.center], False)
        services.reactivate_service_center(service_center=self.center)
        services.reactivate_department(department=self.department)
        self.assert_active([self.company, self.region, self.center, self.department], True)
        self.assert_active([self.destination], False)

    def test_same_company_region_move_preserves_code_and_company(self):
        moved = services.move_service_center(service_center=self.center, destination_region=self.destination)
        self.assertEqual(moved.region_id, self.destination.pk)
        self.assertEqual(moved.company_id, self.company.pk)
        self.assertEqual(moved.code, "CENTER")

    def test_active_center_cannot_move_to_inactive_region(self):
        services.deactivate_region(region=self.destination)
        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_id, self.region.pk)

    def test_inactive_center_can_move_to_inactive_region(self):
        services.deactivate_region(region=self.destination)
        services.deactivate_service_center(service_center=self.center)
        moved = services.move_service_center(service_center=self.center, destination_region=self.destination)
        self.assertEqual(moved.region_id, self.destination.pk)
        self.assertFalse(moved.is_active)

    def test_cross_company_move_rejected_even_when_inactive(self):
        services.deactivate_service_center(service_center=self.center)
        with self.assertRaises(ValidationError):
            services.move_service_center(service_center=self.center, destination_region=self.other_tree[1])

    def test_all_child_company_ownership_stable(self):
        for obj in [self.region, self.department, self.center]:
            obj.company = self.other_tree[0]
            if isinstance(obj, ServiceCenter):
                obj.region = self.other_tree[1]
            with self.subTest(model=type(obj).__name__), self.assertRaises(ValidationError):
                obj.save()

    def test_code_corrections_allowed_and_normalized(self):
        for obj in [self.company, self.region, self.center, self.department]:
            obj.code = " corrected "
            obj.save(update_fields=["code"])
            obj.refresh_from_db()
            self.assertEqual(obj.code, "CORRECTED")

    def test_partial_save_validates_persisted_active_state(self):
        services.deactivate_region(region=self.destination)
        self.center.is_active = False  # Not included in the write.
        self.center.region = self.destination
        with self.assertRaises(ValidationError):
            self.center.save(update_fields=["region"])
        self.center.refresh_from_db()
        self.assertEqual(self.center.region_id, self.region.pk)
        self.assertTrue(self.center.is_active)

    def test_partial_reactivation_validates_persisted_region(self):
        services.deactivate_region(region=self.region)
        self.center.refresh_from_db()
        self.center.region = self.destination  # Not included in the write.
        self.center.is_active = True
        with self.assertRaises(ValidationError):
            self.center.save(update_fields=["is_active"])

    def test_stale_parent_cache_rejected(self):
        services.deactivate_region(region=self.region)
        with self.assertRaises(ValidationError):
            self.center.save()

    def test_services_use_fresh_state_without_overwriting_unrelated_fields(self):
        stale = ServiceCenter.objects.get(pk=self.center.pk)
        self.center.code = "CORRECTED"
        self.center.save()
        moved = services.move_service_center(service_center=stale, destination_region=self.destination)
        self.assertEqual(moved.code, "CORRECTED")

    def test_company_deactivation_rolls_back_on_error(self):
        before = self.center.updated_at
        with patch.object(Company, "save", side_effect=RuntimeError("simulated failure")):
            with self.assertRaises(RuntimeError):
                services.deactivate_company(company=self.company)
        self.assert_active([self.company, self.region, self.center, self.department], True)
        self.assertEqual(self.center.updated_at, before)

    def test_region_deactivation_rolls_back_on_error(self):
        with patch.object(Region, "save", side_effect=RuntimeError("simulated failure")):
            with self.assertRaises(RuntimeError):
                services.deactivate_region(region=self.region)
        self.assert_active([self.region, self.center], True)

    def test_cascade_updates_timestamps_and_retains_inactive_children(self):
        services.deactivate_service_center(service_center=self.center)
        self.center.refresh_from_db()
        center_timestamp = self.center.updated_at
        department_timestamp = self.department.updated_at
        services.deactivate_company(company=self.company)
        self.center.refresh_from_db()
        self.department.refresh_from_db()
        self.assertEqual(self.center.updated_at, center_timestamp)
        self.assertGreater(self.department.updated_at, department_timestamp)

    def test_repeated_deactivation_safe(self):
        services.deactivate_company(company=self.company)
        services.deactivate_company(company=self.company)
        self.assert_active([self.company, self.region, self.center, self.department], False)


class LifecycleAdminTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.company, cls.region, cls.center, cls.department = make_tree("CCARE")
        cls.user = get_user_model().objects.create_superuser(username="lifecycle-admin", password="test-only-password")

    def setUp(self):
        self.client.force_login(self.user)

    def test_company_action_uses_service(self):
        with patch.object(services, "deactivate_company", wraps=services.deactivate_company) as operation:
            response = self.client.post(reverse("admin:organization_company_changelist"), {
                "action": "deactivate_selected", "_selected_action": [str(self.company.pk)],
            })
        self.assertEqual(response.status_code, 302)
        operation.assert_called_once()
        self.center.refresh_from_db()
        self.assertFalse(self.center.is_active)

    def test_admin_reactivation_action_reports_invalid_parent(self):
        services.deactivate_company(company=self.company)
        response = self.client.post(reverse("admin:organization_region_changelist"), {
            "action": "reactivate_selected", "_selected_action": [str(self.region.pk)],
        }, follow=True)
        self.assertContains(response, "requires an active company")
        self.region.refresh_from_db()
        self.assertFalse(self.region.is_active)

    def test_admin_ownership_readonly_after_creation(self):
        from django.test import RequestFactory
        request = RequestFactory().get("/admin/")
        request.user = self.user
        for obj in [self.region, self.center, self.department]:
            registered = admin.site._registry[type(obj)]
            self.assertIn("company", registered.get_readonly_fields(request, obj))
            self.assertNotIn("company", registered.get_readonly_fields(request))

    def test_admin_rejects_active_child_under_inactive_parent(self):
        services.deactivate_company(company=self.company)
        response = self.client.post(reverse("admin:organization_region_add"), {
            "company": self.company.pk, "code": "NEW", "name": "New", "is_active": "on", "_save": "Save",
        })
        self.assertContains(response, "requires an active company")
        self.assertFalse(Region.objects.filter(code="NEW").exists())

    def test_admin_direct_deactivation_shows_service_instruction(self):
        response = self.client.post(reverse("admin:organization_company_change", args=[self.company.pk]), {
            "code": self.company.code, "name": self.company.name, "legal_name": "", "_save": "Save",
        })
        self.assertContains(response, "deactivation service/action")
        self.company.refresh_from_db()
        self.assertTrue(self.company.is_active)


class LifecycleConcurrencyTests(TransactionTestCase):
    def setUp(self):
        self.company, self.region, self.center, self.department = make_tree("CCARE")

    def assert_write_waits_then_rejects(self, lifecycle, write):
        locked, release, attempting = Event(), Event(), Event()
        backend_pid = []

        def owner():
            try:
                with transaction.atomic():
                    Company.objects.select_for_update().get(pk=self.company.pk)
                    locked.set()
                    if not release.wait(10):
                        raise AssertionError("Timed out releasing lifecycle transaction")
                    lifecycle()
            finally:
                connections.close_all()

        def contender():
            try:
                with connection.cursor() as cursor:
                    cursor.execute("SELECT pg_backend_pid()")
                    backend_pid.append(cursor.fetchone()[0])
                    cursor.execute("SET lock_timeout = '10s'")
                attempting.set()
                try:
                    write()
                except ValidationError:
                    return "rejected"
                return "unexpectedly allowed"
            finally:
                connections.close_all()

        with ThreadPoolExecutor(max_workers=2) as executor:
            first = executor.submit(owner)
            try:
                self.assertTrue(locked.wait(5))
                second = executor.submit(contender)
                self.assertTrue(attempting.wait(5))
                deadline = monotonic() + 5
                blocked = False
                while monotonic() < deadline:
                    with connection.cursor() as cursor:
                        cursor.execute("SELECT cardinality(pg_blocking_pids(%s)) > 0", [backend_pid[0]])
                        blocked = cursor.fetchone()[0]
                    if blocked:
                        break
                    sleep(0.02)
                self.assertTrue(blocked, "Expected a real PostgreSQL row-lock wait")
            finally:
                release.set()
            first.result(timeout=10)
            self.assertEqual(second.result(timeout=10), "rejected")

    def test_company_deactivation_serializes_active_child_creation(self):
        self.assert_write_waits_then_rejects(
            lambda: services.deactivate_company(company=self.company),
            lambda: Department.objects.create(company=self.company, code="NEW", name="New"),
        )
        self.assertFalse(Department.objects.filter(code="NEW").exists())

    def test_region_deactivation_serializes_center_reactivation(self):
        services.deactivate_service_center(service_center=self.center)
        self.assert_write_waits_then_rejects(
            lambda: services.deactivate_region(region=self.region),
            lambda: services.reactivate_service_center(service_center=self.center),
        )
        self.center.refresh_from_db()
        self.assertFalse(self.center.is_active)

    def test_destination_deactivation_serializes_center_move(self):
        destination = Region.objects.create(company=self.company, code="DEST", name="Destination")
        self.assert_write_waits_then_rejects(
            lambda: services.deactivate_region(region=destination),
            lambda: services.move_service_center(service_center=self.center, destination_region=destination),
        )
        self.center.refresh_from_db()
        self.assertEqual(self.center.region_id, self.region.pk)
