import uuid

from django.core.exceptions import ValidationError
from django.db import transaction
from django.test import TransactionTestCase

from apps.organization import test_assignment_concurrency as concurrency_helpers
from apps.parts import services as parts
from . import services as s, queries as q
from .models import SerializedStockUnit, StockMovement
from .tests import make_inventory_fixture


class InventoryConcurrencyTests(TransactionTestCase):
    run_concurrent = concurrency_helpers.AssignmentConcurrencyTests.run_concurrent

    def setUp(self):
        make_inventory_fixture(self)

    def receive(self, **kwargs):
        return s.receive_stock(**dict(actor=self.actor, destination=self.location, spare_part=self.part,
            quantity=1, reference="CONCURRENT-RECEIPT", idempotency_key=uuid.uuid4()) | kwargs)

    def move(self, **kwargs):
        return s.move_stock(**dict(actor=self.actor, source=self.location, destination=self.destination, spare_part=self.part,
            quantity=1, reference="CONCURRENT-MOVE", idempotency_key=uuid.uuid4()) | kwargs)

    def register(self, **kwargs):
        return s.register_serialized_unit(**dict(actor=self.actor, company=self.company,
            spare_part=self.serial_part, identifier="RACE-SERIAL") | kwargs)

    def test_concurrent_receipts_same_position(self):
        self.run_concurrent(self.receive, self.receive, expected="success")
        self.assertEqual(q.stock_on_hand(actor=self.actor, location=self.location, spare_part=self.part), 2)

    def test_last_unit_cannot_move_twice(self):
        self.receive()
        self.run_concurrent(self.move, self.move, expected="validation")
        self.assertEqual(StockMovement.objects.count(), 2)

    def test_duplicate_identifier_registration(self):
        self.run_concurrent(self.register, self.register, expected="integrity")
        self.assertEqual(SerializedStockUnit.objects.count(), 1)

    def test_policy_change_first_rejects_anonymous_posting(self):
        self.run_concurrent(lambda: parts.update_spare_part(spare_part=self.part, serialization_policy="REQUIRED_SERIAL"),
            self.receive, expected="validation")
        self.assertEqual(StockMovement.objects.count(), 0)

    def test_posting_first_permanently_locks_policy(self):
        self.run_concurrent(self.receive,
            lambda: parts.update_spare_part(spare_part=self.part, serialization_policy="OPTIONAL_SERIAL"), expected="validation")
        self.part.refresh_from_db()
        self.assertEqual(self.part.serialization_policy, "NOT_SERIALIZED")

    def test_unit_registration_first_locks_policy(self):
        self.run_concurrent(self.register,
            lambda: parts.update_spare_part(spare_part=self.serial_part, serialization_policy="NOT_SERIALIZED"), expected="validation")

    def test_policy_change_first_rejects_unit_registration(self):
        self.run_concurrent(lambda: parts.update_spare_part(spare_part=self.serial_part, serialization_policy="NOT_SERIALIZED"),
            self.register, expected="validation")
        self.assertEqual(SerializedStockUnit.objects.count(), 0)

    def test_location_deactivation_first_rejects_posting(self):
        self.run_concurrent(lambda: s.deactivate_location(actor=self.actor, location=self.location), self.receive, expected="validation")

    def test_posting_first_prevents_nonempty_location_deactivation(self):
        self.run_concurrent(self.receive, lambda: s.deactivate_location(actor=self.actor, location=self.location), expected="validation")

    def test_part_deactivation_first_rejects_posting(self):
        self.run_concurrent(lambda: parts.deactivate_spare_part(spare_part=self.part), self.receive, expected="validation")

    def test_posting_first_then_part_deactivation_preserves_history(self):
        self.run_concurrent(self.receive, lambda: parts.deactivate_spare_part(spare_part=self.part), expected="success")
        self.assertEqual(q.stock_on_hand(actor=self.actor, location=self.location, spare_part=self.part), 1)
        self.assertEqual(q.available_stock(actor=self.actor, location=self.location, spare_part=self.part), 0)

    def test_serialized_unit_movement_race(self):
        unit = self.register()
        self.receive(spare_part=self.serial_part, units=[unit])
        self.run_concurrent(lambda: self.move(spare_part=self.serial_part, units=[unit]),
            lambda: self.move(spare_part=self.serial_part, units=[unit]), expected="validation")

    def test_opposite_direction_moves_use_same_lock_order(self):
        self.receive()
        self.run_concurrent(self.move, lambda: self.move(source=self.destination, destination=self.location), expected="success")
        self.assertEqual(q.stock_on_hand(actor=self.actor, location=self.location, spare_part=self.part), 1)

    def test_different_positions_do_not_serialize(self):
        self.run_concurrent(self.receive, lambda: self.receive(destination=self.destination), expected="success", should_block=False)

    def test_failed_posting_rollback_allows_waiting_writer(self):
        class Abort(Exception):
            pass
        def rollback():
            try:
                with transaction.atomic():
                    self.receive()
                    raise Abort()
            except Abort:
                pass
            # Keep a compatible dependency lock after the deliberate rollback.
            from apps.parts.models import SparePart
            SparePart.objects.select_for_update().get(pk=self.part.pk)
        self.run_concurrent(rollback, self.receive, expected="success")
        self.assertEqual(StockMovement.objects.count(), 1)

    def test_actual_stale_admin_location_submission(self):
        from django.test import Client
        from django.urls import reverse
        client = Client()
        client.force_login(self.actor)
        url = reverse("admin:inventory_inventorylocation_change", args=[self.location.pk])
        token = client.get(url).context["adminform"].form.initial["revision"]
        responses = []
        def submit():
            responses.append(client.post(url, dict(name="Stale edit", revision=token, _save="Save"), follow=True))
        self.run_concurrent(lambda: s.deactivate_location(actor=self.actor, location=self.location), submit, expected="success")
        self.assertContains(responses[0], "Operation rejected")
        self.location.refresh_from_db()
        self.assertFalse(self.location.is_active)
        self.assertEqual(self.location.name, "Main")
