"""Repair lifecycle, evidence, isolation and database invariants."""
import uuid
from datetime import timedelta
from unittest.mock import patch

from django.core.exceptions import ValidationError
from django.db import IntegrityError, transaction
from django.db.models.deletion import Collector, ProtectedError
from django.test import TestCase
from django.utils import timezone

from apps.access import services as access
from apps.catalog import services as catalog
from apps.devices import services as devices
from apps.organization import services as organization
from apps.service_catalog import services as taxonomy
from apps.service_catalog.models import RepairAction
from . import repair_services as services, repair_queries as queries, engineer_queries
from .models import ServiceCase, ServiceEngineerAssignment, ServiceDiagnosticAssessment, ServiceRepairExecution, ServiceRepairAction
from .services import cancel_service_case
from . import test_diagnosis as diagnosis
from .test_engineer_assignment import assign, reassign, unassign
from .tests import intake


def setup_repair(test):
    diagnosis.setup_diagnosis(test)
    test.assessment = diagnosis.begin(test)
    diagnosis.add(test, test.assessment)  # A completed unknown cause is valid evidence.
    test.assessment = diagnosis.complete(test, test.assessment)
    test.action_type = RepairAction.objects.create(code="SYNTHETIC-REPAIR", name="Synthetic repair", applies_to_all_product_categories=True)
    test.action_type2 = RepairAction.objects.create(code="SYNTHETIC-REPAIR-2", name="Synthetic second repair", applies_to_all_product_categories=True)


def begin(test, **kwargs):
    return services.begin_service_case_repair(**dict(dict(service_case=test.case, actor=test.engineer), **kwargs))


def add(test, execution, **kwargs):
    return services.add_repair_action(**dict(dict(repair_execution=execution, actor=test.engineer, repair_action=test.action_type), **kwargs))


def perform(test, action, **kwargs):
    return services.mark_repair_action_performed(**dict(dict(action=action, actor=test.engineer), **kwargs))


def complete(test, execution, **kwargs):
    return services.complete_service_case_repair(**dict(dict(repair_execution=execution, actor=test.engineer, outcome="REPAIRED"), **kwargs))


def abandon(test, execution, **kwargs):
    return services.abandon_service_case_repair(**dict(dict(repair_execution=execution, actor=test.user, reason="Synthetic recovery"), **kwargs))


def prepared(test, performed=True):
    execution = begin(test)
    action = add(test, execution)
    if performed:
        action = perform(test, action)
    return execution, action


def assert_repair_invariants(test):
    for case in ServiceCase.objects.all():
        assignments = list(ServiceEngineerAssignment.objects.filter(service_case=case, ended_at=None))
        diagnoses = ServiceDiagnosticAssessment.objects.filter(service_case=case, completed_at=None, abandoned_at=None)
        executions = list(ServiceRepairExecution.objects.filter(service_case=case, status="OPEN"))
        test.assertEqual(len(assignments), int(case.status in ("ASSIGNED", "DIAGNOSING", "DIAGNOSED", "REPAIRING", "REPAIRED")))
        test.assertEqual(diagnoses.count(), int(case.status == "DIAGNOSING"))
        test.assertEqual(len(executions), int(case.status == "REPAIRING"))
        if executions:
            test.assertEqual(executions[0].engineer_assignment_id, assignments[0].pk)
            test.assertIsNotNone(executions[0].diagnostic_assessment.completed_at)
        if case.status == "REPAIRED":
            test.assertTrue(ServiceRepairExecution.objects.filter(service_case=case, status="COMPLETED", outcome="REPAIRED").exists())


class RepairTests(TestCase):
    def setUp(self):
        setup_repair(self)

    def tearDown(self):
        assert_repair_invariants(self)

    def test_begin_links_uuid_actor_and_unknown_cause_diagnosis(self):
        execution = begin(self, expected_engineer_assignment_id=self.assignment.pk)
        self.assertIsInstance(execution.pk, uuid.UUID)
        self.assertEqual(execution.engineer_assignment, self.assignment)
        self.assertEqual(execution.diagnostic_assessment, self.assessment)
        self.assertEqual(execution.started_by, self.engineer)
        self.assertTrue(timezone.is_aware(execution.started_at))
        self.assertEqual(execution.status, "OPEN")
        self.assertIsNone(self.assessment.findings.get().root_cause)

    def test_only_diagnosed_can_begin(self):
        with self.assertRaises(ValidationError):
            begin(self, service_case=intake(self))
        begin(self)
        with self.assertRaises(ValidationError):
            begin(self)
        self.assertEqual(ServiceRepairExecution.objects.count(), 1)

    def test_completed_diagnosis_required(self):
        # Simulate inconsistent upstream state inside a rolled-back transaction.
        with self.assertRaisesMessage(ValidationError, "Completed diagnosis"), transaction.atomic():
            case = intake(self)
            assign(self, service_case=case)
            ServiceCase.objects.filter(pk=case.pk).update(status="DIAGNOSED")
            begin(self, service_case=case)
        self.assertFalse(ServiceRepairExecution.objects.exists())

    def test_current_assignment_required(self):
        with self.assertRaises(ValidationError), transaction.atomic():
            case = intake(self)
            ServiceCase.objects.filter(pk=case.pk).update(status="DIAGNOSED")
            begin(self, service_case=case)
        self.assertFalse(ServiceRepairExecution.objects.exists())

    def test_cross_center_and_company_eligibility_cannot_be_borrowed(self):
        from apps.organization.assignment_services import create_assignment
        for kwargs in (dict(service_center=self.center2), dict(company=self.other_company, service_center=self.other_center, customer=self.outsider)):
            case = intake(self, **kwargs)
            if case.company_id == self.other_company.pk:
                path = create_assignment(user=self.engineer2, company=self.other_company)
                access.create_role_assignment(user=self.engineer2, role=self.role, organization_assignment=path)
            assign(self, service_case=case, engineer=self.engineer2)
            assessment = diagnosis.begin(self, service_case=case, actor=self.engineer2)
            diagnosis.add(self, assessment, actor=self.engineer2)
            diagnosis.complete(self, assessment, actor=self.engineer2)
            with self.subTest(company=case.company_id), self.assertRaises(ValidationError):
                begin(self, service_case=case)
            execution = begin(self, service_case=case, actor=self.engineer2)
            with self.assertRaises(ValidationError):
                add(self, execution)
            self.assertFalse(queries.repairing_cases_for_engineer(self.engineer).filter(pk=case.pk).exists())

    def test_wrong_engineer_superuser_cannot_impersonate(self):
        self.user.is_superuser = True
        self.user.save()
        for actor in (self.user, self.engineer2):
            with self.subTest(actor=actor), self.assertRaises(ValidationError):
                begin(self, actor=actor)

    def test_inactive_engineer_rejected(self):
        self.engineer.is_active = False
        self.engineer.save()
        with self.assertRaises(ValidationError):
            begin(self)

    def test_revoked_role_rejected(self):
        access.deactivate_role_assignment(assignment=self.role_assignment)
        with self.assertRaises(ValidationError):
            begin(self)

    def test_direct_permission_and_staff_do_not_replace_scope(self):
        access.deactivate_role_assignment(assignment=self.role_assignment)
        self.engineer.is_staff = True
        self.engineer.save()
        self.engineer.user_permissions.add(self.permission)
        with self.assertRaises(ValidationError):
            begin(self)

    def test_inactive_company_rejected(self):
        organization.deactivate_company(company=self.company)
        with self.assertRaises(ValidationError):
            begin(self)

    def test_inactive_center_rejected(self):
        organization.deactivate_service_center(service_center=self.center)
        with self.assertRaises(ValidationError):
            begin(self)

    def test_inactive_device_rejected(self):
        devices.deactivate_device(device=self.device)
        with self.assertRaises(ValidationError):
            begin(self)

    def test_inactive_catalog_rejected(self):
        catalog.deactivate_category(category=self.category)
        with self.assertRaises(ValidationError):
            begin(self)

    def test_stale_case_and_assignment_preconditions(self):
        for kwargs in ({"expected_updated_at": self.case.updated_at}, {"expected_engineer_assignment_id": uuid.uuid4()}):
            with self.subTest(kwargs=kwargs), self.assertRaises(ValidationError):
                begin(self, **kwargs)

    def test_diagnosis_is_preserved_even_if_old_taxonomy_deactivated(self):
        before = ServiceDiagnosticAssessment.objects.values().get(pk=self.assessment.pk)
        taxonomy.deactivate_fault_diagnosis(diagnosis=self.fault)
        execution, _ = prepared(self)
        complete(self, execution)
        self.assertEqual(ServiceDiagnosticAssessment.objects.values().get(pk=self.assessment.pk), before)

    def test_global_and_specific_category_actions(self):
        taxonomy.set_repair_action_applicability(repair_action=self.action_type2, applies_to_all_product_categories=False, product_categories=[self.category])
        execution = begin(self)
        add(self, execution)
        add(self, execution, repair_action=self.action_type2)
        self.assertEqual(queries.active_repair_actions(execution).count(), 2)

    def test_inactive_action_rejected_fresh(self):
        execution = begin(self)
        taxonomy.deactivate_repair_action(repair_action=self.action_type)
        with self.assertRaises(ValidationError):
            add(self, execution)

    def test_inapplicable_action_rejected(self):
        execution = begin(self)
        taxonomy.set_repair_action_applicability(repair_action=self.action_type, applies_to_all_product_categories=False, product_categories=[])
        with self.assertRaises(ValidationError):
            add(self, execution)

    def test_duplicate_active_rejected(self):
        execution = begin(self)
        add(self, execution)
        with self.assertRaises(ValidationError):
            add(self, execution)
        self.assertEqual(queries.active_repair_actions(execution).count(), 1)

    def test_soft_removed_action_history_and_readdition(self):
        execution, action = prepared(self, performed=False)
        services.remove_repair_action(action=action, actor=self.engineer)
        replacement = add(self, execution)
        self.assertNotEqual(action.pk, replacement.pk)
        self.assertEqual(queries.repair_action_history(execution).count(), 2)
        self.assertEqual(queries.active_repair_actions(execution).count(), 1)
        with self.assertRaises(ValidationError):
            perform(self, action)

    def test_planning_not_performance(self):
        execution, action = prepared(self, performed=False)
        self.assertIsNone(action.performed_at)
        self.assertIsNone(action.performed_by)
        for outcome in ("REPAIRED", "NOT_REPAIRED"):
            with self.subTest(outcome=outcome), self.assertRaises(ValidationError):
                complete(self, execution, outcome=outcome)

    def test_performance_attributed_and_server_timestamp(self):
        _, action = prepared(self, performed=False)
        before = timezone.now()
        action = perform(self, action)
        self.assertGreaterEqual(action.performed_at, before)
        self.assertLessEqual(action.performed_at, timezone.now())
        self.assertEqual(action.performed_by, self.engineer)
        with self.assertRaises(ValidationError):
            perform(self, action)

    def test_future_or_naive_performance_rejected(self):
        _, action = prepared(self, performed=False)
        for value in (timezone.now() + timedelta(days=1), timezone.now().replace(tzinfo=None)):
            action.performed_at, action.performed_by = value, self.engineer
            with self.subTest(value=value), self.assertRaises(ValidationError):
                action.clean()

    def test_update_unperformed_action(self):
        _, action = prepared(self, performed=False)
        row = services.update_repair_action(action=action, actor=self.engineer, repair_action=self.action_type2, note=" Synthetic plan ")
        self.assertEqual(row.repair_action, self.action_type2)
        self.assertEqual(row.note, "Synthetic plan")

    def test_update_revalidates_taxonomy(self):
        _, action = prepared(self, performed=False)
        taxonomy.deactivate_repair_action(repair_action=self.action_type2)
        with self.assertRaises(ValidationError):
            services.update_repair_action(action=action, actor=self.engineer, repair_action=self.action_type2)

    def test_performed_action_cannot_be_rewritten(self):
        _, action = prepared(self)
        with self.assertRaises(ValidationError):
            services.update_repair_action(action=action, actor=self.engineer, note="Changed")

    def test_removal_retains_performed_evidence_but_not_completion_credit(self):
        execution, action = prepared(self)
        before = action.performed_at
        services.remove_repair_action(action=action, actor=self.engineer)
        action.refresh_from_db()
        self.assertEqual(action.performed_at, before)
        self.assertFalse(action.is_active)
        with self.assertRaises(ValidationError):
            complete(self, execution)

    def test_empty_completion_rejected(self):
        execution = begin(self)
        with self.assertRaises(ValidationError):
            complete(self, execution)

    def test_all_active_actions_must_be_performed(self):
        execution, _ = prepared(self)
        second = add(self, execution, repair_action=self.action_type2)
        with self.assertRaises(ValidationError):
            complete(self, execution)
        perform(self, second)
        complete(self, execution)

    def test_success_retains_assignment_and_completed_history(self):
        execution, _ = prepared(self)
        row = complete(self, execution)
        self.case.refresh_from_db()
        self.assertEqual(self.case.status, "REPAIRED")
        self.assertEqual(row.outcome, "REPAIRED")
        self.assertEqual(row.completed_by, self.engineer)
        self.assertEqual(engineer_queries.current_engineer_assignment(self.case).pk, self.assignment.pk)
        self.assertIsNone(queries.current_repair_execution(self.case))

    def test_unsuccessful_then_successful_attempt(self):
        first, _ = prepared(self)
        complete(self, first, outcome="NOT_REPAIRED")
        self.case.refresh_from_db()
        self.assertEqual(self.case.status, "DIAGNOSED")
        second, _ = prepared(self)
        complete(self, second)
        self.assertEqual(list(queries.repair_execution_history(self.case).values_list("outcome", flat=True)), ["NOT_REPAIRED", "REPAIRED"])

    def test_unknown_outcome_rejected(self):
        execution, _ = prepared(self)
        for outcome in (None, "", "SUCCESS", "anything"):
            with self.subTest(outcome=outcome), self.assertRaises(ValidationError):
                complete(self, execution, outcome=outcome)

    def test_completion_revalidates_inactive_action(self):
        execution, _ = prepared(self)
        taxonomy.deactivate_repair_action(repair_action=self.action_type)
        with self.assertRaises(ValidationError):
            complete(self, execution)

    def test_completion_revalidates_applicability(self):
        execution, _ = prepared(self)
        taxonomy.set_repair_action_applicability(repair_action=self.action_type, applies_to_all_product_categories=False, product_categories=[])
        with self.assertRaises(ValidationError):
            complete(self, execution)

    def test_completion_revalidates_permission(self):
        execution, _ = prepared(self)
        access.set_role_permissions(role=self.role, permissions=[])
        with self.assertRaises(ValidationError):
            complete(self, execution)

    def test_technical_mutations_require_actual_engineer(self):
        execution, action = prepared(self, performed=False)
        for operation in (
            lambda: add(self, execution, actor=self.engineer2, repair_action=self.action_type2),
            lambda: perform(self, action, actor=self.engineer2),
            lambda: complete(self, execution, actor=self.engineer2),
            lambda: services.update_repair_action(action=action, actor=self.engineer2, note="invalid"),
            lambda: services.remove_repair_action(action=action, actor=self.engineer2),
        ):
            with self.subTest(operation=operation), self.assertRaises(ValidationError):
                operation()

    def test_abandon_preserves_assignment_diagnosis_and_actions(self):
        before = ServiceDiagnosticAssessment.objects.values().get(pk=self.assessment.pk)
        execution, action = prepared(self)
        row = abandon(self, execution)
        self.assertEqual(row.status, "ABANDONED")
        self.assertEqual(row.abandoned_by, self.user)
        self.assertIsNone(row.outcome)
        self.assertEqual(queries.repair_action_history(row).get().pk, action.pk)
        self.assertEqual(ServiceDiagnosticAssessment.objects.values().get(pk=self.assessment.pk), before)
        self.assertEqual(engineer_queries.current_engineer_assignment(self.case).pk, self.assignment.pk)
        begin(self)

    def test_abandon_requires_reason_and_active_actor(self):
        execution = begin(self)
        with self.assertRaises(ValidationError):
            abandon(self, execution, reason="  ")
        self.user.is_active = False
        self.user.save()
        with self.assertRaises(ValidationError):
            abandon(self, execution)

    def test_recovery_allowed_after_eligibility_revocation(self):
        execution = begin(self)
        access.deactivate_role_assignment(assignment=self.role_assignment)
        abandon(self, execution)

    def test_cancel_abandons_and_closes_assignment_atomically(self):
        execution, action = prepared(self)
        cancel_service_case(service_case=self.case, cancelled_by=self.user)
        execution.refresh_from_db()
        self.assignment.refresh_from_db()
        self.case.refresh_from_db()
        self.assertEqual(execution.status, "ABANDONED")
        self.assertEqual(execution.abandoned_at, self.case.cancelled_at)
        self.assertEqual(self.assignment.ended_at, self.case.cancelled_at)
        self.assertEqual(queries.repair_action_history(execution).get().pk, action.pk)
        with self.assertRaises(ValidationError):
            add(self, execution, repair_action=self.action_type2)

    def test_cancel_repaired_denied(self):
        execution, _ = prepared(self)
        complete(self, execution)
        with self.assertRaises(ValidationError):
            cancel_service_case(service_case=self.case, cancelled_by=self.user)

    def test_reassignment_and_unassignment_denied_during_and_after_repair(self):
        execution, _ = prepared(self)
        for completed in (False, True):
            if completed:
                complete(self, execution)
            for operation in (reassign, unassign):
                with self.subTest(completed=completed, operation=operation), self.assertRaises(ValidationError):
                    operation(self)

    def test_all_final_outcomes_block_mutation(self):
        for outcome in ("NOT_REPAIRED", "ABANDONED", "REPAIRED"):
            execution, action = prepared(self)
            abandon(self, execution) if outcome == "ABANDONED" else complete(self, execution, outcome=outcome)
            for operation in (
                lambda: add(self, execution, repair_action=self.action_type2),
                lambda: services.update_repair_execution(repair_execution=execution, actor=self.engineer, note="change"),
                lambda: services.remove_repair_action(action=action, actor=self.engineer),
                lambda: perform(self, action), lambda: abandon(self, execution), lambda: complete(self, execution),
            ):
                with self.subTest(outcome=outcome, operation=operation), self.assertRaises(ValidationError):
                    operation()

    def test_stale_child_and_aggregate_preconditions(self):
        execution, action = prepared(self, performed=False)
        execution.refresh_from_db()
        services.update_repair_execution(repair_execution=execution, actor=self.engineer, note="newer")
        with self.assertRaises(ValidationError):
            perform(self, action, expected_execution_updated_at=execution.updated_at)
        services.update_repair_action(action=action, actor=self.engineer, note="newer action")
        with self.assertRaises(ValidationError):
            perform(self, action, expected_updated_at=action.updated_at)

    def test_failed_completion_rolls_back_exact_state(self):
        execution, _ = prepared(self)
        before = ServiceRepairExecution.objects.values().get(pk=execution.pk)
        case_before = ServiceCase.objects.values().get(pk=self.case.pk)
        with patch.object(ServiceCase, "_persist", side_effect=ValidationError("Synthetic late failure")):
            with self.assertRaises(ValidationError):
                complete(self, execution)
        self.assertEqual(ServiceRepairExecution.objects.values().get(pk=execution.pk), before)
        self.assertEqual(ServiceCase.objects.values().get(pk=self.case.pk), case_before)

    def test_failed_cancel_rolls_back_abandonment(self):
        execution, _ = prepared(self)
        before = ServiceRepairExecution.objects.values().get(pk=execution.pk)
        with self.assertRaises(ValidationError):
            cancel_service_case(service_case=self.case, cancelled_by=self.user, reason="x" * 2001)
        self.assertEqual(ServiceRepairExecution.objects.values().get(pk=execution.pk), before)
        self.assignment.refresh_from_db()
        self.assertIsNone(self.assignment.ended_at)

    def test_direct_save_delete_and_queryset_delete_disabled(self):
        execution, action = prepared(self)
        for row in (execution, action):
            for operation in (row.save, row.delete, lambda row=row: type(row).objects.filter(pk=row.pk).delete()):
                with self.subTest(row=row, operation=operation), self.assertRaises(ValidationError):
                    operation()

    def test_protected_links(self):
        execution, _ = prepared(self)
        for row in (self.action_type, self.assessment, self.assignment, execution):
            with self.subTest(row=row), self.assertRaises(ProtectedError):
                Collector(using="default").collect([row])

    def test_database_status_outcome_coherence(self):
        execution = begin(self)
        for changes in ({"status": "OTHER"}, {"outcome": "REPAIRED"}, {"status": "COMPLETED"}, {"status": "ABANDONED"}, {"completed_by": self.engineer}):
            with self.subTest(changes=changes), self.assertRaises(IntegrityError), transaction.atomic():
                ServiceRepairExecution.objects.filter(pk=execution.pk).update(**changes)

    def test_database_one_open_execution(self):
        begin(self)
        with self.assertRaises(IntegrityError), transaction.atomic():
            ServiceRepairExecution.objects.bulk_create([ServiceRepairExecution(service_case=self.case, engineer_assignment=self.assignment,
                diagnostic_assessment=self.assessment, started_by=self.engineer)])

    def test_database_active_action_uniqueness(self):
        execution, _ = prepared(self)
        with self.assertRaises(IntegrityError), transaction.atomic():
            ServiceRepairAction.objects.bulk_create([ServiceRepairAction(repair_execution=execution, repair_action=self.action_type)])

    def test_database_performance_coherence(self):
        _, action = prepared(self, performed=False)
        for changes in ({"performed_at": timezone.now()}, {"performed_by": self.engineer},
                        {"performed_at": action.created_at - timedelta(seconds=1), "performed_by": self.engineer}):
            with self.subTest(changes=changes), self.assertRaises(IntegrityError), transaction.atomic():
                ServiceRepairAction.objects.filter(pk=action.pk).update(**changes)

    def test_history_queries_lazy_stable_and_no_n_plus_one(self):
        execution, _ = prepared(self)
        with self.assertNumQueries(0):
            history = queries.repair_execution_history(self.case)
            actions = queries.repair_action_history(execution)
        with self.assertNumQueries(1):
            for row in history:
                str(row.service_case), str(row.engineer_assignment.engineer), row.diagnostic_assessment.completed_at, str(row.started_by)
        with self.assertNumQueries(1):
            for row in actions:
                str(row.repair_action), str(row.performed_by), row.repair_execution.status
        self.assertEqual(history.query.order_by, ())  # Model ordering includes UUID tie breaker.
        self.assertEqual(ServiceRepairExecution._meta.ordering, ["started_at", "id"])
        self.assertEqual(ServiceRepairAction._meta.ordering, ["created_at", "id"])

    def test_queues_follow_current_state_and_eligibility(self):
        execution, _ = prepared(self)
        self.assertEqual(list(queries.repairing_cases_for_engineer(self.engineer)), [self.case])
        self.assertEqual(list(queries.repairing_cases_for_center(self.center)), [self.case])
        self.assertFalse(queries.repairing_cases_for_engineer(self.engineer2).exists())
        self.assertFalse(queries.repairing_cases_for_center(self.other_center).exists())
        complete(self, execution)
        self.assertFalse(queries.repairing_cases_for_engineer(self.engineer).exists())
        self.assertEqual(list(queries.repaired_cases_for_engineer(self.engineer)), [self.case])
        access.deactivate_role_assignment(assignment=self.role_assignment)
        self.assertFalse(queries.repaired_cases_for_engineer(self.engineer).exists())

    def test_invalid_query_inputs_deny_empty(self):
        self.assertIsNone(queries.current_repair_execution(None))
        self.assertFalse(queries.repair_execution_history(None).exists())
        self.assertFalse(queries.active_repair_actions(None).exists())
        self.assertFalse(queries.repairing_cases_for_engineer(None).exists())
        self.assertFalse(queries.repairing_cases_for_center(None).exists())
