package theorycrafter.fitting

import eve.data.*
import eve.data.AttributeModifier.Operation
import eve.data.utils.ValueByEnum
import eve.data.utils.valueByEnum
import kotlinx.coroutines.test.runTest
import kotlin.test.assertEquals
import kotlin.test.assertNull


/**
 * The standard [EveData].
 */
val eveData by lazy {
    EveData.loadStandard()
}


/**
 * Runs a fitting test
 */
internal fun runFittingTest(test: suspend FittingEngineTest.() -> Unit) = runTest {
    test(FittingEngineTest(eveData))
}


/**
 * Allows for a convenient way to create and run fitting engine tests.
 * To use it:
 * 1. Call functions that create various [EveData] properties, e.g. [attribute], [moduleType].
 * 2. Call the [fit] and [modify] functions to create and modify fits that use them.
 *   This creates the custom [EveData] and a [FittingEngine] from it.
 * 3. Test the result by examining the properties of the resulting fit(s).
 */
internal class FittingEngineTest(
    baseEveData: EveData
) {


    /**
     * The builder of the custom [EveData] we'll be creating.
     */
    private val customEveDataBuilder = CustomEveDataBuilder(baseEveData)


    /**
     * The [Attributes] in the base [EveData].
     */
    val attributes = customEveDataBuilder.baseAttributes


    /**
     * The test category where we put all our items.
     */
    private val testCategory = customEveDataBuilder.category(
        id = customEveDataBuilder.reserveAvailableItemId(),
        name = "TestCategory"
    )


    /**
     * The fitting engine we'll be testing.
     */
    private var fittingEngine: FittingEngine? = null


    /**
     * Counts the various types if entities we add to the eve data.
     */
    private object Counters{


        /**
         * Maps entity type names to the number of the entities added so far.
         */
        private val counterMap = mutableMapOf<String, Int>()


        /**
         * Increments and returns the number of the entities of the given type.
         */
        fun next(name: String) = counterMap.compute(name){ _, value -> (value ?: 0) + 1 }


    }


    /**
     * Throws an exception if the fitting engine has already been created.
     * The functions that modify the [EveData] can't be called once the fitting engine has been created.
     */
    private fun verifyNoFitting() {
        if (fittingEngine != null)
            throw IllegalStateException("Can't modify EveData after fitting")
    }


    /**
     * Returns the current memory allocations of the fitting engine.
     */
    fun memoryAllocations() = makeFittingEngine().memoryAllocations()


    /**
     * Asserts that the current allocations are the same as the given ones.
     */
    fun assertCurrentAllocationsEqual(allocations: FittingEngine.MemoryAllocations) {
        val difference = memoryAllocations().difference(allocations)
        assertNull(difference, "Fitting engine allocations are not equal")
    }


    /**
     * Returns and reserves an available item id.
     */
    fun reserveAvailableItemId() = customEveDataBuilder.reserveAvailableItemId()


    /**
     * Adds a new [Attribute] to the eve data.
     */
    fun attribute(isStackingPenalized: Boolean = false): Attribute<Double> {
        verifyNoFitting()
        return customEveDataBuilder.attribute(
            name = "testAttribute${Counters.next("attribute")}",
            isStackingPenalized = isStackingPenalized
        )
    }


    /**
     * Adds a new [Effect] to the eve data.
     */
    fun effect(
        category: Effect.Category,
        flags: EffectFlags = EffectFlags(),
        condition: Effect.Condition? = null,
        builder: CustomEveDataBuilder.EffectBuilder.() -> Unit
    ): Effect {
        verifyNoFitting()
        return customEveDataBuilder.effect(
            name = "testEffect${Counters.next("effect")}",
            category = category,
            flags = flags,
            condition = condition,
            builder = builder
        )
    }


    /**
     * Adds a new [TypeGroup] to the eve data.
     */
    fun newTestGroup(): TypeGroup {
        verifyNoFitting()

        return customEveDataBuilder.group(
            category = testCategory,
            name = "TestGroup${Counters.next("group")}"
        )
    }


    /**
     * Adds a new [SkillType] to the eve data.
     */
    fun testSkillType(
        itemId: Int? = null,
        group: TypeGroup? = null,
        skillLevel: Int,
        builder: (CustomEveDataBuilder.SkillTypeBuilder.() -> Unit)? = null
    ): SkillType {
        verifyNoFitting()

        return customEveDataBuilder.skillType(
            itemId = itemId,
            group = group ?: newTestGroup(),
            name = "testSkill${Counters.next("skill")}",
            volume = 0.01,
        ) {
            with(customEveDataBuilder.baseAttributes){
                attributeValue(primaryAttribute, intelligence)
                attributeValue(secondaryAttribute, memory)
                attributeValue(this.skillLevel, skillLevel)
            }

            builder?.invoke(this)
        }
    }


    /**
     * Adds to the skill:
     * 1. A "bonus value" attribute and an effect that modifies that attribute by the `skillLevel` attribute with an
     *   [Operation.PRE_MULTIPLY] operation.
     * 2. A "bonus" effect that modifies the bonused attribute by the value of the "skill bonus" attribute.
     *
     * This is the way skill bonuses like "5% bonus to shield capacity per skill level" are implemented. That is
     * 1. The "bonus value" attribute has a value of 5 and is multiplied by the skill level. So at skill level 5, the
     *   value of "skill bonus" is 25.
     * 2. The "bonus" effect modifies the ship's shield capacity attribute by the value of the skill bonus (with
     *   [Operation.ADD_PERCENT]).
     */
    fun CustomEveDataBuilder.SkillTypeBuilder.skillBonus(
        baseSkillBonusValue: Double,
        attribute: Attribute<Double>,
        itemType: AttributeModifier.AffectedItemKind,
        operation: Operation = Operation.ADD_PERCENT,
        groupId: Int? = null,
        skillTypeId: Int? = null
    ) {
        // Set up the bonus value attribute and the effect that modifies it based on skill level
        val bonusValueAttribute = attribute()
        attributeValue(bonusValueAttribute, baseSkillBonusValue)
        effectReference(
            effect(category = Effect.Category.ALWAYS){
                modifier(
                    modifiedAttribute = bonusValueAttribute,
                    modifyingAttribute = attributes.skillLevel,
                    affectedItemKind = AttributeModifier.AffectedItemKind.SELF,
                    affectedItemFilter = AttributeModifier.AffectedItemFilter.ALL,
                    operation = Operation.PRE_MULTIPLY,
                )
            }
        )

        // Set up the bonus effect itself
        effectReference(
            effect(category = Effect.Category.ALWAYS) {
                modifier(
                    modifiedAttribute = attribute,
                    modifyingAttribute = bonusValueAttribute,
                    affectedItemKind = itemType,
                    affectedItemFilter = affectedItemFilter(groupId = groupId, skillTypeId = skillTypeId),
                    operation = operation,
                    groupId = groupId,
                    skillTypeId = skillTypeId
                )
            }
        )
    }


    /**
     * Modifies the [CharacterType] of the eve data.
     */
    fun characterType(editor: CustomEveDataBuilder.CharacterTypeBuilder.() -> Unit) =
        customEveDataBuilder.characterType(editor)


    /**
     * Modifies the [WarfareBuffsType] of the eve data.
     */
    fun warfareBuffsType(editor: CustomEveDataBuilder.WarfareBuffsTypeBuilder.() -> Unit) =
        customEveDataBuilder.warfareBuffsType(editor)


    /**
     * Adds a new [ModuleType].
     */
    fun moduleType(
        group: TypeGroup? = null,
        slotType: ModuleSlotType = ModuleSlotType.MEDIUM,
        flags: ModuleFlags = ModuleFlags.PASSIVE,
        builder: (CustomEveDataBuilder.ModuleTypeBuilder.() -> Unit)? = null
    ): ModuleType {
        verifyNoFitting()
        return customEveDataBuilder.moduleType(
            group = group ?: newTestGroup(),
            name = "TestModule${Counters.next("module")}",
            volume = 5.0,
            slotType = slotType,
            flags = flags,
        ){
            builder?.invoke(this)
        }
    }


    /**
     * Adds a (fake) Reactive Armor Hardener.
     */
    fun reactiveArmorHardenerType(
        baseResonance: Double,
        resistanceShiftAmount: Double,
    ) = moduleType(slotType = ModuleSlotType.LOW, flags = ModuleFlags.ACTIVE){
        val baseAttrs = customEveDataBuilder.baseAttributes
        val baseEffects = customEveDataBuilder.baseEffects
        baseAttrs.armorResonance.values.forEach {
            attributeValue(it, baseResonance)
        }
        attributeValue(baseAttrs.resistanceShiftAmount, resistanceShiftAmount)
        attributeValue(baseAttrs.adaptationCycles, 0)
        effectReference(baseEffects.adaptiveArmorHardener)
    }


    /**
     * Adds a new module that can load charges.
     */
    @Suppress("MemberVisibilityCanBePrivate")
    fun moduleTypeLoadableWithCharges(
        group: TypeGroup? = null,
        slotType: ModuleSlotType = ModuleSlotType.MEDIUM,
        flags: ModuleFlags = ModuleFlags.ACTIVE,
        chargeGroupIds: List<Int>,
        chargeSize: ItemSize? = null,
        chargeCapacity: Double = 1.0,
        builder: (CustomEveDataBuilder.ModuleTypeBuilder.() -> Unit)? = null
    ): ModuleType = moduleType(
        group = group ?: newTestGroup(),
        slotType = slotType,
        flags = flags
    ){
        if (chargeGroupIds.size > attributes.chargeGroups.size)
            throw IllegalArgumentException("Too many charge group IDs")
        chargeGroupIds.forEachIndexed{ index, chargeGroupId ->
            attributeValue(attributes.chargeGroups[index], chargeGroupId)
        }
        if (chargeSize != null)
            attributeValue(attributes.chargeSize, chargeSize)
        attributeValue(attributes.capacity, chargeCapacity)

        builder?.invoke(this)
    }



    /**
     * Adds a new module that can load charges.
     */
    fun moduleTypeLoadableWithCharges(
        group: TypeGroup? = null,
        slotType: ModuleSlotType = ModuleSlotType.MEDIUM,
        flags: ModuleFlags = ModuleFlags.ACTIVE,
        chargeGroupId: Int,
        chargeSize: ItemSize? = null,
        chargeCapacity: Double = 1.0,
        builder: (CustomEveDataBuilder.ModuleTypeBuilder.() -> Unit)? = null
    ): ModuleType = moduleTypeLoadableWithCharges(
        group = group ?: newTestGroup(),
        slotType = slotType,
        flags = flags,
        chargeGroupIds = listOf(chargeGroupId),
        chargeSize = chargeSize,
        chargeCapacity = chargeCapacity,
        builder = builder
    )


    /**
     * Adds a new rig (module type).
     */
    fun rigType(
        group: TypeGroup? = null,
        rigSize: ItemSize,
        builder: (CustomEveDataBuilder.ModuleTypeBuilder.() -> Unit)? = null
    ): ModuleType {
        verifyNoFitting()
        return customEveDataBuilder.moduleType(
            group = group ?: newTestGroup(),
            name = "TestModule${Counters.next("module")}",
            volume = 5.0,
            slotType = ModuleSlotType.RIG,
            flags = ModuleFlags.PASSIVE,
        ){
            attributeValue(attributes.rigSize, rigSize)
            builder?.invoke(this)
        }
    }


    /**
     * Adds a new [ChargeType] to the eve data.
     */
    fun chargeType(
        group: TypeGroup? = null,
        size: ItemSize? = null,
        volume: Double = 1.0,
        builder: (CustomEveDataBuilder.ChargeTypeBuilder.() -> Unit)? = null
    ): ChargeType {
        verifyNoFitting()
        return customEveDataBuilder.chargeType(
            group = group ?: newTestGroup(),
            name = "TestCharge${Counters.next("charge")}",
            volume = volume,
        ){
            if (size != null)
                attributeValue(attributes.chargeSize, size)

            builder?.invoke(this)
        }
    }


    /**
     * Adds a new [DroneType] to the eve data.
     */
    fun testDroneType(
        group: TypeGroup? = null,
        volume: Double = 10.0,
        bandwidth: Int = 10,
        builder: (CustomEveDataBuilder.DroneTypeBuilder.() -> Unit)? = null
    ): DroneType {
        verifyNoFitting()
        return customEveDataBuilder.droneType(
            group = group ?: newTestGroup(),
            name = "TestDrone${Counters.next("drone")}",
            volume = volume,
        ){
            attributeValue(attributes.droneBandwidthUsed, bandwidth)

            attributeValue(attributes.durationAttribute, attributes.speed)
            attributeValue(attributes.speed, 2000.0)

            attributeValue(attributes.optimalRangeAttribute, attributes.maxRange)
            attributeValue(attributes.maxRange, 50_000.0)

            attributeValue(attributes.falloffRangeAttribute, attributes.falloff)
            attributeValue(attributes.falloff, 20_000.0)

            attributeValue(attributes.trackingSpeedAttribute, attributes.trackingSpeed)
            attributeValue(attributes.trackingSpeed, 10.0)

            builder?.invoke(this)
        }
    }


    /**
     * Adds a new ship type, with default values for all the required attributes.
     */
    fun testShipType(
        itemId: Int? = null,
        group: TypeGroup? = null,
        hasTacticalModes: Boolean = false,
        subsystemCount: Int = 0,
        builder: (CustomEveDataBuilder.ShipTypeBuilder.() -> Unit)? = null
    ): ShipType {
        verifyNoFitting()

        return customEveDataBuilder.shipType(
            itemId = itemId,
            group = group ?: newTestGroup(),
            name = "TestShip${Counters.next("ship")}",
            volume = 500.0,
        ) {
            // Add all the attributes a ship must have
            with(customEveDataBuilder.baseAttributes){
                attributeValue(techLevel, 1)
                attributeValue(signatureRadius, 100.0)
                attributeValue(capacity, 100.0)
                attributeValue(highSlots, ModuleSlotType.HIGH.maxSlotCount)
                attributeValue(medSlots, ModuleSlotType.MEDIUM.maxSlotCount)
                attributeValue(lowSlots, ModuleSlotType.LOW.maxSlotCount)
                attributeValue(rigSlots, ModuleSlotType.RIG.maxSlotCount)
                attributeValue(cpuOutput, 100.0)
                attributeValue(powerOutput, 100.0)
                attributeValue(calibration, 300)
                attributeValue(turretHardpoints, ModuleSlotType.HIGH.maxSlotCount)
                attributeValue(launcherHardpoints, ModuleSlotType.HIGH.maxSlotCount)
                attributeValue(rigSize, ItemSize.MEDIUM)
                attributeValue(shieldHp, 1000.0)
                shieldResonance.values.forEach{ attributeValue(it, 0.5) }
                attributeValue(shieldRechargeTime, 1000.0)
                attributeValue(armorHp, 1000.0)
                armorResonance.values.forEach { attributeValue(it, 0.4) }
                attributeValue(structureHp, 1000.0)
                structureResonance.values.forEach{ attributeValue(it, 0.33) }
                attributeValue(capacitorCapacity, 1000.0)
                attributeValue(capacitorRechargeTime, 1000.0)
                attributeValue(targetingRange, 100_000.0)
                attributeValue(maxLockedTargets, 10)
                attributeValue(scanResolution, 100.0)
                SensorType.entries.forEach{
                    attributeValue(sensorStrength[it], if (it == SensorType.RADAR) 20.0 else 0.0)
                }
                attributeValue(mass, 10_000.0)
                attributeValue(inertiaModifier, 0.3)
                attributeValue(maxVelocity, 200.0)
                attributeValue(speedLimit, -1.0)
                attributeValue(droneCapacity, 200)
                attributeValue(droneBandwidth, 100.0)
                attributeValue(propulsionModuleSpeedFactor, 1.0)
                attributeValue(propulsionModuleThrust, 0.0)

                if (hasTacticalModes)
                    attributeValue(this.hasTacticalModes, true)
                if (subsystemCount > 0)
                    attributeValue(this.maxSubSystems, subsystemCount)
            }

            builder?.invoke(this)
        }
    }


    /**
     * Adds a new [ImplantType] to the eve data.
     */
    fun implantType(
        group: TypeGroup? = null,
        slot: Int = 1,
        builder: (CustomEveDataBuilder.ImplantTypeBuilder.() -> Unit)? = null
    ): ImplantType {
        verifyNoFitting()
        return customEveDataBuilder.implantType(
            group = group ?: newTestGroup(),
            name = "TestImplant${Counters.next("implant")}",
            slot = slot,
        ){
            builder?.invoke(this)
        }
    }


    /**
     * Adds a new [BoosterType] to the eve data.
     */
    fun boosterType(
        group: TypeGroup? = null,
        slot: Int = 2,
        sideEffects: List<BoosterType.SideEffect> = emptyList(),
        builder: (CustomEveDataBuilder.BoosterTypeBuilder.() -> Unit)? = null
    ): BoosterType {
        verifyNoFitting()
        return customEveDataBuilder.boosterType(
            group = group ?: newTestGroup(),
            name = "TestBooster${Counters.next("booster")}",
            slot = slot,
            sideEffects = sideEffects
        ){
            builder?.invoke(this)
        }
    }


    /**
     * Adds a new [TacticalModeType] to the eve data.
     */
    @Suppress("MemberVisibilityCanBePrivate")
    fun tacticalModeType(
        shipType: ShipType,
        kind: TacticalModeType.Kind,
        group: TypeGroup? = null,
        builder: (CustomEveDataBuilder.TacticalModeTypeBuilder.() -> Unit)? = null
    ): TacticalModeType {
        verifyNoFitting()
        return customEveDataBuilder.tacticalModeType(
            shipType = shipType,
            kind = kind,
            group = group ?: newTestGroup(),
        ){
            builder?.invoke(this)
        }
    }


    /**
     * Adds all the types [TacticalModeType] to the eve data.
     */
    fun tacticalModeTypes(
        shipType: ShipType,
        group: TypeGroup? = null,
        defenseModeBuilder: (CustomEveDataBuilder.TacticalModeTypeBuilder.() -> Unit)? = null,
        sharpshooterModeBuilder: (CustomEveDataBuilder.TacticalModeTypeBuilder.() -> Unit)? = null,
        propulsionModeBuilder: (CustomEveDataBuilder.TacticalModeTypeBuilder.() -> Unit)? = null
    ): ValueByEnum<TacticalModeType.Kind, TacticalModeType> {
        return valueByEnum {
            tacticalModeType(
                shipType = shipType,
                kind = it,
                group = group,
                builder = when (it){
                    TacticalModeType.Kind.DEFENSE -> defenseModeBuilder
                    TacticalModeType.Kind.SHARPSHOOTER -> sharpshooterModeBuilder
                    TacticalModeType.Kind.PROPULSION -> propulsionModeBuilder
                }
            )
        }
    }


    /**
     * Adds a [SubsystemType] to the eve data.
     */
    fun subsystemType(
        shipType: ShipType,
        kind: SubsystemType.Kind,
        group: TypeGroup? = null,
        builder: (CustomEveDataBuilder.SubsystemTypeBuilder.() -> Unit)? = null
    ): SubsystemType {
        verifyNoFitting()
        return customEveDataBuilder.subsystemType(
            shipType = shipType,
            kind = kind,
            group = group ?: newTestGroup(),
            name = "TestSubsystem${Counters.next("subsystem")}",
        ){
            builder?.invoke(this)
        }
    }


    /**
     * Creates, if needed, and returns a [FittingEngine] for the custom [EveData] we've been building.
     */
    private fun makeFittingEngine(): FittingEngine {
        fittingEngine?.let { return it }

        return FittingEngine(customEveDataBuilder.build()).also {
            this.fittingEngine = it
        }
    }


    /**
     * Creates a new [Fit].
     * Once this function has been called, the functions that modify the eve data can no longer be called.
     */
    suspend fun fit(block: FittingEngine.ModificationScope.() -> Fit): Fit {
        return makeFittingEngine().modify(block = block)
    }


    /**
     * Modifies the fitting engine.
     * Once this function has been called, the functions that modify the eve data can no longer be called.
     */
    suspend fun <T> modify(block: FittingEngine.ModificationScope.() -> T): T {
        return makeFittingEngine().modify(block = block)
    }


}


/**
 * Encapsulates an attribute modifier (modifying value and operation) and the expected resulting value when applying it.
 */
internal data class ModAndResult(
    val modifyingValue: Double,
    val operation: Operation,
    val expectedResultValue: Double,
)


/**
 * Asserts that the value of the given attribute is equal to the given expected value.
 */
internal fun <T : Any> EveItem<*>.assertPropertyEquals(
    attribute: Attribute<T>,
    expected: T,
    message: String
) {
    assertEquals(
        expected = expected,
        actual = properties.get(attribute).value,
        message = message
    )
}


/**
 * Asserts that the value of the given attribute is equal to the given expected value.
 */
internal fun EveItem<*>.assertPropertyEquals(
    attribute: Attribute<Double>,
    expected: Double,
    absoluteTolerance: Double,
    message: String
) {
    assertEquals(
        expected = expected,
        actual = properties.get(attribute).value,
        absoluteTolerance = absoluteTolerance,
        message = message
    )
}


/**
 * Returns an [Effect] with a single modifier which modifies the item's own attribute.
 */
internal fun FittingEngineTest.effectOnSelf(
    category: Effect.Category,
    modifiedAttribute: Attribute<*>,
    modifyingAttribute: Attribute<*>,
    operation: Operation,
) = effect(category = category) {
    modifier(
        modifiedAttribute = modifiedAttribute,
        modifyingAttribute = modifyingAttribute,
        affectedItemKind = AttributeModifier.AffectedItemKind.SELF,
        affectedItemFilter = AttributeModifier.AffectedItemFilter.ALL,
        operation = operation,
        groupId = null,
        skillTypeId = null,
    )
}


/**
 * Returns an [Effect] with a single modifier which modifies a ship's attribute.
 */
internal fun FittingEngineTest.effectOnShip(
    category: Effect.Category,
    modifiedAttribute: Attribute<*>,
    modifyingAttribute: Attribute<*>,
    attenuatingAttribute: Attribute<*>? = null,
    operation: Operation,
    flags: EffectFlags = EffectFlags(),
    groupId: Int? = null,
    skillTypeId: Int? = null,
) = effect(category = category, flags = flags) {
    modifier(
        modifiedAttribute = modifiedAttribute,
        modifyingAttribute = modifyingAttribute,
        attenuatingAttribute = attenuatingAttribute,
        affectedItemKind = AttributeModifier.AffectedItemKind.SHIP,
        affectedItemFilter = affectedItemFilter(groupId, skillTypeId),
        operation = operation,
        groupId = groupId,
        skillTypeId = skillTypeId,
    )
}


/**
 * Returns a charge [Effect] with a single modifier which modifies the module into which the charge is loaded.
 */
internal fun FittingEngineTest.effectOnChargeModule(
    modifiedAttribute: Attribute<*>,
    modifyingAttribute: Attribute<*>,
    operation: Operation,
) = effect(category = Effect.Category.ALWAYS){
    modifier(
        modifiedAttribute = modifiedAttribute,
        modifyingAttribute = modifyingAttribute,
        affectedItemKind = AttributeModifier.AffectedItemKind.LAUNCHER,
        affectedItemFilter = AttributeModifier.AffectedItemFilter.ALL,
        operation = operation
    )
}


/**
 * Returns the [AttributeModifier.AffectedItemFilter] corresponding to the given `groupId` and `skillTypeId`.
 */
internal fun affectedItemFilter(groupId: Int?, skillTypeId: Int?): AttributeModifier.AffectedItemFilter {
    if ((groupId != null) && (skillTypeId != null))
        throw IllegalArgumentException("Both groupId and skillTypeId may not be null")

    return when{
        groupId != null -> AttributeModifier.AffectedItemFilter.MATCH_GROUP
        skillTypeId != null -> AttributeModifier.AffectedItemFilter.MATCH_REQUIRED_SKILL
        else -> AttributeModifier.AffectedItemFilter.ALL
    }
}


/**
 * Returns an [Effect] with a single modifier which modifies a charge attribute.
 */
internal fun FittingEngineTest.effectOnCharges(
    category: Effect.Category,
    modifiedAttribute: Attribute<*>,
    modifyingAttribute: Attribute<*>,
    attenuatingAttribute: Attribute<*>? = null,
    operation: Operation,
    flags: EffectFlags = EffectFlags(),
    groupId: Int? = null,
    skillTypeId: Int? = null,
) = effect(category = category, flags = flags) {
    modifier(
        modifiedAttribute = modifiedAttribute,
        modifyingAttribute = modifyingAttribute,
        attenuatingAttribute = attenuatingAttribute,
        affectedItemKind = AttributeModifier.AffectedItemKind.LAUNCHABLES,
        affectedItemFilter = affectedItemFilter(groupId, skillTypeId),
        operation = operation,
        groupId = groupId,
        skillTypeId = skillTypeId,
    )
}


/**
 * Returns an [Effect] with a single modifier which modifies a module's attribute.
 */
internal fun FittingEngineTest.effectOnModules(
    category: Effect.Category = Effect.Category.ALWAYS,
    modifiedAttribute: Attribute<*>,
    modifyingAttribute: Attribute<*>,
    attenuatingAttribute: Attribute<*>? = null,
    operation: Operation,
    flags: EffectFlags = EffectFlags(),
    groupId: Int? = null,
    skillTypeId: Int? = null,
) = effect(category = category, flags = flags) {
    modifier(
        modifiedAttribute = modifiedAttribute,
        modifyingAttribute = modifyingAttribute,
        attenuatingAttribute = attenuatingAttribute,
        affectedItemKind = AttributeModifier.AffectedItemKind.MODULES,
        affectedItemFilter = affectedItemFilter(groupId, skillTypeId),
        operation = operation,
        groupId = groupId,
        skillTypeId = skillTypeId,
    )
}


/**
 * Returns an [Effect] with a single modifier which modifies a character's attribute.
 */
internal fun FittingEngineTest.effectOnCharacter(
    category: Effect.Category = Effect.Category.ALWAYS,
    modifiedAttribute: Attribute<*>,
    modifyingAttribute: Attribute<*>,
    operation: Operation,
    groupId: Int? = null,
    skillTypeId: Int? = null,
) = effect(category = category) {
    modifier(
        modifiedAttribute = modifiedAttribute,
        modifyingAttribute = modifyingAttribute,
        affectedItemKind = AttributeModifier.AffectedItemKind.CHARACTER,
        affectedItemFilter = affectedItemFilter(groupId = groupId, skillTypeId = skillTypeId),
        operation = operation,
        groupId = groupId,
        skillTypeId = skillTypeId
    )
}


/**
 * Returns an [Effect] with a single modifier which modifies a [WarfareBuffsType] attribute.
 */
internal fun FittingEngineTest.effectOnWarfareBuffs(
    category: Effect.Category = Effect.Category.ALWAYS,
    condition: Effect.Condition? = null,
    modifiedAttribute: Attribute<*>,
    modifyingAttribute: Attribute<*>,
    operation: Operation,
) = effect(category = category, condition = condition) {
    modifier(
        modifiedAttribute = modifiedAttribute,
        modifyingAttribute = modifyingAttribute,
        affectedItemKind = AttributeModifier.AffectedItemKind.WARFARE_BUFFS,
        affectedItemFilter = AttributeModifier.AffectedItemFilter.ALL,
        operation = operation,
    )
}


/**
 * Returns an [Effect] with a single modifier which modifies a drone attribute.
 */
internal fun FittingEngineTest.effectOnDrones(
    category: Effect.Category = Effect.Category.ALWAYS,
    modifiedAttribute: Attribute<*>,
    modifyingAttribute: Attribute<*>,
    operation: Operation,
    groupId: Int? = null,
    skillTypeId: Int? = null,
) = effect(category = category) {
    modifier(
        modifiedAttribute = modifiedAttribute,
        modifyingAttribute = modifyingAttribute,
        affectedItemKind = AttributeModifier.AffectedItemKind.LAUNCHABLES,
        affectedItemFilter = affectedItemFilter(groupId, skillTypeId),
        operation = operation,
        groupId = groupId,
        skillTypeId = skillTypeId
    )
}


/**
 * Returns an [Effect] with a single modifier which modifies an implant or booster attribute.
 */
internal fun FittingEngineTest.effectOnImplantsAndBoosters(
    category: Effect.Category = Effect.Category.ALWAYS,
    modifiedAttribute: Attribute<*>,
    modifyingAttribute: Attribute<*>,
    operation: Operation,
    groupId: Int? = null,
    skillTypeId: Int? = null,
) = effect(category = category) {
    modifier(
        modifiedAttribute = modifiedAttribute,
        modifyingAttribute = modifyingAttribute,
        affectedItemKind = AttributeModifier.AffectedItemKind.IMPLANTS_BOOSTERS,
        affectedItemFilter = affectedItemFilter(groupId, skillTypeId),
        operation = operation,
        groupId = groupId,
        skillTypeId = skillTypeId,
    )
}


/**
 * Returns an [Effect] with a modifier which modifies a module's attribute when the module is overloaded.
 */
internal fun FittingEngineTest.overloadEffect(
    modifiedAttribute: Attribute<*>,
    modifyingAttribute: Attribute<*>,
    operation: Operation
) = effect(category = Effect.Category.OVERLOADED) {
    modifier(
        modifiedAttribute = modifiedAttribute,
        modifyingAttribute = modifyingAttribute,
        affectedItemKind = AttributeModifier.AffectedItemKind.SELF,
        affectedItemFilter = AttributeModifier.AffectedItemFilter.ALL,
        operation = operation
    )
}


/**
 * Sets up the attributes and chain of effects for a command burst. Returns the command burst module and charge.
 * See `Fixups.fixCommandBursts` for how command bursts are implemented.
 */
internal fun FittingEngineTest.setupCommandBurst(


    /**
     * The id of the buff.
     */
    buffId: Int,


    /**
     * The value of the buff, to be used as the charge's value attribute.
     */
    buffValue: Double,


    /**
     * The value attribute of [WarfareBuffsType]. If `null`, a new attribute will be used.
     */
    warfareBuffValueAttribute: Attribute<Double>? = null,


    /**
     * The effect to add to the [WarfareBuffsType] and the default value of the [warfareBuffValueAttribute].
     * If `null`, [WarfareBuffsType] will not be modified (neither an attribute nor an effect will be added to it).
     * This is useful for creating two command burst modules/charges that trigger the same [WarfareBuffsType] effect.
     */
    warfareBuffEffectAndDefaultValue: ((Attribute<Double>) -> Pair<Effect, Double>)? = null


): Pair<ModuleType, ChargeType> {
    val chargeBuffIdAttribute = attribute()
    val chargeBuffValueAttribute = attribute()
    val moduleBuffIdAttribute = attribute()
    val moduleBuffValueAttribute = attribute()
    val warfareBuffValueActualAttribute = warfareBuffValueAttribute ?: attribute()

    val chargeType = chargeType {
        attributeValue(chargeBuffIdAttribute, buffId.toDouble())
        attributeValue(chargeBuffValueAttribute, buffValue)
        effectReference(
            effectOnChargeModule(  // Copies the buff id to the command burst module
                modifiedAttribute = moduleBuffIdAttribute,
                modifyingAttribute = chargeBuffIdAttribute,
                operation = Operation.SET
            )
        )
        effectReference(
            effectOnChargeModule(  // Multiplies command burst's buff value by its own
                modifiedAttribute = moduleBuffValueAttribute,
                modifyingAttribute = chargeBuffValueAttribute,
                operation = Operation.POST_MULTIPLY
            )
        )
    }

    val commandBurstModuleType = moduleTypeLoadableWithCharges(chargeGroupId = chargeType.groupId) {
        attributeValue(moduleBuffIdAttribute, 0.0)
        attributeValue(moduleBuffValueAttribute, 1.0)
        effectReference(
            effectOnWarfareBuffs(
                category = Effect.Category.ACTIVE,
                condition = Effect.Condition(
                    attributeId = moduleBuffIdAttribute.id,
                    attributeValue = buffId
                ),
                modifiedAttribute = warfareBuffValueActualAttribute,
                modifyingAttribute = moduleBuffValueAttribute,
                operation = Operation.SET_MAX_ABS
            )
        )
    }

    if (warfareBuffEffectAndDefaultValue != null){
        warfareBuffsType {
            val (effect, defaultValue) = warfareBuffEffectAndDefaultValue(warfareBuffValueActualAttribute)
            attributeValue(warfareBuffValueActualAttribute, defaultValue)
            effectReference(effect)
        }
    }

    return Pair(commandBurstModuleType, chargeType)
}


/**
 * Creates a [Fit] with the given modules fitted to it.
 * The order of the modules in the returned list is the same as in the [modules] argument.
 */
internal suspend fun FittingEngineTest.fit(
    ship: ShipType,
    modules: List<ModuleType>,
    bringOnline: Boolean = true,
    block: (FittingEngine.ModificationScope.(Fit, List<Module>) -> Unit)? = null
): Pair<Fit, List<Module>> {
    val moduleList = mutableListOf<Module>()
    val fit = fit {
        newFit(ship).also {
            val moduleIndexBySlot = mutableMapOf<ModuleSlotType, Int>()
            for (moduleTypes in modules) {
                val slotIndex = moduleIndexBySlot.compute(moduleTypes.slotType) { _, index -> index?.plus(1) ?: 0 }!!
                val module = it.fitModule(moduleTypes, slotIndex)
                if (bringOnline)
                    module.setState(Module.State.ONLINE)
                moduleList.add(module)
            }
            block?.invoke(this, it, moduleList)
        }
    }

    return fit to moduleList
}


/**
 * Creates a [Fit] with the given modules fitted to it.
 * The order of the modules in the returned list is the same as in the [module] argument.
 */
internal suspend fun FittingEngineTest.fit(
    ship: ShipType,
    vararg module: ModuleType,
    bringOnline: Boolean = true,
    block: (FittingEngine.ModificationScope.(Fit, List<Module>) -> Unit)? = null
): Pair<Fit, List<Module>> {
    return fit(ship, module.asList(), bringOnline, block)
}


/**
 * Creates a [Fit] with the given module fitted to it.
 */
internal suspend fun FittingEngineTest.fit(
    ship: ShipType,
    module: ModuleType,
    bringOnline: Boolean = true,
    block: (FittingEngine.ModificationScope.(Fit, Module) -> Unit)? = null
): Pair<Fit, Module> {
    return fit(ship, listOf(module), bringOnline) { fit, modules ->
        block?.invoke(this, fit, modules[0])
    }.let { it.first to it.second[0] }
}


/**
 * Asserts that the given item has a non-null [EveItem.illegalFittingReason].
 */
internal fun assertIsFittedIllegally(item: EveItem<*>) {
    assert(item.illegalFittingReason != null) { "Item ${item.name} expected to have an illegality reason, but does not" }
}


/**
 * Asserts that the given item has a null [EveItem.illegalFittingReason].
 */
internal fun assertIsFittedLegally(item: EveItem<*>) {
    val illegalityReason = item.illegalFittingReason
    assert(illegalityReason == null) {
        "Item ${item.name} expected to not have an illegality reason, but has: ${item.illegalFittingReason}"
    }
}