package theorycrafter.ui.fiteditor

import androidx.compose.ui.input.key.Key
import androidx.compose.ui.test.ComposeUiTest
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.performKeyInput
import androidx.compose.ui.test.pressKey
import androidx.compose.ui.test.runDesktopComposeUiTest
import androidx.compose.ui.test.withKeyDown
import eve.data.SubsystemType
import eve.data.TacticalModeType
import theorycrafter.*
import theorycrafter.TheorycrafterContext.fits
import theorycrafter.fits.requiredSubsystemByKind
import theorycrafter.fitting.Fit
import theorycrafter.fitting.Module
import theorycrafter.ui.fiteditor.Nodes.FitEditor
import theorycrafter.ui.shortName
import kotlin.test.*


/**
 * Tests the undo/redo functionality.
 */
@OptIn(ExperimentalTestApi::class)
class UndoRedoTest: TheorycrafterTest() {


    /**
    * Runs a test with a fit of the given ship name.
     */
    private fun runTestWithFit(
        shipName: String,
        block: suspend ComposeUiTest.(Fit) -> Unit,
    ) = runDesktopComposeUiTest {
        val fit = newFit(
            shipName = shipName
        )

        setApplicationContent {
            FitEditor(fit)
        }

        try {
            block(fit)
        } finally {
            fits.delete(listOf(fits.handleOf(fit)))
        }
    }


    /**
     * Sends an "undo" key shortcut to the fit editor.
     */
    @OptIn(ExperimentalTestApi::class)
    private fun ComposeUiTest.sendUndo() {
        onNode(FitEditor.KeyEventsReceiver).apply {
            performKeyInput {
                withKeyDown(CommandKey) {
                    pressKey(Key.Z)
                }
            }
        }
        waitForIdle()
    }


    /**
     * Sends a "redo" key shortcut to the fit editor.
     */
    @OptIn(ExperimentalTestApi::class)
    private fun ComposeUiTest.sendRedo() {
        onNode(FitEditor.KeyEventsReceiver).apply {
            performKeyInput {
                withKeyDown(CommandKey) {
                    pressKey(Key.Y)
                }
            }
        }
        waitForIdle()
    }


    /**
     * Test unto/redo on a simple module fitting.
     */
    @Test
    fun fitModuleTest() = runTestWithFit("Caracal") { fit ->
        val module = TheorycrafterContext.eveData.moduleType("Small Energy Neutralizer II")
        fit.validate {
            it.fitting.slots[module.slotType] >= 1
        }


        fit.validate {
            it.fitting.slots[module.slotType] >= 1
        }

        onNode(FitEditor.moduleRow(module.slotType, 0)).fitItemIntoSlot(module.name)
        assertEquals(
            expected = module,
            actual = fit.modules.inSlot(module.slotType, 0)?.type
        )

        sendUndo()
        assertEquals(
            expected = null,
            actual = fit.modules.inSlot(module.slotType, 0)
        )

        sendRedo()
        assertEquals(
            expected = module,
            actual = fit.modules.inSlot(module.slotType, 0)?.type
        )
    }


    /**
     * Test unto/redo on a module with a charge.
     */
    @Test
    fun moduleWithChargeTest() = runTestWithFit("Caracal") { fit ->
        val module = TheorycrafterContext.eveData.moduleType("Medium Capacitor Booster II")
        val charge = TheorycrafterContext.eveData.chargeType("Cap Booster 25")

        fit.validate {
            it.fitting.slots[module.slotType] >= 1
        }

        // Fit the capacitor booster and verify it's been fitted
        onNode(FitEditor.moduleRow(module.slotType, 0)).fitItemIntoSlot(module.name)
        var fitModule = fit.modules.inSlot(module.slotType, 0)
        assertNotNull(fitModule)
        assertEquals(
            expected = module,
            actual = fitModule.type
        )
        val defaultCharge = fitModule.loadedCharge
        assertNotNull(defaultCharge)

        // Fit a different charge into it and verify it's been fitted
        onNode(FitEditor.chargeRow(module.slotType, 0)).fitItemIntoSlot(charge.name)
        assertEquals(
            expected = charge,
            actual = fitModule.loadedCharge?.type
        )

        // Undo and verify that the default charge is loaded
        sendUndo()
        assertEquals(
            expected = defaultCharge.type,
            actual = fitModule.loadedCharge?.type
        )

        // Undo and verify that the module was removed
        sendUndo()
        assertEquals(
            expected = null,
            actual = fit.modules.inSlot(module.slotType, 0)
        )

        // Redo and verify the module is fitted again, with the default charge
        sendRedo()
        fitModule = fit.modules.inSlot(module.slotType, 0)
        assertNotNull(fitModule)
        assertEquals(
            expected = module,
            actual = fitModule.type
        )
        assertEquals(
            expected = defaultCharge.type,
            actual = fitModule.loadedCharge?.type
        )

        // Redo again and verify the module has the different charge
        sendRedo()
        assertEquals(
            expected = charge,
            actual = fitModule.loadedCharge?.type
        )
    }


    /**
     * Test undo/redo on a grouping module.
     */
    @Test
    fun groupingModuleTest() = runTestWithFit("Caracal") { fit ->
        val module = TheorycrafterContext.eveData.moduleType("Heavy Missile Launcher II")
        fit.validate {
            it.ship.canFit(module) && it.fitting.launcherHardpoints.total >= 4
        }

        val hardpointCount = fit.fitting.launcherHardpoints.total

        fun assertModuleCountEquals(count: Int) = assertEquals(
            expected = count,
            actual = fit.modules.high.count { it.type == module }
        )

        val moduleRow = onNode(FitEditor.moduleRow(module.slotType, 0))
        moduleRow.fitItemIntoSlot(module.name)
        assertModuleCountEquals(hardpointCount)

        moduleRow.press(FitEditorSlotKeys.RemoveOneItem)
        assertModuleCountEquals(hardpointCount-1)

        moduleRow.press(FitEditorSlotKeys.RemoveOneItem)
        assertModuleCountEquals(hardpointCount-2)

        moduleRow.press(FitEditorSlotKeys.RemoveOneItem)
        assertModuleCountEquals(hardpointCount-3)

        sendUndo()
        assertModuleCountEquals(hardpointCount-2)

        sendUndo()
        assertModuleCountEquals(hardpointCount-1)

        sendUndo()
        assertModuleCountEquals(hardpointCount)

        sendUndo()
        assertModuleCountEquals(0)

        sendRedo()
        assertModuleCountEquals(hardpointCount)

        sendRedo()
        assertModuleCountEquals(hardpointCount-1)

        sendRedo()
        assertModuleCountEquals(hardpointCount-2)

        sendRedo()
        assertModuleCountEquals(hardpointCount-3)
    }


    /**
     * Tests undo/redo on a rig.
     */
    @Test
    fun fitRigTest() = runTestWithFit("Caracal") { fit ->
        val module = TheorycrafterContext.eveData.moduleType("Medium Explosive Armor Reinforcer II")
        fit.validate {
            it.ship.canFit(module)
        }

        val moduleRow = onNode(FitEditor.moduleRow(module.slotType, 0))
        moduleRow.fitItemIntoSlot(module.name)
        assertEquals(
            expected = module,
            actual = fit.modules.inSlot(module.slotType, 0)?.type
        )

        sendUndo()
        assertEquals(
            expected = null,
            actual = fit.modules.inSlot(module.slotType, 0)?.type
        )
    }


    /**
     * Test undo/redo on module state.
     */
    @Test
    fun moduleStateTest() = runTestWithFit("Caracal") { fit ->
        val module = TheorycrafterContext.eveData.moduleType("Small Energy Neutralizer II")
        fit.validate {
            it.fitting.slots[module.slotType] >= 1
        }

        val moduleRow = onNode(FitEditor.moduleRow(module.slotType, 0))
        moduleRow.fitItemIntoSlot(module.name)
        val fitModule = fit.modules.inSlot(module.slotType, 0)

        assertNotNull(fitModule)
        assertEquals(
            expected = module,
            actual = fitModule.type
        )

        fun assertModuleStateEquals(moduleState: Module.State) = assertEquals(
            expected = moduleState,
            actual = fitModule.state
        )

        assertModuleStateEquals(Module.State.ACTIVE)

        moduleRow.press(FitEditorSlotKeys.ToggleItemPrimary)
        assertModuleStateEquals(Module.State.ONLINE)

        moduleRow.press(FitEditorSlotKeys.ToggleItemOnline)
        assertModuleStateEquals(Module.State.OFFLINE)

        moduleRow.press(FitEditorSlotKeys.ToggleItemOverheated)
        assertModuleStateEquals(Module.State.OVERLOADED)

        sendUndo()
        assertModuleStateEquals(Module.State.OFFLINE)

        sendUndo()
        assertModuleStateEquals(Module.State.ONLINE)

        sendUndo()
        assertModuleStateEquals(Module.State.ACTIVE)
    }


    /**
     * A basic booster undo/redo test.
     */
    @Test
    fun basicBoosterTest() = runTestWithFit("Caracal") { fit ->
        val booster1 = TheorycrafterContext.eveData.boosterType("Standard Blue Pill Booster")
        val booster2 = TheorycrafterContext.eveData.boosterType("Standard Exile Booster")
        assertEquals(booster1.slotIndex, booster2.slotIndex)
        val slotIndex = booster1.slotIndex

        val emptyBoosterRow = onNode(FitEditor.EmptyBoosterRow)
        emptyBoosterRow.fitItemIntoSlot(booster1.name)

        var fitBooster = fit.boosters.inSlot(slotIndex)
        assertNotNull(fitBooster)
        assertEquals(booster1, fitBooster.type)
        assertTrue(fitBooster.enabled)

        emptyBoosterRow.press(FitEditorSlotKeys.ToggleItemOnline)
        assertFalse(fitBooster.enabled)

        onNode(FitEditor.boosterRow(0)).fitItemIntoSlot(booster2.name)
        fitBooster = fit.boosters.inSlot(slotIndex)
        assertNotNull(fitBooster)
        assertEquals(booster2, fitBooster.type)

        sendUndo()
        fitBooster = fit.boosters.inSlot(slotIndex)
        assertNotNull(fitBooster)
        assertEquals(booster1, fitBooster.type)
        assertFalse(fitBooster.enabled)

        sendUndo()
        assertTrue(fitBooster.enabled)

        sendUndo()
        assertEquals(null, fit.boosters.inSlot(slotIndex))

        sendRedo()
        fitBooster = fit.boosters.inSlot(slotIndex)
        assertNotNull(fitBooster)
        assertEquals(booster1, fitBooster.type)
        assertTrue(fitBooster.enabled)

        sendRedo()
        assertFalse(fitBooster.enabled)

        sendRedo()
        fitBooster = fit.boosters.inSlot(slotIndex)
        assertNotNull(fitBooster)
        assertEquals(booster2, fitBooster.type)
        assertTrue(fitBooster.enabled)
    }


    /**
     * A basic cargo item undo/redo test.
     */
    @Test
    fun basicCargoItemTest() = runTestWithFit("Caracal") { fit ->
        val itemType = TheorycrafterContext.eveData.cargoItemType("Nanite Repair Paste")
        val amount = 100

        val emptyCargoRow = onNode(FitEditor.EmptyCargoholdRow)
        emptyCargoRow.fitItemIntoSlot("${amount}x ${itemType.name}")

        fun cargoItem() = fit.cargohold.contents[0]

        assertEquals(itemType, cargoItem().type)
        assertEquals(amount, cargoItem().amount)

        val cargoItemRow = onNode(FitEditor.cargoholdRow(0))

        fun assertCargoItemAmountEquals(amount: Int) {
            waitForIdle()
            assertEquals(
                expected = amount,
                actual = cargoItem().amount
            )
        }

        cargoItemRow.press(FitEditorSlotKeys.AddOneItem)
        assertCargoItemAmountEquals(amount+1)

        cargoItemRow.press(FitEditorSlotKeys.AddOneItem)
        assertCargoItemAmountEquals(amount+2)

        cargoItemRow.press(FitEditorSlotKeys.AddOneItem)
        assertCargoItemAmountEquals(amount+3)

        cargoItemRow.fitItemIntoSlot("${amount*2}", printDebugInfo = true)
        assertCargoItemAmountEquals(amount*2)

        sendUndo()
        assertCargoItemAmountEquals(amount+3)

        sendUndo()
        assertCargoItemAmountEquals(amount+2)

        sendUndo()
        assertCargoItemAmountEquals(amount+1)

        sendUndo()
        assertCargoItemAmountEquals(amount)

        sendUndo()
        assertTrue(fit.cargohold.contents.isEmpty())

        sendRedo()
        assertCargoItemAmountEquals(amount)

        sendRedo()
        assertCargoItemAmountEquals(amount+1)

        sendRedo()
        assertCargoItemAmountEquals(amount+2)
    }


    /**
     * A basic drone undo/redo test.
     */
    @Test
    fun basicDroneTest() = runTestWithFit("Vexor") { fit ->
        fit.validate {
            (it.drones.capacity.total >= 25) && (it.drones.bandwidth.total >= 25)
        }

        val drone = TheorycrafterContext.eveData.droneType("Acolyte II")
        val initialDroneAmount = 5

        val emptyDroneRow = onNode(FitEditor.EmptyDroneRow)
        emptyDroneRow.fitItemIntoSlot(drone.name)

        var fittedDroneGroup = fit.drones.all[0]

        fun assertDroneAmountEquals(amount: Int) = assertEquals(
            expected = amount,
            actual = fittedDroneGroup.size
        )

        assertEquals(drone, fittedDroneGroup.type)
        assertDroneAmountEquals(initialDroneAmount)

        val droneGroupRow = onNode(FitEditor.droneRow(0))

        droneGroupRow.press(FitEditorSlotKeys.RemoveOneItem)
        assertDroneAmountEquals(initialDroneAmount-1)

        droneGroupRow.press(FitEditorSlotKeys.RemoveOneItem)
        assertDroneAmountEquals(initialDroneAmount-2)

        droneGroupRow.press(FitEditorSlotKeys.RemoveOneItem)
        assertDroneAmountEquals(initialDroneAmount-3)

        sendUndo()
        assertDroneAmountEquals(initialDroneAmount-2)

        sendUndo()
        assertDroneAmountEquals(initialDroneAmount-1)

        sendUndo()
        assertDroneAmountEquals(initialDroneAmount)

        sendUndo()
        assertTrue(fit.drones.all.isEmpty())

        sendRedo()
        fittedDroneGroup = fit.drones.all[0]
        assertEquals(drone, fittedDroneGroup.type)
        assertDroneAmountEquals(initialDroneAmount)

        sendRedo()
        assertDroneAmountEquals(initialDroneAmount-1)

        sendRedo()
        assertDroneAmountEquals(initialDroneAmount-2)

        sendRedo()
        assertDroneAmountEquals(initialDroneAmount-3)

        // Test undo/redo on the active state
        droneGroupRow.fitItemIntoSlot(drone.name)
        assertTrue(fittedDroneGroup.active)

        droneGroupRow.press(FitEditorSlotKeys.ToggleItemPrimary)
        assertFalse(fittedDroneGroup.active)

        sendUndo()
        assertTrue(fittedDroneGroup.active)

        sendRedo()
        assertFalse(fittedDroneGroup.active)
    }


    /**
     * Basic implant undo/redo test.
     */
    @Test
    fun basicImplantTest() = runTestWithFit("Caracal") { fit ->
        val implant1 = TheorycrafterContext.eveData.implantTypes.getOrNull("Zainou 'Gypsy' CPU Management EE-605")!!
        val implant2 = TheorycrafterContext.eveData.implantTypes.getOrNull("Inherent Implants 'Squire' Power Grid Management EG-605")!!
        assertEquals(implant1.slotIndex, implant2.slotIndex)
        val slotIndex = implant1.slotIndex

        val emptyImplantRow = onNode(FitEditor.EmptyImplantRow)
        emptyImplantRow.fitItemIntoSlot(implant1.name)

        var fitImplant = fit.implants.inSlot(slotIndex)
        assertNotNull(fitImplant)
        assertEquals(implant1, fitImplant.type)
        assertTrue(fitImplant.enabled)

        emptyImplantRow.press(FitEditorSlotKeys.ToggleItemOnline)
        waitForIdle()
        assertFalse(fitImplant.enabled)

        onNode(FitEditor.implantRow(0)).fitItemIntoSlot(implant2.name)
        fitImplant = fit.implants.inSlot(slotIndex)
        assertNotNull(fitImplant)
        assertEquals(implant2, fitImplant.type)

        sendUndo()
        fitImplant = fit.implants.inSlot(slotIndex)
        assertNotNull(fitImplant)
        assertEquals(implant1, fitImplant.type)
        assertFalse(fitImplant.enabled)

        sendUndo()
        assertTrue(fitImplant.enabled)

        sendUndo()
        assertEquals(null, fit.implants.inSlot(slotIndex))

        sendRedo()
        fitImplant = fit.implants.inSlot(slotIndex)
        assertNotNull(fitImplant)
        assertEquals(implant1, fitImplant.type)
        assertTrue(fitImplant.enabled)

        sendRedo()
        assertFalse(fitImplant.enabled)

        sendRedo()
        fitImplant = fit.implants.inSlot(slotIndex)
        assertNotNull(fitImplant)
        assertEquals(implant2, fitImplant.type)
        assertTrue(fitImplant.enabled)
    }


    /**
     * Basic subsystems undo/redo test.
     */
    @Test
    fun basicSubsystemTest() = runTestWithFit("Legion") { fit ->
        fit.validate {
            it.ship.type.usesSubsystems
        }

        val subsystemKind = SubsystemType.Kind.DEFENSIVE
        val subsystems = TheorycrafterContext.eveData.subsystemTypes(fit.ship.type)[subsystemKind]

        fun assertSubsystemIs(subsystemType: SubsystemType) = assertEquals(
            expected = subsystemType,
            actual = fit.requiredSubsystemByKind!![subsystemKind].type
        )

        val subsystemRow = onNode(FitEditor.subsystemRow(subsystemKind))
        subsystemRow.fitItemIntoSlot(subsystems[0].shortName(includeKind = false))
        assertSubsystemIs(subsystems[0])

        subsystemRow.fitItemIntoSlot(subsystems[1].shortName(includeKind = false))
        assertSubsystemIs(subsystems[1])

        sendUndo()
        assertSubsystemIs(subsystems[0])
    }


    /**
     * Basic tactical modes undo/redo test.
     */
    @Test
    fun basicTacticalModesTest() = runTestWithFit("Svipul") { fit ->
        with(TheorycrafterContext.eveData) {
            val tacticalModes = tacticalModeTypes(fit.ship.type)

            fun assertTacticalModeIs(tacticalModeKind: TacticalModeType.Kind) = assertEquals(
                expected = tacticalModes[tacticalModeKind],
                actual = fit.tacticalMode!!.type
            )

            val tacticalModeRow = onNode(FitEditor.TacticalModeSlot)
            tacticalModeRow.fitItemIntoSlot(tacticalModes[TacticalModeType.Kind.SHARPSHOOTER].shortName())
            assertTacticalModeIs(TacticalModeType.Kind.SHARPSHOOTER)

            tacticalModeRow.fitItemIntoSlot(tacticalModes[TacticalModeType.Kind.DEFENSE].shortName())
            assertTacticalModeIs(TacticalModeType.Kind.DEFENSE)

            sendUndo()
            assertTacticalModeIs(TacticalModeType.Kind.SHARPSHOOTER)
        }
    }


}
