package theorycrafter

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.test.*
import compose.input.KeyboardModifierMatcher
import compose.input.KeyboardModifierRequirement
import eve.data.EveData
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.runBlocking
import theorycrafter.TheorycrafterContext.eveData
import theorycrafter.TheorycrafterContext.fits
import theorycrafter.fitting.Fit
import theorycrafter.ui.fiteditor.FitEditorSlotKeys
import theorycrafter.utils.LocalIsTest
import java.io.File
import java.nio.file.Files
import kotlin.test.AfterTest
import kotlin.test.BeforeTest


/**
 * The base class for application tests.
 *
 * An application test is a test that requires the full context of Theorycrafter: the [TheorycrafterContext] etc.
 */
open class TheorycrafterTest {


    /**
     * The fits file for this test.
     */
    private var fitsFile: File? = null


    /**
     * The directory for tournament repositories.
     */
    private lateinit var tournamentsDirectory: File


    /**
     * Initializes the system for an application test.
     */
    @BeforeTest
    fun initialize() {
        TheorycrafterContext.settings = TheorycrafterSettings.forTest()
        val fitsFile = File.createTempFile("theorycrafter-test", ".dat").also {
            fitsFile = it
        }
        tournamentsDirectory = Files.createTempDirectory("tournaments").toFile()
        runBlocking {
            TheorycrafterContext.initialize(
                fitsFile = fitsFile,
                tournamentsDirectory = tournamentsDirectory,
                eveDataDeferred = CompletableDeferred(TestEveData),
                isRunningTest = true,
                onProgress = { },
            )
        }
    }


    /**
     * Cleans up after running an application test.
     */
    @AfterTest
    fun cleanup() {
        TheorycrafterContext.close()
        fitsFile?.delete()
        tournamentsDirectory.deleteRecursively()
    }


    /**
     * Creates and returns a new, empty [Fit].
     */
    suspend fun newFit(
        shipName: String,
        fitName: String = shipName.filter { it != ' ' },
        isValid: (Fit) -> Boolean = { true },
    ): Fit {
        val shipType = eveData.shipType(shipName)
        val fitHandle = fits.addEmpty(shipType, fitName)

        val fit = fits.engineFitOf(fitHandle)
        fit.validate(isValid)

        return fit
    }


    /**
     * Throws an appropriate exception if the given fit isn't valid according to the given function.
     */
    fun Fit.validate(isValid: (Fit) -> Boolean) {
        if (!isValid(this))
            error("${this.ship.type.name} may no longer be suitable for this test")
    }


    /**
     * Fits an item with the given name into the slot associated with the given node by editing it via UI actions.
     */
    @OptIn(ExperimentalTestApi::class)
    context(ComposeUiTest)
    fun SemanticsNodeInteraction.fitItemIntoSlot(text: String, printDebugInfo: Boolean = false) {
        scrollToAndClick()
        press(FitEditorSlotKeys.StartEditingSlot)
        onNode(isFocused()).let {
            if (printDebugInfo) {
                println("Typing \"$text\" into:")
                println(it.printToString(maxDepth = 1))
            }
            it.performTextInput(text)
        }
        press(FitEditorSlotKeys.FinishEditingSlot)
        waitForIdle()
    }


    /**
     * This function exists here so that attempting to use [doubleClick] in a test fails immediately.
     */
    @Suppress("unused")
    @OptIn(ExperimentalTestApi::class)
    fun MouseInjectionScope.doubleClick(@Suppress("UNUSED_PARAMETER") position: Offset = center) {
        error("Double-clicking can't be detected in testing until we stop relying on AWT events")
    }


}


/**
 * Load [EveData] once.
 */
private val TestEveData = EveData.loadStandard()


/**
 * Sets the given [content] into the [DesktopComposeUiTest] after wrapping it with all the required Compose context
 * for a Theorycrafter application test.
 */
@OptIn(ExperimentalTestApi::class)
fun ComposeUiTest.setApplicationContent(content: @Composable () -> Unit) {
    setContent {
        CompositionLocalProvider(LocalIsTest provides true) {
            OffscreenApplicationUi {
                content()
                val windowManager = LocalTheorycrafterWindowManager.current
                LaunchedEffect(windowManager) {
                    windowManager.onAppReady()
                }
            }
        }
    }
}


/**
 * Presses and releases the given key.
 */
@OptIn(ExperimentalTestApi::class)
fun SemanticsNodeInteraction.press(key: Key) = performKeyInput {
    pressKey(key)
}


/**
 * Finds all semantics nodes that either match [matcher], or have an ancestor that matches it.
 */
fun SemanticsNodeInteractionsProvider.onSubtreeRootedAt(matcher: SemanticsMatcher): SemanticsNodeInteractionCollection {
    return onAllNodes(matcher or hasAnyAncestor(matcher))
}


/**
 * Scrolls to the node and clicks it.
 */
fun SemanticsNodeInteraction.scrollToAndClick() {
    performScrollTo()
    performClick()
}


/**
 * Same as [runBlocking] but returns Unit. Without this the linter complains about a non-void return type from test
 * functions.
 */
fun runBlockingTest(block: suspend CoroutineScope.() -> Unit) {
    runBlocking {
        block()
    }
}


/**
 * Returns the size of the first drone group in the given fit.
 */
fun Fit.firstDroneGroupSize() = drones.all.first().size


/**
 * The key press to trigger "Command" shortcuts.
 */
val CommandKey = run {
    val commandKeyMatcher = KeyboardModifierMatcher.Command
    when {
        (commandKeyMatcher.meta == KeyboardModifierRequirement.PRESSED) -> Key.MetaLeft
        (commandKeyMatcher.ctrl == KeyboardModifierRequirement.PRESSED) -> Key.CtrlLeft
        else -> error("Unrecognized Command key matcher")
    }
}
