package theorycrafter.fitting

import eve.data.*
import eve.data.AttributeModifier.Operation
import eve.data.Effect.Category
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails


/**
 * Fitting engine tests that don't belong under another, more specific category.
 */
class AssortedTests {


    /**
     * Test two effects with a same-sign modifying attribute on a stacking penalized attribute where one of the effects
     * has its modifying property inverted by another effect before it applied.
     * The two effects should not be stacking penalized as one is positive and the other negative.
     * Basically, this verifies that the stacking penalty group is based on the final, rather than the base, value of
     * the modifying property.
     */
    @Test
    fun testTwoSameSignEffectsOneInvertedStackingPenalty() = runFittingTest {
        val targetAttribute = attribute(isStackingPenalized = true)
        val modifyingAttribute = attribute()
        val invertingAttribute = attribute()

        val shipModifyingEffect = effectOnShip(
            category = Category.ALWAYS,
            modifiedAttribute = targetAttribute,
            modifyingAttribute = modifyingAttribute,
            operation = Operation.ADD_PERCENT
        )

        val invertingEffect = effectOnSelf(
            category = Category.ALWAYS,
            modifiedAttribute = modifyingAttribute,
            modifyingAttribute = invertingAttribute,
            operation = Operation.POST_MULTIPLY
        )

        val regularModuleType = moduleType {
            attributeValue(modifyingAttribute, 100.0)
            effectReference(shipModifyingEffect)
        }

        val moduleTypeWithInvertedEffect = moduleType {
            attributeValue(modifyingAttribute, 50.0)
            effectReference(shipModifyingEffect)
            attributeValue(invertingAttribute, -1.0)
            effectReference(invertingEffect)
        }

        val shipType = testShipType {
            attributeValue(targetAttribute, 100.0)
        }

        val (fit, _) = fit(shipType, regularModuleType, moduleTypeWithInvertedEffect)

        fit.ship.assertPropertyEquals(
            attribute = targetAttribute,
            expected = 100.0,
            message = "Wrong stacking penalty when one of the effects has been inverted"
        )
    }


    /**
     * Tests a ship effect that bonuses a charge attribute.
     */
    @Test
    fun testShipEffectOnCharge() = runFittingTest{
        val chargeAttribute = attribute()

        val chargeType = chargeType {
            attributeValue(chargeAttribute, 200.0)
        }

        val moduleType = moduleTypeLoadableWithCharges(chargeGroupId = chargeType.groupId)

        val shipType = testShipType{
            val shipAttribute = attribute()
            attributeValue(shipAttribute, 20.0)
            effectReference(
                effectOnCharges(
                    category = Category.ALWAYS,
                    modifiedAttribute = chargeAttribute,
                    modifyingAttribute = shipAttribute,
                    operation = Operation.ADD_PERCENT
                )
            )
        }

        val (_, module) = fit(shipType, moduleType)
        val charge = modify {
            module.setCharge(chargeType)
        }

        charge.assertPropertyEquals(
            attribute = chargeAttribute,
            expected = 240.0,
            message = "Incorrect ship effect applied to charge"
        )
    }


    /**
     * Tests the effect of a script loaded into the module.
     */
    @Test
    fun testScriptEffectOnShipViaModule() = runFittingTest {
        val shipAttribute = attribute()
        val moduleAttribute = attribute()

        val scriptType = chargeType{
            val scriptAttribute = attribute()
            attributeValue(scriptAttribute, 100.0)
            effectReference(
                effectOnChargeModule(
                    modifiedAttribute = moduleAttribute,
                    modifyingAttribute = scriptAttribute,
                    operation = Operation.ADD_PERCENT
                )
            )
        }

        val moduleType = moduleTypeLoadableWithCharges(
            chargeGroupId = scriptType.groupId,
        ) {
            attributeValue(moduleAttribute, 10.0)
            effectReference(
                effectOnShip(
                    category = Category.ACTIVE,
                    modifiedAttribute = shipAttribute,
                    modifyingAttribute = moduleAttribute,
                    operation = Operation.ADD
                )
            )
        }

        val shipType = testShipType {
            attributeValue(shipAttribute, 20.0)
        }

        val (fit, modules) = fit(shipType, moduleType, moduleType)
        val (moduleWithScript, moduleWithoutScript) = modules

        modify {
            moduleWithScript.setCharge(scriptType)
            moduleWithScript.setState(Module.State.ACTIVE)
            moduleWithoutScript.setState(Module.State.ACTIVE)
        }
        moduleWithScript.assertPropertyEquals(
            attribute = moduleAttribute,
            expected = 20.0,
            message = "Effect of script on module applied incorrectly"
        )
        moduleWithoutScript.assertPropertyEquals(
            attribute = moduleAttribute,
            expected = 10.0,
            message = "Effect of script applied to unrelated module"
        )
        fit.ship.assertPropertyEquals(
            attribute = shipAttribute,
            expected = 50.0,
            message = "Effect of module with loaded script applied incorrectly"
        )

        modify {
            moduleWithScript.removeCharge()
        }
        fit.ship.assertPropertyEquals(
            attribute = shipAttribute,
            expected = 40.0,
            message = "Effect of module with loaded script remains after script is removed"
        )
    }


    /**
     * Tests a character effect on a ship.
     */
    @Test
    fun testCharacterEffectOnShip() = runFittingTest {
        val shipSkillType = testSkillType(skillLevel = 1)
        val shipAttribute = attribute()
        val shipType = testShipType {
            attributeValue(shipAttribute, 20.0)
            requiresSkill(skillRequirementIndex = 0, skillId = shipSkillType.itemId, skillLevel = 1)
        }

        val charAttribute = attribute()
        characterType {
            attributeValue(charAttribute, 10.0)
            effectReference(
                effectOnShip(
                    category = Category.ALWAYS,
                    modifiedAttribute = shipAttribute,
                    modifyingAttribute = charAttribute,
                    operation = Operation.ADD,
                    skillTypeId = shipSkillType.itemId
                )
            )
        }

        val (fit, _) = fit(shipType)

        fit.ship.assertPropertyEquals(
            attribute = shipAttribute,
            expected = 30.0,
            message = "Effect of character on ship applied incorrectly"
        )
    }


    /**
     * Tests a character effect on a module.
     */
    @Test
    fun testCharacterEffectOnModule() = runFittingTest {
        val moduleSkillType = testSkillType(skillLevel = 1)
        val moduleAttribute = attribute()
        val moduleType = moduleType {
            attributeValue(moduleAttribute, 20.0)
            requiresSkill(skillRequirementIndex = 0, skillId = moduleSkillType.itemId, skillLevel = 1)
        }

        val shipType = testShipType()

        val charAttribute = attribute()
        characterType {
            attributeValue(charAttribute, 10.0)
            effectReference(
                effectOnModules(
                    modifiedAttribute = moduleAttribute,
                    modifyingAttribute = charAttribute,
                    operation = Operation.ADD,
                    skillTypeId = moduleSkillType.itemId
                )
            )
        }

        val (_, module) = fit(shipType, moduleType)

        module.assertPropertyEquals(
            attribute = moduleAttribute,
            expected = 30.0,
            message = "Effect of character on module applied incorrectly"
        )
    }


    /**
     * Tests a character effect on a charge.
     */
    @Test
    fun testCharacterEffectOnCharge() = runFittingTest {
        val chargeSkillType = testSkillType(skillLevel = 1)
        val chargeGroup = newTestGroup()
        val chargeAttribute = attribute()

        val chargeType = chargeType(group = chargeGroup) {
            attributeValue(chargeAttribute, 20.0)
            requiresSkill(skillRequirementIndex = 0, skillId = chargeSkillType.itemId, skillLevel = 1)
        }

        val moduleType = moduleTypeLoadableWithCharges(
            chargeGroupId = chargeGroup.id
        )

        val shipType = testShipType()

        val charAttribute = attribute()
        characterType {
            attributeValue(charAttribute, 10.0)
            effectReference(
                effectOnCharges(
                    category = Category.ALWAYS,
                    modifiedAttribute = chargeAttribute,
                    modifyingAttribute = charAttribute,
                    operation = Operation.ADD,
                    skillTypeId = chargeSkillType.itemId
                )
            )
        }

        val (_, module) = fit(shipType, moduleType){  _, module: Module ->
            module.setCharge(chargeType)
        }

        module.loadedCharge!!.assertPropertyEquals(
            attribute = chargeAttribute,
            expected = 30.0,
            message = "Effect of character on charge applied incorrectly"
        )
    }


    /**
     * Tests a character effect on a drone.
     */
    @Test
    fun testCharacterEffectOnDrone() = runFittingTest {
        val droneSkillType = testSkillType(skillLevel = 1)
        val droneAttribute = attribute()

        val droneType = testDroneType {
            attributeValue(droneAttribute, 20.0)
            requiresSkill(skillRequirementIndex = 0, skillId = droneSkillType.itemId, skillLevel = 1)
        }

        val shipType = testShipType()

        val charAttribute = attribute()
        characterType {
            attributeValue(charAttribute, 10.0)
            effectReference(
                effectOnDrones(
                    modifiedAttribute = droneAttribute,
                    modifyingAttribute = charAttribute,
                    operation = Operation.ADD,
                    skillTypeId = droneSkillType.itemId
                )
            )
        }

        val (fit, _) = fit(shipType)
        val droneGroup = modify {
            fit.addDroneGroup(droneType, 5)
        }

        droneGroup.assertPropertyEquals(
            attribute = droneAttribute,
            expected = 30.0,
            message = "Effect of character on drone group applied incorrectly"
        )
    }


    /**
     * Tests loading a charge into a module that doesn't take charges.
     */
    @Test
    fun testLoadChargeIntoModuleWithoutCharges() = runFittingTest {
        val moduleType = moduleType()
        val chargeType = chargeType()
        val shipType = testShipType()

        assertFails {
            fit(shipType, moduleType) { _, module: Module ->
                module.setCharge(chargeType)
            }
        }
    }


    /**
     * Tests loading a charge of the wrong group id into a module.
     */
    @Test
    fun testWrongChargeGroupId() = runFittingTest {
        val chargeGroup = newTestGroup()
        val moduleType = moduleTypeLoadableWithCharges(
            chargeGroupId = chargeGroup.id
        )
        val chargeType = chargeType(group = newTestGroup())
        val shipType = testShipType()

        val (_, module) = fit(shipType, moduleType) { _, module: Module ->
            module.setCharge(chargeType)
        }
        assertIsFittedIllegally(module.loadedCharge!!)
    }


    /**
     * Tests loading a charge of the wrong size into a module.
     */
    @Test
    fun testWrongChargeSize() = runFittingTest {
        val chargeGroup = newTestGroup()
        val moduleType = moduleTypeLoadableWithCharges(
            chargeGroupId = chargeGroup.id,
            chargeSize = ItemSize.SMALL
        )

        val chargeType = chargeType(
            group = chargeGroup,
            size = ItemSize.MEDIUM
        )
        val shipType = testShipType()

        val (_, module) = fit(shipType, moduleType) { _, module: Module ->
            module.setCharge(chargeType)
        }
        assertIsFittedIllegally(module.loadedCharge!!)
    }


    /**
     * Tests loading a charge whose volume is larger than the capacity of the module.
     */
    @Test
    fun testChargeTooLarge() = runFittingTest {
        val chargeGroup = newTestGroup()
        val moduleType = moduleTypeLoadableWithCharges(
            chargeGroupId = chargeGroup.id,
            chargeCapacity = 10.0
        )
        val chargeType = chargeType(
            group = chargeGroup,
            volume = 11.0
        )
        val shipType = testShipType()

        val (_, module) = fit(shipType, moduleType){ _, module: Module ->
            module.setCharge(chargeType)
        }
        assertIsFittedIllegally(module.loadedCharge!!)
    }


    /**
     * Test fitting a rig of the wrong size.
     */
    @Test
    fun testWrongRigSize() = runFittingTest {
        val shipType = testShipType {
            attributeValue(attributes.rigSize, ItemSize.LARGE)
        }
        val rigType = rigType(rigSize = ItemSize.MEDIUM)

        val (_, module) = fit(shipType, rigType)
        assertIsFittedIllegally(module)
    }


    /**
     * Test fitting more modules of a group than is allowed by [Attributes.maxGroupFitted].
     */
    @Test
    fun testMaxGroupFitted() = runFittingTest {
        val group = newTestGroup()
        fun moduleTypeInGroup() = moduleType(group = group){
            attributeValue(attributes.maxGroupFitted, 2)
        }

        val moduleType1 = moduleTypeInGroup()
        val moduleType2 = moduleTypeInGroup()

        val shipType = testShipType()

        val (fit, modules) = fit(shipType, moduleType1, moduleType2, moduleType2)
        fit.modules.all.forEach { assertIsFittedIllegally(it) }

        modify {
            fit.removeModule(modules[0])
        }
        fit.modules.all.forEach { assertIsFittedLegally(it) }
    }


    /**
     * Test fitting more modules of a group than is allowed by [ShipType.Fitting.turretHardpoints].
     */
    @Test
    fun testTurretHardpoints() = runFittingTest {
        fun turretType() = moduleType(flags = ModuleFlags.TURRET)

        val turretType1 = turretType()
        val turretType2 = turretType()

        val shipType = testShipType {
            attributeValue(attributes.turretHardpoints, 2)
        }

        val (fit, modules) = fit(shipType, turretType1, turretType2, turretType2)
        fit.modules.all.forEach { assertIsFittedIllegally(it) }

        modify {
            fit.removeModule(modules[2])
        }
        fit.modules.all.forEach { assertIsFittedLegally(it) }
    }


    /**
     * Test fitting more modules of a group than is allowed by [ShipType.Fitting.launcherHardpoints].
     */
    @Test
    fun testMissileLauncherHardpoints() = runFittingTest {
        fun launcherType() = moduleType(flags = ModuleFlags.MISSILE_LAUNCHER)

        val launcherType1 = launcherType()
        val launcherType2 = launcherType()

        val shipType = testShipType {
            attributeValue(attributes.launcherHardpoints, 2)
        }

        val (fit, modules) = fit(shipType, launcherType1, launcherType2, launcherType2)
        fit.modules.all.forEach { assertIsFittedIllegally(it) }

        modify {
            fit.removeModule(modules[1])
        }
        fit.modules.all.forEach { assertIsFittedLegally(it) }
    }


    /**
     * Test fitting modules restricted to certain ship groups via [Attributes.canFitShipGroups] to a ship not in any
     * of the groups.
     */
    @Test
    fun testCanFitShipGroup() = runFittingTest {
        val matchingGroup = newTestGroup()
        val nonMatchingGroup = newTestGroup()

        fun moduleTypeWithCanFitShipGroup(shipGroupId: Int) = moduleType {
            attributeValue(attributes.canFitShipGroups.first(), shipGroupId)
        }

        val matchingModuleType = moduleTypeWithCanFitShipGroup(shipGroupId = matchingGroup.id)
        val nonMatchingModuleType = moduleTypeWithCanFitShipGroup(shipGroupId = nonMatchingGroup.id)

        val shipType = testShipType(group = matchingGroup)

        val (_, matchingModule) = fit(shipType, matchingModuleType)
        assertIsFittedLegally(matchingModule)

        val (_, nonMatchingModule) = fit(shipType, nonMatchingModuleType)
        assertIsFittedIllegally(nonMatchingModule)
    }


    /**
     * Test fitting modules restricted to certain ship types via [Attributes.canFitShipTypes] to a different ship type.
     */
    @Test
    fun testCanFitShipType() = runFittingTest {
        val matchingShipTypeId = reserveAvailableItemId()
        val nonMatchingShipTypeId = reserveAvailableItemId()

        fun moduleTypeWithCanFitShipType(shipTypeId: Int) = moduleType {
            attributeValue(attributes.canFitShipTypes.first(), shipTypeId)
        }

        val matchingModuleType = moduleTypeWithCanFitShipType(shipTypeId = matchingShipTypeId)
        val nonMatchingModuleType = moduleTypeWithCanFitShipType(shipTypeId = nonMatchingShipTypeId)

        val shipType = testShipType(itemId = matchingShipTypeId)

        val (_, matchingModule) = fit(shipType, matchingModuleType)
        assertIsFittedLegally(matchingModule)

        val (_, nonMatchingModule) = fit(shipType, nonMatchingModuleType)
        assertIsFittedIllegally(nonMatchingModule)
    }


    /**
     * Test fitting modules restricted to either ship groups or ship types.
     */
    @Test
    fun testCanFitShipGroupOrType() = runFittingTest {
        val matchingShipTypeId = reserveAvailableItemId()
        val nonMatchingShipTypeId = reserveAvailableItemId()
        val matchingShipGroup = newTestGroup()
        val nonMatchingGroup = newTestGroup()

        fun moduleType(shipTypeId: Int, shipGroupId: Int) = moduleType {
            attributeValue(attributes.canFitShipTypes.first(), shipTypeId)
            attributeValue(attributes.canFitShipGroups.first(), shipGroupId)
        }

        val matchingModuleType = moduleType(shipTypeId = matchingShipTypeId, shipGroupId = matchingShipGroup.id)
        val nonMatchingModuleType = moduleType(shipTypeId = nonMatchingShipTypeId, shipGroupId = nonMatchingGroup.id)

        val shipTypeMatchingTypeId = testShipType(itemId = matchingShipTypeId)
        val shipTypeMatchingGroupId = testShipType(group = matchingShipGroup)

        val (_, moduleWithMatchingType) = fit(shipTypeMatchingTypeId, matchingModuleType)
        assertIsFittedLegally(moduleWithMatchingType)
        val (_, moduleWithMatchingGroup) = fit(shipTypeMatchingGroupId, matchingModuleType)
        assertIsFittedLegally(moduleWithMatchingGroup)

        val (_, moduleNonMatchingModuleType) = fit(shipTypeMatchingTypeId, nonMatchingModuleType)
        assertIsFittedIllegally(moduleNonMatchingModuleType)
        val (_, moduleWithNonMatchingGroup) = fit(shipTypeMatchingGroupId, nonMatchingModuleType)
        assertIsFittedIllegally(moduleWithNonMatchingGroup)
    }


    /**
     * Tests activating more modules than is allowed by [Attributes.maxGroupActive].
     */
    @Test
    fun testMaxGroupActive() = runFittingTest {
        val group = newTestGroup()

        fun moduleTypeWithMaxGroupActive(count: Int) = moduleType(group = group, flags = ModuleFlags.OVERLOADABLE) {
            attributeValue(attributes.maxGroupActive, count)
        }

        val moduleType1 = moduleTypeWithMaxGroupActive(1)
        val moduleType2 = moduleTypeWithMaxGroupActive(1)

        val shipType = testShipType()

        val (fit, modules) = fit(shipType, moduleType1, moduleType2)
        val (module1, module2) = modules

        fun countActive() = fit.modules.active.size

        // Test that the fitting engine ignores the module being changed when counting the number of active modules
        modify {
            module1.setState(Module.State.ACTIVE)
        }
        assertEquals(expected = 1, actual = countActive(), message = "First module did not activate")

        modify {
            // Just verify that this doesn't throw anything
            module1.setState(Module.State.ACTIVE)
            module1.setState(Module.State.OVERLOADED)
            module1.setState(Module.State.ACTIVE)
            module1.setState(Module.State.OVERLOADED)
        }
        assertIsFittedLegally(module1)

        modify {
            module2.setState(Module.State.ACTIVE)
        }
        assertEquals(expected = 2, actual = countActive(), message = "Unexpected number of active modules")
        assertIsFittedIllegally(module1)
        assertIsFittedIllegally(module2)

        modify {
            module1.setState(Module.State.ONLINE)
        }
        assertEquals(expected = 1, actual = countActive(), message = "Unexpected number of active modules")
        assertIsFittedLegally(module1)
        assertIsFittedLegally(module2)
    }


    /**
     * Tests [Attributes.maxGroupOnline] functionality.
     */
    @Test
    fun testMaxGroupOnline() = runFittingTest {
        val group1 = newTestGroup()
        val group2 = newTestGroup()

        fun moduleTypeWithMaxGroupOnline(count: Int, group: TypeGroup) =
            moduleType(group = group, flags = ModuleFlags.OVERLOADABLE) {
                attributeValue(attributes.maxGroupOnline, count)
            }

        val moduleGroup1Type1 = moduleTypeWithMaxGroupOnline(1, group1)
        val moduleGroup1Type2 = moduleTypeWithMaxGroupOnline(1, group1)
        val moduleGroup2Type = moduleTypeWithMaxGroupOnline(2, group2)

        val shipType = testShipType()

        fun Fit.onlineModuleCount() = this.modules.online.size

        // Test group 1, where at most 1 module can be online
        val (fit1, modulesGroup1) = fit(shipType, moduleGroup1Type1, moduleGroup1Type2, bringOnline = false)
        val (group1Module1, group1Module2) = modulesGroup1

        modulesGroup1.forEach { assertIsFittedLegally(it) }

        modify {
            group1Module1.setState(Module.State.ONLINE)
        }
        modulesGroup1.forEach { assertIsFittedLegally(it) }
        assertEquals(expected = 1, actual = fit1.onlineModuleCount(), message = "First module did not online")

        modify {
            // Just verify that this doesn't throw anything
            group1Module1.setState(Module.State.ONLINE)
            group1Module1.setState(Module.State.ACTIVE)
            group1Module1.setState(Module.State.ONLINE)
            group1Module1.setState(Module.State.OVERLOADED)
        }
        modulesGroup1.forEach { assertIsFittedLegally(it) }

        // Online 2nd module, making them both illegal
        modify {
            group1Module2.setState(Module.State.ONLINE)
        }
        assertEquals(expected = 2, actual = fit1.onlineModuleCount(), message = "Unexpected number of online modules")
        assertEquals(expected = Module.State.ONLINE, actual = group1Module2.state, "Module asked to online is not online")
        modulesGroup1.forEach { assertIsFittedIllegally(it) }

        // Offline the 1st module, making them both legal again
        modify {
            group1Module1.setState(Module.State.OFFLINE)
        }
        modulesGroup1.forEach { assertIsFittedLegally(it) }

        // Test group 2, where at most 2 modules can be online
        val (fit2, modulesGroup2) = fit(shipType, moduleGroup2Type, moduleGroup2Type, moduleGroup2Type, bringOnline = false)
        val (group2Module1, group2Module2, group2Module3) = modulesGroup2
        modify {
            group2Module1.setState(Module.State.ONLINE)
        }
        assertEquals(expected = 1, actual = fit2.onlineModuleCount(), message = "First module did not online")
        modulesGroup2.forEach { assertIsFittedLegally(it) }

        modify {
            group2Module2.setState(Module.State.ONLINE)
        }
        assertEquals(expected = 2, actual = fit2.onlineModuleCount(), message = "Second module did not online")
        modulesGroup2.forEach { assertIsFittedLegally(it) }

        // Online the 3rd module, making them all illegal
        modify {
            group2Module3.setState(Module.State.ONLINE)
        }
        assertEquals(expected = 3, actual = fit2.onlineModuleCount(), message = "Unexpected number of online modules")
        assertEquals(expected = Module.State.ONLINE, actual = group2Module3.state, "Module asked to online is not online")
        modulesGroup2.forEach { assertIsFittedIllegally(it) }

        // Offline the 2nd module, making them all legal again
        modify {
            group2Module3.setState(Module.State.OFFLINE)
        }
        assertEquals(expected = 2, actual = fit2.onlineModuleCount(), message = "Unexpected number of online modules")
        assertEquals(expected = Module.State.OFFLINE, actual = group2Module3.state, "Module asked to online is not online")
        modulesGroup2.forEach { assertIsFittedLegally(it) }
    }


    /**
     * Test that a bonus to [Attributes.maxGroupOnline] works correctly.
     */
    @Test
    fun testMaxGroupOnlineBonus() = runFittingTest {
        val moduleGroup = newTestGroup()

        val targetModuleType = moduleType(group = moduleGroup, flags = ModuleFlags.ACTIVE) {
            attributeValue(attributes.maxGroupOnline, 1)
        }

        val maxGroupOnlineBonus = attribute()

        val shipType = testShipType {
            attributeValue(maxGroupOnlineBonus, 1.0)
            effectReference(
                effectOnModules(
                    modifiedAttribute = attributes.maxGroupOnline,
                    modifyingAttribute = maxGroupOnlineBonus,
                    operation = Operation.ADD,
                    groupId = moduleGroup.id
                )
            )
        }

        val moduleTypeWithBonusToMaxOnline = moduleType{
            attributeValue(maxGroupOnlineBonus, 1.0)
            effectReference(
                effectOnModules(
                    category = Category.ONLINE,
                    modifiedAttribute = attributes.maxGroupOnline,
                    modifyingAttribute = maxGroupOnlineBonus,
                    operation = Operation.ADD,
                    groupId = moduleGroup.id
                )
            )
        }

        fun Fit.countOnlineInGroup() = this.modules.all.count {
            (it.type.groupId == moduleGroup.id) && it.state.isAtLeastOnline()
        }

        // Test fitting and onlining separately
        val (fit1, modules) = fit(shipType,
            targetModuleType, targetModuleType, targetModuleType, targetModuleType, moduleTypeWithBonusToMaxOnline, bringOnline = false)
        val (module1, module2, module3, module4, bonusModule) = modules

        modules.forEach { assertIsFittedLegally(it) }

        modify {
            module1.setState(Module.State.ONLINE)
        }
        assertEquals(expected = 1, actual = fit1.countOnlineInGroup(), message = "Unexpected number of online modules")
        modules.forEach { assertIsFittedLegally(it) }

        modify {
            module2.setState(Module.State.ONLINE)
        }
        assertEquals(expected = 2, actual = fit1.countOnlineInGroup(), message = "Unexpected number of online modules")
        modules.forEach { assertIsFittedLegally(it) }

        modify {
            module3.setState(Module.State.ONLINE)
        }
        assertEquals(expected = 3, actual = fit1.countOnlineInGroup(), message = "Unexpected number of online modules")
        assertEquals(module3.state, Module.State.ONLINE, "Onlined module was immediately made offline")
        modules.take(3).forEach { assertIsFittedIllegally(it) }

        // Online the module with bonus to max online, making the 3 modules already fitted legal
        modify {
            bonusModule.setState(Module.State.ONLINE)
        }
        assertEquals(expected = 3, actual = fit1.countOnlineInGroup(), message = "Unexpected number of online modules")
        modules.forEach { assertIsFittedLegally(it) }

        modify {
            module4.setState(Module.State.ONLINE)
        }
        modules.take(4).forEach { assertIsFittedIllegally(it) }

        // Test that giving the bonus within the same modify block, even if after the modules were fitted, allows them
        // to be online. This is needed, for example, when opening a fit with Command Processors
        val (fit2, _) = fit(shipType)
        modify {
            fun Fit.fitAndOnline(moduleType: ModuleType, slotIndex: Int) =
                fitModule(moduleType, slotIndex).also { it.setState(Module.State.ONLINE) }

            fit2.fitAndOnline(targetModuleType, 0)
            fit2.fitAndOnline(targetModuleType, 1)
            fit2.fitAndOnline(targetModuleType, 2)

            fit2.fitAndOnline(moduleTypeWithBonusToMaxOnline, 3)
        }
        assertEquals(
            expected = 3,
            actual = fit2.countOnlineInGroup(),
            message = "Bonus to max online modules was not applied retroactively"
        )
        fit2.modules.all.forEach { assertIsFittedLegally(it) }
    }


    /**
     * Tests fitting a drone group.
     */
    @Test
    fun testDrones() = runFittingTest {
        val droneType = testDroneType()

        val shipType = testShipType()

        val (fit, _) = fit(shipType)
        val droneGroup = modify {
            fit.addDroneGroup(droneType, 5)
        }
        assertEquals(
            expected = 5,
            actual = droneGroup.size,
            message = "Fitted drone group size doesn't match requested"
        )

        modify {
            droneGroup.setSize(3)
        }
        assertEquals(
            expected = 3,
            actual = droneGroup.size,
            message = "Drone group size doesn't match requested after change"
        )

        modify {
            droneGroup.setActive(true)
        }
        assertEquals(
            expected = true,
            actual = droneGroup.active,
            message = "Drone group isn't active after activating"
        )

        modify {
            fit.removeDroneGroup(droneGroup)
            assertFails {
                fit.removeDroneGroup(droneGroup)
            }
        }
    }


    /**
     * Tests adding, removing and setting amounts on cargohold items.
     */
    @Test
    fun testCargo() = runFittingTest {
        val shipType = testShipType{
            attributeValue(attributes.capacity, 200.0)
        }
        val chargeType = chargeType(volume = 2.0)

        val (fit, _) = fit(shipType)

        assertEquals(200.0, fit.cargohold.capacity.total)

        val cargoItem = modify {
            fit.addCargoItem(chargeType, 3)
        }
        assertEquals(6.0, fit.cargohold.capacity.used)

        modify {
            cargoItem.setAmount(4)
        }
        assertEquals(8.0, fit.cargohold.capacity.used)

        modify {
            fit.addCargoItem(chargeType, 2)
        }
        assertEquals(12.0, fit.cargohold.capacity.used)

        modify {
            fit.removeCargoItem(cargoItem)
        }
        assertEquals(4.0, fit.cargohold.capacity.used)
    }


}