# -*- coding: utf-8 -*-

from django.test import TestCase

from django.core.exceptions import ValidationError
from django.db.utils import IntegrityError

from build.models import Build, BuildItem
from stock.models import StockItem
from part.models import Part, BomItem
from InvenTree import status_codes as status


class BuildTest(TestCase):
    """
    Run some tests to ensure that the Build model is working properly.
    """

    def setUp(self):
        """
        Initialize data to use for these tests.
        """

        # Create a base "Part"
        self.assembly = Part.objects.create(
            name="An assembled part",
            description="Why does it matter what my description is?",
            assembly=True,
            trackable=True,
        )

        self.sub_part_1 = Part.objects.create(
            name="Widget A",
            description="A widget",
            component=True
        )

        self.sub_part_2 = Part.objects.create(
            name="Widget B",
            description="A widget",
            component=True
        )

        # Create BOM item links for the parts
        BomItem.objects.create(
            part=self.assembly,
            sub_part=self.sub_part_1,
            quantity=10
        )

        BomItem.objects.create(
            part=self.assembly,
            sub_part=self.sub_part_2,
            quantity=25
        )

        # Create a "Build" object to make 10x objects
        self.build = Build.objects.create(
            title="This is a build",
            part=self.assembly,
            quantity=10
        )

        # Create some build output (StockItem) objects
        self.output_1 = StockItem.objects.create(
            part=self.assembly,
            quantity=5,
            is_building=True,
            build=self.build
        )

        self.output_2 = StockItem.objects.create(
            part=self.assembly,
            quantity=5,
            is_building=True,
            build=self.build,
        )

        # Create some stock items to assign to the build
        self.stock_1_1 = StockItem.objects.create(part=self.sub_part_1, quantity=1000)
        self.stock_1_2 = StockItem.objects.create(part=self.sub_part_1, quantity=100)

        self.stock_2_1 = StockItem.objects.create(part=self.sub_part_2, quantity=5000)

    def test_init(self):
        # Perform some basic tests before we start the ball rolling

        self.assertEqual(StockItem.objects.count(), 5)
        
        # Build is PENDING
        self.assertEqual(self.build.status, status.BuildStatus.PENDING)

        # Build has two build outputs
        self.assertEqual(self.build.output_count, 2)

        # None of the build outputs have been completed
        for output in self.build.get_build_outputs().all():
            self.assertFalse(self.build.isFullyAllocated(output))

        self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1, self.output_1))
        self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2))

        self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_1), 50)
        self.assertEqual(self.build.unallocatedQuantity(self.sub_part_1, self.output_2), 50)
        self.assertEqual(self.build.unallocatedQuantity(self.sub_part_2, self.output_1), 125)
        self.assertEqual(self.build.unallocatedQuantity(self.sub_part_2, self.output_2), 125)

        self.assertFalse(self.build.is_complete)

    def test_build_item_clean(self):
        # Ensure that dodgy BuildItem objects cannot be created

        stock = StockItem.objects.create(part=self.assembly, quantity=99)

        # Create a BuiltItem which points to an invalid StockItem
        b = BuildItem(stock_item=stock, build=self.build, quantity=10)
        
        with self.assertRaises(ValidationError):
            b.save()

        # Create a BuildItem which has too much stock assigned
        b = BuildItem(stock_item=self.stock_1_1, build=self.build, quantity=9999999)

        with self.assertRaises(ValidationError):
            b.clean()

        # Negative stock? Not on my watch!
        b = BuildItem(stock_item=self.stock_1_1, build=self.build, quantity=-99)

        with self.assertRaises(ValidationError):
            b.clean()

        # Ok, what about we make one that does *not* fail?
        b = BuildItem(stock_item=self.stock_1_1, build=self.build, install_into=self.output_1, quantity=10)
        b.save()

    def test_duplicate_bom_line(self):
        # Try to add a duplicate BOM item - it should fail!

        with self.assertRaises(IntegrityError):
            BomItem.objects.create(
                part=self.assembly,
                sub_part=self.sub_part_1,
                quantity=99
            )

    def allocate_stock(self, q11, q12, q21, output):
        # Assign stock to this build

        if q11 > 0:
            BuildItem.objects.create(
                build=self.build,
                stock_item=self.stock_1_1,
                quantity=q11,
                install_into=output
            )

        if q12 > 0:
            BuildItem.objects.create(
                build=self.build,
                stock_item=self.stock_1_2,
                quantity=q12,
                install_into=output
            )

        if q21 > 0:
            BuildItem.objects.create(
                build=self.build,
                stock_item=self.stock_2_1,
                quantity=q21,
                install_into=output,
            )

            # Attempt to create another identical BuildItem
            b = BuildItem(
                build=self.build,
                stock_item=self.stock_2_1,
                quantity=q21
            )

            with self.assertRaises(ValidationError):
                b.clean()

    def test_partial_allocation(self):
        """
        Partially allocate against output 1
        """

        self.allocate_stock(50, 50, 200, self.output_1)

        self.assertTrue(self.build.isFullyAllocated(self.output_1))
        self.assertFalse(self.build.isFullyAllocated(self.output_2))
        self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_1, self.output_1))
        self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_2, self.output_1))
        
        self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_1, self.output_2))
        self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2))

        # Check that the part has been allocated
        self.assertEqual(self.build.allocatedQuantity(self.sub_part_1, self.output_1), 100)

        self.build.unallocateStock(output=self.output_1)
        self.assertEqual(BuildItem.objects.count(), 0)

        # Check that the part has been unallocated
        self.assertEqual(self.build.allocatedQuantity(self.sub_part_1, self.output_1), 0)

    def test_auto_allocate(self):
        """
        Test auto-allocation functionality against the build outputs
        """

        allocations = self.build.getAutoAllocations(self.output_1)

        self.assertEqual(len(allocations), 1)

        self.build.autoAllocate(self.output_1)
        self.assertEqual(BuildItem.objects.count(), 1)

        # Check that one part has been fully allocated to the build output
        self.assertTrue(self.build.isPartFullyAllocated(self.sub_part_2, self.output_1))

        # But, the *other* build output has not been allocated against
        self.assertFalse(self.build.isPartFullyAllocated(self.sub_part_2, self.output_2))

    def test_cancel(self):
        """
        Test cancellation of the build
        """

        # TODO

        """
        self.allocate_stock(50, 50, 200, self.output_1)
        self.build.cancelBuild(None)

        self.assertEqual(BuildItem.objects.count(), 0)
        """
        pass

    def test_complete(self):
        """
        Test completion of a build output
        """

        self.allocate_stock(50, 50, 250, self.output_1)
        self.allocate_stock(50, 50, 250, self.output_2)

        self.assertTrue(self.build.isFullyAllocated(self.output_1))
        self.assertTrue(self.build.isFullyAllocated(self.output_2))

        self.build.completeBuildOutput(self.output_1, None)

        self.assertFalse(self.build.can_complete)

        self.build.completeBuildOutput(self.output_2, None)

        self.assertTrue(self.build.can_complete)

        self.build.complete_build(None)
    
        self.assertEqual(self.build.status, status.BuildStatus.COMPLETE)

        # the original BuildItem objects should have been deleted!
        self.assertEqual(BuildItem.objects.count(), 0)

        # New stock items should have been created!
        self.assertEqual(StockItem.objects.count(), 4)

        A = StockItem.objects.get(pk=self.stock_1_1.pk)

        # This stock item has been depleted!
        with self.assertRaises(StockItem.DoesNotExist):
            StockItem.objects.get(pk=self.stock_1_2.pk)
        
        C = StockItem.objects.get(pk=self.stock_2_1.pk)

        # Stock should have been subtracted from the original items
        self.assertEqual(A.quantity, 900)
        self.assertEqual(C.quantity, 4500)
        
        # And 10 new stock items created for the build output
        outputs = StockItem.objects.filter(build=self.build)

        self.assertEqual(outputs.count(), 2)

        for output in outputs:
            self.assertFalse(output.is_building)
