package theorycrafter.fitting

import eve.data.*
import eve.data.utils.forEach
import eve.data.utils.valueByEnum
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails


/**
 * Tests booster-related fitting functionality.
 */
class SubsystemTest {


    /**
     * Tests that subsystems can be set on a ship.
     */
    @Test
    fun subsystemsCanBeSet() = runFittingTest {
        val shipType = testShipWithSubsystems()

        val subsystemTypes = valueByEnum<SubsystemType.Kind, SubsystemType> {
            subsystemType(
                shipType = shipType,
                kind = it
            )
        }

        val (fit, _) = fit(shipType)

        modify {
            subsystemTypes.forEach { _, subsystemType ->
                fit.setSubsystem(subsystemType)
            }
        }

        subsystemTypes.forEach { kind, subsystemType ->
            assertEquals(subsystemType, fit.subsystemByKind?.get(kind)?.type)
        }
    }


    /**
     * Tests that subsystems can't be set on a ship that doesn't have the [Attributes.maxSubSystems] attribute.
     */
    @Test
    fun subsystemsCanNotBeSetOnShipWithoutMaxSubSystemsAttribute() = runFittingTest {
        val shipType = testShipType()

        val subsystemType = subsystemType(
            shipType = shipType,
            kind = SubsystemType.Kind.CORE
        )

        val (fit, _) = fit(shipType)

        assertFails("Able to set a subsystem on a ship that doesn't accept subsystems") {
            modify {
                fit.setSubsystem(subsystemType)
            }
        }
    }


    /**
     * Tests the effect of a subsystem on a ship's attribute.
     */
    @Test
    fun testSubsystemEffectOnShip() = runFittingTest {
        val shipAttribute = attribute()

        val shipType = testShipWithSubsystems {
            attributeValue(shipAttribute, 100.0)
        }

        val subsystemAttribute = attribute()
        val affectingSubsystemType = subsystemType(shipType, SubsystemType.Kind.CORE){
            attributeValue(subsystemAttribute, 10.0)
            effectReference(
                effectOnShip(
                    category = Effect.Category.ALWAYS,
                    modifiedAttribute = shipAttribute,
                    modifyingAttribute = subsystemAttribute,
                    operation = AttributeModifier.Operation.ADD
                )
            )
        }

        val nonAffectingSubsystemType = subsystemType(shipType, SubsystemType.Kind.CORE){
            attributeValue(subsystemAttribute, 10.0)
        }

        val (fit, _) = fit(shipType)

        // Validate initial value
        fit.ship.assertPropertyEquals(shipAttribute, 100.0, "Wrong initial ship property value")

        // Validate that an irrelevant subsystem doesn't do anything
        modify {
            fit.setSubsystem(nonAffectingSubsystemType)
        }
        fit.ship.assertPropertyEquals(shipAttribute, 100.0, "Wrong ship property value with irrelevant subsystem")

        // Validate that the affecting subsystem applies its effect
        modify {
            fit.setSubsystem(affectingSubsystemType)
        }
        fit.ship.assertPropertyEquals(shipAttribute, 110.0, "Wrong application of subsystem on ship property")

        // Validate that (re)setting the irrelevant subsystem re-sets the property
        modify {
            fit.setSubsystem(nonAffectingSubsystemType)
        }
        fit.ship.assertPropertyEquals(shipAttribute, 100.0, "Ship property not re-set after subsystem mode is re-set")
    }


    /**
     * Tests the effect of a subsystem on the character.
     */
    @Test
    fun testSubsystemEffectOnCharacter() = runFittingTest {
        val charAttribute = attribute()
        characterType {
            attributeValue(charAttribute, 100.0)
        }

        val shipType = testShipWithSubsystems()

        val subsystemAttribute = attribute()
        val affectingSubsystemType = subsystemType(shipType, SubsystemType.Kind.CORE){
            attributeValue(subsystemAttribute, 10.0)
            effectReference(
                effectOnCharacter(
                    category = Effect.Category.ALWAYS,
                    modifiedAttribute = charAttribute,
                    modifyingAttribute = subsystemAttribute,
                    operation = AttributeModifier.Operation.ADD
                )
            )
        }

        val nonAffectingSubsystemType = subsystemType(shipType, SubsystemType.Kind.CORE){
            attributeValue(subsystemAttribute, 10.0)
        }

        val (fit, _) = fit(shipType)

        // Validate initial value
        fit.character.assertPropertyEquals(charAttribute, 100.0, "Wrong initial character property value")

        // Validate that an irrelevant subsystem doesn't do anything
        modify {
            fit.setSubsystem(nonAffectingSubsystemType)
        }
        fit.character.assertPropertyEquals(charAttribute, 100.0, "Wrong character property value with irrelevant subsystem")

        // Validate that the affecting subsystem applies its effect
        modify {
            fit.setSubsystem(affectingSubsystemType)
        }
        fit.character.assertPropertyEquals(charAttribute, 110.0, "Wrong application of subsystem on character property")

        // Validate that (re)setting the irrelevant subsystem re-sets the property
        modify {
            fit.setSubsystem(nonAffectingSubsystemType)
        }
        fit.character.assertPropertyEquals(charAttribute, 100.0, "Character property not re-set after subsystem mode is re-set")
    }


    /**
     * Tests the effect of a subsystem on modules.
     */
    @Test
    fun testSubsystemEffectOnModule() = runFittingTest {
        val moduleAttribute = attribute()
        val moduleType = moduleType {
            attributeValue(moduleAttribute, 100.0)
        }

        val shipType = testShipWithSubsystems()

        val subsystemAttribute = attribute()
        val affectingSubsystemType = subsystemType(shipType, SubsystemType.Kind.CORE){
            attributeValue(subsystemAttribute, 10.0)
            effectReference(
                effectOnModules(
                    category = Effect.Category.ALWAYS,
                    modifiedAttribute = moduleAttribute,
                    modifyingAttribute = subsystemAttribute,
                    operation = AttributeModifier.Operation.ADD
                )
            )
        }

        val nonAffectingSubsystemType = subsystemType(shipType, SubsystemType.Kind.CORE){
            attributeValue(subsystemAttribute, 10.0)
        }

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

        // Validate initial value
        module.assertPropertyEquals(moduleAttribute, 100.0, "Wrong initial module property value")

        // Validate that an irrelevant subsystem doesn't do anything
        modify {
            fit.setSubsystem(nonAffectingSubsystemType)
        }
        module.assertPropertyEquals(moduleAttribute, 100.0, "Wrong module property value with irrelevant subsystem")

        // Validate that the affecting subsystem applies its effect
        modify {
            fit.setSubsystem(affectingSubsystemType)
        }
        module.assertPropertyEquals(moduleAttribute, 110.0, "Wrong application of subsystem on module property")

        // Validate that (re)setting the irrelevant subsystem re-sets the property
        modify {
            fit.setSubsystem(nonAffectingSubsystemType)
        }
        module.assertPropertyEquals(moduleAttribute, 100.0, "Module property not re-set after subsystem mode is re-set")

        // Now check that if we first set the subsystem and then add the module, that also works
        modify {
            fit.removeModule(module)
            fit.setSubsystem(affectingSubsystemType)
        }
        val anotherModule = modify { fit.fitModule(moduleType, 0) }
        anotherModule.assertPropertyEquals(
            attribute = moduleAttribute,
            expected = 110.0,
            message = "Wrong application of subsystem on module (fitted afterwards) property"
        )
    }


    /**
     * Tests the effect of a subsystem on charge attributes.
     */
    @Test
    fun testSubsystemEffectOnCharge() = runFittingTest {
        val chargeAttribute = attribute()
        val chargeType = chargeType {
            attributeValue(chargeAttribute, 100.0)
        }

        val moduleType = moduleTypeLoadableWithCharges(chargeGroupId = chargeType.groupId)

        val shipType = testShipWithSubsystems()

        val subsystemAttribute = attribute()
        val affectingSubsystemType = subsystemType(shipType, SubsystemType.Kind.CORE){
            attributeValue(subsystemAttribute, 10.0)
            effectReference(
                effectOnCharges(
                    category = Effect.Category.ALWAYS,
                    modifiedAttribute = chargeAttribute,
                    modifyingAttribute = subsystemAttribute,
                    operation = AttributeModifier.Operation.ADD
                )
            )
        }

        val nonAffectingSubsystemType = subsystemType(shipType, SubsystemType.Kind.CORE){
            attributeValue(subsystemAttribute, 10.0)
        }

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

        // Validate initial value
        charge.assertPropertyEquals(chargeAttribute, 100.0, "Wrong initial charge property value")

        // Validate that an irrelevant subsystem doesn't do anything
        modify {
            fit.setSubsystem(nonAffectingSubsystemType)
        }
        charge.assertPropertyEquals(chargeAttribute, 100.0, "Wrong charge property value with irrelevant subsystem")

        // Validate that the affecting subsystem applies its effect
        modify {
            fit.setSubsystem(affectingSubsystemType)
        }
        charge.assertPropertyEquals(chargeAttribute, 110.0, "Wrong application of subsystem on charge property")

        // Validate that (re)setting the irrelevant mode re-sets the property
        modify {
            fit.setSubsystem(nonAffectingSubsystemType)
        }
        charge.assertPropertyEquals(chargeAttribute, 100.0, "Charge property not re-set after subsystem is re-set")

        // Now check that if we first set the subsystem and then the charge, that also works
        modify {
            module.removeCharge()
            fit.setSubsystem(affectingSubsystemType)
        }
        val anotherCharge = modify { module.setCharge(chargeType) }
        anotherCharge.assertPropertyEquals(
            attribute = chargeAttribute,
            expected = 110.0,
            message = "Wrong application of subsystem on charge (fitted afterwards) property"
        )

        // Now check that if we first set the tactical mode and then add the module with the charge, that also works
        modify {
            fit.setSubsystem(nonAffectingSubsystemType)
            fit.removeModule(module)
        }
        val anotherChargeOnAnotherModule = modify {
            fit.setSubsystem(affectingSubsystemType)
            val anotherModule = fit.fitModule(moduleType, 0)
            anotherModule.setCharge(chargeType)
        }
        anotherChargeOnAnotherModule.assertPropertyEquals(
            attribute = chargeAttribute,
            expected = 110.0,
            message = "Wrong application of tactical mode on charge property if module and charge fit after tactical" +
                    " mode is set"
        )
    }


    /**
     * Tests a subsystem's effect on drones.
     */
    @Test
    fun testSubsystemEffectOnDrones() = runFittingTest {
        val droneAttribute = attribute()
        val droneType = testDroneType {
            attributeValue(droneAttribute, 100.0)
        }

        val shipType = testShipWithSubsystems()

        val subsystemAttribute = attribute()
        val affectingSubsystemType = subsystemType(shipType, SubsystemType.Kind.CORE){
            attributeValue(subsystemAttribute, 10.0)
            effectReference(
                effectOnDrones(
                    category = Effect.Category.ALWAYS,
                    modifiedAttribute = droneAttribute,
                    modifyingAttribute = subsystemAttribute,
                    operation = AttributeModifier.Operation.ADD
                )
            )
        }

        val nonAffectingSubsystemType = subsystemType(shipType, SubsystemType.Kind.CORE){
            attributeValue(subsystemAttribute, 10.0)
        }

        val (fit, _) = fit(shipType)

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

        // Validate initial value
        droneGroup.assertPropertyEquals(droneAttribute, 100.0, "Wrong initial drone property value")

        // Validate that an irrelevant subsystem doesn't do anything
        modify {
            fit.setSubsystem(nonAffectingSubsystemType)
        }
        droneGroup.assertPropertyEquals(droneAttribute, 100.0, "Wrong drone property value with irrelevant subsystem")

        // Validate that the affecting subsystem applies its effect
        modify {
            fit.setSubsystem(affectingSubsystemType)
        }
        droneGroup.assertPropertyEquals(droneAttribute, 110.0, "Wrong application of subsystem on drone property")

        // Validate that (re)setting the irrelevant subsystem re-sets the property
        modify {
            fit.setSubsystem(nonAffectingSubsystemType)
        }
        droneGroup.assertPropertyEquals(droneAttribute, 100.0, "Drone property not re-set after subsystem mode is re-set")

        // Now check that if we first set the subsystem and then add the drones, that also works
        modify {
            fit.removeDroneGroup(droneGroup)
            fit.setSubsystem(affectingSubsystemType)
        }
        val anotherDroneGroup = modify { fit.addDroneGroup(droneType, 5) }
        anotherDroneGroup.assertPropertyEquals(
            attribute = droneAttribute,
            expected = 110.0,
            message = "Wrong application of subsystem on drones (fitted afterwards) property"
        )
    }



}


/**
 * Creates a test ship that has subsystems.
 */
private fun FittingEngineTest.testShipWithSubsystems(
    subsystemCount: Int = 4,
    builder: (CustomEveDataBuilder.ShipTypeBuilder.() -> Unit)? = null
): ShipType {
    return testShipType(subsystemCount = subsystemCount, builder = builder)
}
