import io
import json
import uuid
from django.utils import timezone
from django.core.management import call_command
from django.core.management.base import CommandError
from django.test import TestCase
from .test_control import ControlFixture
from .test_usage import UsageFixture, setup_usage
from .tests import InventoryFixture
from . import control_queries as q, services as stock, control_services as s
from apps.parts import services as parts
from apps.service import test_repair as repair
from .models import StockMovement, StockLedgerEntry, SerializedStockUnit, StockReservation


class ReconciliationTests(ControlFixture, InventoryFixture):
    def test_healthy_positions_have_no_anomalies(self):
        self.receive(3)
        self.move(1)
        self.adjust(-1)
        self.assertFalse(any(rows.exists() for rows in q.inventory_anomalies(actor=self.actor).values()))

    def test_control_position_query_is_bounded(self):
        self.receive(3)
        with self.assertNumQueries(1):
            rows = list(q.control_positions(actor=self.actor))
            self.assertEqual(rows[0].on_hand, 3)
            str(rows[0].location)
            str(rows[0].spare_part)

    def test_control_queries_are_scoped(self):
        self.adjust(1)
        self.draft_count()
        self.assertFalse(q.stock_counts(actor=self.staff).exists())
        self.assertFalse(q.stock_adjustments(actor=self.staff).exists())
        self.assertFalse(q.control_positions(actor=self.staff).exists())

    def test_audit_command_is_readonly_and_requires_visible_scope(self):
        self.receive()
        output = io.StringIO()
        call_command("audit_inventory", actor=self.actor.username, stdout=output)
        self.assertFalse(any(json.loads(output.getvalue()).values()))
        with self.assertRaises(CommandError):
            call_command("audit_inventory", actor=self.staff.username, stdout=io.StringIO())

    def test_serialized_net_positions_reconcile_after_loss_and_found(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="AUDIT-UNIT")
        self.receive(1, units=[unit])
        self.adjust(-1, units=[unit])
        self.assertFalse(q.inventory_anomalies(actor=self.actor)["serialized_position_mismatch"].exists())
        self.adjust(1, units=[unit])
        self.assertFalse(q.inventory_anomalies(actor=self.actor)["serialized_position_mismatch"].exists())
        from .queries import serialized_history
        with self.assertNumQueries(1):
            self.assertEqual(len([str(link.unit) for link in serialized_history(actor=self.actor, unit=unit)]), 3)

    def test_detects_negative_position_from_unsupported_direct_writer(self):
        from .locking import lock_positions
        lock_positions([self.location, self.destination], [self.part])
        movement = StockMovement(company=self.company, spare_part=self.part, source=self.location, destination=self.destination,
            quantity=1, kind="MOVE", actor=self.actor, reference="UNSUPPORTED-DIRECT-WRITE", idempotency_key=uuid.uuid4())
        movement._persist()
        for location, delta in ((self.location, -1), (self.destination, 1)):
            StockLedgerEntry(movement=movement, company=self.company, spare_part=self.part, location=location, quantity_delta=delta)._persist()
        self.assertEqual(q.inventory_anomalies(actor=self.actor)["negative_stock"].get().location, self.location)
        with self.assertRaises(CommandError):
            call_command("audit_inventory", actor=self.actor.username, stdout=io.StringIO())

    def test_detects_stale_serial_projection_without_rewriting_history(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="STALE-PROJECTION")
        receipt = self.receive(1, units=[unit])
        self.move(1, units=[unit])
        SerializedStockUnit.objects.filter(pk=unit.pk).update(current_location=self.location, current_movement=receipt, state="IN_STOCK")
        self.assertEqual(q.inventory_anomalies(actor=self.actor)["serialized_position_mismatch"].get().pk, unit.pk)

    def test_detects_false_registered_projection_with_posted_history(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="RESET-PROJECTION")
        self.receive(1, units=[unit])
        SerializedStockUnit.objects.filter(pk=unit.pk).update(current_location=None, current_movement=None, state="REGISTERED")
        self.assertEqual(q.inventory_anomalies(actor=self.actor)["serialized_position_mismatch"].get().pk, unit.pk)


class UsageReconciliationTests(UsageFixture, TestCase):
    @classmethod
    def setUpTestData(cls):
        setup_usage(cls)

    def test_serialized_consumption_has_no_position_anomaly(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="AUDIT-CONSUMED")
        issue = self.prepared_issue(quantity=1, units=[unit])
        _, action = repair.prepared(self)
        self.consume(issue, action, units=[unit])
        self.assertFalse(any(rows.exists() for rows in q.inventory_anomalies(actor=self.actor).values()))
        from .usage_queries import issue_detail
        with self.assertNumQueries(6):
            detail = issue_detail(actor=self.actor, issue=issue)
            for disposition in detail.dispositions.all():
                for link in disposition.movement.unit_links.all():
                    str(link.unit)

    def test_detects_oversubscribed_reservation_from_unsupported_writer(self):
        self.receive(quantity=1)
        request = self.approve(self.request())
        StockReservation(company=self.company, service_case=self.case, line=request.lines.get(), spare_part=self.part,
            location=self.location, quantity=2, reserved_by=self.actor)._persist()
        self.assertEqual(q.inventory_anomalies(actor=self.actor)["over_reserved"].get().location, self.location)

    def test_detects_issued_reservation_without_issue_evidence(self):
        self.receive(quantity=1)
        request = self.approve(self.request())
        timestamp = timezone.now()
        row = StockReservation(company=self.company, service_case=self.case, line=request.lines.get(), spare_part=self.part,
            location=self.location, quantity=1, reserved_by=self.actor, reserved_at=timestamp, status="ISSUED", ended_by=self.actor, ended_at=timestamp)
        row._persist()
        self.assertEqual(q.inventory_anomalies(actor=self.actor)["issued_reservation_without_issue"].get(), row)
