"""Tests for the build model database migrations."""

from django_test_migrations.contrib.unittest_case import MigratorTestCase

from InvenTree import unit_test


class TestForwardMigrations(MigratorTestCase):
    """Test entire schema migration sequence for the build app."""

    migrate_from = ('build', unit_test.getOldestMigrationFile('build'))
    migrate_to = ('build', unit_test.getNewestMigrationFile('build'))

    def prepare(self):
        """Create initial data!"""
        Part = self.old_state.apps.get_model('part', 'part')

        buildable_part = Part.objects.create(
            name='Widget', description='Buildable Part', active=True
        )

        Build = self.old_state.apps.get_model('build', 'build')

        Build.objects.create(
            part=buildable_part, title='A build of some stuff', quantity=50
        )

    def test_items_exist(self):
        """Test to ensure that the 'assembly' field is correctly configured."""
        Part = self.new_state.apps.get_model('part', 'part')

        self.assertEqual(Part.objects.count(), 1)

        Build = self.new_state.apps.get_model('build', 'build')

        self.assertEqual(Build.objects.count(), 1)

        # Check that the part object now has an assembly field
        part = Part.objects.all().first()
        part.assembly = True
        part.save()
        part.assembly = False
        part.save()


class TestReferenceMigration(MigratorTestCase):
    """Test custom migration which adds 'reference' field to Build model."""

    migrate_from = ('build', unit_test.getOldestMigrationFile('build'))
    migrate_to = ('build', '0018_build_reference')

    def prepare(self):
        """Create some builds."""
        Part = self.old_state.apps.get_model('part', 'part')

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

        Build = self.old_state.apps.get_model('build', 'build')

        Build.objects.create(part=part, title='My very first build', quantity=10)

        Build.objects.create(part=part, title='My very second build', quantity=10)

        Build.objects.create(part=part, title='My very third build', quantity=10)

        # Ensure that the builds *do not* have a 'reference' field
        for build in Build.objects.all():
            with self.assertRaises(AttributeError):
                print(build.reference)

    def test_build_reference(self):
        """Test that the build reference is correctly assigned to the PK of the Build."""
        Build = self.new_state.apps.get_model('build', 'build')

        self.assertEqual(Build.objects.count(), 3)

        # Check that the build reference is properly assigned
        for build in Build.objects.all():
            self.assertEqual(str(build.reference), str(build.pk))


class TestReferencePatternMigration(MigratorTestCase):
    """Unit test for data migration which converts reference to new format.

    Ref: https://github.com/inventree/InvenTree/pull/3267
    """

    migrate_from = ('build', '0019_auto_20201019_1302')
    migrate_to = ('build', '0037_build_priority')

    def prepare(self):
        """Create some initial data prior to migration."""
        Setting = self.old_state.apps.get_model('common', 'inventreesetting')

        # Create a custom existing prefix so we can confirm the operation is working
        Setting.objects.create(key='BUILDORDER_REFERENCE_PREFIX', value='BuildOrder-')

        Part = self.old_state.apps.get_model('part', 'part')

        assembly = Part.objects.create(
            name='Assy 1', description='An assembly', level=0, lft=0, rght=0, tree_id=0
        )

        Build = self.old_state.apps.get_model('build', 'build')

        for idx in range(1, 11):
            Build.objects.create(
                part=assembly,
                title=f'Build {idx}',
                quantity=idx,
                reference=f'{idx + 100}',
                level=0,
                lft=0,
                rght=0,
                tree_id=0,
            )

    def test_reference_migration(self):
        """Test that the reference fields have been correctly updated."""
        Build = self.new_state.apps.get_model('build', 'build')

        for build in Build.objects.all():
            self.assertTrue(build.reference.startswith('BuildOrder-'))

        Setting = self.new_state.apps.get_model('common', 'inventreesetting')

        pattern = Setting.objects.get(key='BUILDORDER_REFERENCE_PATTERN')

        self.assertEqual(pattern.value, 'BuildOrder-{ref:04d}')


class TestBuildLineCreation(MigratorTestCase):
    """Test that build lines are correctly created for existing builds.

    Ref: https://github.com/inventree/InvenTree/pull/4855

    This PR added the 'BuildLine' model, which acts as a link between a Build and a BomItem.

    - Migration 0044 creates BuildLine objects for existing builds.
    - Migration 0046 links any existing BuildItem objects to corresponding BuildLine
    """

    migrate_from = ('build', '0041_alter_build_title')
    migrate_to = ('build', '0047_auto_20230606_1058')

    def prepare(self):
        """Create data to work with."""
        # Model references
        Part = self.old_state.apps.get_model('part', 'part')
        BomItem = self.old_state.apps.get_model('part', 'bomitem')
        Build = self.old_state.apps.get_model('build', 'build')
        BuildItem = self.old_state.apps.get_model('build', 'builditem')
        StockItem = self.old_state.apps.get_model('stock', 'stockitem')

        # The "BuildLine" model does not exist yet
        with self.assertRaises(LookupError):
            self.old_state.apps.get_model('build', 'buildline')

        # Create a part
        assembly = Part.objects.create(
            name='Assembly',
            description='An assembly',
            assembly=True,
            level=0,
            lft=0,
            rght=0,
            tree_id=0,
        )

        # Create components
        for idx in range(1, 11):
            part = Part.objects.create(
                name=f'Part {idx}',
                description=f'Part {idx}',
                level=0,
                lft=0,
                rght=0,
                tree_id=0,
            )

            # Create plentiful stock
            StockItem.objects.create(
                part=part, quantity=1000, level=0, lft=0, rght=0, tree_id=0
            )

            # Create a BOM item
            BomItem.objects.create(
                part=assembly, sub_part=part, quantity=idx, reference=f'REF-{idx}'
            )

        # Create some builds
        for idx in range(1, 4):
            build = Build.objects.create(
                part=assembly,
                title=f'Build {idx}',
                quantity=idx * 10,
                reference=f'REF-{idx}',
                level=0,
                lft=0,
                rght=0,
                tree_id=0,
            )

            # Allocate stock to the build
            for bom_item in BomItem.objects.all():
                stock_item = StockItem.objects.get(part=bom_item.sub_part)
                BuildItem.objects.create(
                    build=build,
                    bom_item=bom_item,
                    stock_item=stock_item,
                    quantity=bom_item.quantity,
                )

    def test_build_line_creation(self):
        """Test that the BuildLine objects have been created correctly."""
        Build = self.new_state.apps.get_model('build', 'build')
        BomItem = self.new_state.apps.get_model('part', 'bomitem')
        BuildLine = self.new_state.apps.get_model('build', 'buildline')
        BuildItem = self.new_state.apps.get_model('build', 'builditem')
        StockItem = self.new_state.apps.get_model('stock', 'stockitem')

        # There should be 3x builds
        self.assertEqual(Build.objects.count(), 3)

        # 10x BOMItem objects
        self.assertEqual(BomItem.objects.count(), 10)

        # 10x StockItem objects
        self.assertEqual(StockItem.objects.count(), 10)

        # And 30x BuildLine items (1 for each BomItem for each Build)
        self.assertEqual(BuildLine.objects.count(), 30)

        # And 30x BuildItem objects (1 for each BomItem for each Build)
        self.assertEqual(BuildItem.objects.count(), 30)

        # Check that each BuildItem has been linked to a BuildLine
        for item in BuildItem.objects.all():
            self.assertIsNotNone(item.build_line)
            self.assertEqual(item.stock_item.part, item.build_line.bom_item.sub_part)

        item = BuildItem.objects.first()

        # Check that the "build" field has been removed
        with self.assertRaises(AttributeError):
            item.build

        # Check that the "bom_item" field has been removed
        with self.assertRaises(AttributeError):
            item.bom_item

        # Check that each BuildLine is correctly configured
        for line in BuildLine.objects.all():
            # Check that the quantity is correct
            self.assertEqual(
                line.quantity, line.build.quantity * line.bom_item.quantity
            )

            # Check that the linked parts are correct
            self.assertEqual(line.build.part, line.bom_item.part)
