package theorycrafter.ui.fiteditor

import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.test.ComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.runDesktopComposeUiTest
import eve.data.ModuleSlotType
import eve.data.SubsystemType
import eve.data.TacticalModeType
import theorycrafter.*
import theorycrafter.TheorycrafterContext.eveData
import theorycrafter.TheorycrafterContext.fits
import theorycrafter.fitting.Fit
import theorycrafter.ui.MainWindowContent
import theorycrafter.utils.CompositionCounters
import theorycrafter.utils.LocalCompositionCounters
import theorycrafter.utils.ProvideCompositionCounters
import kotlin.test.*


/**
 * Tests the amount of recompositions on various events.
 */
@OptIn(ExperimentalTestApi::class)
class RecompositionTest: TheorycrafterTest() {


    /**
     * The [CompositionCounters] for this test.
     */
    private val compositionCounters = CompositionCounters()


    /**
     * Sets the application content to show the given fit, while also providing [compositionCounters] via
     * [LocalCompositionCounters].
     */
    private fun ComposeUiTest.setApplicationContentShowingFitWithCompositionCounters(fit: Fit) {
        setApplicationContent {
            ProvideCompositionCounters(compositionCounters) {
                MainWindowContent()
                val windowManager = LocalTheorycrafterWindowManager.current
                LaunchedEffect(Unit) {
                    val fitHandle = fits.handleOf(fit)
                    windowManager.showFitInMainWindow(fitHandle)
                }
            }
        }
    }


    /**
     * Uses the carousel on the given node.
     */
    context(ComposeUiTest)
    private fun SemanticsNodeInteraction.resetCountersAndPress(key: Key) {
        scrollToAndClick()
        waitForIdle()
        compositionCounters.reset()
        press(key)
    }


    /**
     * Asserts that all fit slots have not been recomposed, except the one with the given tag, which has been
     * recomposed at most once.
     */
    private fun assertNoRecompositionsExcept(fit: Fit, recomposedTag: String) {

        fun assertRecompositionCount(tag: String) {
            val recompositionCount = compositionCounters[tag]
            if (tag == recomposedTag)
                assertTrue(recompositionCount <= 1, "Modified slot $tag recomposed more than once: $recompositionCount")
            else
                assertEquals(0, recompositionCount, "Unrelated slot $tag recomposed $recompositionCount times")
        }

        // Modules
        for (slotType in ModuleSlotType.entries) {
            for (slotIndex in 0 until fit.fitting.slots[slotType]) {
                assertRecompositionCount(TestTags.FitEditor.moduleRow(slotType, slotIndex))
            }
        }

        // Charges
        for (slotType in ModuleSlotType.entries) {
            for (slotIndex in 0 until fit.fitting.slots[slotType]) {
                val module = fit.modules.inSlot(slotType, slotIndex)
                if (module?.canLoadCharges == true)
                    assertRecompositionCount(TestTags.FitEditor.chargeRow(slotType, slotIndex))
            }
        }

        // Drones
        for (slotIndex in 0 .. fit.drones.all.lastIndex)
            assertRecompositionCount(TestTags.FitEditor.droneRow(slotIndex))

        // Cargo
        for (slotIndex in 0 .. fit.cargohold.contents.lastIndex)
            assertRecompositionCount(TestTags.FitEditor.cargoholdRow(slotIndex))

        // Implants
        for (slotIndex in 0 .. fit.implants.fitted.lastIndex)
            assertRecompositionCount(TestTags.FitEditor.implantRow(slotIndex))

        // Boosters
        for (slotIndex in 0 .. fit.boosters.fitted.lastIndex)
            assertRecompositionCount(TestTags.FitEditor.boosterRow(slotIndex))

        // Subsystems
        for (subsystemKind in SubsystemType.Kind.entries) {
            assertRecompositionCount(TestTags.FitEditor.subsystemRow(subsystemKind))
        }

        // Tactical mode
        assertRecompositionCount(TestTags.FitEditor.TacticalModeSlot)

        // Hostile modules
        for (slotIndex in 0 .. fit.remoteEffects.hostile.module.size)
            assertRecompositionCount(TestTags.FitEditor.hostileModuleEffectRow(slotIndex))
    }


    /**
     * Validates that replacing a module in a slot doesn't cause recompositions in other slots.
     */
    @Test
    fun unrelatedSlotsNotRecomposedOnModuleChange() = runDesktopComposeUiTest {
        val fit = newFit(
            shipName = "Vexor",
            isValid = { it.fitting.slots.medium.value >= 3 }
        )

        val mwd = eveData.moduleType("50MN Microwarpdrive I")
        val web = eveData.moduleType("Stasis Webifier I")
        fits.modifyAndSave {
            fit.fitModule(mwd, 0)
            fit.fitModule(web, 1)
        }

        setApplicationContentShowingFitWithCompositionCounters(fit)
        onNode(Nodes.FitEditor.moduleRow(ModuleSlotType.MEDIUM, 0))
            .resetCountersAndPress(FitEditorSlotKeys.CarouselNext)

        // Check that we actually changed the module
        assertNotEquals(
            illegal = mwd,
            actual = fit.modules.inSlot(ModuleSlotType.MEDIUM, 0)?.type
        )

        // Assert that other slots were not recomposed
        assertNoRecompositionsExcept(fit, TestTags.FitEditor.moduleRow(ModuleSlotType.MEDIUM, 0))
    }


    /**
     * Validates that replacing a charge doesn't cause recompositions in other slots.
     */
    @Test
    fun unrelatedSlotsNotRecomposedOnChargeChange() = runDesktopComposeUiTest {
        val fit = newFit(
            shipName = "Vexor",
            isValid = { it.fitting.slots.medium.value >= 3 }
        )

        val sensorBooster = eveData.moduleType("Sensor Booster I")
        val scanResScript = eveData.chargeType("Scan Resolution Script")
        val targetRangeScript = eveData.chargeType("Targeting Range Script")
        fits.modifyAndSave {
            fit.fitModule(sensorBooster, 0).also {
                it.setCharge(scanResScript)
            }
            fit.fitModule(sensorBooster, 1).also {
                it.setCharge(targetRangeScript)
            }
        }

        setApplicationContentShowingFitWithCompositionCounters(fit)
        onNode(Nodes.FitEditor.chargeRow(ModuleSlotType.MEDIUM, 0))
            .resetCountersAndPress(FitEditorSlotKeys.CarouselNext)

        // Check that we actually changed the module
        assertNotEquals(
            illegal = scanResScript,
            actual = fit.modules.inSlot(ModuleSlotType.MEDIUM, 0)?.loadedCharge?.type
        )

        // Assert that other slots were not recomposed
        assertNoRecompositionsExcept(fit, TestTags.FitEditor.chargeRow(ModuleSlotType.MEDIUM, 0))
    }


    /**
     * Validates that replacing a rig in a slot doesn't cause recompositions in other slots.
     */
    @Test
    fun unrelatedSlotsNotRecomposedOnRigChange() = runDesktopComposeUiTest {
        val fit = newFit(
            shipName = "Vexor",
            isValid = { it.fitting.slots.rig.value >= 3 }
        )

        val ccc = eveData.moduleType("Medium Capacitor Control Circuit I")
        val emShieldReinforcer = eveData.moduleType("Medium EM Shield Reinforcer I")
        fits.modifyAndSave {
            fit.fitModule(ccc, 0)
            fit.fitModule(emShieldReinforcer, 1)
        }

        setApplicationContentShowingFitWithCompositionCounters(fit)
        onNode(Nodes.FitEditor.moduleRow(ModuleSlotType.RIG, 0))
            .resetCountersAndPress(FitEditorSlotKeys.CarouselNext)

        // Check that we actually changed the module
        assertNotEquals(
            illegal = ccc,
            actual = fit.modules.inSlot(ModuleSlotType.RIG, 0)?.type
        )

        // Assert that other slots were not recomposed
        assertNoRecompositionsExcept(fit, TestTags.FitEditor.moduleRow(ModuleSlotType.RIG, 0))
    }


    /**
     * Validates that replacing a drone doesn't cause recompositions in other slots.
     */
    @Test
    fun unrelatedSlotsNotRecomposedOnDroneChange() = runDesktopComposeUiTest {
        val acolyte = eveData.droneType("Acolyte I")
        val warrior = eveData.droneType("Warrior I")
        val fit = newFit(
            shipName = "Vexor",
            isValid = {
                (it.drones.bandwidth.total >= acolyte.bandwidthUsed + warrior.bandwidthUsed) &&
                        (it.drones.capacity.total >= acolyte.volume + warrior.volume)
            }
        )

        fits.modifyAndSave {
            fit.addDroneGroup(acolyte, 1)
            fit.addDroneGroup(warrior, 1)
        }

        setApplicationContentShowingFitWithCompositionCounters(fit)
        onNode(Nodes.FitEditor.droneRow(0)).resetCountersAndPress(FitEditorSlotKeys.CarouselNext)

        // Check that we actually changed the drone
        assertNotEquals(
            illegal = acolyte,
            actual = fit.drones.all[0].type
        )

        assertNoRecompositionsExcept(fit, TestTags.FitEditor.droneRow(0))
    }


    /**
     * Validates that replacing a cargo item doesn't cause recompositions in other slots.
     */
    @Test
    fun unrelatedSlotsNotRecomposedOnCargoChange() = runDesktopComposeUiTest {
        val paste = eveData.cargoItemType("Nanite Repair Paste")
        val capBooster = eveData.cargoItemType("Cap Booster 25")
        val fit = newFit(
            shipName = "Vexor",
            isValid = { it.cargohold.capacity.total >= paste.volume + capBooster.volume }
        )

        fits.modifyAndSave {
            fit.addCargoItem(paste, 1)
            fit.addCargoItem(capBooster, 1)
        }

        setApplicationContentShowingFitWithCompositionCounters(fit)
        onNode(Nodes.FitEditor.cargoholdRow(0))
            .resetCountersAndPress(FitEditorSlotKeys.AddOneItem)

        // Check that we actually changed the amount of paste
        assertEquals(
            expected = 2,
            actual = fit.cargohold.contents[0].amount
        )

        assertNoRecompositionsExcept(fit, TestTags.FitEditor.cargoholdRow(0))
    }


    /**
     * Validates that replacing an implant doesn't cause recompositions in other slots.
     */
    @Test
    fun unrelatedSlotsNotRecomposedOnImplantChange() = runDesktopComposeUiTest {
        val ee603 = eveData.implantTypes.getOrNull("Zainou 'Gypsy' CPU Management EE-603")!!
        val mr703 = eveData.implantTypes.getOrNull("Eifyr and Co. 'Gunslinger' Motion Prediction MR-703")!!

        val fit = newFit(shipName = "Vexor")

        fits.modifyAndSave {
            fit.fitImplant(ee603)
            fit.fitImplant(mr703)
        }

        setApplicationContentShowingFitWithCompositionCounters(fit)
        onNode(Nodes.FitEditor.implantRow(0))
            .resetCountersAndPress(FitEditorSlotKeys.CarouselNext)

        // Check that we actually changed the implant
        assertNotEquals(
            illegal = ee603,
            actual = fit.implants.fitted[0].type
        )

        assertNoRecompositionsExcept(fit, TestTags.FitEditor.implantRow(0))
    }


    /**
     * Validates that replacing a booster doesn't cause recompositions in other slots.
     */
    @Test
    fun unrelatedSlotsNotRecomposedOnBoosterChange() = runDesktopComposeUiTest {
        val slot1Booster = eveData.boosterType("Synth Blue Pill Booster")
        val slot2Booster = eveData.boosterType("Synth Frentix Booster")
        assertNotEquals(slot1Booster.slotIndex, slot2Booster.slotIndex, "The two test boosters have the same slot")

        val fit = newFit(shipName = "Vexor")

        fits.modifyAndSave {
            fit.fitBooster(slot1Booster)
            fit.fitBooster(slot2Booster)
        }

        setApplicationContentShowingFitWithCompositionCounters(fit)
        onNode(Nodes.FitEditor.boosterRow(0))
            .resetCountersAndPress(FitEditorSlotKeys.CarouselNext)

        // Check that we actually changed the booster
        assertNotEquals(
            illegal = slot1Booster,
            actual = fit.boosters.fitted[0].type
        )
        assertNotEquals(
            illegal = slot2Booster,
            actual = fit.boosters.fitted[0].type
        )

        assertNoRecompositionsExcept(fit, TestTags.FitEditor.boosterRow(0))
    }


    /**
     * Validates that replacing a tactical mode doesn't cause recompositions in other slots.
     */
    @Test
    fun unrelatedSlotsNotRecomposedOnTacticalModeChange() = runDesktopComposeUiTest {
        val fit = newFit(
            shipName = "Svipul",
            isValid = { it.ship.type.hasTacticalModes }
        )
        val tacticalModesByKind = eveData.tacticalModeTypes(fit.ship.type)
        val tacticalMode = tacticalModesByKind[TacticalModeType.Kind.DEFENSE]
        fits.modifyAndSave {
            fit.setTacticalMode(tacticalMode)
        }

        setApplicationContentShowingFitWithCompositionCounters(fit)
        onNode(Nodes.FitEditor.TacticalModeSlot)
            .resetCountersAndPress(FitEditorSlotKeys.CarouselNext)

        // Check that we actually changed the tactical mode
        assertNotEquals(
            illegal = tacticalMode,
            actual = fit.tacticalMode?.type
        )

        assertNoRecompositionsExcept(fit, TestTags.FitEditor.TacticalModeSlot)
    }


    /**
     * Validates that replacing a hostile effect module doesn't cause recompositions in other slots.
     */
    @Test
    @Ignore  // Fails because of the compacting of remote effect modules in the aux fit.
             // Attempted to fix this by wrapping `ModuleSlotGroup.repModule` in a `derivedStateOf`, but for some
             // reason this is not helping. The ModuleSlotRow for modules that follow the modified slot in the same rack
             // of the auxiliary fit still gets recomposed.
    fun unrelatedSlotsNotRecomposedOnHostileModuleChange() = runDesktopComposeUiTest {
        val web = eveData.moduleType("Stasis Webifier I")
        val rsd = eveData.moduleType("Remote Sensor Dampener I")

        val fit = newFit(shipName = "Vexor")

        fits.modifyAndSave {
            fit.addModuleEffect(web)
            fit.addModuleEffect(rsd)
        }

        setApplicationContentShowingFitWithCompositionCounters(fit)
        onNode(Nodes.FitEditor.hostileModuleEffectRow(0))
            .resetCountersAndPress(FitEditorSlotKeys.CarouselNext)

        // Check that we actually changed the module
        assertTrue(fit.remoteEffects.hostile.module.none { it.module.type == web })

        assertNoRecompositionsExcept(fit, TestTags.FitEditor.hostileModuleEffectRow(0))
    }


}