package theorycrafter.fitting

import eve.data.*
import eve.data.AttributeModifier.Operation
import kotlin.test.Test


/**
 * Tests module-related fitting functionality.
 */
class ModuleTest {


    /**
     * A utility function to test the effect of a single passive module on a ship.
     */
    private fun testSinglePassiveModuleShipEffect(
        baseValue: Double,
        modAndResult: ModAndResult,
    ) = runFittingTest {
        val attribute = attribute()

        val shipType = testShipType {
            attributeValue(attribute, baseValue)
        }

        val moduleType = moduleType(
            flags = ModuleFlags.PASSIVE
        ){
            attributeValue(attribute, modAndResult.modifyingValue)

            effectReference(
                effectOnShip(
                    category = Effect.Category.ONLINE,
                    modifiedAttribute = attribute,
                    modifyingAttribute = attribute,
                    operation = modAndResult.operation
                )
            )
        }

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

        fit.ship.assertPropertyEquals(
            attribute = attribute,
            expected = modAndResult.expectedResultValue,
            message = "Incorrect passive module application with $modAndResult"
        )

        // Remove module and verify its effect is gone
        modify {
            fit.removeModule(module)
        }
        fit.ship.assertPropertyEquals(
            attribute = attribute,
            expected = baseValue,
            message = "Effect of module remains after module is removed"
        )
    }


    /**
     * Tests that a passive module with an [Operation.ADD] operation works correctly.
     */
    @Test
    fun testPassiveModuleAddOperation() = testSinglePassiveModuleShipEffect(
        baseValue = 1000.0,
        modAndResult = ModAndResult(
            modifyingValue = 500.0,
            operation = Operation.ADD,
            expectedResultValue = 1500.0
        )
    )


    /**
     * Tests that a passive module with a [Operation.SUBTRACT] operation works correctly.
     */
    @Test
    fun testPassiveModuleSubtractOperation() = testSinglePassiveModuleShipEffect(
        baseValue = 1000.0,
        modAndResult = ModAndResult(
            modifyingValue = 500.0,
            operation = Operation.SUBTRACT,
            expectedResultValue = 500.0
        )
    )


    /**
     * Tests that a passive module with an [Operation.ADD_PERCENT] operation works correctly.
     */
    @Test
    fun testPassiveModuleAddPercentOperation() = testSinglePassiveModuleShipEffect(
        baseValue = 100.0,
        modAndResult = ModAndResult(
            modifyingValue = 20.0,
            operation = Operation.ADD_PERCENT,
            expectedResultValue = 120.0
        )
    )


    /**
     * Tests that a passive module with a [Operation.PRE_MULTIPLY] operation works correctly.
     */
    @Test
    fun testPassiveModulePreMultiplyOperation() = testSinglePassiveModuleShipEffect(
        baseValue = 100.0,
        modAndResult = ModAndResult(
            modifyingValue = 2.0,
            operation = Operation.PRE_MULTIPLY,
            expectedResultValue = 200.0
        )
    )


    /**
     * Tests that a passive module with a [Operation.POST_MULTIPLY] operation works correctly.
     */
    @Test
    fun testPassiveModulePostMultiplyOperation() = testSinglePassiveModuleShipEffect(
        baseValue = 100.0,
        modAndResult = ModAndResult(
            modifyingValue = 2.0,
            operation = Operation.POST_MULTIPLY,
            expectedResultValue = 200.0
        )
    )


    /**
     * Tests that a passive module with a [Operation.POST_MULTIPLY] operation works correctly.
     */
    @Test
    fun testPassiveModuleSetOperation() = testSinglePassiveModuleShipEffect(
        baseValue = 100.0,
        modAndResult = ModAndResult(
            modifyingValue = 10.0,
            operation = Operation.SET,
            expectedResultValue = 10.0
        )
    )


    /**
     * Test the effect of a single active, overloadable module on a ship.
     */
    private fun testSingleOverloadableModuleShipEffect(
        baseValue: Double,
        modAndResult: ModAndResult,
        overloadModAndResult: ModAndResult,
    ) = runFittingTest {
        val attribute = attribute()
        val overloadBonusAttribute = attribute()

        val shipType = testShipType {
            attributeValue(attribute, baseValue)
        }

        val moduleType = moduleType(flags = ModuleFlags.OVERLOADABLE) {
            attributeValue(attribute, modAndResult.modifyingValue)
            effectReference(
                effectOnShip(
                    category = Effect.Category.ACTIVE,
                    modifiedAttribute = attribute,
                    modifyingAttribute = attribute,
                    operation = modAndResult.operation
                )
            )

            attributeValue(overloadBonusAttribute, overloadModAndResult.modifyingValue)
            effectReference(
                overloadEffect(
                    modifiedAttribute = attribute,
                    modifyingAttribute = overloadBonusAttribute,
                    operation = overloadModAndResult.operation
                )
            )
        }

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

        // Check that there's no effect when the module is inactive
        modify {
            module.setState(Module.State.ONLINE)
        }
        fit.ship.assertPropertyEquals(
            attribute = attribute,
            expected = baseValue,
            message = "Active module provides effect when inactive"
        )

        // Check the effect when the module is active
        modify {
            module.setState(Module.State.ACTIVE)
        }
        fit.ship.assertPropertyEquals(
            attribute = attribute,
            expected = modAndResult.expectedResultValue,
            message = "Incorrect active module application with $modAndResult"
        )

        // Check the effect when the module is overloaded
        modify {
            module.setState(Module.State.OVERLOADED)
        }
        fit.ship.assertPropertyEquals(
            attribute = attribute,
            expected = overloadModAndResult.expectedResultValue,
            message = "Incorrect overloaded module application with $modAndResult and overload $overloadModAndResult"
        )
    }


    /**
     * Test the effect of an overloadable module that adds to an attribute, and overloading it increases the bonus by
     * a percentage.
     */
    @Test
    fun testActiveModuleShipEffectAddAddPercent() = testSingleOverloadableModuleShipEffect(
        baseValue = 100.0,
        modAndResult = ModAndResult(
            modifyingValue = 50.0,
            operation = Operation.ADD,
            expectedResultValue = 150.0
        ),
        overloadModAndResult = ModAndResult(
            modifyingValue = 20.0,
            operation = Operation.ADD_PERCENT,
            expectedResultValue = 160.0
        )
    )


    /**
     * Test the effect of an overloadable module that adds to an attribute, and overloading it increases the bonus by
     * a factor.
     */
    @Test
    fun testActiveModuleShipEffectAddPremultiply() = testSingleOverloadableModuleShipEffect(
        baseValue = 100.0,
        modAndResult = ModAndResult(
            modifyingValue = 50.0,
            operation = Operation.ADD,
            expectedResultValue = 150.0
        ),
        overloadModAndResult = ModAndResult(
            modifyingValue = 2.0,
            operation = Operation.PRE_MULTIPLY,
            expectedResultValue = 200.0
        )
    )


    /**
     * Test the effect of an overloadable module that adds to an attribute, and overloading it increases the bonus by
     * a number.
     */
    @Test
    fun testActiveModuleShipEffectAddAdd() = testSingleOverloadableModuleShipEffect(
        baseValue = 100.0,
        modAndResult = ModAndResult(
            modifyingValue = 50.0,
            operation = Operation.ADD,
            expectedResultValue = 150.0
        ),
        overloadModAndResult = ModAndResult(
            modifyingValue = 20.0,
            operation = Operation.ADD,
            expectedResultValue = 170.0
        )
    )


    /**
     * Test the effect of an overloadable module that adds a percentage to an attribute, and overloading it increases
     * the bonus by a percentage.
     */
    @Test
    fun testActiveModuleShipEffectAddPercentAddPercent() = testSingleOverloadableModuleShipEffect(
        baseValue = 200.0,
        modAndResult = ModAndResult(
            modifyingValue = 20.0,
            operation = Operation.ADD_PERCENT,
            expectedResultValue = 240.0
        ),
        overloadModAndResult = ModAndResult(
            modifyingValue = 50.0,
            operation = Operation.ADD_PERCENT,
            expectedResultValue = 260.0
        )
    )


    /**
     * Test the effect of an overloadable module that subtracts a number from an attribute, and overloading it increases
     * the bonus by a percentage.
     */
    @Test
    fun testActiveModuleShipEffectSubtractAddPercent() = testSingleOverloadableModuleShipEffect(
        baseValue = 200.0,
        modAndResult = ModAndResult(
            modifyingValue = 20.0,
            operation = Operation.SUBTRACT,
            expectedResultValue = 180.0
        ),
        overloadModAndResult = ModAndResult(
            modifyingValue = 50.0,
            operation = Operation.ADD_PERCENT,
            expectedResultValue = 170.0
        )
    )


    /**
     * Tests stacking penalties.
     * Creates a stacking-penalized ship attribute with the given base value, and then applies to it the given
     * modifiers, one by one, each time verifying that the value of the corresponding ship property matches the
     * [ModAndResult]'s expected value.
     */
    @Suppress("SameParameterValue")
    private fun testStackingPenalizedAttribute(baseValue: Double, vararg modAndResult: ModAndResult) = runFittingTest {
        val attribute = attribute(isStackingPenalized = true)

        val effects = modAndResult.map {
            effectOnShip(
                category = Effect.Category.ACTIVE,
                modifiedAttribute = attribute,
                modifyingAttribute = attribute,
                operation = it.operation
            )
        }

        val shipType = testShipType {
            attributeValue(attribute, baseValue)
        }

        val moduleTypes = modAndResult.zip(effects).map { (modAndResult, effect) ->
            moduleType(flags = ModuleFlags.ACTIVE) {
                attributeValue(attribute, modAndResult.modifyingValue)
                effectReference(effect)
            }
        }

        val (fit, modules) = fit(shipType, moduleTypes) { _, modules ->
            // Make sure all the modules are inactive
            modules.forEach {
                it.setState(Module.State.ONLINE)
            }
        }

        modAndResult.zip(modules).forEachIndexed { index, (modAndResult, module) ->
            modify {
                module.setState(Module.State.ACTIVE)
            }
            fit.ship.assertPropertyEquals(
                attribute = attribute,
                expected = modAndResult.expectedResultValue,
                message = "Stacking penalties for module ${index + 1} applied incorrectly"
            )
        }
    }


    /**
     * Tests the stacking penalty of eight identical modules that add a percentage to the attribute's value.
     */
    @Test
    fun testEightEqualModulesAddPercentStackingPenalty(){
        val baseValue = 100.0
        val addedPercent = 20.0
        val modsAndResults = mutableListOf<ModAndResult>()
        var value = baseValue
        for (i in 0 until 8){
            value *= 1 + stackingPenaltyFactor(i) * addedPercent / 100.0
            modsAndResults.add(
                ModAndResult(
                    modifyingValue = addedPercent,
                    operation = Operation.ADD_PERCENT,
                    expectedResultValue = value
                )
            )
        }
        testStackingPenalizedAttribute(baseValue, modAndResult = modsAndResults.toTypedArray())
    }


    /**
     * Tests the stacking penalty of eight identical modules that multiply the attribute's value.
     */
    @Test
    fun testEightEqualModulesPostMultiplyStackingPenalty(){
        val baseValue = 100.0
        val factor = 1.5
        val modsAndResults = mutableListOf<ModAndResult>()
        var value = baseValue
        for (i in 0 until 8){
            // We can't replace this with `value *= ...` because that changes the order of the operations, which
            // produces a very slightly different result. Funny.
            @Suppress("ReplaceWithOperatorAssignment")
            value = value * (1 + (factor - 1) * stackingPenaltyFactor(i))
            modsAndResults.add(
                ModAndResult(
                    modifyingValue = factor,
                    operation = Operation.POST_MULTIPLY,
                    expectedResultValue = value
                )
            )
        }
        testStackingPenalizedAttribute(baseValue, modAndResult = modsAndResults.toTypedArray())
    }


    /**
     * Tests the effect of two different modules that add a percentage to the attribute's value.
     * Only the weaker effect should be stacking-penalized.
     */
    @Test
    fun testTwoDifferentModulesAddPercentStackingPenalty() {
        // Add the stronger effect first
        testStackingPenalizedAttribute(
            baseValue = 100.0,
            ModAndResult(
                modifyingValue = 40.0,
                operation = Operation.ADD_PERCENT,
                expectedResultValue = 140.0
            ),
            ModAndResult(
                modifyingValue = 20.0,
                operation = Operation.ADD_PERCENT,
                expectedResultValue = 140.0 * (1 + (20.0 / 100.0) * stackingPenaltyFactor(1))
            )
        )

        // Add the weaker effect first
        testStackingPenalizedAttribute(
            baseValue = 100.0,
            ModAndResult(
                modifyingValue = 20.0,
                operation = Operation.ADD_PERCENT,
                expectedResultValue = 120.0
            ),
            ModAndResult(
                modifyingValue = 40.0,
                operation = Operation.ADD_PERCENT,
                expectedResultValue = 140.0 * (1 + (20.0 / 100.0) * stackingPenaltyFactor(1))
            )
        )
    }


    /**
     * Tests the effect of two different modules that multiply the attribute's value.
     * Only the weaker effect should be stacking-penalized.
     */
    @Test
    fun testTwoDifferentModulesPostMultiplyStackingPenalty() {
        // Add the stronger effect first
        testStackingPenalizedAttribute(
            baseValue = 100.0,
            ModAndResult(
                modifyingValue = 4.0,
                operation = Operation.POST_MULTIPLY,
                expectedResultValue = 400.0
            ),
            ModAndResult(
                modifyingValue = 2.0,
                operation = Operation.POST_MULTIPLY,
                expectedResultValue = 400.0 * (1.0 + (2.0 - 1.0) * stackingPenaltyFactor(1))
            )
        )

        // Add the weaker effect first
        testStackingPenalizedAttribute(
            baseValue = 100.0,
            ModAndResult(
                modifyingValue = 2.0,
                operation = Operation.POST_MULTIPLY,
                expectedResultValue = 200.0
            ),
            ModAndResult(
                modifyingValue = 4.0,
                operation = Operation.POST_MULTIPLY,
                expectedResultValue = 400.0 * (1.0 + (2.0 - 1.0) * stackingPenaltyFactor(1))
            )
        )
    }


    /**
     * Tests the effect of two different modules: one that adds a percentage and the other subtracts a percentage from
     * the attribute's value.
     * There should be no stacking-penalty.
     */
    @Test
    fun testOppositeModulesAddPercentStackingPenalty() {
        // Add the increasing effect first
        testStackingPenalizedAttribute(
            baseValue = 100.0,
            ModAndResult(
                modifyingValue = 20.0,
                operation = Operation.ADD_PERCENT,
                expectedResultValue = 120.0
            ),
            ModAndResult(
                modifyingValue = -20.0,
                operation = Operation.ADD_PERCENT,
                expectedResultValue = 96.0
            )
        )

        // Add the decreasing effect first
        testStackingPenalizedAttribute(
            baseValue = 100.0,
            ModAndResult(
                modifyingValue = -20.0,
                operation = Operation.ADD_PERCENT,
                expectedResultValue = 80.0
            ),
            ModAndResult(
                modifyingValue = 20.0,
                operation = Operation.ADD_PERCENT,
                expectedResultValue = 96.0
            ),
        )
    }


    /**
     * Tests the effect of two different modules: one that multiplies the attribute's value by a factor greater than 1,
     * and the by a factor smaller than 1.
     * There should be no stacking-penalty.
     */
    @Test
    fun testOppositeModulesPostMultiplyStackingPenalty() {
        // Add the increasing effect first
        testStackingPenalizedAttribute(
            baseValue = 100.0,
            ModAndResult(
                modifyingValue = 2.0,
                operation = Operation.POST_MULTIPLY,
                expectedResultValue = 200.0
            ),
            ModAndResult(
                modifyingValue = 0.8,
                operation = Operation.POST_MULTIPLY,
                expectedResultValue = 160.0
            )
        )

        // Add the decreasing effect first
        testStackingPenalizedAttribute(
            baseValue = 100.0,
            ModAndResult(
                modifyingValue = 0.8,
                operation = Operation.POST_MULTIPLY,
                expectedResultValue = 80.0
            ),
            ModAndResult(
                modifyingValue = 2.0,
                operation = Operation.POST_MULTIPLY,
                expectedResultValue = 160.0
            ),
        )
    }


    /**
     * Tests the effect of several modules with different [Operation]s.
     * There should be no stacking-penalty.
     */
    @Test
    fun testDifferentTypeModulesStackingPenalty() = testStackingPenalizedAttribute(
        baseValue = 100.0,
        ModAndResult(
            modifyingValue = 2.0,
            operation = Operation.PRE_MULTIPLY,
            expectedResultValue = 200.0
        ),
        ModAndResult(
            modifyingValue = 1.2,
            operation = Operation.POST_MULTIPLY,
            expectedResultValue = 240.0
        ),
        ModAndResult(
            modifyingValue = 20.0,
            operation = Operation.ADD_PERCENT,
            expectedResultValue = 288.0
        )
    )


    /**
     * Tests the effect of a module on another module.
     */
    @Test
    fun testModuleEffectOnModule() = runFittingTest {
        val attribute = attribute()
        val affectedGroupId = newTestGroup()

        val affectedModuleType = moduleType(group = affectedGroupId) {
            attributeValue(attribute, 2.0)
        }

        val affectingModuleType = moduleType(flags = ModuleFlags.PASSIVE) {
            attributeValue(attribute, 1.0)

            effectReference(
                effectOnModules(
                    category = Effect.Category.ONLINE,
                    modifiedAttribute = attribute,
                    modifyingAttribute = attribute,
                    operation = Operation.ADD,
                    groupId = affectedGroupId.id
                )
            )
        }

        val shipType = testShipType()

        // Fit affected, then affecting module
        run {
            val (fit, modules) = fit(shipType, affectedModuleType, affectingModuleType)
            val (affectedModule, affectingModule) = modules

            affectedModule.assertPropertyEquals(
                attribute = attribute,
                expected = 3.0,
                message = "Incorrect module effect application on module"
            )

            // Remove affecting module and verify its effect is gone
            modify {
                fit.removeModule(affectingModule)
            }
            affectedModule.assertPropertyEquals(
                attribute = attribute,
                expected = 2.0,
                message = "Effect of module on module remains after module is removed"
            )

            // Test that removing the affected module doesn't crash
            modify {
                fit.removeModule(affectedModule)
            }
        }

        // Fit affecting, then affected module
        run {
            val (fit, modules) = fit(shipType, affectingModuleType, affectedModuleType)
            val (affectingModule, affectedModule) = modules

            affectedModule.assertPropertyEquals(
                attribute = attribute,
                expected = 3.0,
                message = "Incorrect module effect application on module when affecting module is fitted first"
            )

            // Remove affecting module and verify its effect is gone
            modify {
                fit.removeModule(affectingModule)
            }
            affectedModule.assertPropertyEquals(
                attribute = attribute,
                expected = 2.0,
                message = "Effect of module on module remains after module is removed when affecting module is fitted first"
            )

            // Test that removing the affected module doesn't crash
            modify {
                fit.removeModule(affectedModule)
            }
        }
    }


    /**
     * Tests a disabled module's affects.
     */
    @Test
    fun testDisabledModule() = runFittingTest {
        val shipAttribute = attribute()
        val shipType = testShipType {
            attributeValue(shipAttribute, 100.0)
        }

        val moduleAttribute = attribute()
        val moduleType = moduleType {
            attributeValue(moduleAttribute, 2.0)
            effectReference(
                effectOnShip(
                    category = Effect.Category.ALWAYS,
                    modifiedAttribute = shipAttribute,
                    modifyingAttribute = moduleAttribute,
                    operation = Operation.POST_MULTIPLY
                )
            )
        }

        val (fit, _) = fit(shipType)
        val module = modify {
            fit.fitModule(moduleType, 0).also {
                it.setEnabled(false)
            }
        }

        val ship = fit.ship
        ship.assertPropertyEquals(
            attribute = shipAttribute,
            expected = shipType.attributeValue(shipAttribute),
            message = "Disabled module has effect on ship"
        )

        modify {
            module.setEnabled(true)
        }

        ship.assertPropertyEquals(
            attribute = shipAttribute,
            expected = 200.0,
            message = "(Re)enabled module has wrong on ship effect"
        )

        modify {
            module.setEnabled(false)
        }
        ship.assertPropertyEquals(
            attribute = shipAttribute,
            expected = shipType.attributeValue(shipAttribute),
            message = "Disabled module has effect on ship"
        )
    }


    /**
     * Tests disabling a module with no effects.
     */
    @Test
    fun testDisabledModuleWithNoEffects() = runFittingTest {
        val shipType = testShipType()
        val moduleType = moduleType()

        val (_, module) = fit(shipType, moduleType)
        modify {
            module.setEnabled(false)
        }
    }


}