"""Real PostgreSQL waits and separate connections; no mocked race outcomes."""
from django.contrib.auth import get_user_model
from django.core.exceptions import ValidationError
from django.test import TransactionTestCase

from apps.access import services as access
from apps.organization import assignment_services as org_assignments, services as organization
from apps.organization import test_assignment_concurrency as concurrency
from . import engineer_queries as queries
from .models import ServiceEngineerAssignment
from .services import cancel_service_case
from .test_engineer_assignment import setup_engineers, assign, reassign, unassign, assert_invariant


class EngineerConcurrencyTests(TransactionTestCase):
    run_concurrent = concurrency.AssignmentConcurrencyTests.run_concurrent

    def setUp(self):
        setup_engineers(self)
        self.actor2 = get_user_model().objects.create_user(username="synthetic-second-actor")

    def race(self, first, second, expected="validation"):
        self.run_concurrent(first, second, expected=expected)
        assert_invariant(self)

    def cancel(self):
        return cancel_service_case(service_case=self.case, cancelled_by=self.actor2)

    def test_two_actors_different_engineers_same_case(self):
        self.race(lambda: assign(self), lambda: assign(self, engineer=self.engineer2, assigned_by=self.actor2))
        self.assertEqual(ServiceEngineerAssignment.objects.count(), 1)

    def test_two_actors_same_engineer_same_case(self):
        self.race(lambda: assign(self), lambda: assign(self, assigned_by=self.actor2))
        self.assertEqual(ServiceEngineerAssignment.objects.count(), 1)

    def test_assign_then_cancel(self):
        self.race(lambda: assign(self), self.cancel, "success")
        self.assertEqual(queries.engineer_assignment_history(self.case).count(), 1)

    def test_cancel_then_assign(self):
        self.race(self.cancel, lambda: assign(self))

    def test_reassign_then_stale_reassign(self):
        old = assign(self)
        self.race(lambda: reassign(self), lambda: reassign(self, new_engineer=self.engineer,
            reassigned_by=self.actor2, expected_current_assignment_id=old.pk))
        self.assertEqual(ServiceEngineerAssignment.objects.count(), 2)

    def test_reassign_then_cancel(self):
        assign(self)
        self.race(lambda: reassign(self), self.cancel, "success")

    def test_cancel_then_reassign(self):
        assign(self)
        self.race(self.cancel, lambda: reassign(self))

    def test_unassign_then_reassign(self):
        old = assign(self)
        self.race(lambda: unassign(self), lambda: reassign(self, expected_current_assignment_id=old.pk))

    def test_reassign_then_stale_unassign(self):
        old = assign(self)
        self.race(lambda: reassign(self), lambda: unassign(self, unassigned_by=self.actor2, expected_current_assignment_id=old.pk))

    def test_unassign_then_cancel(self):
        assign(self)
        self.race(lambda: unassign(self), self.cancel, "success")

    def test_user_deactivation_then_assignment(self):
        self.race(lambda: get_user_model().objects.filter(pk=self.engineer.pk).update(is_active=False), lambda: assign(self))

    def test_role_assignment_deactivation_then_assignment(self):
        self.race(lambda: access.deactivate_role_assignment(assignment=self.role_assignment), lambda: assign(self))

    def test_org_assignment_deactivation_then_assignment(self):
        self.race(lambda: org_assignments.deactivate_assignment(assignment=self.path), lambda: assign(self))

    def test_center_deactivation_then_assignment(self):
        self.race(lambda: organization.deactivate_service_center(service_center=self.center), lambda: assign(self))

    def test_company_deactivation_then_assignment(self):
        self.race(lambda: organization.deactivate_company(company=self.company), lambda: assign(self))

    def test_role_deactivation_then_assignment(self):
        self.race(lambda: access.deactivate_role(role=self.role), lambda: assign(self))

    def test_permission_revocation_then_assignment(self):
        self.race(lambda: access.set_role_permissions(role=self.role, permissions=[]), lambda: assign(self))

    def test_scope_move_then_assignment(self):
        def move():
            self.path.service_center = self.center2
            self.path.save()
        self.race(move, lambda: assign(self))

    def test_assignment_then_user_deactivation_preserves_history(self):
        self.race(lambda: assign(self), lambda: get_user_model().objects.filter(pk=self.engineer.pk).update(is_active=False), "success")
        self.assertIsNotNone(queries.current_engineer_assignment(self.case))
        self.assertFalse(queries.assigned_service_cases_for_engineer(self.engineer).exists())

    def test_assignment_then_role_revocation_preserves_history(self):
        self.race(lambda: assign(self), lambda: access.deactivate_role_assignment(assignment=self.role_assignment), "success")
        self.assertIsNotNone(queries.current_engineer_assignment(self.case))

    def test_assignment_then_company_deactivation_preserves_history(self):
        self.race(lambda: assign(self), lambda: organization.deactivate_company(company=self.company), "success")
        self.assertIsNotNone(queries.current_engineer_assignment(self.case))

    def test_assignment_then_permission_revocation_preserves_history(self):
        self.race(lambda: assign(self), lambda: access.set_role_permissions(role=self.role, permissions=[]), "success")
        self.assertIsNotNone(queries.current_engineer_assignment(self.case))

    def test_failed_reassignment_closure_rollback_then_unassign(self):
        old = assign(self)
        def fail_after_close():
            with self.assertRaises(ValidationError):
                reassign(self, note="x" * 2001)
            # Savepoint rollback releases locks; hold the unchanged case again so
            # the contender proves a PostgreSQL wait on the restored state.
            from .models import ServiceCase
            ServiceCase.objects.select_for_update().get(pk=self.case.pk)
        self.race(fail_after_close, lambda: unassign(self, expected_current_assignment_id=old.pk), "success")
        self.assertEqual(ServiceEngineerAssignment.objects.count(), 1)
