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 androidx.compose.ui.test.junit4.ComposeContentTestRule
import androidx.compose.ui.test.junit4.createComposeRule
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 org.junit.After
import org.junit.Before
import org.junit.Rule
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


/**
 * 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 test "rule".
     */
    @get:Rule
    val rule: ComposeContentTestRule = createComposeRule()


    /**
     * 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.
     */
    @Before
    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.
     */
    @After
    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.
     */
    fun SemanticsNodeInteraction.fitItemIntoSlot(text: String, printDebugInfo: Boolean = false) {
        scrollToAndClick()
        press(FitEditorSlotKeys.StartEditingSlot)
        rule.onNode(isFocused()).let {
            if (printDebugInfo) {
                println("Typing \"$text\" into:")
                println(it.printToString(maxDepth = 1))
            }
            it.performTextInput(text)
        }
        press(FitEditorSlotKeys.FinishEditingSlot)
        rule.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 [ComposeContentTestRule] after wrapping it with all the required Compose context
 * for a Theorycrafter application test.
 */
fun ComposeContentTestRule.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")
    }
}
