import uuid

from django.test import TransactionTestCase

from apps.organization import test_assignment_concurrency as concurrency_helpers
from apps.parts import services as parts
from . import document_services as d, services as s
from .models import GoodsReceipt, SerializedStockUnit, StockMovement
from .tests import make_inventory_fixture
from . import test_documents as helpers


class DocumentConcurrencyTests(TransactionTestCase):
    run_concurrent = concurrency_helpers.AssignmentConcurrencyTests.run_concurrent
    receipt = helpers.DocumentFixture.receipt
    transfer = helpers.DocumentFixture.transfer
    post = helpers.DocumentFixture.post
    dispatch = helpers.DocumentFixture.dispatch
    receive_transfer = helpers.DocumentFixture.receive_transfer

    def setUp(self):
        make_inventory_fixture(self)

    def test_concurrent_company_number_allocation(self):
        def create():
            d.create_goods_receipt(actor=self.actor, destination=self.location)
        self.run_concurrent(create, create, expected="success")
        self.assertEqual(list(GoodsReceipt.objects.order_by("number").values_list("number", flat=True)), ["GRN-00000001", "GRN-00000002"])

    def test_duplicate_receipt_posting(self):
        row = self.receipt()
        revision = row.updated_at.isoformat()
        def post():
            return d.post_goods_receipt(actor=self.actor, receipt=row, expected_revision=revision)
        self.run_concurrent(post, post, expected="validation")
        self.assertEqual(StockMovement.objects.count(), 1)

    def test_two_transfers_compete_for_last_stock(self):
        self.post(self.receipt(1))
        first, second = self.transfer(), self.transfer()
        self.run_concurrent(lambda: self.dispatch(first), lambda: self.dispatch(second), expected="validation")
        second.refresh_from_db()
        self.assertEqual(second.status, "DRAFT")

    def test_transfer_competes_with_receipt_at_source(self):
        self.post(self.receipt(1))
        transfer, receipt = self.transfer(), self.receipt(2)
        self.run_concurrent(lambda: self.dispatch(transfer), lambda: self.post(receipt), expected="success")

    def test_destination_deactivation_before_dispatch(self):
        self.post(self.receipt())
        transfer = self.transfer()
        self.run_concurrent(lambda: s.deactivate_location(actor=self.actor, location=self.destination),
            lambda: self.dispatch(transfer), expected="validation")

    def test_dispatch_before_destination_deactivation(self):
        self.post(self.receipt())
        transfer = self.transfer()
        self.run_concurrent(lambda: self.dispatch(transfer),
            lambda: s.deactivate_location(actor=self.actor, location=self.destination), expected="validation")

    def test_serialized_unit_cannot_dispatch_on_two_documents(self):
        self.post(self.receipt(1, part=self.serial_part, identifiers=["ONLY"] ))
        unit = SerializedStockUnit.objects.get()
        first, second = self.transfer(part=self.serial_part, units=[unit]), self.transfer(part=self.serial_part, units=[unit])
        self.run_concurrent(lambda: self.dispatch(first), lambda: self.dispatch(second), expected="validation")

    def test_receive_cannot_use_stale_pre_dispatch_revision(self):
        self.post(self.receipt())
        transfer = self.transfer()
        old = transfer.updated_at.isoformat()
        self.run_concurrent(lambda: self.dispatch(transfer),
            lambda: d.receive_stock_transfer(actor=self.actor, transfer=transfer, expected_revision=old), expected="validation")
        self.receive_transfer(transfer)

    def test_duplicate_transfer_receipt(self):
        self.post(self.receipt())
        transfer = self.dispatch(self.transfer())
        old = transfer.updated_at.isoformat()
        def receive():
            return d.receive_stock_transfer(actor=self.actor, transfer=transfer, expected_revision=old)
        self.run_concurrent(receive, receive, expected="validation")

    def test_cancel_before_dispatch(self):
        self.post(self.receipt())
        transfer = self.transfer()
        old = transfer.updated_at.isoformat()
        self.run_concurrent(lambda: d.cancel_stock_transfer(actor=self.actor, transfer=transfer, expected_revision=old, reason="Cancelled"),
            lambda: d.dispatch_stock_transfer(actor=self.actor, transfer=transfer, expected_revision=old), expected="validation")

    def test_dispatch_before_cancel(self):
        self.post(self.receipt())
        transfer = self.transfer()
        old = transfer.updated_at.isoformat()
        self.run_concurrent(lambda: d.dispatch_stock_transfer(actor=self.actor, transfer=transfer, expected_revision=old),
            lambda: d.cancel_stock_transfer(actor=self.actor, transfer=transfer, expected_revision=old, reason="Cancelled"), expected="validation")

    def test_policy_change_before_first_formal_posting(self):
        receipt = self.receipt(1)
        self.run_concurrent(lambda: parts.update_spare_part(spare_part=self.part, serialization_policy="REQUIRED_SERIAL"),
            lambda: self.post(receipt), expected="validation")

    def test_first_formal_posting_before_policy_change(self):
        receipt = self.receipt(1)
        self.run_concurrent(lambda: self.post(receipt),
            lambda: parts.update_spare_part(spare_part=self.part, serialization_policy="OPTIONAL_SERIAL"), expected="validation")

    def test_two_locations_cannot_receive_same_serial(self):
        first = self.receipt(1, part=self.serial_part, identifiers=["RACE"])
        second = self.receipt(1, part=self.serial_part, identifiers=["RACE"], destination=self.destination)
        self.run_concurrent(lambda: self.post(first), lambda: self.post(second), expected="integrity")
        self.assertEqual(SerializedStockUnit.objects.count(), 1)

    def test_stale_admin_post_confirmation_race(self):
        from django.test import Client
        from django.urls import reverse
        receipt = self.receipt()
        old = receipt.updated_at.isoformat()
        client = Client()
        client.force_login(self.actor)
        url = reverse("admin:inventory_goodsreceipt_changelist")
        data = dict(action="post_selected", _selected_action=[str(receipt.pk)])
        page = client.post(url, data)
        data.update(confirm_transition="1", transition_revision=page.context["transition_revision"])
        responses = []
        self.run_concurrent(lambda: d.update_goods_receipt(actor=self.actor, receipt=receipt, expected_revision=old, note="New review needed"),
            lambda: responses.append(client.post(url, data, follow=True)), expected="success")
        self.assertContains(responses[0], "Transition rejected")
        self.assertEqual(StockMovement.objects.count(), 0)
