"""Tests barcode parsing for all suppliers."""

from django.urls import reverse

from company.models import Company, ManufacturerPart, SupplierPart
from InvenTree.unit_test import InvenTreeAPITestCase
from order.models import PurchaseOrder, PurchaseOrderLineItem
from part.models import Part
from plugin.registry import registry
from stock.models import StockItem, StockLocation


class SupplierBarcodeTests(InvenTreeAPITestCase):
    """Tests barcode parsing for all suppliers."""

    SCAN_URL = reverse('api-barcode-scan')
    roles = [
        'stock.view',
        'stock_location.view',
        'part.view',
        'company.view',
        'order.view',
    ]

    @classmethod
    def setUpTestData(cls):
        """Create supplier parts for barcodes."""
        super().setUpTestData()

        # Enable the plugins
        registry.set_plugin_state('digikeyplugin', True)
        registry.set_plugin_state('mouserplugin', True)
        registry.set_plugin_state('lcscplugin', True)
        registry.set_plugin_state('tmeplugin', True)

        part = Part.objects.create(name='Test Part', description='Test Part')

        manufacturer = Company.objects.create(
            name='Test Manufacturer', is_manufacturer=True
        )

        mpart1 = ManufacturerPart.objects.create(
            part=part, manufacturer=manufacturer, MPN='MC34063ADR'
        )
        mpart2 = ManufacturerPart.objects.create(
            part=part, manufacturer=manufacturer, MPN='LDK320ADU33R'
        )

        digikey_supplier = Company.objects.create(name='Supplier', is_supplier=True)
        mouser_supplier = Company.objects.create(name='Mouser Test', is_supplier=True)

        supplier_parts = [
            SupplierPart(
                SKU='296-LM358BIDDFRCT-ND', part=part, supplier=digikey_supplier
            ),
            SupplierPart(SKU='C312270', part=part, supplier=digikey_supplier),
            SupplierPart(SKU='WBP-302', part=part, supplier=digikey_supplier),
            SupplierPart(
                SKU='1', part=part, manufacturer_part=mpart1, supplier=mouser_supplier
            ),
            SupplierPart(
                SKU='2', part=part, manufacturer_part=mpart2, supplier=mouser_supplier
            ),
        ]

        SupplierPart.objects.bulk_create(supplier_parts)

        # Assign supplier information to the plugins
        # Add supplier information to each custom plugin
        registry.set_plugin_state('digikeyplugin', True)
        digikey_plugin = registry.get_plugin('digikeyplugin')
        digikey_plugin.set_setting('SUPPLIER_ID', digikey_supplier.pk)

        registry.set_plugin_state('mouserplugin', True)
        mouser_plugin = registry.get_plugin('mouserplugin')
        mouser_plugin.set_setting('SUPPLIER_ID', mouser_supplier.pk)

    def test_digikey_barcode(self):
        """Test digikey barcode."""
        result = self.post(
            self.SCAN_URL, data={'barcode': DIGIKEY_BARCODE}, expected_code=200
        )
        self.assertEqual(result.data['plugin'], 'DigiKeyBarcodePlugin')

        supplier_part_data = result.data.get('supplierpart')
        self.assertIn('pk', supplier_part_data)

        supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk'])
        self.assertEqual(supplier_part.SKU, '296-LM358BIDDFRCT-ND')

    def test_digikey_2_barcode(self):
        """Test digikey barcode which uses 30P instead of P."""
        result = self.post(
            self.SCAN_URL, data={'barcode': DIGIKEY_BARCODE_2}, expected_code=200
        )

        self.assertEqual(result.data['plugin'], 'DigiKeyBarcodePlugin')

        supplier_part_data = result.data.get('supplierpart')
        self.assertIn('pk', supplier_part_data)

        supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk'])
        self.assertEqual(supplier_part.SKU, '296-LM358BIDDFRCT-ND')

    def test_digikey_3_barcode(self):
        """Test digikey barcode which is invalid."""
        self.post(self.SCAN_URL, data={'barcode': DIGIKEY_BARCODE_3}, expected_code=400)

    def test_mouser_barcode(self):
        """Test mouser barcode with custom order number."""
        result = self.post(
            self.SCAN_URL, data={'barcode': MOUSER_BARCODE}, expected_code=200
        )

        supplier_part_data = result.data.get('supplierpart')
        self.assertIn('pk', supplier_part_data)

        supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk'])
        self.assertEqual(supplier_part.SKU, '1')

    def test_old_mouser_barcode(self):
        """Test old mouser barcode with messed up header."""
        registry.set_plugin_state('mouserplugin', False)

        # Initial scan should fail - plugin not enabled
        self.post(
            self.SCAN_URL, data={'barcode': MOUSER_BARCODE_OLD}, expected_code=400
        )

        registry.set_plugin_state('mouserplugin', True)

        result = self.post(
            self.SCAN_URL, data={'barcode': MOUSER_BARCODE_OLD}, expected_code=200
        )

        supplier_part_data = result.data.get('supplierpart')
        self.assertIn('pk', supplier_part_data)
        supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk'])
        self.assertEqual(supplier_part.SKU, '2')

    def test_lcsc_barcode(self):
        """Test LCSC barcode."""
        result = self.post(
            self.SCAN_URL, data={'barcode': LCSC_BARCODE}, expected_code=200
        )

        self.assertEqual(result.data['plugin'], 'LCSCBarcodePlugin')

        supplier_part_data = result.data.get('supplierpart')
        self.assertIn('pk', supplier_part_data)

        supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk'])
        self.assertEqual(supplier_part.SKU, 'C312270')

    def test_tme_qrcode(self):
        """Test TME QR-Code."""
        result = self.post(
            self.SCAN_URL, data={'barcode': TME_QRCODE}, expected_code=200
        )

        self.assertEqual(result.data['plugin'], 'TMEBarcodePlugin')

        supplier_part_data = result.data.get('supplierpart')
        self.assertIn('pk', supplier_part_data)
        supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk'])
        self.assertEqual(supplier_part.SKU, 'WBP-302')

    def test_tme_barcode2d(self):
        """Test TME DataMatrix-Code."""
        result = self.post(
            self.SCAN_URL, data={'barcode': TME_DATAMATRIX_CODE}, expected_code=200
        )

        self.assertEqual(result.data['plugin'], 'TMEBarcodePlugin')

        supplier_part_data = result.data.get('supplierpart')
        self.assertIn('pk', supplier_part_data)

        supplier_part = SupplierPart.objects.get(pk=supplier_part_data['pk'])
        self.assertEqual(supplier_part.SKU, 'WBP-302')


class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase):
    """Tests barcode scanning to receive a purchase order item."""

    roles = ['stock.view', 'stock_location.view']

    def setUp(self):
        """Create supplier part and purchase_order."""
        super().setUp()

        registry.set_plugin_state('digikeyplugin', True)
        registry.set_plugin_state('mouserplugin', True)
        registry.set_plugin_state('lcscplugin', True)
        registry.set_plugin_state('tmeplugin', True)

        self.loc_1 = StockLocation.objects.create(name='Location 1')
        self.loc_2 = StockLocation.objects.create(name='Location 2')

        part = Part.objects.create(name='Test Part', description='Test Part')
        digikey_supplier = Company.objects.create(name='Supplier', is_supplier=True)
        manufacturer = Company.objects.create(
            name='Test Manufacturer', is_manufacturer=True
        )

        mouser = Company.objects.create(name='Mouser Test', is_supplier=True)
        mpart = ManufacturerPart.objects.create(
            part=part, manufacturer=manufacturer, MPN='MC34063ADR'
        )

        self.purchase_order1 = PurchaseOrder.objects.create(
            supplier_reference='72991337',
            supplier=digikey_supplier,
            destination=self.loc_1,
        )

        supplier_parts1 = [
            SupplierPart(SKU=f'1_{i}', part=part, supplier=digikey_supplier)
            for i in range(6)
        ]

        supplier_parts1.insert(
            2,
            SupplierPart(
                SKU='296-LM358BIDDFRCT-ND', part=part, supplier=digikey_supplier
            ),
        )

        for supplier_part in supplier_parts1:
            supplier_part.save()
            self.purchase_order1.add_line_item(supplier_part, 8, destination=self.loc_2)

        self.purchase_order2 = PurchaseOrder.objects.create(
            reference='P0-1337', supplier=mouser, destination=self.loc_1
        )

        self.purchase_order2.place_order()
        supplier_parts2 = [
            SupplierPart(SKU=f'2_{i}', part=part, supplier=mouser) for i in range(6)
        ]

        supplier_parts2.insert(
            3,
            SupplierPart(SKU='42', part=part, manufacturer_part=mpart, supplier=mouser),
        )

        for supplier_part in supplier_parts2:
            supplier_part.save()
            self.purchase_order2.add_line_item(supplier_part, 5, destination=self.loc_2)

        # Add supplier information to each custom plugin
        registry.set_plugin_state('digikeyplugin', True)
        digikey_plugin = registry.get_plugin('digikeyplugin')
        digikey_plugin.set_setting('SUPPLIER_ID', digikey_supplier.pk)

        registry.set_plugin_state('mouserplugin', True)
        mouser_plugin = registry.get_plugin('mouserplugin')
        mouser_plugin.set_setting('SUPPLIER_ID', mouser.pk)

    def test_receive(self):
        """Test receiving an item from a barcode."""
        url = reverse('api-barcode-po-receive')

        # First attempt - PO is not yet "placed"
        result1 = self.post(url, data={'barcode': DIGIKEY_BARCODE}, expected_code=400)

        self.assertIn('received against an order marked as', result1.data['error'])

        # Next, place the order - receipt should then work
        self.purchase_order1.place_order()

        result2 = self.post(url, data={'barcode': DIGIKEY_BARCODE}, expected_code=200)
        self.assertIn('success', result2.data)

        # Attempt to receive the same item again
        # Already received - failure expected
        result3 = self.post(url, data={'barcode': DIGIKEY_BARCODE}, expected_code=400)
        self.assertEqual(result3.data['error'], 'Item has already been received')

        result5 = self.post(
            reverse('api-barcode-scan'),
            data={'barcode': DIGIKEY_BARCODE},
            expected_code=200,
        )

        stock_item = StockItem.objects.get(pk=result5.data['stockitem']['pk'])
        self.assertEqual(stock_item.supplier_part.SKU, '296-LM358BIDDFRCT-ND')
        self.assertEqual(stock_item.quantity, 10)
        self.assertEqual(stock_item.location, self.loc_2)

    def test_no_auto_allocate(self):
        """Test with auto_allocate explicitly disabled."""
        url = reverse('api-barcode-po-receive')
        self.purchase_order1.place_order()

        response = self.post(
            url,
            data={'barcode': DIGIKEY_BARCODE, 'auto_allocate': False},
            expected_code=200,
        )

        self.assertEqual(response.data['plugin'], 'DigiKeyBarcodePlugin')
        self.assertIn('action_required', response.data)
        item = response.data['lineitem']
        self.assertEqual(item['quantity'], 10.0)
        self.assertEqual(item['purchase_order'], self.purchase_order1.pk)
        self.assertEqual(item['location'], self.loc_2.pk)

    def test_receive_custom_order_number(self):
        """Test receiving an item from a barcode with a custom order number."""
        url = reverse('api-barcode-po-receive')
        result1 = self.post(url, data={'barcode': MOUSER_BARCODE}, expected_code=200)
        self.assertIn('success', result1.data)

        # Scan the same barcode again - should be resolved to the created item
        result2 = self.post(
            reverse('api-barcode-scan'),
            data={'barcode': MOUSER_BARCODE},
            expected_code=200,
        )
        stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk'])

        self.assertEqual(stock_item.supplier_part.SKU, '42')
        self.assertEqual(stock_item.supplier_part.manufacturer_part.MPN, 'MC34063ADR')
        self.assertEqual(stock_item.quantity, 3)
        self.assertEqual(stock_item.location, self.loc_2)
        self.assertEqual(stock_item.barcode_data, MOUSER_BARCODE)

    def test_receive_stock_location(self):
        """Test receiving an item when the location is provided."""
        stock_location = StockLocation.objects.create(name='Test Location')

        url = reverse('api-barcode-po-receive')

        result1 = self.post(
            url,
            data={'barcode': MOUSER_BARCODE, 'location': stock_location.pk},
            expected_code=200,
        )

        self.assertIn('success', result1.data)

        result2 = self.post(
            reverse('api-barcode-scan'),
            data={'barcode': MOUSER_BARCODE},
            expected_code=200,
        )

        stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk'])
        self.assertEqual(stock_item.location, stock_location)

    def test_receive_default_line_item_location(self):
        """Test receiving an item into the default line_item location."""
        StockLocation.objects.create(name='Test Location 1')
        stock_location2 = StockLocation.objects.create(name='Test Location 2')

        line_item = PurchaseOrderLineItem.objects.filter(part__SKU='42')[0]
        line_item.destination = stock_location2
        line_item.save()

        url = reverse('api-barcode-po-receive')
        result1 = self.post(url, data={'barcode': MOUSER_BARCODE}, expected_code=200)
        self.assertIn('success', result1.data)

        result2 = self.post(
            reverse('api-barcode-scan'),
            data={'barcode': MOUSER_BARCODE},
            expected_code=200,
        )
        stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk'])
        self.assertEqual(stock_item.location, stock_location2)

    def test_receive_default_part_location(self):
        """Test receiving an item into the default part location."""
        StockLocation.objects.create(name='Test Location 1')
        stock_location2 = StockLocation.objects.create(name='Test Location 2')

        # Ensure no other fallback locations are set
        # This is to ensure that the part location is used instead
        self.purchase_order2.destination = None
        self.purchase_order2.save()

        for line in self.purchase_order2.lines.all():
            line.destination = None
            line.save()

        part = Part.objects.all()[0]
        part.default_location = stock_location2
        part.save()

        url = reverse('api-barcode-po-receive')
        result1 = self.post(url, data={'barcode': MOUSER_BARCODE}, expected_code=200)
        self.assertIn('success', result1.data)

        result2 = self.post(
            reverse('api-barcode-scan'),
            data={'barcode': MOUSER_BARCODE},
            expected_code=200,
        )
        stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk'])
        self.assertEqual(stock_item.location, stock_location2)

    def test_receive_specific_order_and_location(self):
        """Test receiving an item from a specific order into a specific location."""
        StockLocation.objects.create(name='Test Location 1')
        stock_location2 = StockLocation.objects.create(name='Test Location 2')

        url = reverse('api-barcode-po-receive')
        barcode = MOUSER_BARCODE.replace('\x1dKP0-1337', '')
        result1 = self.post(
            url,
            data={
                'barcode': barcode,
                'purchase_order': self.purchase_order2.pk,
                'location': stock_location2.pk,
            },
            expected_code=200,
        )
        self.assertIn('success', result1.data)

        result2 = self.post(
            reverse('api-barcode-scan'), data={'barcode': barcode}, expected_code=200
        )
        stock_item = StockItem.objects.get(pk=result2.data['stockitem']['pk'])
        self.assertEqual(stock_item.location, stock_location2)

    def test_receive_missing_quantity(self):
        """Test receiving an with missing quantity information."""
        url = reverse('api-barcode-po-receive')
        barcode = MOUSER_BARCODE.replace('\x1dQ3', '')
        response = self.post(url, data={'barcode': barcode}, expected_code=200)

        self.assertIn('action_required', response.data)

        self.assertIn('lineitem', response.data)

        # Quantity should be pre-filled with the remaining quantity
        self.assertEqual(5, response.data['lineitem']['quantity'])

    def test_receive_includes_no_match_false(self):
        """Successful receive merges no_match=False from the plugin response."""
        self.purchase_order1.place_order()
        url = reverse('api-barcode-po-receive')

        result = self.post(url, data={'barcode': DIGIKEY_BARCODE}, expected_code=200)

        self.assertIn('success', result.data)
        self.assertIn('no_match', result.data)
        self.assertFalse(result.data['no_match'])

    def test_partial_match_po_found_no_supplier_part(self):
        """Barcode references a known PO but the SKU has no matching supplier part."""
        url = reverse('api-barcode-po-receive')

        # '1K72991337' matches purchase_order1 via supplier_reference; SKU is unknown
        result = self.post(
            url, data={'barcode': DIGIKEY_BARCODE_PO_NO_PART}, expected_code=400
        )

        self.assertIn('error', result.data)
        self.assertIn('Purchase order Found', result.data['error'])
        self.assertIn('No supplier Part Match', result.data['error'])

        self.assertIn('supplier_matches', result.data)
        matches = result.data['supplier_matches']
        self.assertIsNotNone(matches['purchase_order'])
        # DRF's ValidationError converts None leaves to ErrorDetail('None')
        self.assertEqual(str(matches['supplier_part']), 'None')
        self.assertIsNotNone(matches['supplier'])
        self.assertTrue(matches['no_match'])

    def test_partial_match_supplier_part_found_no_po(self):
        """Barcode references a known supplier part but the PO reference has no match."""
        url = reverse('api-barcode-po-receive')

        # 'P296-LM358BIDDFRCT-ND' matches the existing supplier part; PO ref is unknown
        result = self.post(
            url, data={'barcode': DIGIKEY_BARCODE_PART_NO_PO}, expected_code=400
        )

        self.assertIn('error', result.data)
        self.assertIn('Supplier Part Found', result.data['error'])
        self.assertIn('No Purchase Order Match', result.data['error'])

        self.assertIn('supplier_matches', result.data)
        matches = result.data['supplier_matches']
        # DRF's ValidationError converts None leaves to ErrorDetail('None')
        self.assertEqual(str(matches['purchase_order']), 'None')
        self.assertIsNotNone(matches['supplier_part'])
        self.assertIsNotNone(matches['supplier'])
        self.assertTrue(matches['no_match'])

    def test_no_supplier_plugin_error(self):
        """Plugins that cannot resolve a supplier are listed in no_supplier_plugin_error."""
        url = reverse('api-barcode-po-receive')

        digikey_plugin = registry.get_plugin('digikeyplugin')
        original_supplier_id = digikey_plugin.get_setting('SUPPLIER_ID')

        # Use a non-existent PK so get_supplier() returns None
        digikey_plugin.set_setting('SUPPLIER_ID', 99999)
        if hasattr(digikey_plugin, '_supplier'):
            del digikey_plugin._supplier

        try:
            result = self.post(
                url, data={'barcode': DIGIKEY_BARCODE}, expected_code=400
            )

            self.assertIn('no_supplier_plugin_error', result.data)
            self.assertIn('digikeyplugin', result.data['no_supplier_plugin_error'])
        finally:
            digikey_plugin.set_setting('SUPPLIER_ID', original_supplier_id)
            if hasattr(digikey_plugin, '_supplier'):
                del digikey_plugin._supplier

    def test_no_supplier_matches_when_both_missing(self):
        """When neither PO nor supplier part can be resolved, no supplier_matches is set."""
        url = reverse('api-barcode-po-receive')

        # Completely unknown barcode — no PO, no part, no supplier
        result = self.post(
            url, data={'barcode': 'COMPLETELY-UNKNOWN-BARCODE-XYZ'}, expected_code=400
        )

        self.assertIn('error', result.data)
        self.assertNotIn('supplier_matches', result.data)


DIGIKEY_BARCODE = (
    '[)>\x1e06\x1dP296-LM358BIDDFRCT-ND\x1d1PLM358BIDDFR\x1dK\x1d1K72991337\x1d'
    '10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK\x1d12Z15221337\x1d13Z361337'
    '\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000'
    '00000000000000000000000000000000000000000000000000000000000000000000000000'
    '0000000000000000000000000000000000'
)

# Uses 30P instead of P
DIGIKEY_BARCODE_2 = (
    '[)>\x1e06\x1d30P296-LM358BIDDFRCT-ND\x1dK\x1d1K72991337\x1d'
    '10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK\x1d12Z15221337\x1d13Z361337'
    '\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000'
    '00000000000000000000000000000000000000000000000000000000000000000000000000'
    '0000000000000000000000000000000000'
)

# Invalid code
DIGIKEY_BARCODE_3 = (
    '[)>\x1e06\x1dPnonsense\x1d30Pnonsense\x1d1Pnonsense\x1dK\x1d1K72991337\x1d'
    '10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK\x1d12Z15221337\x1d13Z361337'
    '\x1d20Z0000000000000000000000000000000000000000000000000000000000000000000'
    '00000000000000000000000000000000000000000000000000000000000000000000000000'
    '0000000000000000000000000000000000'
)

MOUSER_BARCODE = (
    '[)>\x1e06\x1dKP0-1337\x1d14K011\x1d1PMC34063ADR\x1dQ3\x1d11K073121337\x1d4'
    'LMX\x1d1VTI\x1e\x04'
)

MOUSER_BARCODE_OLD = (
    '>[)>06\x1dK21421337\x1d14K033\x1d1PLDK320ADU33R\x1dQ32\x1d11K060931337\x1d'
    '4LCN\x1d1VSTMicro'
)

LCSC_BARCODE = (
    '{pbn:PICK2009291337,on:SO2009291337,pc:C312270,pm:ST-1-102-A01-T000-RS,qty'
    ':2,mc:,cc:1,pdi:34421807}'
)

TME_QRCODE = (
    'QTY:1 PN:WBP-302 PO:19361337/1 CPO:PO-2023-06-08-001337 MFR:WISHERENTERPRI'
    'SE MPN:WBP-302 RoHS https://www.tme.eu/details/WBP-302'
)

TME_DATAMATRIX_CODE = 'PWBP-302 1PMPNWBP-302 Q1 K19361337/1'

# DigiKey barcode: '1K72991337' matches purchase_order1 via supplier_reference,
# but 'PNONEXISTENT-SKU' has no matching supplier part in the test database.
DIGIKEY_BARCODE_PO_NO_PART = (
    '[)>\x1e06\x1dPNONEXISTENT-SKU-XXXX\x1d1PNONEXISTENT-MPN\x1dK\x1d1K72991337\x1d'
    '10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK'
)

# DigiKey barcode: 'P296-LM358BIDDFRCT-ND' matches the existing supplier part,
# but '1KBADORDER-XXXXX' matches no purchase order in the test database.
DIGIKEY_BARCODE_PART_NO_PO = (
    '[)>\x1e06\x1dP296-LM358BIDDFRCT-ND\x1d1PLM358BIDDFR\x1dK\x1d1KBADORDER-XXXXX\x1d'
    '10K85781337\x1d11K1\x1d4LPH\x1dQ10\x1d11ZPICK'
)
