"""Dedicated Phase 3B audit, after all implementation checkpoints."""
import uuid
from django.core.exceptions import ValidationError, PermissionDenied
from django.db import IntegrityError, transaction
from django.test import TestCase
from django.urls import reverse
from apps.parts import services as parts
from apps.service import test_repair as repair, quality_control_services as qc
from apps.service.handover_services import close_service_case
from .tests import InventoryFixture, grant
from .test_usage import UsageFixture, setup_usage, deliver_case
from . import document_services as documents, control_services as control, usage_services as usage, services as stock
from . import queries, control_queries
from .models import StockLedgerEntry, StockMovement, SerializedStockUnit, PartsDisposition


class AuditAdminTests(InventoryFixture):
    def setUp(self):
        self.client.force_login(self.actor)

    def test_location_lifecycle_requires_reviewable_signed_confirmation(self):
        response = self.client.post(reverse("admin:inventory_inventorylocation_changelist"),
            {"action": "deactivate_selected", "_selected_action": str(self.location.pk)})
        self.assertEqual(response.status_code, 200)
        self.location.refresh_from_db()
        self.assertTrue(self.location.is_active)
        self.assertIn("transition_revision", response.context_data)

    def test_stale_location_lifecycle_confirmation_rejected(self):
        url = reverse("admin:inventory_inventorylocation_changelist")
        data = {"action": "deactivate_selected", "_selected_action": str(self.location.pk)}
        preview = self.client.post(url, data)
        stock.update_location(actor=self.actor, location=self.location, name="Changed after review", expected_revision=self.location.updated_at.isoformat())
        self.client.post(url, data | {"confirm_transition": "1", "transition_revision": preview.context_data["transition_revision"]})
        self.location.refresh_from_db()
        self.assertTrue(self.location.is_active)

    def test_tampered_location_confirmation_rejected(self):
        self.client.post(reverse("admin:inventory_inventorylocation_changelist"), {"action": "deactivate_selected",
            "_selected_action": str(self.location.pk), "confirm_transition": "1", "transition_revision": "tampered"})
        self.location.refresh_from_db()
        self.assertTrue(self.location.is_active)


class AuditWorkflowTests(UsageFixture, TestCase):
    @classmethod
    def setUpTestData(cls):
        setup_usage(cls)

    def receipt(self, quantity):
        row = documents.create_goods_receipt(actor=self.actor, destination=self.destination, external_reference="AUDIT-RECEIPT")
        row = documents.set_goods_receipt_lines(actor=self.actor, receipt=row, expected_revision=row.updated_at.isoformat(), lines=[dict(spare_part=self.part, quantity=quantity, identifiers=[])])
        return documents.post_goods_receipt(actor=self.actor, receipt=row, expected_revision=row.updated_at.isoformat())

    def transfer(self, quantity):
        row = documents.create_stock_transfer(actor=self.actor, source=self.destination, destination=self.location)
        row = documents.set_stock_transfer_lines(actor=self.actor, transfer=row, expected_revision=row.updated_at.isoformat(), lines=[dict(spare_part=self.part, quantity=quantity, units=[])])
        row = documents.dispatch_stock_transfer(actor=self.actor, transfer=row, expected_revision=row.updated_at.isoformat())
        return documents.receive_stock_transfer(actor=self.actor, transfer=row, expected_revision=row.updated_at.isoformat())

    def test_complete_service_and_stock_equation(self):
        self.receipt(4)
        transfer = self.transfer(3)
        request = self.approve(self.request())
        issue = self.issue(self.reserve(request, quantity=2))
        execution, action = repair.prepared(self)
        consumed = self.consume(issue, action)
        self.return_unused(issue)
        quarantine = stock.create_location(actor=self.actor, company=self.company, service_center=self.center,
            code="AUDIT-DEFECTIVE", name="Defective custody", location_type="DEFECTIVE")
        usage.recover_defective_component(actor=self.engineer, repair_action=action, location=quarantine,
            component_description="Removed damaged display", replacement=consumed, command_key=uuid.uuid4())
        count = control.create_stock_count(actor=self.actor, location=self.location, spare_part=self.part)
        count = control.start_stock_count(actor=self.actor, count=count, expected_revision=count.updated_at.isoformat())
        count = control.record_stock_count(actor=self.actor, count=count, counted_quantity=1, expected_revision=count.updated_at.isoformat())
        control.reconcile_stock_count(actor=self.actor, count=count, expected_revision=count.updated_at.isoformat())
        control.adjust_stock(actor=self.actor, location=self.location, spare_part=self.part, quantity_delta=2,
            reason="FOUND", command_key=uuid.uuid4(), reference="AUDIT-FOUND", note="Verified physical discovery")
        deliver_case(self, execution=execution)
        close_service_case(service_case=self.case, actor=self.actor)
        self.case.refresh_from_db()
        self.assertEqual(self.case.status, "CLOSED")
        balances = {row["location_id"]: row["on_hand"] for row in queries.stock_positions_for_part(actor=self.actor, spare_part=self.part)}
        self.assertEqual(balances[self.destination.pk], 1)
        self.assertEqual(balances[self.location.pk], 3)
        self.assertEqual(balances[issue.custody_location_id], 0)
        self.assertEqual(balances[transfer.transit_location_id], 0)
        self.assertEqual(sum(balances.values()), 4 - 1 - 1 + 2)
        self.assertFalse(any(rows.exists() for rows in control_queries.inventory_anomalies(actor=self.actor).values()))

    def test_request_and_count_drafts_do_not_freeze_policy(self):
        self.request()
        control.create_stock_count(actor=self.actor, location=self.location, spare_part=self.part)
        documents.create_goods_receipt(actor=self.actor, destination=self.location)
        parts.update_spare_part(spare_part=self.part, serialization_policy="REQUIRED_SERIAL")
        self.part.refresh_from_db()
        self.assertEqual(self.part.serialization_policy, "REQUIRED_SERIAL")
        self.assertFalse(StockMovement.objects.exists())
        self.assertFalse(SerializedStockUnit.objects.exists())

    def test_posted_ledger_never_allows_edit_or_delete(self):
        receipt = self.receipt(1)
        movement = receipt.lines.get().movement
        with self.assertRaises(IntegrityError), transaction.atomic():
            StockMovement.objects.filter(pk=movement.pk).update(quantity=2)
        with self.assertRaises(IntegrityError), transaction.atomic():
            StockLedgerEntry.objects.filter(movement=movement).update(quantity_delta=2)
        with self.assertRaises(ValidationError):
            movement.delete()

    def test_native_permissions_and_staff_do_not_authorize_inventory(self):
        from django.contrib.auth.models import Permission
        self.user.is_staff = True
        self.user.save()
        self.user.user_permissions.set(Permission.objects.filter(content_type__app_label="inventory"))
        with self.assertRaises(PermissionDenied):
            self.receive(actor=self.user)
        self.assertFalse(queries.authorized_locations(actor=self.user).exists())

    def test_qc_rework_preserves_old_consumption_and_uses_new_attempt(self):
        from django.contrib.auth.models import Permission
        from apps.access.services import set_role_permissions
        issue = self.prepared_issue(quantity=2)
        first, first_action = repair.prepared(self)
        consumed = self.consume(issue, first_action)
        repair.complete(self, first)
        set_role_permissions(role=self.role, permissions=[*self.role.permissions.all(), Permission.objects.get(content_type__app_label="service", codename="perform_quality_control")])
        self.case.refresh_from_db()
        qc.submit_for_quality_control(service_case=self.case, inspector=self.engineer2)
        self.case.refresh_from_db()
        inspection = qc.begin_quality_control(service_case=self.case, inspector=self.engineer2)
        qc.set_quality_control_check(quality_control=inspection, inspector=self.engineer2, check_code="BOOT", result="FAIL", note="Rework required")
        inspection.refresh_from_db()
        qc.complete_quality_control_fail(quality_control=inspection, inspector=self.engineer2, failure_note="Failed boot test")
        second, second_action = repair.prepared(self)
        second_use = self.consume(issue, second_action)
        self.assertNotEqual(first.pk, second.pk)
        consumed.refresh_from_db()
        self.assertEqual(consumed.repair_action_id, first_action.pk)
        self.assertEqual(second_use.repair_action_id, second_action.pk)
        first.refresh_from_db()
        self.assertEqual((first.status, first.outcome), ("COMPLETED", "REPAIRED"))
        deliver_case(self, execution=second)
        close_service_case(service_case=self.case, actor=self.actor)
        self.assertEqual(PartsDisposition.objects.filter(issue=issue).count(), 2)

    def test_consumed_serial_cannot_be_found_as_an_inventory_adjustment(self):
        parts.update_spare_part(spare_part=self.part, serialization_policy="REQUIRED_SERIAL")
        unit = stock.register_serialized_unit(actor=self.actor, company=self.company, spare_part=self.part, identifier="PERMANENT-CONSUMPTION")
        issue = self.prepared_issue(quantity=1, units=[unit])
        _, action = repair.prepared(self)
        self.consume(issue, action, units=[unit])
        with self.assertRaises(ValidationError):
            control.adjust_stock(actor=self.actor, location=self.location, spare_part=self.part, quantity_delta=1,
                reason="FOUND", command_key=uuid.uuid4(), reference="INVALID-RESTORE", note="Cannot restore installed stock", units=[unit])

    def test_resolved_partial_issue_allows_cancelling_remaining_demand(self):
        from .request_services import cancel_parts_request
        self.receive()
        request = self.approve(self.request())
        issue = self.issue(self.reserve(request, quantity=1))
        request.refresh_from_db()
        with self.assertRaises(ValidationError):
            cancel_parts_request(actor=self.actor, request=request, reason="Still in custody", expected_revision=request.updated_at.isoformat())
        self.return_unused(issue)
        request.refresh_from_db()
        row = cancel_parts_request(actor=self.actor, request=request, reason="Remaining part no longer needed", expected_revision=request.updated_at.isoformat())
        self.assertEqual(row.status, "CANCELLED")
        self.assertEqual(row.lines.get().quantity, 2)
        self.assertEqual(issue.dispositions.get().quantity, 1)

    def test_count_reconciliation_review_displays_explicit_variance(self):
        self.receive(quantity=2)
        count = control.create_stock_count(actor=self.actor, location=self.location, spare_part=self.part)
        count = control.start_stock_count(actor=self.actor, count=count, expected_revision=count.updated_at.isoformat())
        control.record_stock_count(actor=self.actor, count=count, counted_quantity=1, expected_revision=count.updated_at.isoformat())
        self.client.force_login(self.actor)
        response = self.client.post(reverse("admin:inventory_stockcount_changelist"), {"action": "reconcile_selected", "_selected_action": str(count.pk)})
        self.assertContains(response, "<dt>Variance</dt><dd>-1</dd>", html=True)
