from decimal import Decimal

from django.test import TestCase
from rest_framework.test import APIRequestFactory

from api.models.data.packing import PackingRun
from api.models.data.inventory_batch import InventoryBatch
from api.models.data.products import Product
from api.models.data.raw_material_process import RawMaterialProcess
from api.models.data.raw_materials import (
    RawMaterial,
    RawMaterialPurchase,
    RawMaterialPurchaseItem,
    RawMaterialStock,
)
from api.models.data.stock import Stock
from api.models.data.storage_tank import ProductionStorageTransfer, StorageTank
from api.models.data.vendors import Vendor
from api.serializers.data.packing import PackingComponentInputSerializer
from api.serializers.data.stock import StockSerializer
from api.services.packing import (
    get_available_packing_components,
    get_available_packing_tanks,
)
from api.views.data.packing import PackingRunViewSet


class PackingRunApiTests(TestCase):
    def setUp(self):
        self.tank = StorageTank.objects.create(
            name='Packing Tank',
            capacity_liters=Decimal('40'),
        )
        production = RawMaterialProcess.objects.create(
            product_name='Stored liquid',
            status=RawMaterialProcess.STATUS_MOVED_TO_STORAGE,
            output_quantity_liters=Decimal('20'),
            cost_per_liter_afn=Decimal('2'),
        )
        ProductionStorageTransfer.objects.create(
            production=production,
            storage_tank=self.tank,
            quantity_liters=Decimal('20'),
        )
        vendor = Vendor.objects.create(name='Packaging Supplier')
        purchase = RawMaterialPurchase.objects.create(
            vendor=vendor,
            currency=1,
            exchange_rate=Decimal('1'),
            status='received',
        )
        self.components = []
        self.initial_material_balances = {}
        categories = {
            'bottle': RawMaterial.TYPE_BOTTLE,
            'cap': RawMaterial.TYPE_CAP,
            'label': RawMaterial.TYPE_LABEL,
            'plastic': RawMaterial.TYPE_PLASTIC,
            'carton': RawMaterial.TYPE_CARTON,
        }
        for component_type in ('bottle', 'cap', 'label', 'plastic', 'carton'):
            material = RawMaterial.objects.create(
                name=f'{component_type.title()} material',
                material_type=categories[component_type],
                unit=(RawMaterial.UNIT_KG if component_type ==
                      'cap' else RawMaterial.UNIT_PIECE),
                capacity_liters={
                    'bottle': Decimal('1'),
                    'plastic': Decimal('1'),
                    'carton': Decimal('1'),
                }.get(component_type),
                bottles_per_carton=8 if component_type == 'carton' else None,
            )
            RawMaterialStock.objects.create(
                raw_material=material,
                quantity=Decimal('150' if component_type ==
                                 'bottle' else '100'),
                piece_count=100 if material.unit == RawMaterial.UNIT_KG else 0,
            )
            self.initial_material_balances[material.id] = Decimal(
                '150' if component_type == 'bottle' else '100',
            )
            purchase_item = RawMaterialPurchaseItem.objects.create(
                purchase=purchase,
                raw_material=material,
                quantity=Decimal('100'),
                piece_count=100 if material.unit == RawMaterial.UNIT_KG else 0,
                price_per_unit=Decimal('1'),
                total_price=Decimal('100'),
                price_per_unit_base=Decimal('1'),
                total_price_base=Decimal('100'),
            )
            if component_type == 'bottle':
                second_purchase = RawMaterialPurchase.objects.create(
                    vendor=vendor,
                    currency=1,
                    exchange_rate=Decimal('1'),
                    status='received',
                )
                purchase_item = RawMaterialPurchaseItem.objects.create(
                    purchase=second_purchase,
                    raw_material=material,
                    quantity=Decimal('50'),
                    piece_count=0,
                    price_per_unit=Decimal('2'),
                    total_price=Decimal('100'),
                    price_per_unit_base=Decimal('2'),
                    total_price_base=Decimal('100'),
                )
            self.components.append({
                'component_type': component_type,
                'raw_material': material.id,
                'purchase_item': purchase_item.id,
                # Bottle-based for 8 L @ 1 L bottle / matching plastic / 8 bottles per carton.
                # Backend recalculates these; values here are for request payloads.
                'quantity_pieces': {
                    'bottle': 8,
                    'cap': 8,
                    'label': 8,
                    'plastic': 8,
                    'carton': 1,
                }[component_type],
            })

    def test_packaging_component_accepts_matching_material_purchase(self):
        bottle_selection = self.components[0]
        serializer = PackingComponentInputSerializer(data={
            'component_type': 'bottle',
            'raw_material': bottle_selection['raw_material'],
            'purchase_item': bottle_selection['purchase_item'],
            'quantity_pieces': 3,
        })

        self.assertTrue(serializer.is_valid(), serializer.errors)

    def test_packaging_component_rejects_wrong_material_category(self):
        cap_selection = self.components[1]
        serializer = PackingComponentInputSerializer(data={
            'component_type': 'bottle',
            'raw_material': cap_selection['raw_material'],
            'purchase_item': cap_selection['purchase_item'],
            'quantity_pieces': 3,
        })

        self.assertFalse(serializer.is_valid())
        self.assertIn('raw_material', serializer.errors)

    def test_available_components_include_kg_stock_package_counts(self):
        materials = get_available_packing_components()

        self.assertEqual(len(materials), 5)
        kg_material = next(
            material for material in materials
            if material['raw_material_name'] == 'Cap material'
        )
        self.assertEqual(kg_material['available_quantity'], '100.0000')
        self.assertEqual(kg_material['unit'], 'piece')

        bottle = next(
            material for material in materials if material['material_type'] == 'bottle')
        self.assertEqual(len(bottle['purchase_lots']), 2)
        self.assertEqual(bottle['purchase_lots'][1]
                         ['unit_cost_afn'], '2.000000')

    def test_create_packing_uses_material_volume_to_calculate_packaging_quantity(self):
        class UnrestrictedPackingRunViewSet(PackingRunViewSet):
            permission_classes = []
            authentication_classes = []

        vendor = Vendor.objects.create(name='Volume Vendor')
        purchase = RawMaterialPurchase.objects.create(
            vendor=vendor,
            currency=1,
            exchange_rate=Decimal('1'),
            status='received',
        )

        bottle_material = RawMaterial.objects.create(
            name='Bottle volume material',
            material_type=RawMaterial.TYPE_BOTTLE,
            unit=RawMaterial.UNIT_PIECE,
            capacity_liters=Decimal('0.750'),
        )
        label_material = RawMaterial.objects.create(
            name='Label volume material',
            material_type=RawMaterial.TYPE_LABEL,
            unit=RawMaterial.UNIT_PIECE,
            capacity_liters=Decimal('1'),
        )
        plastic_material = RawMaterial.objects.create(
            name='Plastic volume material',
            material_type=RawMaterial.TYPE_PLASTIC,
            unit=RawMaterial.UNIT_PIECE,
            capacity_liters=Decimal('0.750'),
        )
        carton_material = RawMaterial.objects.create(
            name='Carton volume material',
            material_type=RawMaterial.TYPE_CARTON,
            unit=RawMaterial.UNIT_PIECE,
            capacity_liters=Decimal('0.750'),
            bottles_per_carton=8,
        )
        for material in (bottle_material, label_material, plastic_material, carton_material):
            RawMaterialStock.objects.create(
                raw_material=material,
                quantity=Decimal('200'),
            )
            RawMaterialPurchaseItem.objects.create(
                purchase=purchase,
                raw_material=material,
                quantity=Decimal('200'),
                price_per_unit=Decimal('1'),
                total_price=Decimal('200'),
                price_per_unit_base=Decimal('1'),
                total_price_base=Decimal('200'),
            )

        components = [
            {
                'component_type': 'bottle',
                'raw_material': bottle_material.id,
                'purchase_item': RawMaterialPurchaseItem.objects.filter(raw_material=bottle_material).latest('id').id,
                'quantity_pieces': 20,
            },
            {
                'component_type': 'label',
                'raw_material': label_material.id,
                'purchase_item': RawMaterialPurchaseItem.objects.filter(raw_material=label_material).latest('id').id,
                'quantity_pieces': 20,
            },
            {
                'component_type': 'plastic',
                'raw_material': plastic_material.id,
                'purchase_item': RawMaterialPurchaseItem.objects.filter(raw_material=plastic_material).latest('id').id,
                'quantity_pieces': 20,
            },
            {
                'component_type': 'carton',
                'raw_material': carton_material.id,
                'purchase_item': RawMaterialPurchaseItem.objects.filter(raw_material=carton_material).latest('id').id,
                'quantity_pieces': 3,
            },
        ]

        create_run = UnrestrictedPackingRunViewSet.as_view({'post': 'create'})
        response = create_run(
            APIRequestFactory().post(
                '/packing-runs/',
                {
                    'storage_tank': self.tank.id,
                    'quantity_liters': '15',
                    'bottle_volume_liters': '0.750',
                    'components': components,
                },
                format='json',
            ),
        )

        self.assertEqual(response.status_code, 201, response.data)
        run = PackingRun.objects.get(pk=response.data['id'])
        self.assertEqual(run.bottle_count, 20)
        self.assertEqual(run.product.bottle_capacity_liters, Decimal('0.7500'))
        self.assertEqual(run.product.carton_capacity_liters, Decimal('6.0000'))
        self.assertEqual(run.product.bottles_per_carton, 8)
        self.assertEqual(run.components.get(
            component_type='bottle').quantity_pieces, Decimal('20'))
        self.assertEqual(run.components.get(
            component_type='plastic').quantity_pieces, Decimal('20'))
        self.assertEqual(run.components.get(
            component_type='carton').quantity_pieces, Decimal('3'))
        self.assertEqual(run.finished_stock.piece_amount, Decimal('20'))
        stock_data = StockSerializer(run.finished_stock).data
        self.assertEqual(stock_data['carton_amount'], 2)
        self.assertEqual(stock_data['loose_pieces'], 4)
        batch = InventoryBatch.objects.get(
            product=run.product,
            notes__contains=run.packing_number,
        )
        self.assertEqual(batch.pieces_received, Decimal('20'))
        self.assertEqual(batch.cartons_received, Decimal('2.5000'))

    def test_create_packing_consumes_raw_material_stock_fifo(self):
        class UnrestrictedPackingRunViewSet(PackingRunViewSet):
            permission_classes = []
            authentication_classes = []

        create_run = UnrestrictedPackingRunViewSet.as_view({'post': 'create'})
        response = create_run(
            APIRequestFactory().post(
                '/packing-runs/',
                {
                    'storage_tank': self.tank.id,
                    'quantity_liters': '8',
                    'bottle_volume_liters': '1',
                    'components': self.components,
                },
                format='json',
            ),
        )

        self.assertEqual(response.status_code, 201, response.data)
        run = PackingRun.objects.get(pk=response.data['id'])
        self.assertEqual(run.product.name, '1 L · plastic · 8/ctn')
        self.assertIn('Stored liquid', run.product.description or '')
        self.assertEqual(run.finished_stock.product_id, run.product_id)
        self.assertEqual(
            Stock.objects.get(pk=run.finished_stock_id).piece_amount,
            Decimal('8'),
        )
        self.assertEqual(run.bottle_count, 8)
        self.assertEqual(run.components.count(), 5)
        expected_by_type = {
            'bottle': Decimal('8'),
            'cap': Decimal('8'),
            'label': Decimal('8'),
            'plastic': Decimal('8'),
            'carton': Decimal('1'),
        }
        for component in run.components.select_related('raw_material_stock__raw_material'):
            expected = expected_by_type[component.component_type]
            self.assertEqual(component.quantity_pieces, expected)
            raw_stock = component.raw_material_stock
            if raw_stock.raw_material.unit == RawMaterial.UNIT_KG:
                self.assertEqual(raw_stock.piece_count, 100 - int(expected))
                self.assertEqual(raw_stock.quantity, Decimal('100'))
            else:
                self.assertEqual(
                    raw_stock.quantity,
                    self.initial_material_balances[raw_stock.raw_material_id] - expected,
                )
            if component.component_type == 'bottle':
                self.assertEqual(component.unit_cost_afn, Decimal('2.000000'))
                self.assertEqual(
                    component.inventory_allocations[0]['purchase_item'],
                    self.components[0]['purchase_item'],
                )
            self.assertEqual(
                Decimal(component.inventory_allocations[0]['quantity']),
                Decimal(
                    '0') if raw_stock.raw_material.unit == RawMaterial.UNIT_KG else expected,
            )
            self.assertEqual(
                component.inventory_allocations[0]['piece_count'],
                int(expected) if raw_stock.raw_material.unit == RawMaterial.UNIT_KG else 0,
            )
        self.assertEqual(
            Decimal(get_available_packing_tanks()[0]['available_liters']),
            Decimal('12'),
        )

    def test_create_packing_uses_client_component_quantities(self):
        class UnrestrictedPackingRunViewSet(PackingRunViewSet):
            permission_classes = []
            authentication_classes = []

        # Client can use extra packaging pieces (e.g. waste); finished bottles still follow liquid math.
        custom_components = [
            {
                **item,
                'quantity_pieces': {
                    'bottle': 10,
                    'cap': 10,
                    'label': 10,
                    'plastic': 10,
                    'carton': 2,
                }[item['component_type']],
            }
            for item in self.components
        ]
        create_run = UnrestrictedPackingRunViewSet.as_view({'post': 'create'})
        response = create_run(
            APIRequestFactory().post(
                '/packing-runs/',
                {
                    'storage_tank': self.tank.id,
                    'quantity_liters': '8',
                    'bottle_volume_liters': '1',
                    'components': custom_components,
                },
                format='json',
            ),
        )

        self.assertEqual(response.status_code, 201, response.data)
        run = PackingRun.objects.get(pk=response.data['id'])
        self.assertEqual(run.bottle_count, 8)
        expected_by_type = {
            'bottle': Decimal('10'),
            'cap': Decimal('10'),
            'label': Decimal('10'),
            'plastic': Decimal('10'),
            'carton': Decimal('2'),
        }
        for component in run.components.all():
            self.assertEqual(
                component.quantity_pieces,
                expected_by_type[component.component_type],
            )
        self.assertEqual(
            Stock.objects.get(pk=run.finished_stock_id).piece_amount,
            Decimal('8'),
        )

    def test_create_packing_allows_cap_to_be_omitted(self):
        class UnrestrictedPackingRunViewSet(PackingRunViewSet):
            permission_classes = []
            authentication_classes = []

        cap_stock = RawMaterialStock.objects.get(
            raw_material_id=self.components[1]['raw_material'],
        )
        components_without_cap = [
            item for item in self.components if item['component_type'] != 'cap'
        ]
        create_run = UnrestrictedPackingRunViewSet.as_view({'post': 'create'})
        response = create_run(
            APIRequestFactory().post(
                '/packing-runs/',
                {
                    'storage_tank': self.tank.id,
                    'quantity_liters': '8',
                    'bottle_volume_liters': '1',
                    'components': components_without_cap,
                },
                format='json',
            ),
        )

        self.assertEqual(response.status_code, 201, response.data)
        run = PackingRun.objects.get(pk=response.data['id'])
        self.assertEqual(run.components.count(), 4)
        self.assertFalse(run.components.filter(component_type='cap').exists())
        cap_stock.refresh_from_db()
        self.assertEqual(cap_stock.piece_count, 100)

    def test_update_replaces_tank_and_component_stock_deductions(self):
        class UnrestrictedPackingRunViewSet(PackingRunViewSet):
            permission_classes = []
            authentication_classes = []

        create_run = UnrestrictedPackingRunViewSet.as_view({'post': 'create'})
        create_response = create_run(
            APIRequestFactory().post(
                '/packing-runs/',
                {
                    'storage_tank': self.tank.id,
                    'quantity_liters': '8',
                    'bottle_volume_liters': '1',
                    'components': self.components,
                },
                format='json',
            ),
        )
        self.assertEqual(create_response.status_code,
                         201, create_response.data)
        run_id = create_response.data['id']

        # Client may send adjusted used quantities for 6 L; backend must keep them.
        revised_components = [
            {
                **item,
                'quantity_pieces': {
                    'bottle': 7,
                    'label': 7,
                    'plastic': 7,
                    'carton': 2,
                }[item['component_type']],
            }
            for item in self.components
            if item['component_type'] != 'cap'
        ]
        update_run = UnrestrictedPackingRunViewSet.as_view({'put': 'update'})
        update_response = update_run(
            APIRequestFactory().put(
                f'/packing-runs/{run_id}/',
                {
                    'storage_tank': self.tank.id,
                    'quantity_liters': '6',
                    'bottle_volume_liters': '1',
                    'components': revised_components,
                    'notes': 'Adjusted usage',
                },
                format='json',
            ),
            pk=run_id,
        )

        self.assertEqual(update_response.status_code,
                         200, update_response.data)
        self.assertEqual(PackingRun.objects.count(), 1)
        run = PackingRun.objects.get(pk=run_id)
        self.assertEqual(run.quantity_liters, Decimal('6.0000'))
        self.assertEqual(run.bottle_count, 6)
        self.assertEqual(run.notes, 'Adjusted usage')
        self.assertEqual(run.components.count(), 4)
        self.assertEqual(
            Stock.objects.get(pk=run.finished_stock_id).piece_amount,
            Decimal('6.0000'),
        )
        self.assertEqual(
            Decimal(get_available_packing_tanks()[0]['available_liters']),
            Decimal('14'),
        )
        self.assertEqual(
            InventoryBatch.objects.filter(
                notes__endswith=run.packing_number).count(),
            1,
        )
        self.assertEqual(
            InventoryBatch.objects.get(
                notes__endswith=run.packing_number).remaining_pieces,
            Decimal('6'),
        )
        expected_by_type = {
            'bottle': Decimal('7'),
            'label': Decimal('7'),
            'plastic': Decimal('7'),
            'carton': Decimal('2'),
        }
        for component in run.components.select_related('raw_material_stock__raw_material'):
            expected = expected_by_type[component.component_type]
            self.assertEqual(component.quantity_pieces, expected)
            stock = component.raw_material_stock
            initial = self.initial_material_balances[stock.raw_material_id]
            if stock.raw_material.unit == RawMaterial.UNIT_KG:
                self.assertEqual(stock.piece_count, int(initial - expected))
            else:
                self.assertEqual(stock.quantity, initial - expected)
        cap_stock = RawMaterialStock.objects.get(
            raw_material_id=self.components[1]['raw_material'],
        )
        self.assertEqual(cap_stock.piece_count, 100)

    def test_update_rejected_if_finished_batch_was_consumed(self):
        class UnrestrictedPackingRunViewSet(PackingRunViewSet):
            permission_classes = []
            authentication_classes = []

        create_run = UnrestrictedPackingRunViewSet.as_view({'post': 'create'})
        create_response = create_run(
            APIRequestFactory().post(
                '/packing-runs/',
                {
                    'storage_tank': self.tank.id,
                    'quantity_liters': '8',
                    'bottle_volume_liters': '1',
                    'components': self.components,
                },
                format='json',
            ),
        )
        run_id = create_response.data['id']
        bottle_material_id = self.components[0]['raw_material']
        bottle_stock_before_update = RawMaterialStock.objects.get(
            raw_material_id=bottle_material_id,
        ).quantity
        output_batch = InventoryBatch.objects.get(
            notes__endswith=create_response.data['packing_number'],
        )
        output_batch.remaining_pieces -= Decimal('1')
        output_batch.save(update_fields=['remaining_pieces', 'updated_at'])

        update_run = UnrestrictedPackingRunViewSet.as_view({'put': 'update'})
        response = update_run(
            APIRequestFactory().put(
                f'/packing-runs/{run_id}/',
                {
                    'storage_tank': self.tank.id,
                    'quantity_liters': '6',
                    'bottle_volume_liters': '1',
                    'components': self.components,
                },
                format='json',
            ),
            pk=run_id,
        )

        self.assertEqual(response.status_code, 400, response.data)
        self.assertEqual(
            PackingRun.objects.get(pk=run_id).quantity_liters,
            Decimal('8.0000'),
        )
        self.assertEqual(
            RawMaterialStock.objects.get(
                raw_material_id=bottle_material_id).quantity,
            bottle_stock_before_update,
        )
        self.assertEqual(
            Decimal(get_available_packing_tanks()[0]['available_liters']),
            Decimal('12'),
        )