"""Synthetic engineer eligibility, transitions, database, queries and Admin."""
import uuid
from datetime import timedelta
from unittest.mock import patch

from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction
from django.db.models.deletion import ProtectedError
from django.test import Client, TestCase
from django.urls import reverse
from django.utils import timezone

from apps.access import services as access
from apps.access.authorization import is_authorized
from apps.access.models import Role
from apps.organization import assignment_services as org_assignments, services as organization
from apps.organization.models import Department, UserOrganizationAssignment
from . import engineer_queries as queries, engineer_services as services
from .models import ServiceCase, ServiceEngineerAssignment
from .services import cancel_service_case
from .tests import setup, intake


def setup_engineers(test):
    setup(test)
    test.case = intake(test)
    test.permission = Permission.objects.get(content_type__app_label="service", codename="handle_servicecase")
    test.role = Role.objects.create(code="SYNTHETIC-CAPABILITY", name="Arbitrary role name")
    access.set_role_permissions(role=test.role, permissions=[test.permission])
    test.engineer = get_user_model().objects.create_user(username="synthetic-engineer-a")
    test.engineer2 = get_user_model().objects.create_user(username="synthetic-engineer-b")
    test.path = org_assignments.create_assignment(user=test.engineer, company=test.company, region=test.region, service_center=test.center)
    test.role_assignment = access.create_role_assignment(user=test.engineer, role=test.role, organization_assignment=test.path)
    test.path2 = org_assignments.create_assignment(user=test.engineer2, company=test.company)
    access.create_role_assignment(user=test.engineer2, role=test.role, organization_assignment=test.path2)


def assign(test, **kwargs):
    return services.assign_service_case_engineer(**dict(dict(service_case=test.case, engineer=test.engineer, assigned_by=test.user), **kwargs))


def reassign(test, **kwargs):
    return services.reassign_service_case_engineer(**dict(dict(service_case=test.case, new_engineer=test.engineer2, reassigned_by=test.user), **kwargs))


def unassign(test, **kwargs):
    return services.unassign_service_case_engineer(**dict(dict(service_case=test.case, unassigned_by=test.user), **kwargs))


def assert_invariant(test):
    for case in ServiceCase.objects.all():
        count = ServiceEngineerAssignment.objects.filter(service_case=case, ended_at__isnull=True).count()
        test.assertEqual(count, int(case.status == "ASSIGNED"))


class EngineerTests(TestCase):
    def setUp(self):
        setup_engineers(self)

    def denied(self):
        self.assertFalse(queries.eligible_engineers_for_service_case(self.case).filter(pk=self.engineer.pk).exists())
        with self.assertRaises(ValidationError):
            assign(self)
        assert_invariant(self)

    def test_assign_identity_status_and_attribution(self):
        row = assign(self, note="  Synthetic intake handoff  ")
        self.assertIsInstance(row.pk, uuid.UUID)
        self.assertEqual(row.assigned_by, self.user)
        self.assertEqual(row.engineer, self.engineer)
        self.assertTrue(timezone.is_aware(row.assigned_at))
        self.assertIsNone(row.ended_at)
        self.assertIsNone(row.ended_by)
        self.assertEqual(row.note, "Synthetic intake handoff")
        self.assertEqual(queries.current_engineer(self.case), self.engineer)
        assert_invariant(self)

    def test_duplicate_and_implicit_replacement_rejected(self):
        assign(self)
        for engineer in (self.engineer, self.engineer2):
            with self.assertRaises(ValidationError):
                assign(self, engineer=engineer)
        self.assertEqual(ServiceEngineerAssignment.objects.count(), 1)
        assert_invariant(self)

    def test_assign_revision_rejects_aba(self):
        old = self.case.updated_at
        assign(self)
        unassign(self)
        with self.assertRaises(ValidationError):
            assign(self, expected_updated_at=old)
        assert_invariant(self)

    def test_reassign_same_timestamp_and_history(self):
        old = assign(self)
        new = reassign(self, expected_current_assignment_id=old.pk, reason="Synthetic transfer")
        old.refresh_from_db()
        self.assertEqual(old.ended_at, new.assigned_at)
        self.assertEqual(old.ended_by, self.user)
        self.assertEqual(old.end_reason, "Synthetic transfer")
        self.assertEqual(list(queries.engineer_assignment_history(self.case)), [old, new])
        assert_invariant(self)

    def test_reassign_same_engineer_rejects(self):
        old = assign(self)
        with self.assertRaises(ValidationError):
            reassign(self, new_engineer=self.engineer)
        self.assertEqual(queries.current_engineer_assignment(self.case), old)

    def test_reassign_requires_current(self):
        with self.assertRaises(ValidationError):
            reassign(self)

    def test_stale_reassign_rejected(self):
        old = assign(self)
        new = reassign(self)
        with self.assertRaises(ValidationError):
            reassign(self, new_engineer=self.engineer, expected_current_assignment_id=old.pk)
        self.assertEqual(queries.current_engineer_assignment(self.case), new)

    def test_stale_unassign_rejected(self):
        old = assign(self)
        new = reassign(self)
        with self.assertRaises(ValidationError):
            unassign(self, expected_current_assignment_id=old.pk)
        self.assertEqual(queries.current_engineer_assignment(self.case), new)

    def test_unassign_status_and_attribution(self):
        row = assign(self)
        ended = unassign(self, expected_current_assignment_id=row.pk, reason="Synthetic release")
        self.assertEqual(ended.ended_by, self.user)
        self.assertIsNone(queries.current_engineer(self.case))
        self.case.refresh_from_db()
        self.assertEqual(self.case.status, "RECEIVED")
        assert_invariant(self)

    def test_unassign_without_current_rejected(self):
        with self.assertRaises(ValidationError):
            unassign(self)

    def test_reassignment_insert_failure_rolls_back_closure(self):
        old = assign(self)
        # Invalid new note fails AFTER closure, using actual model validation.
        with self.assertRaises(ValidationError):
            reassign(self, note="x" * 2001)
        self.assertEqual(queries.current_engineer_assignment(self.case), old)
        self.assertEqual(ServiceEngineerAssignment.objects.count(), 1)
        assert_invariant(self)

    def test_cancellation_closes_with_same_timestamp_and_actor(self):
        row = assign(self)
        case = cancel_service_case(service_case=self.case, cancelled_by=self.user, reason="Synthetic cancellation")
        row.refresh_from_db()
        self.assertEqual(row.ended_at, case.cancelled_at)
        self.assertEqual(row.ended_by, self.user)
        self.assertEqual(row.end_reason, case.cancellation_reason)
        self.assertEqual(queries.engineer_assignment_history(case).count(), 1)
        assert_invariant(self)

    def test_assigned_cancellation_requires_explicit_actor(self):
        assign(self)
        with self.assertRaises(ValidationError):
            cancel_service_case(service_case=self.case)
        assert_invariant(self)

    def test_received_cancellation_accepts_actor_and_legacy_call(self):
        cancel_service_case(service_case=self.case, cancelled_by=self.user)
        cancel_service_case(service_case=intake(self))
        assert_invariant(self)

    def test_cancellation_failure_rolls_back_assignment_end(self):
        old = assign(self)
        with patch.object(ServiceCase, "_persist", side_effect=ValidationError("Synthetic persistence failure")):
            with self.assertRaises(ValidationError):
                cancel_service_case(service_case=self.case, cancelled_by=self.user)
        self.assertEqual(queries.current_engineer_assignment(self.case), old)
        assert_invariant(self)

    def test_post_cancel_mutations_rejected(self):
        assign(self)
        cancel_service_case(service_case=self.case, cancelled_by=self.user)
        for operation in (assign, reassign, unassign):
            with self.subTest(operation=operation), self.assertRaises(ValidationError):
                operation(self)
        assert_invariant(self)

    def test_all_actor_apis_reject_unsaved_and_inactive(self):
        inactive = get_user_model().objects.create_user(username="synthetic-inactive", is_active=False)
        for actor in (None, get_user_model()(username="unsaved"), inactive):
            for operation, field in ((assign, "assigned_by"), (reassign, "reassigned_by"), (unassign, "unassigned_by")):
                with self.subTest(operation=operation, actor=actor), self.assertRaises(ValidationError):
                    operation(self, **{field: actor})
        with self.assertRaises(ValidationError):
            cancel_service_case(service_case=self.case, cancelled_by=inactive)

    def test_inactive_engineer_fresh_state(self):
        get_user_model().objects.filter(pk=self.engineer.pk).update(is_active=False)
        self.denied()

    def test_wrong_company(self):
        self.path.company = self.other_company
        self.path.region = self.other_center.region
        self.path.service_center = self.other_center
        self.path.save()
        self.denied()

    def test_wrong_center(self):
        self.path.service_center = self.center2
        self.path.save()
        self.denied()

    def test_company_and_region_scope(self):
        for region in (None, self.region):
            self.path.service_center = None
            self.path.region = region
            self.path.save()
            self.assertTrue(queries.eligible_engineers_for_service_case(self.case).filter(pk=self.engineer.pk).exists())

    def test_department_scope_matrix(self):
        department = Department.objects.filter(company=self.company).first()
        self.path.department = department
        self.path.save()
        self.assertTrue(queries.eligible_engineers_for_service_case(self.case).filter(pk=self.engineer.pk).exists())
        self.path.service_center = None
        self.path.save()
        self.denied()  # region + department does not contain a center
        self.path.region = None
        self.path.save()
        self.denied()  # department-only does not contain a center

    def test_inactive_department_invalidates_exact_center_path(self):
        self.path.department = Department.objects.filter(company=self.company).first()
        self.path.save()
        organization.deactivate_department(department=self.path.department)
        self.denied()

    def test_org_assignment_deactivated(self):
        org_assignments.deactivate_assignment(assignment=self.path)
        self.denied()

    def test_role_assignment_deactivated(self):
        access.deactivate_role_assignment(assignment=self.role_assignment)
        self.denied()

    def test_role_deactivated(self):
        access.deactivate_role(role=self.role)
        self.denied()

    def test_permission_removed(self):
        access.set_role_permissions(role=self.role, permissions=[])
        self.denied()

    def test_permission_and_scope_cannot_mix(self):
        self.path.service_center = self.center2
        self.path.save()
        org_assignments.create_assignment(user=self.engineer, company=self.company, region=self.region, service_center=self.center)
        self.denied()

    def test_cross_company_permission_and_local_scope_cannot_mix(self):
        self.path.company = self.other_company
        self.path.region = self.other_center.region
        self.path.service_center = self.other_center
        self.path.save()
        org_assignments.create_assignment(user=self.engineer, company=self.company)
        self.denied()

    def test_staff_groups_and_direct_permissions_do_not_grant_work(self):
        access.set_role_permissions(role=self.role, permissions=[])
        self.engineer.is_staff = True
        self.engineer.save()
        self.engineer.user_permissions.add(self.permission)
        group = Group.objects.create(name="Synthetic group")
        group.permissions.add(self.permission)
        self.engineer.groups.add(group)
        self.assertTrue(self.engineer.has_perm("service.handle_servicecase"))
        self.denied()

    def test_superuser_requires_posting_but_authorization_bypass_unchanged(self):
        access.set_role_permissions(role=self.role, permissions=[])
        self.engineer.is_superuser = True
        self.engineer.save()
        self.assertTrue(is_authorized(user=self.engineer, permission="service.handle_servicecase", target=self.center))
        self.denied()
        access.set_role_permissions(role=self.role, permissions=[self.permission])
        assign(self)

    def test_inactive_company_and_center_reject(self):
        organization.deactivate_service_center(service_center=self.center)
        self.denied()
        organization.deactivate_company(company=self.company)
        self.denied()

    def test_upstream_loss_retains_current_history_but_excludes_operational_queue(self):
        old = assign(self)
        access.deactivate_role_assignment(assignment=self.role_assignment)
        self.assertEqual(queries.current_engineer_assignment(self.case), old)
        self.assertFalse(queries.assigned_service_cases_for_engineer(self.engineer).exists())
        self.assertFalse(queries.unassigned_service_cases_for_center(self.center).exists())
        unassign(self)  # recovery does not require former engineer eligibility
        assert_invariant(self)

    def test_inactive_company_allows_explicit_unassignment_and_cancellation(self):
        assign(self)
        organization.deactivate_company(company=self.company)
        unassign(self)
        cancel_service_case(service_case=self.case, cancelled_by=self.user)
        assert_invariant(self)

    def test_closed_history_immutable_and_deletion_disabled(self):
        row = assign(self)
        unassign(self)
        row.refresh_from_db()
        for field, value in (("engineer", self.engineer2), ("assigned_by", self.engineer2),
                             ("assigned_at", timezone.now()), ("ended_at", timezone.now()),
                             ("ended_by", self.engineer2), ("end_reason", "changed"), ("note", "changed"),
                             ("service_case", intake(self))):
            fresh = ServiceEngineerAssignment.objects.get(pk=row.pk)
            setattr(fresh, field, value)
            with self.subTest(field=field), self.assertRaises(ValidationError):
                fresh._persist()
        for operation in (row.save, row.delete, ServiceEngineerAssignment.objects.all().delete):
            with self.assertRaises(ValidationError):
                operation()

    def test_protected_engineer_actor_and_case_references(self):
        assign(self)
        for obj in (self.engineer, self.user):
            with self.assertRaises(ProtectedError):
                obj.delete()
        from django.db.models.deletion import Collector
        with self.assertRaises(ProtectedError):
            Collector(using="default").collect([self.case])

    def test_database_current_uniqueness(self):
        assign(self)
        with self.assertRaises(IntegrityError), transaction.atomic():
            ServiceEngineerAssignment.objects.bulk_create([ServiceEngineerAssignment(service_case=self.case, engineer=self.engineer2, assigned_by=self.user)])

    def test_database_period_and_null_coherence(self):
        row = assign(self)
        for facts in (dict(ended_by=self.user), dict(end_reason="premature"),
                      dict(ended_at=timezone.now()), dict(ended_at=row.assigned_at-timedelta(seconds=1), ended_by=self.user)):
            with self.subTest(facts=facts), self.assertRaises(IntegrityError), transaction.atomic():
                ServiceEngineerAssignment.objects.filter(pk=row.pk).update(**facts)

    def test_ambiguous_permission_denies(self):
        ct = ContentType.objects.get_for_model(ServiceEngineerAssignment)
        Permission.objects.create(content_type=ct, codename="handle_servicecase", name="Synthetic ambiguous permission")
        self.denied()

    def test_lazy_query_counts_and_related_access(self):
        with self.assertNumQueries(0):
            eligible = queries.eligible_engineers_for_service_case(self.case)
            unassigned = queries.unassigned_service_cases_for_center(self.center)
        with self.assertNumQueries(1):
            self.assertEqual(list(eligible), [self.engineer, self.engineer2])
        with self.assertNumQueries(1):
            self.assertEqual([row.customer.display_name for row in unassigned], [self.customer.display_name])
        assign(self)
        with self.assertNumQueries(1):
            self.assertEqual(queries.current_engineer_assignment(self.case).engineer.username, self.engineer.username)
        with self.assertNumQueries(1):
            self.assertEqual([row.company.pk for row in queries.assigned_service_cases_for_engineer(self.engineer)], [self.company.pk])

    def test_queue_center_company_ordering_and_duplicates(self):
        second = intake(self)
        other = intake(self, service_center=self.center2)
        foreign = intake(self, company=self.other_company, service_center=self.other_center, customer=self.outsider)
        org_assignments.create_assignment(user=self.engineer, company=self.company)  # extra non-granting path
        self.assertEqual(list(queries.unassigned_service_cases_for_center(self.center)), [self.case, second])
        self.assertEqual(list(queries.unassigned_service_cases_for_center(self.center2)), [other])
        self.assertEqual(list(queries.unassigned_service_cases_for_center(self.other_center)), [foreign])
        assign(self)
        assign(self, service_case=second)
        self.assertEqual(list(queries.assigned_service_cases_for_engineer(self.engineer)), [self.case, second])
        self.assertEqual(list(queries.service_center_engineer_work_queue(self.center)), [self.case, second])
        self.assertFalse(queries.service_center_engineer_work_queue(self.center2).exists())

    def test_queue_excludes_cancelled_and_inactive_hierarchy(self):
        assign(self)
        cancel_service_case(service_case=self.case, cancelled_by=self.user)
        self.assertFalse(queries.assigned_service_cases_for_engineer(self.engineer).exists())
        intake(self)
        organization.deactivate_service_center(service_center=self.center)
        self.assertFalse(queries.unassigned_service_cases_for_center(self.center).exists())
        self.assertEqual(queries.engineer_assignment_history(self.case).count(), 1)

    def test_invalid_query_inputs_return_empty(self):
        for value in (None, ServiceCase()):
            self.assertFalse(queries.eligible_engineers_for_service_case(value).exists())
            self.assertIsNone(queries.current_engineer(value))
            self.assertFalse(queries.engineer_assignment_history(value).exists())
        self.assertFalse(queries.assigned_service_cases_for_engineer(None).exists())
        self.assertFalse(queries.unassigned_service_cases_for_center(None).exists())

    def test_invalid_candidate_rejects_without_closing_history(self):
        old = assign(self)
        for candidate in (None, get_user_model()(username="synthetic-unsaved"), self.case):
            with self.subTest(candidate=candidate), self.assertRaises(ValidationError):
                reassign(self, new_engineer=candidate)
        self.assertEqual(queries.current_engineer_assignment(self.case), old)

    def test_multiple_granting_roles_and_paths_do_not_duplicate_queries(self):
        extra = org_assignments.create_assignment(user=self.engineer, company=self.company)
        access.create_role_assignment(user=self.engineer, role=self.role, organization_assignment=extra)
        role = Role.objects.create(code="SYNTHETIC-EXTRA", name="Another arbitrary role")
        access.set_role_permissions(role=role, permissions=[self.permission])
        access.create_role_assignment(user=self.engineer, role=role, organization_assignment=self.path)
        with self.assertNumQueries(1):
            self.assertEqual(list(queries.eligible_engineers_for_service_case(self.case)), [self.engineer, self.engineer2])
        assign(self)
        with self.assertNumQueries(1):
            self.assertEqual(list(queries.assigned_service_cases_for_engineer(self.engineer)), [self.case])

    def test_role_assignment_user_mismatch_denies(self):
        type(self.role_assignment).objects.filter(pk=self.role_assignment.pk).update(user=self.user)
        self.denied()

    def test_role_cannot_borrow_permission_from_inactive_role_join(self):
        access.deactivate_role_assignment(assignment=self.role_assignment)
        empty_role = Role.objects.create(code="SYNTHETIC-EMPTY", name="No capability")
        access.create_role_assignment(user=self.engineer, role=empty_role, organization_assignment=self.path)
        self.denied()

    def test_unassign_status_failure_rolls_back_end(self):
        old = assign(self)
        with patch.object(ServiceCase, "_persist", side_effect=ValidationError("Synthetic failure")):
            with self.assertRaises(ValidationError):
                unassign(self)
        self.assertEqual(queries.current_engineer_assignment(self.case), old)
        assert_invariant(self)

    def test_assign_status_failure_rolls_back_new_history(self):
        with patch.object(ServiceCase, "_persist", side_effect=ValidationError("Synthetic failure")):
            with self.assertRaises(ValidationError):
                assign(self)
        self.assertFalse(ServiceEngineerAssignment.objects.exists())
        assert_invariant(self)


class EngineerAdminTests(TestCase):
    def setUp(self):
        setup_engineers(self)
        self.admin_user = get_user_model().objects.create_superuser(username="synthetic-admin", email="admin@example.invalid")
        self.client.force_login(self.admin_user)
        self.url = reverse("admin:service_servicecase_engineer", args=[self.case.pk])

    def form(self):
        response = self.client.get(self.url)
        self.assertEqual(response.status_code, 200)
        return response.context["form"].initial["revision"]

    def test_admin_assign_reassign_unassign_delegate_and_attribute(self):
        response = self.client.post(self.url, dict(operation="assign", engineer=self.engineer.pk, revision=self.form()))
        self.assertEqual(response.status_code, 302)
        self.assertEqual(queries.current_engineer_assignment(self.case).assigned_by, self.admin_user)
        response = self.client.post(self.url, dict(operation="reassign", engineer=self.engineer2.pk, revision=self.form(), reason="Synthetic"))
        self.assertEqual(response.status_code, 302)
        response = self.client.post(self.url, dict(operation="unassign", revision=self.form()))
        self.assertEqual(response.status_code, 302)
        self.assertEqual(queries.engineer_assignment_history(self.case).count(), 2)
        self.assertFalse(ServiceEngineerAssignment.objects.filter(ended_at__isnull=True).exists())
        assert_invariant(self)

    def test_admin_stale_reassignment_form(self):
        assign(self)
        revision = self.form()
        new = reassign(self)
        response = self.client.post(self.url, dict(operation="reassign", engineer=self.engineer.pk, revision=revision))
        self.assertContains(response, "changed; reload")
        self.assertEqual(queries.current_engineer_assignment(self.case), new)

    def test_admin_stale_unassignment_form(self):
        assign(self)
        revision = self.form()
        new = reassign(self)
        response = self.client.post(self.url, dict(operation="unassign", revision=revision))
        self.assertContains(response, "changed; reload")
        self.assertEqual(queries.current_engineer_assignment(self.case), new)

    def test_admin_signed_revision_and_csrf(self):
        response = self.client.post(self.url, dict(operation="assign", engineer=self.engineer.pk, revision="forged"))
        self.assertContains(response, "Reload the assignment form")
        client = Client(enforce_csrf_checks=True)
        client.force_login(self.admin_user)
        self.assertEqual(client.post(self.url, dict(operation="assign", engineer=self.engineer.pk, revision=self.form())).status_code, 403)
        assert_invariant(self)

    def test_admin_history_readonly_delete_and_status_protected(self):
        row = assign(self)
        history_url = reverse("admin:service_serviceengineerassignment_change", args=[row.pk])
        response = self.client.get(history_url)
        self.assertEqual(response.status_code, 200)
        self.assertNotContains(response, 'name="engineer"')
        self.assertEqual(self.client.post(history_url, dict(engineer=self.engineer2.pk)).status_code, 403)
        self.assertEqual(self.client.post(reverse("admin:service_serviceengineerassignment_delete", args=[row.pk])).status_code, 403)
        self.assertEqual(self.client.get(reverse("admin:service_serviceengineerassignment_add")).status_code, 403)
        response = self.client.get(reverse("admin:service_servicecase_change", args=[self.case.pk]))
        self.assertEqual(response.status_code, 200)
        self.assertNotContains(response, 'name="status"')
        self.assertContains(response, "Assign / reassign / unassign")

    def test_admin_permission_gate(self):
        self.engineer.is_staff = True
        self.engineer.save()
        self.client.force_login(self.engineer)
        self.assertEqual(self.client.get(self.url).status_code, 403)

    def test_admin_cancellation_ends_assignment_with_request_actor(self):
        old = assign(self)
        response = self.client.post(reverse("admin:service_servicecase_changelist"), dict(action="cancel_selected", _selected_action=[self.case.pk]))
        self.assertEqual(response.status_code, 302)
        old.refresh_from_db()
        self.assertEqual(old.ended_by, self.admin_user)
        assert_invariant(self)
