package theorycrafter.ui.fiteditor

import androidx.compose.ui.input.key.Key
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.performKeyInput
import androidx.compose.ui.test.runDesktopComposeUiTest
import eve.data.*
import theorycrafter.TheorycrafterContext.eveData
import theorycrafter.TheorycrafterContext.fits
import theorycrafter.TheorycrafterTest
import theorycrafter.scrollToAndClick
import theorycrafter.setApplicationContent
import theorycrafter.ui.fiteditor.FitEditorSlotKeys.CarouselNext
import theorycrafter.ui.fiteditor.FitEditorSlotKeys.CarouselPrev
import theorycrafter.ui.fiteditor.FitEditorSlotKeys.ClearSlot
import theorycrafter.ui.fiteditor.FitEditorSlotKeys.ToggleItemOnline
import theorycrafter.ui.fiteditor.FitEditorSlotKeys.ToggleItemOverheated
import theorycrafter.ui.fiteditor.FitEditorSlotKeys.ToggleItemPrimary
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.assertNotNull

/**
 * Tests sending several key events rapidly to various fit editor slots.
 *
 * This validates fixing a bug where events are received before a recomposition occurs, and the action on the slot
 * has not synchronized with the new state yet, and it attempts to perform something illegal, such as removing a
 * non-existent item (because it has already removed it in response to an earlier event).
 */
@OptIn(ExperimentalTestApi::class)
class RapidKeyEventsOnSlotTest: TheorycrafterTest() {

    
    /**
     * The synth blue pill booster.
     */
    private val synthBluePill: BoosterType by lazy {
        eveData.boosterType("Synth Blue Pill Booster")
    }


    /**
     * Test that pressing "next" twice rapidly on a booster slot doesn't crash.
     */
    @Test
    fun usingBoosterCarouselQuicklyDoesNotCrash() = runDesktopComposeUiTest {
        val fit = newFit("Vexor")

        fits.modifyAndSave {
            fit.fitBooster(synthBluePill)
        }

        setApplicationContent {
            FitEditor(fit)
        }

        onNode(Nodes.FitEditor.boosterRow(0))
            .pressRapidly(CarouselNext, CarouselNext)

        assertEquals(expected = 1, actual = fit.boosters.fitted.size)
        assertNotEquals(illegal = synthBluePill, actual = fit.boosters.fitted[0].type)
    }


    /**
     * Test that pressing "delete" to remove a booster and then "next" quickly doesn't crash.
     */
    @Test
    fun removeBoosterThenUseCarouselDoesNotCrash() = runDesktopComposeUiTest {
        val fit = newFit("Vexor")

        fits.modifyAndSave {
            fit.fitBooster(synthBluePill)
        }

        setApplicationContent {
            FitEditor(fit)
        }

        onNode(Nodes.FitEditor.boosterRow(0))
            .pressRapidly(ClearSlot, CarouselNext)

        assertEquals(expected = 0, actual = fit.boosters.fitted.size)
    }


    /**
     * The "Warrior I" drone type.
     */
    private val warrior: DroneType by lazy {
        eveData.droneType("Warrior I")
    }


    /**
     * Test that pressing "next" twice rapidly on a drone slot doesn't crash.
     */
    @Test
    fun usingDroneCarouselQuicklyDoesNotCrash() = runDesktopComposeUiTest {
        val fit = newFit("Vexor")
        if (fit.drones.bandwidth.total < 25.0)
            error("${fit.ship.type.name} may no longer be suitable for this test")

        fits.modifyAndSave {
            fit.addDroneGroup(warrior, 5)
        }

        setApplicationContent {
            FitEditor(fit)
        }

        onNode(Nodes.FitEditor.droneRow(0))
            .pressRapidly(CarouselNext, CarouselNext)

        assertEquals(expected = 1, actual = fit.drones.all.size)
        assertNotEquals(illegal = warrior, fit.drones.all[0].type)
    }


    /**
     * Test that pressing "delete" to remove a drone group and then "next".
     */
    @Test
    fun removeDroneThenUseCarouselDoesNotCrash() = runDesktopComposeUiTest {
        val fit = newFit("Vexor")
        if (fit.drones.bandwidth.total < 25.0)
            error("${fit.ship.type.name} may no longer be suitable for this test")

        fits.modifyAndSave {
            fit.addDroneGroup(warrior, 5)
        }

        setApplicationContent {
            FitEditor(fit)
        }

        onNode(Nodes.FitEditor.droneRow(0))
            .pressRapidly(ClearSlot, CarouselNext)

        assertEquals(expected = 0, actual = fit.drones.all.size)
    }


    /**
     * The EE-601 implant.
     */
    private val ee601: ImplantType by lazy {
        eveData.implantTypes.getOrNull("Zainou 'Gypsy' CPU Management EE-601")!!
    }


    /**
     * Test that pressing "next" twice rapidly on an implant slot doesn't crash.
     */
    @Test
    fun usingImplantCarouselQuicklyDoesNotCrash() = runDesktopComposeUiTest {
        val fit = newFit("Vexor")

        fits.modifyAndSave {
            fit.fitImplant(ee601)
        }

        setApplicationContent {
            FitEditor(fit)
        }

        onNode(Nodes.FitEditor.implantRow(0))
            .pressRapidly(CarouselNext, CarouselNext)

        assertEquals(expected = 1, actual = fit.implants.fitted.size)
        assertNotEquals(illegal = ee601, fit.implants.fitted[0].type)
    }


    /**
     * Test that pressing "delete" to remove an implant and then "next" quickly doesn't crash.
     */
    @Test
    fun removeImplantThenUseCarouselDoesNotCrash() = runDesktopComposeUiTest {
        val fit = newFit("Vexor")

        fits.modifyAndSave {
            fit.fitImplant(ee601)
        }

        setApplicationContent {
            FitEditor(fit)
        }

        onNode(Nodes.FitEditor.implantRow(0))
            .pressRapidly(ClearSlot, CarouselNext)

        assertEquals(expected = 0, actual = fit.implants.fitted.size)
    }


    /**
     * Nanite repair paste.
     */
    private val nanitePaste by lazy {
        eveData.cargoItemType("Nanite Repair Paste")
    }


    /**
     * Test that pressing "delete" twice quickly on a cargo item slot doesn't crash.
     */
    @Test
    fun removeCargoItemTwiceDoesNotCrash() = runDesktopComposeUiTest {
        val fit = newFit("Vexor")

        fits.modifyAndSave {
            fit.addCargoItem(nanitePaste, 100)
        }

        setApplicationContent {
            FitEditor(fit)
        }

        onNode(Nodes.FitEditor.cargoholdRow(0))
            .pressRapidly(ClearSlot, ClearSlot)

        assertEquals(expected = 0, actual = fit.cargohold.contents.size)
    }


    /**
     * Test that pressing "delete" twice quickly on a command effect slot doesn't crash.
     */
    @Test
    fun removeCommandEffectItemTwiceDoesNotCrash() = runDesktopComposeUiTest {
        val fit = newFit("Vexor")
        val commandFit = newFit("Eos")

        fits.modifyAndSave {
            fit.addCommandEffect(commandFit)
        }

        setApplicationContent {
            FitEditor(fit)
        }

        onNode(Nodes.FitEditor.commandEffectRow(0))
            .pressRapidly(ClearSlot, ClearSlot)

        assertEquals(expected = 0, actual = fit.remoteEffects.command.allExcludingAuxiliary.size)
    }


    /**
     * The 50MN microwarpdrive module.
     */
    private val mwd50: ModuleType by lazy {
        eveData.moduleType("50MN Microwarpdrive II")
    }


    /**
     * Test that pressing "next" twice rapidly on a module slot doesn't crash.
     */
    @Test
    fun usingModuleCarouselQuicklyDoesNotCrash() = runDesktopComposeUiTest {
        val fit = newFit("Vexor")

        fits.modifyAndSave {
            fit.fitModule(mwd50, 0)
        }

        setApplicationContent {
            FitEditor(fit)
        }

        onNode(Nodes.FitEditor.moduleRow(mwd50.slotType, 0))
            .pressRapidly(CarouselNext, CarouselNext)

        val fittedModule = fit.modules.inSlot(mwd50.slotType, 0)
        assertNotNull(fittedModule)
        assertNotEquals(illegal = mwd50, actual = fittedModule.type)
    }


    /**
     * Test that pressing "delete" to remove a module and then "next" quickly doesn't crash.
     */
    @Test
    fun removeModuleThenUseCarouselDoesNotCrash() = runDesktopComposeUiTest {
        val fit = newFit("Vexor")

        fits.modifyAndSave {
            fit.fitModule(mwd50, 0)
        }

        setApplicationContent {
            FitEditor(fit)
        }

        onNode(Nodes.FitEditor.moduleRow(mwd50.slotType, 0))
            .pressRapidly(ClearSlot, CarouselNext)

        assertEquals(expected = null, actual = fit.modules.inSlot(mwd50.slotType, 0))
    }


    /**
     * Test that pressing "delete" twice quickly on a module slot doesn't crash.
     */
    @Test
    fun removeModuleTwiceDoesNotCrash() = runDesktopComposeUiTest {
        val fit = newFit("Vexor")

        fits.modifyAndSave {
            fit.fitModule(mwd50, 0)
        }

        setApplicationContent {
            FitEditor(fit)
        }

        onNode(Nodes.FitEditor.moduleRow(mwd50.slotType, 0))
            .pressRapidly(ClearSlot, ClearSlot)

        assertEquals(expected = null, actual = fit.modules.inSlot(mwd50.slotType, 0))
    }


    /**
     * Test that pressing "delete" and then toggle state quickly on a module slot doesn't crash.
     */
    @Test
    fun removeModuleThenToggleStateDoesNotCrash() = runDesktopComposeUiTest {
        val fit = newFit("Vexor")

        setApplicationContent {
            FitEditor(fit)
        }

        // Toggle primary
        fits.modifyAndSave { fit.fitModule(mwd50, 0) }
        onNode(Nodes.FitEditor.moduleRow(mwd50.slotType, 0))
            .pressRapidly(ClearSlot, ToggleItemPrimary)
        assertEquals(expected = null, actual = fit.modules.inSlot(mwd50.slotType, 0))

        // Toggle overheat
        fits.modifyAndSave { fit.fitModule(mwd50, 0) }
        onNode(Nodes.FitEditor.moduleRow(mwd50.slotType, 0))
            .pressRapidly(ClearSlot, ToggleItemOverheated)
        assertEquals(expected = null, actual = fit.modules.inSlot(mwd50.slotType, 0))

        // Toggle online
        fits.modifyAndSave { fit.fitModule(mwd50, 0) }
        onNode(Nodes.FitEditor.moduleRow(mwd50.slotType, 0))
            .pressRapidly(ClearSlot, ToggleItemOnline)
        assertEquals(expected = null, actual = fit.modules.inSlot(mwd50.slotType, 0))
    }


    /**
     * Test that pressing prev/next rapidly on a subsystem slot doesn't crash.
     */
    @Test
    fun usingSubsystemCarouselQuicklyDoesNotCrash() = runDesktopComposeUiTest {
        val fit = newFit(
            shipName = "Tengu",
            isValid = { it.ship.type.usesSubsystems }
        )

        setApplicationContent {
            FitEditor(fit)
        }

        onNode(Nodes.FitEditor.subsystemRow(SubsystemType.Kind.DEFENSIVE)).let {
            it.pressRapidly(CarouselNext, CarouselNext)
            it.pressRapidly(CarouselPrev, CarouselPrev)
        }
    }


    /**
     * Test that pressing prev/next rapidly on a tactical mode slot doesn't crash.
     */
    @Test
    fun usingTacticalModeCarouselQuicklyDoesNotCrash() = runDesktopComposeUiTest {
        val fit = newFit(
            shipName = "Svipul",
            isValid = { it.ship.type.hasTacticalModes }
        )

        setApplicationContent {
            FitEditor(fit)
        }

        onNode(Nodes.FitEditor.TacticalModeSlot).let {
            it.pressRapidly(CarouselNext, CarouselNext)
            it.pressRapidly(CarouselPrev, CarouselPrev)
        }
    }


}


/**
 * Presses the given keys rapidly on a slot, without allowing recomposition to occur between them.
 */
@OptIn(ExperimentalTestApi::class)
private fun SemanticsNodeInteraction.pressRapidly(key1: Key, key2: Key) {
    scrollToAndClick()
    performKeyInput {
        // Use keyDown+keyUp instead of pressKey to avoid the delay between presses
        keyDown(key1)
        keyUp(key1)
        keyDown(key2)
        keyUp(key2)
    }
}
