// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.fixtures

import com.intellij.remoterobot.fixtures.CommonContainerFixture
import com.intellij.remoterobot.fixtures.ComponentFixture
import com.intellij.remoterobot.fixtures.ContainerFixture
import com.intellij.remoterobot.fixtures.JTextAreaFixture
import com.intellij.remoterobot.fixtures.JTextFieldFixture
import com.intellij.remoterobot.search.locators.byXpath
import com.intellij.remoterobot.stepsProcessing.step
import com.intellij.remoterobot.utils.keyboard
import org.intellij.lang.annotations.Language
import java.time.Duration

fun ContainerFixture.pressOk() = findAndClick("//div[@text='OK']")
fun ContainerFixture.pressDelete() = findAndClick("//div[@text='Delete']")
fun ContainerFixture.pressCancel() = findAndClick("//div[@text='Cancel']")

fun ContainerFixture.findAndClick(@Language("XPath") xPath: String) = findByXpath(xPath).click()
fun ContainerFixture.findByXpath(xPath: String) = find<ComponentFixture>(byXpath(xPath), Duration.ofSeconds(5))

fun ContainerFixture.fillSingleTextField(text: String) = step("Fill single text field with $text") {
    find<JTextFieldFixture>(byXpath("//div[@class='JTextField']"), Duration.ofSeconds(5)).setTextWithoutFocus(text)
}

// swing robot appears to have issues acquiring focus on MATE desktop with some dialog windows
// org.assertj.swing.exception.ActionFailedException: Focus change to javax.swing.JTextField[name=null, text='', enabled=true, visible=true, showing=true] failed focus owner: Null Component (js#8)
fun JTextFieldFixture.setTextWithoutFocus(text: String) = apply {
    runJs("component.setText('$text')")
}

fun ContainerFixture.fillSearchTextField(text: String) = step("Fill search text field with $text") {
    val field = find<ComponentFixture>(byXpath("//div[@class='SearchTextField']"), Duration.ofSeconds(5))
    field.runJs(
        """
            component.getTextEditor().setText('$text');
            component.getTextEditor().postActionEvent();
        """.trimIndent(),
        runInEdt = true
    )
}

fun ContainerFixture.clearSearchTextField() = step("Clear search text field") {
    val field = find<ComponentFixture>(byXpath("//div[@class='SearchTextField']"), Duration.ofSeconds(5))
    field.runJs(
        """
            component.getTextEditor().getClientProperty('JTextField.Search.CancelAction').actionPerformed(null)
        """.trimIndent(),
        runInEdt = true
    )
}

fun ContainerFixture.fillDeletionAndConfirm() = step("Fill in delete me and delete") {
    find<JTextFieldFixture>(byXpath("//div[@accessiblename='Delete confirmation box']"), Duration.ofSeconds(5)).setTextWithoutFocus("delete me")
    pressOk()
}

// There is no function to write text to this directly :(
fun ContainerFixture.fillSingleJBTextArea(text: String) = step("Fill single JBTextArea with $text") {
    find<JTextAreaFixture>(byXpath("//div[@class='JBTextArea']")).click()
    keyboard { this.enterText(text) }
}

fun ContainerFixture.fillAllJBTextFields(text: String) = step("Fill all visible text fields with $text") {
    findAll(JTextFieldFixture::class.java, byXpath("//div[@class='JBTextField']")).forEach { it.text = text }
}

fun ContainerFixture.pressCreate() = step("Press the \"Create\" button") {
    find<ComponentFixture>(byXpath("//div[@text='Create']")).click()
}

fun ContainerFixture.pressSave() = step("""Press the "Save" button""") {
    find<ComponentFixture>(byXpath("//div[@text='Save']")).click()
}

fun ContainerFixture.pressYes() = step("""Press the visible "yes" button""") {
    findAndClick("//div[@class='JButton' and @text='Yes']")
}

/*
 * Find an action button by button text instead of by xPath
 */
fun CommonContainerFixture.actionButton(buttonText: String) = actionButton(byXpath("//div[@accessiblename='$buttonText' and @class='ActionButton']"))


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.fixtures

import com.intellij.remoterobot.RemoteRobot
import com.intellij.remoterobot.data.RemoteComponent
import com.intellij.remoterobot.fixtures.CommonContainerFixture
import com.intellij.remoterobot.fixtures.ComponentFixture
import com.intellij.remoterobot.fixtures.DefaultXpath
import com.intellij.remoterobot.fixtures.FixtureName
import com.intellij.remoterobot.fixtures.JMenuBarFixture
import com.intellij.remoterobot.search.locators.byXpath
import com.intellij.remoterobot.stepsProcessing.step
import com.intellij.remoterobot.utils.waitFor
import java.time.Duration

fun RemoteRobot.idea(function: IdeaFrame.() -> Unit) {
    val frame = find<IdeaFrame>(timeout = Duration.ofSeconds(10))
    frame.apply(function)
}

@FixtureName("Idea frame")
@DefaultXpath("IdeFrameImpl type", "//div[@class='IdeFrameImpl']")
class IdeaFrame(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : CommonContainerFixture(remoteRobot, remoteComponent) {
    init {
        waitForProjectToBeAssigned()
    }

    val menuBar: JMenuBarFixture
        get() = step("Menu...") {
            return@step remoteRobot.find(JMenuBarFixture::class.java, JMenuBarFixture.byType())
        }

    private fun waitForProjectToBeAssigned() {
        waitFor(duration = Duration.ofSeconds(30)) {
            callJs(
                """
                var frameHelper = com.intellij.openapi.wm.impl.ProjectFrameHelper.getFrameHelper(component);
                frameHelper.project != null
                """.trimIndent(),
                runInEdt = true
            )
        }
    }

    fun dumbAware(timeout: Duration = Duration.ofMinutes(5), function: () -> Unit) {
        step("Wait for smart mode") {
            waitFor(duration = timeout, interval = Duration.ofSeconds(5)) {
                isDumbMode().not()
            }
            function()
            step("..wait for smart mode again") {
                waitFor(duration = timeout, interval = Duration.ofSeconds(5)) {
                    isDumbMode().not()
                }
            }
        }
    }

    fun waitForBackgroundTasks(timeout: Duration = Duration.ofMinutes(5)) {
        step("Wait for background tasks to finish") {
            waitFor(duration = timeout, interval = Duration.ofSeconds(5)) {
                // search for the progress bar
                find<ComponentFixture>(byXpath("//div[@class='InlineProgressPanel']")).findAllText().isEmpty()
            }
        }
    }

    fun isDumbMode(): Boolean = callJs(
        """
            var frameHelper = com.intellij.openapi.wm.impl.ProjectFrameHelper.getFrameHelper(component);
            com.intellij.openapi.project.DumbService.isDumb(frameHelper.project);
        """.trimIndent(),
        runInEdt = true
    )

    fun findToast(timeout: Duration = Duration.ofSeconds(5)): ComponentFixture = find(byXpath("//div[@class='StatusPanel']"), timeout)
    fun findToastText(timeout: Duration = Duration.ofSeconds(5)): List<String> = findToast(timeout).findAllText().map { it.text }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.fixtures

import com.intellij.remoterobot.RemoteRobot

fun RemoteRobot.ideMajorVersion() = callJs<Int>("com.intellij.openapi.application.ApplicationInfo.getInstance().getBuild().getBaselineVersion()")


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.fixtures

import com.intellij.remoterobot.RemoteRobot
import com.intellij.remoterobot.data.RemoteComponent
import com.intellij.remoterobot.fixtures.ComponentFixture
import com.intellij.remoterobot.stepsProcessing.step
import com.intellij.remoterobot.utils.Locators
import com.intellij.remoterobot.utils.waitFor
import org.assertj.swing.timing.Pause
import software.aws.toolkits.resources.message
import java.time.Duration
import javax.swing.JTree

/**
 * Fork of JTreeFixture from ui-robot with tweaked expansion logic and selection gathering
 *
 *  https://github.com/JetBrains/intellij-ui-test-robot/issues/38
 */
open class JTreeFixture(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : ComponentFixture(remoteRobot, remoteComponent) {
    companion object {
        fun byType() = Locators.byType(JTree::class.java)
    }

    fun clickPath(vararg path: String, fullMatch: Boolean = true) {
        findExpandedPath(*path, fullMatch = fullMatch)?.let {
            clickRow(it.row)
        } ?: throw PathNotFoundException(path.toList())
    }

    fun doubleClickPath(vararg path: String, fullMatch: Boolean = true) {
        findExpandedPath(*path, fullMatch = fullMatch)?.let {
            doubleClickRow(it.row)
        } ?: throw PathNotFoundException(path.toList())
    }

    fun rightClickPath(vararg path: String, fullMatch: Boolean = true) {
        findExpandedPath(*path, fullMatch = fullMatch)?.let {
            clickRow(it.row)
            rightClickRow(it.row)
        } ?: throw PathNotFoundException(path.toList())
    }

    fun clickRowWithText(text: String, fullMatch: Boolean = true) {
        findExpandedRowWithText(text, fullMatch)?.let {
            clickRow(it.row)
        } ?: throw PathNotFoundException()
    }

    fun doubleClickRowWithText(text: String, fullMatch: Boolean = true) {
        findExpandedRowWithText(text, fullMatch)?.let {
            doubleClickRow(it.row)
        } ?: throw PathNotFoundException()
    }

    fun rightClickRowWithText(text: String, fullMatch: Boolean = true) {
        findExpandedRowWithText(text, fullMatch)?.let {
            rightClickRow(it.row)
        } ?: throw PathNotFoundException()
    }

    fun clickRow(rowNumber: Int) {
        runJs(
            """
           JTreeFixture(robot, component).clickRow($rowNumber)
        """
        )
    }

    fun doubleClickRow(rowNumber: Int) {
        runJs(
            """
           JTreeFixture(robot, component).doubleClickRow($rowNumber)
        """
        )
    }

    fun rightClickRow(rowNumber: Int) {
        runJs(
            """
           JTreeFixture(robot, component).rightClickRow($rowNumber)
        """
        )
    }

    fun collectExpandedPaths(): List<TreePathToRow> = callJs<ArrayList<ArrayList<String?>>>(
        """
            const paths = new java.util.ArrayList()
            com.intellij.util.ui.tree.TreeUtil.visitVisibleRows(component, (p) => {
                const nodes = new java.util.ArrayList()
                for (let node of p.getPath()) {
                    nodes.add(node.toString())
                }
                return nodes
            }, (p) => paths.add(p))
            paths
        """,
        true
    ).mapIndexed { index, path ->
        TreePathToRow(path.filterNotNull().filter { it.isNotEmpty() }, index)
    }

    fun collectSelectedPaths(): List<List<String>> = callJs<ArrayList<ArrayList<String?>>>(
        """
        const paths = new java.util.ArrayList()
        const treePaths = component.getSelectionPaths()

        if (treePaths) {
            for (let i = 0; i < treePaths.length; ++i) {
                const nodes = new java.util.ArrayList()
                for (let node of treePaths[i].getPath()) {
                    nodes.add(node.toString())
                }

                // If the root node is not visible, remove it
                if (component.isRootVisible() === false)
                    nodes.remove(0)
                paths.add(nodes)
            }
        }
        paths
    """,
        true
    ).map { path ->
        path.filterNotNull().filter { it.isNotEmpty() }
    }

    fun collectRows(): List<String> = collectExpandedPaths().map { it.path.last() }

    fun isPathExists(vararg path: String, fullMatch: Boolean = true) = findExpandedPath(*path, fullMatch = fullMatch) != null

    fun isPathSelected(vararg path: String) = collectSelectedPaths().contains(path.toList())

    fun getValueAtRow(row: Int) = collectRows()[row]

    fun expand(vararg path: String): JTreeFixture {
        runJs(
            """
            const expandingPathNodes = [${path.joinToString(",") { "\"${it}\"" }}]
            const ignoreRoot = component.isRootVisible() === false
            const treePath = com.intellij.ui.tree.TreePathUtil.convertArrayToTreePath(expandingPathNodes)
            const toStringConverter = function(obj) {return java.util.Objects.toString(obj)}
            const visitor = new com.intellij.ui.tree.TreeVisitor.ByTreePath(ignoreRoot, treePath, toStringConverter);

            com.intellij.util.ui.tree.TreeUtil.promiseExpand(component, visitor).blockingGet(5000)
        """
        )
        return this
    }

    fun expandAll(): JTreeFixture {
        step("Expand all") {
            runJs("com.intellij.util.ui.tree.TreeUtil.promiseExpandAll(component).blockingGet(5000)")
        }
        return this
    }

    fun expandAllExcept(vararg nodes: String): JTreeFixture {
        runJs(
            """
            var excludedNodes = [${nodes.joinToString(",") { "\"$it\"" }}]
            function visit(treePath) {
                var pathStr = treePath.toString()
                if (excludedNodes.some(function(node) { return pathStr.indexOf(node) !== -1 })) {
                    return com.intellij.ui.tree.TreeVisitor.Action.SKIP_CHILDREN
                } else {
                    return com.intellij.ui.tree.TreeVisitor.Action.CONTINUE
                }
            }
            const visitor = { visit: visit }

            com.intellij.util.ui.tree.TreeUtil.promiseExpand(component, new com.intellij.ui.tree.TreeVisitor(visitor)).blockingGet(5000)
        """
        )
        return this
    }

    fun collapsePath(vararg path: String, fullMatch: Boolean = true) = findExpandedPath(*path, fullMatch = fullMatch)?.let {
        runJs(
            """
            JTreeFixture(robot, component).collapseRow(${it.row})
        """
        )
    }

    private fun findExpandedPath(vararg path: String, fullMatch: Boolean): TreePathToRow? = collectExpandedPaths().singleOrNull { expandedPath ->
        expandedPath.path.size == path.size && expandedPath.path.containsAllNodes(*path, fullMatch = fullMatch) ||
            expandedPath.path.size - 1 == path.size && expandedPath.path.drop(1).containsAllNodes(*path, fullMatch = fullMatch)
    }

    private fun findExpandedRowWithText(text: String, fullMatch: Boolean = true): TreePathToRow? = collectExpandedPaths().singleOrNull { expandedPath ->
        if (fullMatch) expandedPath.path.last() == text else expandedPath.path.last().contains(text, true)
    }

    private fun List<String>.containsAllNodes(vararg treePath: String, fullMatch: Boolean): Boolean = zip(treePath).all {
        if (fullMatch) {
            it.first.equals(it.second, true)
        } else {
            it.first.contains(it.second, true)
        }
    }

    data class TreePathToRow(val path: List<String>, val row: Int)

    class PathNotFoundException(message: String? = null) : Exception(message) {
        constructor(path: List<String>) : this("$path not found")
    }
}

fun JTreeFixture.waitUntilLoaded() {
    step("waiting for loading text to go away...") {
        Pause.pause(100)
        waitFor(duration = Duration.ofMinutes(1)) {
            // FIX_WHEN_MIN_IS_213: changed to unicode ellipses in IDEA-270680
            !hasText("loading...") && !hasText("loadingâ€¦") && !hasText(message("loading_resource.loading"))
        }
        Pause.pause(100)
    }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.fixtures

import com.intellij.remoterobot.RemoteRobot
import com.intellij.remoterobot.data.RemoteComponent
import com.intellij.remoterobot.fixtures.FixtureName
import com.intellij.remoterobot.fixtures.JButtonFixture
import com.intellij.remoterobot.fixtures.JLabelFixture
import com.intellij.remoterobot.fixtures.JTextFieldFixture
import com.intellij.remoterobot.search.locators.byXpath
import com.intellij.remoterobot.stepsProcessing.step
import com.intellij.remoterobot.utils.Locators
import java.time.Duration
import javax.swing.JButton

fun RemoteRobot.newProjectWizard(
    timeout: Duration = Duration.ofSeconds(20),
    function: NewProjectWizardDialog.() -> Unit
) {
    step("Search for new project wizard dialog") {
        val dialog = find<NewProjectWizardDialog>(DialogFixture.byTitle("New Project"), timeout)

        dialog.apply(function)

        if (dialog.isShowing) {
            dialog.close()
        }
    }
}

@FixtureName("New Project Wizard")
open class NewProjectWizardDialog(
    remoteRobot: RemoteRobot,
    remoteComponent: RemoteComponent
) : DialogFixture(remoteRobot, remoteComponent) {
    fun selectProjectCategory(type: String) {
        findText(type).click()
    }

    fun selectProjectType(type: String) {
        jList(byXpath("//div[@class='JBList' and @visible_text='$type']")).click()
    }

    fun setProjectLocation(folder: String) {
        // Give more time to find the label. The UI can load slow or perform a step like SAM validation
        val label = jLabel(JLabelFixture.byText("Project location:"), timeout = Duration.ofSeconds(30))
        textField(JTextFieldFixture.byLabel(label)).text = folder
    }

    fun pressNext() {
        pressButton("Next")
    }

    fun pressFinish() {
        findAll<JButtonFixture>(Locators.byType(JButton::class.java)).first {
            it.text == "Finish" || it.text == "Create"
        }.click()
    }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.fixtures

import com.intellij.remoterobot.RemoteRobot
import com.intellij.remoterobot.data.RemoteComponent
import com.intellij.remoterobot.fixtures.FixtureName
import com.intellij.remoterobot.fixtures.JLabelFixture
import com.intellij.remoterobot.search.locators.byXpath
import com.intellij.remoterobot.stepsProcessing.step
import java.time.Duration

fun RemoteRobot.preferencesDialog(
    timeout: Duration = Duration.ofSeconds(20),
    function: PreferencesDialog.() -> Unit
) {
    step("Search for preferences dialog") {
        val dialog = find<PreferencesDialog>(DialogFixture.byTitleContains(preferencesTitle()), timeout)

        dialog.apply(function)

        if (dialog.isShowing) {
            dialog.close()
        }
    }
}

@FixtureName("Preferences")
open class PreferencesDialog(
    remoteRobot: RemoteRobot,
    remoteComponent: RemoteComponent
) : DialogFixture(remoteRobot, remoteComponent) {
    fun search(query: String) = step("Search $query") {
        textField(byXpath("//div[@class='SettingsSearch']//div[@class='TextFieldWithProcessing']")).text = query
    }

    fun selectPreferencePage(vararg crumbs: String) {
        val preferencesTree = find<JTreeFixture>(byXpath("//div[@class='MyTree']"))

        preferencesTree.expand(*crumbs)
        preferencesTree.clickPath(*crumbs)
    }

    override fun pressOk() {
        super.pressOk()

        assertValidSettings()
    }

    fun assertValidSettings() {
        val invalidSettingsLabel = jLabels(JLabelFixture.byContainsText("Cannot Save Settings"))
        if (invalidSettingsLabel.isNotEmpty()) {
            throw IllegalStateException("Could not save settings: ${invalidSettingsLabel.first().value}")
        }
    }
}

fun RemoteRobot.preferencesTitle() = if (this.isMac()) "Preferences" else "Settings"


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.fixtures

import com.intellij.remoterobot.RemoteRobot
import com.intellij.remoterobot.data.RemoteComponent
import com.intellij.remoterobot.fixtures.FixtureName
import com.intellij.remoterobot.search.locators.byXpath
import com.intellij.remoterobot.stepsProcessing.step
import com.intellij.remoterobot.utils.keyboard
import java.awt.event.KeyEvent
import java.time.Duration

fun IdeaFrame.projectStructureDialog(
    timeout: Duration = Duration.ofSeconds(20),
    function: ProjectStructureDialog.() -> Unit
) {
    step("Project Structure dialog") {
        if (remoteRobot.isMac()) {
            keyboard { hotKey(KeyEvent.VK_META, KeyEvent.VK_SEMICOLON) }
        } else {
            keyboard { hotKey(KeyEvent.VK_CONTROL, KeyEvent.VK_ALT, KeyEvent.VK_SHIFT, KeyEvent.VK_S) }
        }

        val dialog = remoteRobot.find<ProjectStructureDialog>(byXpath("//div[@accessiblename='Project Structure']"), timeout)

        dialog.apply(function)

        if (dialog.isShowing) {
            dialog.close()
        }
    }
}

@FixtureName("ProjectStructure")
open class ProjectStructureDialog(
    remoteRobot: RemoteRobot,
    remoteComponent: RemoteComponent
) : DialogFixture(remoteRobot, remoteComponent)


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.fixtures

import com.intellij.remoterobot.RemoteRobot
import com.intellij.remoterobot.data.RemoteComponent
import com.intellij.remoterobot.fixtures.CommonContainerFixture
import com.intellij.remoterobot.fixtures.ComponentFixture
import com.intellij.remoterobot.fixtures.DefaultXpath
import com.intellij.remoterobot.fixtures.FixtureName
import com.intellij.remoterobot.search.locators.byXpath
import com.intellij.remoterobot.stepsProcessing.log
import com.intellij.remoterobot.stepsProcessing.step
import java.nio.file.Path
import java.time.Duration

fun RemoteRobot.welcomeFrame(function: WelcomeFrame.() -> Unit) {
    // give it a longer time to find the welcome frame, sometimes takes > 2 seconds
    find(WelcomeFrame::class.java, Duration.ofSeconds(10)).apply(function)
}

@FixtureName("Welcome Frame")
@DefaultXpath("type", "//div[@class='FlatWelcomeFrame' and @visible='true']")
class WelcomeFrame(remoteRobot: RemoteRobot, remoteComponent: RemoteComponent) : CommonContainerFixture(remoteRobot, remoteComponent) {
    fun openNewProjectWizard() {
        selectTab("Projects")
        // This can match two things: If no previous projects, its a SVG icon, else a jbutton
        findAll<ComponentFixture>(byXpath("//div[contains(@accessiblename, 'New Project') and (@class='MainButton' or @class='JButton')]")).first().click()
    }

    fun openPreferences() = step("Opening preferences dialog") {
        selectTab("Customize")
        findAndClick("//div[@accessiblename='All settingsâ€¦']")

        log.info("Successfully opened the preferences dialog")
    }

    fun selectTab(tabName: String) {
        find<JTreeFixture>(JTreeFixture.byType()).clickRowWithText(tabName, fullMatch = false)
    }

    fun openFolder(path: Path) {
        selectTab("Projects")
        findAll<ComponentFixture>(byXpath("//div[contains(@accessiblename, 'Open') and (@class='MainButton' or @class='JButton')]")).first().click()
        fileBrowser("Open") {
            selectFile(path)
        }
    }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.tests

import com.intellij.remoterobot.stepsProcessing.log
import com.intellij.remoterobot.stepsProcessing.step
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle
import org.junit.jupiter.api.io.TempDir
import software.amazon.awssdk.services.cloudformation.CloudFormationClient
import software.aws.toolkits.jetbrains.uitests.CoreTest
import software.aws.toolkits.jetbrains.uitests.extensions.uiTest
import software.aws.toolkits.jetbrains.uitests.fixtures.IdeaFrame
import software.aws.toolkits.jetbrains.uitests.fixtures.awsExplorer
import software.aws.toolkits.jetbrains.uitests.fixtures.fillDeletionAndConfirm
import software.aws.toolkits.jetbrains.uitests.fixtures.findAndClick
import software.aws.toolkits.jetbrains.uitests.fixtures.idea
import software.aws.toolkits.jetbrains.uitests.fixtures.welcomeFrame
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Duration
import java.util.UUID

@TestInstance(Lifecycle.PER_CLASS)
class CloudFormationBrowserTest {
    private val templateFileName = "SQSQueue.yml"
    private val templateFile: Path = Paths.get(System.getProperty("testDataPath")).resolve("testFiles").resolve(templateFileName)
    private val stack = "uitest-${UUID.randomUUID()}"

    private val cloudFormation = "CloudFormation"
    private val deleteStackText = "Delete Stack..."

    @TempDir
    lateinit var tempDir: Path
    lateinit var cloudFormationClient: CloudFormationClient

    @BeforeAll
    fun deployStack() {
        log.info("Deploying stack $stack before the test run")
        cloudFormationClient = CloudFormationClient.create()
        cloudFormationClient.createStack { it.templateBody(templateFile.toFile().readText()).stackName(stack) }
        cloudFormationClient.waiter().waitUntilStackCreateComplete { it.stackName(stack) }
        log.info("Successfully deployed $stack")
    }

    @Test
    @CoreTest
    fun testCloudFormationBrowser() = uiTest {
        welcomeFrame {
            openFolder(tempDir)
        }
        idea {
            waitForBackgroundTasks()

            step("Open stack") {
                awsExplorer {
                    expandExplorerNode(cloudFormation)
                    doubleClickExplorer(cloudFormation, stack)
                }
            }
            step("Check events") {
                clickOn("Events")
                step("Assert that there are two CREATE_COMPLETE events shown") {
                    val createComplete = findAllText("CREATE_COMPLETE")
                    assertThat(createComplete).hasSize(2)
                    createComplete.first()
                }
            }
            step("Check outputs") {
                clickOn("Outputs")
                step("Assert that the stack output is there") {
                    findText("Cool description")
                }
            }
            step("Check resources") {
                clickOn("Resources")
                step("Assert that the stack resource is there") {
                    val createComplete = findAllText("CREATE_COMPLETE")
                    assertThat(createComplete).hasSize(1)
                    createComplete.first()
                }
            }
            step("Delete stack $stack") {
                awsExplorer {
                    openExplorerActionMenu(cloudFormation, stack)
                }
                findAndClick("//div[@text='$deleteStackText']")
                fillDeletionAndConfirm()
            }

            waitForStackDeletion()

            step("Check for the stack deletion notification") {
                // Sometimes the toast takes a while to show up so give it a longer timeout
                val toast = findToast(Duration.ofSeconds(10))
                assertThat(toast.hasText { it.text.contains("Deleted Stack '$stack'") })
            }
        }
    }

    @AfterAll
    fun cleanup() {
        log.info("Running final cleanup")
        try {
            cloudFormationClient.deleteStack { it.stackName(stack) }
            waitForStackDeletion()
        } catch (e: Exception) {
            log.error("Delete stack threw an exception", e)
        } finally {
            cloudFormationClient.close()
        }
    }

    private fun IdeaFrame.clickOn(tab: String) {
        findAndClick("//div[@accessiblename='$tab' and @class='JLabel' and @text='$tab']")
    }

    private fun waitForStackDeletion() {
        log.info("Waiting for the deletion of stack $stack")
        if (cloudFormationClient.describeStacks { it.stackName(stack) }.hasStacks()) {
            cloudFormationClient.waiter().waitUntilStackDeleteComplete { it.stackName(stack) }
        }
        log.info("Finished deleting stack $stack")
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.tests

import com.intellij.remoterobot.stepsProcessing.step
import com.intellij.remoterobot.utils.waitFor
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle
import org.junit.jupiter.api.io.TempDir
import software.aws.toolkits.core.utils.test.retryableAssert
import software.aws.toolkits.jetbrains.uitests.CoreTest
import software.aws.toolkits.jetbrains.uitests.extensions.uiTest
import software.aws.toolkits.jetbrains.uitests.fixtures.awsExplorer
import software.aws.toolkits.jetbrains.uitests.fixtures.idea
import software.aws.toolkits.jetbrains.uitests.fixtures.preferencesDialog
import software.aws.toolkits.jetbrains.uitests.fixtures.welcomeFrame
import software.aws.toolkits.resources.message
import java.nio.file.Path
import java.time.Duration

@TestInstance(Lifecycle.PER_CLASS)
class DynamicResourcesSettingsTest {
    @TempDir
    lateinit var tempDir: Path

    private val otherResources = message("explorer.node.other")

    @Test
    @CoreTest
    fun testSettingsPane() = uiTest {
        welcomeFrame {
            openFolder(tempDir)
        }
        idea {
            waitForBackgroundTasks()

            awsExplorer {
                step("Open dynamic resources settings") {
                    expandExplorerNode(otherResources)
                    explorerTree().doubleClickPath(otherResources, message("explorer.node.other.add_remove"), fullMatch = false)
                }
            }

            preferencesDialog {
                // Search for AWS because sometimes it is off the screen
                search(message("aws.settings.title"))

                selectPreferencePage("Tools", message("aws.settings.title"), message("aws.settings.dynamic_resources_configurable.title"))

                val applyButton = button("Apply")

                step("Select all resources") {
                    button(message("aws.settings.dynamic_resources_configurable.select_all")).click()
                    waitFor { applyButton.isEnabled() }
                    applyButton.click()

                    awsExplorer {
                        retryableAssert(timeout = Duration.ofSeconds(5)) {
                            assertThat(explorerTree().findAllText { it.text.contains(message("aws.settings.title")) }).isNotEmpty()
                        }
                    }
                }

                step("Clear all resources") {
                    button(message("aws.settings.dynamic_resources_configurable.clear_all")).click()
                    waitFor { applyButton.isEnabled() }
                    applyButton.click()

                    awsExplorer {
                        retryableAssert(timeout = Duration.ofSeconds(5)) {
                            assertThat(explorerTree().findAllText { it.text.contains("AWS") }).isEmpty()
                        }
                    }
                }
            }
        }
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.tests

import com.intellij.remoterobot.fixtures.ComboBoxFixture
import com.intellij.remoterobot.search.locators.byXpath
import com.intellij.remoterobot.stepsProcessing.log
import com.intellij.remoterobot.stepsProcessing.step
import com.intellij.remoterobot.utils.waitFor
import com.intellij.remoterobot.utils.waitForIgnoringError
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.io.TempDir
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.dynamodb.DynamoDbClient
import software.amazon.awssdk.services.dynamodb.model.AttributeDefinition
import software.amazon.awssdk.services.dynamodb.model.AttributeValue
import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement
import software.amazon.awssdk.services.dynamodb.model.KeyType
import software.amazon.awssdk.services.dynamodb.model.LocalSecondaryIndex
import software.amazon.awssdk.services.dynamodb.model.ProjectionType
import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType
import software.aws.toolkits.core.utils.test.aString
import software.aws.toolkits.jetbrains.uitests.CoreTest
import software.aws.toolkits.jetbrains.uitests.extensions.uiTest
import software.aws.toolkits.jetbrains.uitests.fixtures.awsExplorer
import software.aws.toolkits.jetbrains.uitests.fixtures.findByXpath
import software.aws.toolkits.jetbrains.uitests.fixtures.idea
import software.aws.toolkits.jetbrains.uitests.fixtures.welcomeFrame
import java.nio.file.Path
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.UUID
import kotlin.random.Random

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class DynamoDbTest {

    private val date = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)
    private val tableName = "uitest-$date-${UUID.randomUUID()}"
    private val dynamo = "DynamoDB"
    private val otherIndex = "OtherIndex"

    private lateinit var client: DynamoDbClient
    private lateinit var items: List<AllTypesType>

    @TempDir
    lateinit var tempDir: Path

    @BeforeAll
    fun setUp() {
        log.info("Creating table $tableName")
        client = DynamoDbClient.builder().region(Region.US_WEST_2).build()
        createAndPopulateTable()
        log.info("Table created")
    }

    @AfterAll
    fun cleanup() {
        log.info("Running final cleanup")
        client.deleteTable {
            it.tableName(tableName)
        }
        client.waiter().waitUntilTableNotExists { it.tableName(tableName) }
        log.info("Deleted table $tableName")
    }

    @Test
    @CoreTest
    fun testDynamoDbTableViewer() = uiTest {
        welcomeFrame {
            openFolder(tempDir)
        }

        idea {
            waitForBackgroundTasks()

            step("Open table") {
                awsExplorer {
                    expandExplorerNode(dynamo)
                    doubleClickExplorer(dynamo, tableName)
                }

                step("Check all data loaded") {
                    step("Wait for data to load") {
                        waitFor { findByXpath("//div[@class='JBTableHeader']").retrieveData().textDataList.map { it.text }.isNotEmpty() }
                    }
                    step("Check full table scan (default index)") {
                        val columnCount = step("Check columns") {
                            val cols = findByXpath("//div[@class='JBTableHeader']").retrieveData().textDataList.map { it.text }
                            // Can't do a straight-up string match because the column headers may be truncated for space
                            assertThat(cols).satisfiesExactlyInAnyOrder(
                                { assertThat(it).startsWith("Num") },
                                { assertThat(it).startsWith("Str") },
                                { assertThat(it).startsWith("Ter") },
                                { assertThat(it).startsWith("Boo") },
                                { assertThat(it).startsWith("Int") },
                                { assertThat(it).startsWith("Str") },
                                { assertThat(it).startsWith("Num") },
                                { assertThat(it).startsWith("Map") },
                                { assertThat(it).startsWith("Str") },
                                { assertThat(it).startsWith("Num") }
                            )
                            cols.size
                        }
                        step("Check data") {
                            val table = findByXpath("//div[@class='TableResults']").retrieveData().textDataList
                            val rowCount = table.size / columnCount
                            assertThat(rowCount).isEqualTo(items.size)
                        }
                    }
                    step("Select secondary local index $otherIndex") {
                        step("Expand scan panel") {
                            jLabel("Scan").click()
                        }
                        step("Select and run $otherIndex") {
                            find<ComboBoxFixture>(byXpath("//div[@class='ComboBox']")).selectItemContains(otherIndex)
                            button("Run").click()
                        }
                    }
                    step("Check only cols in $otherIndex") {
                        waitForIgnoringError {
                            assertThat(findByXpath("//div[@class='JBTableHeader']").retrieveData().textDataList.map { it.text }).hasSize(3)
                            true
                        }
                    }
                }
            }
        }
    }

    private fun createAndPopulateTable() {
        client.createTable { table ->
            table.tableName(tableName)
            table.attributeDefinitions(
                AttributeDefinition.builder().attributeName("NumericId").attributeType(ScalarAttributeType.N).build(),
                AttributeDefinition.builder().attributeName("StringSecondary").attributeType(ScalarAttributeType.S)
                    .build(),
                AttributeDefinition.builder().attributeName("TertiaryColumn").attributeType(ScalarAttributeType.S)
                    .build(),
            )
            table.keySchema(
                KeySchemaElement.builder().attributeName("NumericId").keyType(KeyType.HASH).build(),
                KeySchemaElement.builder().attributeName("StringSecondary").keyType(KeyType.RANGE).build()
            )
            table.localSecondaryIndexes(
                LocalSecondaryIndex.builder().indexName(otherIndex).keySchema(
                    KeySchemaElement.builder().attributeName("NumericId").keyType(KeyType.HASH).build(),
                    KeySchemaElement.builder().attributeName("TertiaryColumn").keyType(KeyType.RANGE).build()
                ).projection {
                    it.projectionType(ProjectionType.KEYS_ONLY)
                }.build()
            )
            table.provisionedThroughput {
                it.readCapacityUnits(10)
                it.writeCapacityUnits(5)
            }
        }

        client.waiter().waitUntilTableExists { it.tableName(tableName) }

        items = randomRange().map { AllTypesType() }

        items.forEach { item ->
            client.putItem {
                it.tableName(tableName)
                it.item(
                    mapOf(
                        "NumericId" to attr { n(item.id.toString()) },
                        "StringSecondary" to attr { s(item.secondary) },
                        "TertiaryColumn" to attr { s(item.tertiary) },
                        "Bool" to attr { bool(item.booleanProperty) },
                        "Int" to attr { n(item.intProperty.toString()) },
                        "StringMap" to attr { m(item.stringMapProperty.mapValues { v -> attr { s(v.value) } }) },
                        "NumberMap" to attr { m(item.numberMapProperty.mapValues { v -> attr { n(v.value.toString()) } }) },
                        "MapOfMap" to attr {
                            m(
                                item.mapOfMapProperty.mapValues { v ->
                                    attr {
                                        m(
                                            v.value.mapValues { sv ->
                                                attr {
                                                    s(sv.value)
                                                }
                                            }
                                        )
                                    }
                                }
                            )
                        },
                        "StringList" to attr { ss(item.stringList) },
                        "NumberList" to attr { ns(item.numberList.map { n -> n.toString() }) }
                    )
                )
            }
        }
    }

    private fun attr(block: AttributeValue.Builder.() -> Unit): AttributeValue = AttributeValue.builder().apply(block).build()

    private data class AllTypesType(
        val id: Long = Random.nextLong(0, Long.MAX_VALUE),
        val secondary: String = aString(),
        val tertiary: String = aString(),
        val booleanProperty: Boolean = Random.nextBoolean(),
        val intProperty: Int = Random.nextInt(),
        val stringMapProperty: Map<String, String> = randomRange().associate { aString() to aString() },
        val numberMapProperty: Map<String, Long> = randomRange().associate { aString() to Random.nextLong() },
        val mapOfMapProperty: Map<String, Map<String, String>> = randomRange().associate { aString() to randomRange().associate { aString() to aString() } },
        val stringList: List<String> = randomRange().map { aString() },
        val numberList: List<Long> = randomRange().map { Random.nextLong() }
    )
}

private fun randomRange() = 0..Random.nextInt(1, 10)


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.tests

import com.intellij.remoterobot.fixtures.ComboBoxFixture
import com.intellij.remoterobot.fixtures.ComponentFixture
import com.intellij.remoterobot.fixtures.ContainerFixture
import com.intellij.remoterobot.fixtures.JLabelFixture
import com.intellij.remoterobot.fixtures.JTextFieldFixture
import com.intellij.remoterobot.search.locators.byXpath
import com.intellij.remoterobot.stepsProcessing.log
import com.intellij.remoterobot.stepsProcessing.step
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.io.TempDir
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsClient
import software.amazon.awssdk.services.cloudwatchlogs.model.QueryStatus
import software.amazon.awssdk.services.cloudwatchlogs.model.ResourceNotFoundException
import software.aws.toolkits.jetbrains.uitests.CoreTest
import software.aws.toolkits.jetbrains.uitests.extensions.uiTest
import software.aws.toolkits.jetbrains.uitests.fixtures.IdeaFrame
import software.aws.toolkits.jetbrains.uitests.fixtures.JTreeFixture
import software.aws.toolkits.jetbrains.uitests.fixtures.awsExplorer
import software.aws.toolkits.jetbrains.uitests.fixtures.dialog
import software.aws.toolkits.jetbrains.uitests.fixtures.findAndClick
import software.aws.toolkits.jetbrains.uitests.fixtures.findByXpath
import software.aws.toolkits.jetbrains.uitests.fixtures.idea
import software.aws.toolkits.jetbrains.uitests.fixtures.waitUntilLoaded
import software.aws.toolkits.jetbrains.uitests.fixtures.welcomeFrame
import java.nio.file.Path
import java.time.Duration
import java.time.Instant
import java.util.UUID
import java.util.function.Consumer

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class InsightsQueryTest {
    @TempDir
    lateinit var tempDir: Path

    private val cloudWatchExplorerLabel = "CloudWatch Logs"

    private val executeButtonText = "Execute"
    private val defaultRelativeTimeAmount = "10"
    private val testRelativeTimeAmount = "359"
    private val logGroupName = "uitest-${UUID.randomUUID()}"
    private val logStreamName1 = "uitest-${UUID.randomUUID()}"
    private val logStreamName2 = "uitest-${UUID.randomUUID()}"

    /**
     * We can't reach into jetbrains-core by design, so copy the default string out of
     * QueryEditorUtils.kt . If the string changes it needs to change in both places
     */
    private val DEFAULT_INSIGHTS_QUERY_STRING =
        """fields @timestamp, @message
        | sort @timestamp desc
        | limit 20
        """

    @BeforeAll
    fun setup() {
        val client = CloudWatchLogsClient.create()

        client.createLogGroup {
            it.logGroupName(logGroupName)
        }

        client.createLogStream {
            it.logGroupName(logGroupName)
            it.logStreamName(logStreamName1)
        }

        client.createLogStream {
            it.logGroupName(logGroupName)
            it.logStreamName(logStreamName2)
        }

        client.putLogEvents {
            it.logGroupName(logGroupName)
            it.logStreamName(logStreamName1)
            it.logEvents(
                Consumer { event ->
                    event.message("group1")
                    event.timestamp(Instant.now().toEpochMilli())
                }
            )
        }

        client.putLogEvents {
            it.logGroupName(logGroupName)
            it.logStreamName(logStreamName2)
            it.logEvents(
                Consumer { event ->
                    event.message("group2")
                    event.timestamp(Instant.now().toEpochMilli())
                }
            )
        }

        runBlocking {
            client.waitForResults(logGroupName)
        }
    }

    @Test
    @CoreTest
    fun testInsightsQuery() = uiTest {
        welcomeFrame {
            openFolder(tempDir)
        }
        idea {
            waitForBackgroundTasks()
            step("Expand log groups node") {
                awsExplorer {
                    expandExplorerNode(cloudWatchExplorerLabel)
                }
            }

            step("Query with default settings returns results") {
                openInsightsQueryDialogFromExplorer(logGroupName)
                step("Execute") {
                    findAndClick("//div[@text='$executeButtonText']")
                }

                val logResults = find<JTreeFixture>(byXpath("//div[@class='TableView']"), Duration.ofSeconds(5))
                logResults.waitUntilLoaded()

                assertThat(logResults.findAllText()).anySatisfy {
                    assertThat(it.text).contains("group1")
                }
                assertThat(find<JTreeFixture>(byXpath("//div[@class='TableView']")).findAllText()).anySatisfy {
                    assertThat(it.text).contains("group2")
                }
            }

            step("Revising query from current results") {
                val currentTab = find<JLabelFixture>(byXpath("//div[@class='ContentTabLabel' and starts-with(@accessiblename, 'Query:')]"))
                val currentQueryId = currentTab.value.removePrefix("Query: ")

                openInsightsQueryDialogFromResults()

                step("Change relative time values") {
                    find<JTextFieldFixture>(byXpath("//div[@class='JFormattedTextField' and @visible_text='$defaultRelativeTimeAmount']")).text =
                        testRelativeTimeAmount
                    find<ComboBoxFixture>(byXpath("//div[@class='ComboBox']")).selectItem("Hours")
                }

                step("Execute") {
                    findAndClick("//div[@text='$executeButtonText']")
                }

                step("Verify new result tab selected") {
                    findAndClick(
                        "//div[@class='ContentTabLabel' and starts-with(@accessiblename, 'Query:') and @visible_text!='Query: $currentQueryId'][last()]"
                    )
                }

                val logResults = find<JTreeFixture>(byXpath("//div[@class='TableView']"), Duration.ofSeconds(5))
                logResults.waitUntilLoaded()

                assertThat(logResults.findAllText()).anySatisfy {
                    assertThat(it.text).contains("group1")
                }
                assertThat(find<JTreeFixture>(byXpath("//div[@class='TableView']")).findAllText()).anySatisfy {
                    assertThat(it.text).contains("group2")
                }

                step("Verify new query settings have persisted") {
                    openInsightsQueryDialogFromResults()

                    dialog("Query Log Groups") {
                        find<JTextFieldFixture>(byXpath("//div[@class='JFormattedTextField' and @visible_text='$testRelativeTimeAmount']"))
                        assertThat(find<ComboBoxFixture>(byXpath("//div[@class='ComboBox']")).selectedText()).isEqualTo("Hours")
                        close()
                    }
                }
            }
            step("Open query from log group") {
                step("Open log group") {
                    awsExplorer {
                        doubleClickExplorer(cloudWatchExplorerLabel, logGroupName)
                    }
                }
                step("Click query button") {
                    findAndClick("//div[@accessiblename='Query']")
                }
                step("Verify dialog and close it") {
                    findByXpath("//div[@accessiblename='Query Log Groups' and @class='MyDialog']")
                    findAndClick("//div[@text='Cancel']")
                }
            }
        }
    }

    @AfterAll
    fun cleanup() {
        try {
            val client = CloudWatchLogsClient.create()
            client.verifyDeletedLogGroup(logGroupName)
        } catch (e: Exception) {
            log.error("Unable to remove log groups", e)
        }
    }

    private fun IdeaFrame.openInsightsQueryDialogFromExplorer(groupName: String) = step("Open insights query dialog") {
        awsExplorer {
            openExplorerActionMenu(cloudWatchExplorerLabel, groupName)
        }
        find<ComponentFixture>(byXpath("//div[@accessiblename='Open Query Editor']")).click()
    }

    private fun ContainerFixture.openInsightsQueryDialogFromResults() = step("Open query editor") {
        findAndClick("//div[@accessiblename='Open Query Editor']")
    }

    private fun CloudWatchLogsClient.verifyDeletedLogGroup(logGroup: String) {
        try {
            deleteLogGroup {
                it.logGroupName(logGroupName)
            }
        } catch (e: ResourceNotFoundException) {
            log.info("Log group $logGroup did not exist")
            return
        } catch (e: Exception) {
            log.error("DeleteLogGroup returned an error, cannot attempt deletion again", e)
            return
        }
        log.info("Log group deleted: $logGroup")
    }

    private suspend fun CloudWatchLogsClient.waitForResults(logGroup: String) {
        // unfortunately logs are eventually consistent
        withTimeout(Duration.ofMinutes(5).toMillis()) {
            while (true) {
                val stopTime = Instant.now()
                val startTime = stopTime.minus(Duration.ofHours(1))
                val queryId = startQuery {
                    it.logGroupName(logGroup)
                    it.startTime(startTime.toEpochMilli())
                    it.endTime(stopTime.toEpochMilli())
                    it.queryString(DEFAULT_INSIGHTS_QUERY_STRING)
                }.queryId()
                log.info("started query with id: $queryId")

                while (true) {
                    val result = getQueryResults {
                        it.queryId(queryId)
                    }

                    if (result.results().size >= 2) {
                        log.info("query returned enough results, continuing")
                        return@withTimeout
                    } else if (result.status() in setOf(QueryStatus.FAILED, QueryStatus.CANCELLED)) {
                        throw IllegalStateException(
                            "Reached terminal condition while waiting for log propagation: queryId: $queryId, status: ${result.statusAsString()}"
                        )
                    } else if (result.status() == QueryStatus.COMPLETE) {
                        log.info("query returned no results, restarting query")
                        break
                    } else {
                        log.info("no results yet, checking again in 2 seconds")
                        delay(2000)
                    }
                }
                delay(2000)
            }
        }
    }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.tests

import com.intellij.remoterobot.stepsProcessing.step
import com.intellij.remoterobot.utils.keyboard
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.io.TempDir
import software.aws.toolkits.jetbrains.uitests.CoreTest
import software.aws.toolkits.jetbrains.uitests.extensions.uiTest
import software.aws.toolkits.jetbrains.uitests.fixtures.awsExplorer
import software.aws.toolkits.jetbrains.uitests.fixtures.findAndClick
import software.aws.toolkits.jetbrains.uitests.fixtures.findByXpath
import software.aws.toolkits.jetbrains.uitests.fixtures.idea
import software.aws.toolkits.jetbrains.uitests.fixtures.welcomeFrame
import software.aws.toolkits.resources.message
import java.nio.file.Path
import java.util.function.Predicate

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class OpenAwsLocalTerminalTest {

    @TempDir
    lateinit var tempDir: Path

    @Disabled("CodeWhisperer new feature tooltip blocks the button")
    @Test
    @CoreTest
    fun `can open a terminal from explorer`() = uiTest {
        welcomeFrame {
            openFolder(tempDir)
        }
        idea {
            waitForBackgroundTasks()

            awsExplorer {
                step("click terminal button") {
                    findAndClick("//div[@accessiblename='${message("aws.terminal.action")}' and @class='ActionButton']")
                }
            }

            step("assert terminal shown") {
                val connection = step("find current connection") {
                    findText(Predicate { it.text.startsWith("AWS: ") }).text.substringAfter("AWS: ")
                }
                step("confirm terminal tab showing with connection $connection") {
                    findByXpath("//div[@class='ContentTabLabel' and contains(@text,'$connection')]")
                }
                val terminal = step("find terminal window") {
                    findByXpath("//div[@class='ShellTerminalWidget']")
                }
                step("click in terminal") {
                    terminal.click()
                }
                step("echo out region") {
                    keyboard {
                        enterText("echo \$AWS_REGION")
                        enter()
                    }
                }

                assertThat(terminal.findAllText().joinToString(separator = "") { it.text }).contains(connection.substringAfter("@"))
            }
        }
    }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.tests

import com.intellij.remoterobot.fixtures.ComponentFixture
import com.intellij.remoterobot.fixtures.JTextFieldFixture
import com.intellij.remoterobot.search.locators.byXpath
import com.intellij.remoterobot.stepsProcessing.log
import com.intellij.remoterobot.stepsProcessing.step
import com.intellij.remoterobot.utils.waitFor
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle
import org.junit.jupiter.api.io.TempDir
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.NoSuchBucketException
import software.aws.toolkits.core.s3.deleteBucketAndContents
import software.aws.toolkits.jetbrains.uitests.CoreTest
import software.aws.toolkits.jetbrains.uitests.extensions.uiTest
import software.aws.toolkits.jetbrains.uitests.fixtures.IdeaFrame
import software.aws.toolkits.jetbrains.uitests.fixtures.JTreeFixture
import software.aws.toolkits.jetbrains.uitests.fixtures.actionButton
import software.aws.toolkits.jetbrains.uitests.fixtures.actionMenuItem
import software.aws.toolkits.jetbrains.uitests.fixtures.awsExplorer
import software.aws.toolkits.jetbrains.uitests.fixtures.clearSearchTextField
import software.aws.toolkits.jetbrains.uitests.fixtures.fileBrowser
import software.aws.toolkits.jetbrains.uitests.fixtures.fillDeletionAndConfirm
import software.aws.toolkits.jetbrains.uitests.fixtures.fillSearchTextField
import software.aws.toolkits.jetbrains.uitests.fixtures.fillSingleTextField
import software.aws.toolkits.jetbrains.uitests.fixtures.findAndClick
import software.aws.toolkits.jetbrains.uitests.fixtures.idea
import software.aws.toolkits.jetbrains.uitests.fixtures.pressDelete
import software.aws.toolkits.jetbrains.uitests.fixtures.pressOk
import software.aws.toolkits.jetbrains.uitests.fixtures.waitUntilLoaded
import software.aws.toolkits.jetbrains.uitests.fixtures.welcomeFrame
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Duration
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.UUID

@TestInstance(Lifecycle.PER_CLASS)
class S3BrowserTest {
    private val testDataPath: Path = Paths.get(System.getProperty("testDataPath"))

    private val date = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)
    private val bucket = "uitest-$date-${UUID.randomUUID()}"
    private val folder = UUID.randomUUID().toString()

    private val S3 = "S3"
    private val createBucketText = "Create S3 Bucket"
    private val deleteBucketText = "Delete S3 Bucket"
    private val upload = "Upload..."
    private val newFolder = "New Folder..."
    private val rename = "Rename..."
    private val delete = "Delete..."

    private val jsonFile = "hello.json"
    private val jsonFile2 = "hello2.json"
    private val newJsonName = "helloooooooooo.json"

    @TempDir
    lateinit var tempDir: Path
    lateinit var s3Client: S3Client

    @BeforeAll
    fun setUp() {
        s3Client = S3Client.builder().region(Region.US_WEST_2).build()
    }

    @Test
    @CoreTest
    fun testS3Browser() = uiTest {
        welcomeFrame {
            openFolder(tempDir)
        }
        idea {
            waitForBackgroundTasks()

            step("Create bucket named $bucket") {
                awsExplorer {
                    openExplorerActionMenu(S3)
                }
                actionMenuItem(createBucketText).click()
                find<JTextFieldFixture>(byXpath("//div[@class='JTextField']"), Duration.ofSeconds(5)).text = bucket
                find<ComponentFixture>(byXpath("//div[@text='Create']")).click()
            }

            waitForS3BucketCreation()

            awsExplorer {
                step("Open editor for bucket $bucket") {
                    expandExplorerNode(S3)
                    doubleClickExplorer(S3, bucket)
                }
            }

            // Click on the tree to make sure it's there + we aren't selecting anything else
            s3Tree { click() }

            step("Upload object to top-level") {
                actionButton(upload).click()
                fileBrowser("Select") {
                    selectFile(testDataPath.resolve("testFiles").resolve(jsonFile))
                }
                // Wait for the item to be uploaded
                Thread.sleep(1000)
                s3Tree {
                    findText(jsonFile)
                }
            }

            step("Create folder") {
                actionButton(newFolder).click()
                fillSingleTextField(folder)
                pressOk()
                // Wait for the folder to be created
                Thread.sleep(1000)
                s3Tree {
                    findText(folder)
                }
            }

            step("Upload object to folder") {
                // TODO have to use findText instead of the reasonable clickRow or clickPath because
                // it can't find anything for some reason
                s3Tree {
                    findText(folder).click()
                }
                actionButton(upload).click()
                fileBrowser("Select") {
                    selectFile(testDataPath.resolve("testFiles").resolve(jsonFile2))
                }
                // Wait for the item to be uploaded
                Thread.sleep(1000)
                s3Tree {
                    findText(folder).doubleClick()
                    waitUntilLoaded()
                    findText(jsonFile2)
                }
            }

            step("Rename a file") {
                s3Tree {
                    waitUntilLoaded()
                    findText(jsonFile).click()
                }
                actionButton(rename).click()
                fillSingleTextField(newJsonName)
                pressOk()
                // Wait for the item to be renamed
                Thread.sleep(1000)
                s3Tree {
                    waitUntilLoaded()
                    findText(newJsonName)
                }
            }

            step("Filter by partial prefix") {
                fillSearchTextField("hello")
                s3Tree {
                    waitUntilLoaded()
                    assertThat(this.callJs<String>("component.rootNode.name")).isEqualTo("Prefix: hello")
                    assertThat(findAllText(folder)).isEmpty()
                    findText(newJsonName)
                }

                fillSearchTextField(folder)
                s3Tree {
                    waitUntilLoaded()
                    assertThat(this.callJs<String>("component.rootNode.name")).isEqualTo("Prefix: $folder")
                    findText(folder).doubleClick()
                    waitUntilLoaded()
                    findText(jsonFile2)
                    assertThat(findAllText(newJsonName)).isEmpty()
                }
            }

            step("Filter by delimited prefix") {
                fillSearchTextField("$folder/")
                s3Tree {
                    waitUntilLoaded()
                    assertThat(this.callJs<String>("component.rootNode.name")).isEqualTo("$folder/")
                    // no child with name equal to folder
                    assertThat(findAllText(folder)).isEmpty()
                    findText(jsonFile2)
                    assertThat(findAllText(newJsonName)).isEmpty()
                }
            }

            step("Clear filter") {
                clearSearchTextField()
                s3Tree {
                    waitUntilLoaded()
                    assertThat(this.callJs<String>("component.rootNode.name")).isEqualTo("")
                    // restore tree to original state
                    findText(folder).doubleClick()
                    findText(newJsonName)
                }
            }

            step("Delete a file") {
                s3Tree {
                    findText(jsonFile2).click()
                }
                actionButton(delete).click()
                pressDelete()
                // Wait for the item to be deleted
                Thread.sleep(1000)
                // make sure it's gone
                s3Tree {
                    // Attempt to reopen the folder
                    findText(folder).doubleClick()
                    waitUntilLoaded()
                    assertThat(findAllText(jsonFile2)).isEmpty()
                }
            }

            step("Open known file-types") {
                s3Tree {
                    waitUntilLoaded()
                    findText(newJsonName).doubleClick()
                }

                waitFor {
                    findAll<ComponentFixture>(byXpath("//div[contains(@visible_text, '$newJsonName')]")).isNotEmpty()
                }
            }

            step("Delete bucket named $bucket") {
                awsExplorer {
                    openExplorerActionMenu(S3, bucket)
                }
                findAndClick("//div[@text='$deleteBucketText']")
                fillDeletionAndConfirm()
                waitForS3BucketDeletion()
            }
        }
    }

    @AfterAll
    fun cleanup() {
        log.info("Running final cleanup")
        try {
            s3Client.deleteBucketAndContents(bucket)
            waitForS3BucketDeletion()
        } catch (e: Exception) {
            if (e is NoSuchBucketException) {
                return
            }
            log.error("Delete bucket stack threw an exception", e)
        } finally {
            s3Client.close()
        }
    }

    private fun waitForS3BucketDeletion() {
// TODO: This is consistently timing out starting 12/7
//        s3Client.waiter().waitUntilBucketNotExists(
//            { it.bucket(bucket) },
//            { it.maxAttempts(30) }
//        )
    }

    private fun waitForS3BucketCreation() {
        s3Client.waiter().waitUntilBucketExists { it.bucket(bucket) }
    }

    private fun IdeaFrame.s3Tree(func: (JTreeFixture.() -> Unit)) {
        find<JTreeFixture>(byXpath("//div[@class='S3TreeTable']"), Duration.ofSeconds(5)).apply(func)
    }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.tests

import com.intellij.remoterobot.fixtures.ComboBoxFixture
import com.intellij.remoterobot.fixtures.ComponentFixture
import com.intellij.remoterobot.fixtures.ContainerFixture
import com.intellij.remoterobot.fixtures.JListFixture
import com.intellij.remoterobot.search.locators.byXpath
import com.intellij.remoterobot.stepsProcessing.step
import com.intellij.remoterobot.utils.keyboard
import com.intellij.remoterobot.utils.waitFor
import org.apache.commons.io.FileUtils
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.condition.DisabledIfSystemProperty
import org.junit.jupiter.api.io.TempDir
import software.aws.toolkits.jetbrains.uitests.CoreTest
import software.aws.toolkits.jetbrains.uitests.extensions.uiTest
import software.aws.toolkits.jetbrains.uitests.fixtures.DialogFixture
import software.aws.toolkits.jetbrains.uitests.fixtures.JTreeFixture
import software.aws.toolkits.jetbrains.uitests.fixtures.findAndClick
import software.aws.toolkits.jetbrains.uitests.fixtures.ideMajorVersion
import software.aws.toolkits.jetbrains.uitests.fixtures.idea
import software.aws.toolkits.jetbrains.uitests.fixtures.pressOk
import software.aws.toolkits.jetbrains.uitests.fixtures.welcomeFrame
import software.aws.toolkits.jetbrains.uitests.utils.setupSamCli
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Duration

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@DisabledIfSystemProperty(named = "org.gradle.project.ideProfileName", matches = "2023.1", disabledReason = "Flakes on 231")
class SamRunConfigTest {
    private val dataPath = Paths.get(System.getProperty("testDataPath")).resolve("samProjects/zip/java11")
    private val input = "{}"

    @TempDir
    lateinit var tempDir: Path

    @BeforeAll
    fun setup() {
        setupSamCli()
    }

    @Test
    @CoreTest
    fun samRunConfig() {
        // copy our test data to the temporary folder so we can edit it
        FileUtils.copyDirectory(dataPath.toFile(), tempDir.toFile())
        uiTest {
            welcomeFrame {
                openFolder(tempDir)
            }

            idea {
                waitForBackgroundTasks()
                waitFor(duration = Duration.ofMinutes(2), interval = Duration.ofSeconds(5)) {
                    isDumbMode().not()
                }

                findAndClick("//div[@class='RunConfigurationsComboBoxButton']")
                // FIX_WHEN_MIN_IS_222
                if (ideMajorVersion() > 221) {
                    find<JListFixture>(byXpath("//div[@class='MyList']"), timeout = Duration.ofSeconds(5)).clickItem("Edit ", fullMatch = false)
                }
                step("Create and populate template based run configuration") {
                    addRunConfig()
                    step("Populate run configuration") {
                        step("Set up function from template") {
                            findAndClick("//div[@text='From template']")
                            findAndClick("//div[@class='Wrapper']//div[@class='TextFieldWithBrowseButton']")
                            keyboard { enterText(tempDir.resolve("template.yaml").toAbsolutePath().toString()) }
                        }
                        step("Assert validation works by checking the error") {
                            assertThat(findRunDialog().findAllText()).anySatisfy { assertThat(it.text).contains("Must specify an input") }
                        }
                        step("Enter text") {
                            findAndClick("//div[@class='MyEditorTextField']")
                            keyboard { enterText(input) }
                        }
                        pressOk()
                    }
                }
                step("Validate template run configuration was saved and loads properly") {
                    step("Reopen the run configuration") {
                        findAndClick("//div[@accessiblename='[Local] SomeFunction']")
                        find<JListFixture>(byXpath("//div[@class='MyList']")).clickItem("Edit Configurations", fullMatch = false)
                    }
                    step("Assert the same function is selected") {
                        assertThat(functionModel().selectedText()).isEqualTo("SomeFunction")
                    }
                    step("Assert the run configuration has no errors") {
                        assertThat(findRunDialog().findAllText()).noneSatisfy { assertThat(it.text).contains("Error") }
                    }
                    step("Assert the input is the same") {
                        // As this is a JTextField we don't have a fixture for it. But, we can extract the data by
                        // joining all the text it has into a string
                        val fixture = findRunDialog().find<ContainerFixture>(byXpath("//div[@class='MyEditorTextField']"))
                        assertThat(fixture.findAllText().joinToString("") { it.text }).isEqualTo(input)
                    }
                    pressOk()
                }
                step("Setup handler based run configuration") {
                    step("Reopen the run configuration menu") {
                        findAndClick("//div[@accessiblename='[Local] SomeFunction']")
                        find<JListFixture>(byXpath("//div[@class='MyList']")).clickItem("Edit Configurations", fullMatch = false)
                    }
                    addRunConfig()
                    find<ComboBoxFixture>(
                        byXpath("(//div[@text='Runtime:']/following-sibling::div[@class='ComboBox'])[1]"),
                        Duration.ofSeconds(10)
                    ).selectItem("java11")
                    findAndClick("//div[@class='HandlerPanel']")
                    keyboard { enterText("helloworld.App::handleRequest") }
                    findAndClick("//div[@class='MyEditorTextField']")
                    keyboard { enterText(input) }
                    pressOk()
                }
                step("Validate handler run configuration was saved and loads properly") {
                    step("Reopen the run configuration") {
                        findAndClick("//div[@accessiblename='[Local] App.handleRequest']")
                        find<JListFixture>(byXpath("//div[@class='MyList']")).clickItem("Edit Configurations", fullMatch = false)
                        waitForConfigurationLoad()
                    }
                    step("Assert the same handler is selected") {
                        val fixture = find<ContainerFixture>(byXpath("//div[@class='HandlerPanel']"))
                        assertThat(fixture.findAllText().joinToString("") { it.text }).isEqualTo("helloworld.App::handleRequest")
                    }
                    // We might want to assert no errors here in the future. However, since we do not import the project, we don't
                    // index it, so we can't find the handler. We are not testing that here (that is tested in other tests), so
                    // it would probably not be worth testing in the UI test as well.
                }
            }
        }
    }

    private fun ContainerFixture.waitForConfigurationLoad() = find<ComponentFixture>(byXpath("//div[@text='Configuration']"), Duration.ofSeconds(10))

    private fun ContainerFixture.functionModel(): ComboBoxFixture {
        waitForConfigurationLoad()
        return find(byXpath("//div[@class='TextFieldWithBrowseButton']/following-sibling::div[@class='ComboBox']"))
    }

    private fun ContainerFixture.findRunDialog() = find<DialogFixture>(DialogFixture.byTitleContains("Run"), Duration.ofSeconds(10))

    private fun ContainerFixture.addRunConfig() {
        step("Add a local run configuration") {
            findRunDialog().findAndClick("//div[@accessiblename='Add New Configuration']")
            find<JTreeFixture>(byXpath("//div[@accessiblename='WizardTree' and @class='MyTree']")).clickPath("AWS Lambda", "Local")
            // wait for run config panel to render
            waitForConfigurationLoad()
        }
    }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.tests

import com.intellij.remoterobot.search.locators.byXpath
import com.intellij.remoterobot.stepsProcessing.step
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.condition.DisabledIfSystemProperty
import org.junit.jupiter.api.io.TempDir
import software.aws.toolkits.jetbrains.uitests.CoreTest
import software.aws.toolkits.jetbrains.uitests.extensions.uiTest
import software.aws.toolkits.jetbrains.uitests.fixtures.editorTab
import software.aws.toolkits.jetbrains.uitests.fixtures.idea
import software.aws.toolkits.jetbrains.uitests.fixtures.newProjectWizard
import software.aws.toolkits.jetbrains.uitests.fixtures.projectStructureDialog
import software.aws.toolkits.jetbrains.uitests.fixtures.welcomeFrame
import software.aws.toolkits.jetbrains.uitests.utils.setupSamCli
import java.nio.file.Path

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@DisabledIfSystemProperty(named = "org.gradle.project.ideProfileName", matches = "2023.1", disabledReason = "Flakes on 231")
class SamTemplateProjectWizardTest {
    @TempDir
    lateinit var tempDir: Path

    @BeforeAll
    fun setup() {
        setupSamCli()
    }

    @Test
    @CoreTest
    fun createSamApp() {
        uiTest {
            welcomeFrame {
                openNewProjectWizard()

                step("Run New Project Wizard") {
                    newProjectWizard {
                        selectProjectCategory("AWS")
                        selectProjectType("AWS Serverless Application")

                        pressNext()

                        setProjectLocation(tempDir.toAbsolutePath().toString())

                        // TODO: Runtime
                        // TODO: Sam Template

                        pressFinish()
                    }
                }
            }

            idea {
                // Give some time for the project to open and background jobs to start
                Thread.sleep(5000)

                waitForBackgroundTasks()

                step("Validate Readme is opened") {
                    editorTab("README.md")
                }

                step("Validate project structure") {
                    projectStructureDialog {
                        val fixture = comboBox(byXpath("//div[@class='JdkComboBox']"))
                        // TODO set based on Runtime
                        assertThat(fixture.selectedText()).startsWith("1")
                    }
                }
            }
        }
    }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.tests

import com.intellij.remoterobot.fixtures.ComponentFixture
import com.intellij.remoterobot.fixtures.JTableFixture
import com.intellij.remoterobot.fixtures.JTextFieldFixture
import com.intellij.remoterobot.search.locators.byXpath
import com.intellij.remoterobot.stepsProcessing.log
import com.intellij.remoterobot.stepsProcessing.step
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.io.TempDir
import software.amazon.awssdk.services.sns.SnsClient
import software.amazon.awssdk.services.sqs.SqsClient
import software.amazon.awssdk.services.sqs.model.QueueDoesNotExistException
import software.amazon.awssdk.services.sqs.model.SendMessageBatchRequestEntry
import software.aws.toolkits.jetbrains.uitests.CoreTest
import software.aws.toolkits.jetbrains.uitests.extensions.uiTest
import software.aws.toolkits.jetbrains.uitests.fixtures.IdeaFrame
import software.aws.toolkits.jetbrains.uitests.fixtures.actionMenuItem
import software.aws.toolkits.jetbrains.uitests.fixtures.awsExplorer
import software.aws.toolkits.jetbrains.uitests.fixtures.dialog
import software.aws.toolkits.jetbrains.uitests.fixtures.fillAllJBTextFields
import software.aws.toolkits.jetbrains.uitests.fixtures.fillDeletionAndConfirm
import software.aws.toolkits.jetbrains.uitests.fixtures.fillSingleJBTextArea
import software.aws.toolkits.jetbrains.uitests.fixtures.fillSingleTextField
import software.aws.toolkits.jetbrains.uitests.fixtures.findAndClick
import software.aws.toolkits.jetbrains.uitests.fixtures.findByXpath
import software.aws.toolkits.jetbrains.uitests.fixtures.idea
import software.aws.toolkits.jetbrains.uitests.fixtures.pressCreate
import software.aws.toolkits.jetbrains.uitests.fixtures.pressSave
import software.aws.toolkits.jetbrains.uitests.fixtures.pressYes
import software.aws.toolkits.jetbrains.uitests.fixtures.welcomeFrame
import software.aws.toolkits.jetbrains.uitests.utils.reattemptAssert
import software.aws.toolkits.jetbrains.uitests.utils.recheckAssert
import java.nio.file.Path
import java.time.Duration
import java.util.UUID

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class SqsTest {
    @TempDir
    lateinit var tempDir: Path

    private val sqsNodeLabel = "SQS"
    private val createQueueText = "Create Queue..."
    private val deleteQueueText = "Delete Queue..."
    private val purgeQueueText = "Purge Queue..."
    private val subscribeToSnsText = "Subscribe to SNS topic..."
    private val editQueueAttributesAction = "Edit Queue Parameters..."
    private val editQueueAttributesTitle = "Edit Queue Parameters"

    private val queueName = "uitest-${UUID.randomUUID()}"
    private val snsTopicName = "uitest-${UUID.randomUUID()}"
    private var snsTopicArn = ""
    private val fifoQueueName = "fifouitest-${UUID.randomUUID()}.fifo"

    @Test
    @CoreTest
    fun testSqs() = uiTest {
        val snsClient = SnsClient.create()
        val client = SqsClient.create()
        snsTopicArn = snsClient.createTopic { it.name(snsTopicName) }.topicArn()
        welcomeFrame {
            openFolder(tempDir)
        }
        idea {
            waitForBackgroundTasks()

            step("Create queues") {
                step("Create queue $queueName") {
                    awsExplorer {
                        openExplorerActionMenu(sqsNodeLabel)
                    }
                    actionMenuItem(createQueueText).click()
                    fillSingleTextField(queueName)
                    find<ComponentFixture>(byXpath("//div[@accessiblename='Standard']")).click()
                    pressCreate()
                    client.waitForCreation(queueName)
                }
                step("Create FIFO queue $fifoQueueName") {
                    awsExplorer {
                        openExplorerActionMenu(sqsNodeLabel)
                    }
                    actionMenuItem(createQueueText).click()
                    fillSingleTextField(fifoQueueName.substringBefore(".fifo"))
                    find<ComponentFixture>(byXpath("//div[@accessiblename='FIFO']")).click()
                    pressCreate()
                    client.waitForCreation(fifoQueueName)
                }
            }

            step("Expand SQS node") { awsExplorer { expandExplorerNode(sqsNodeLabel) } }

            val queueUrl = client.getQueueUrl { it.queueName(queueName) }.queueUrl()

            step("Standard queue") {
                openSendMessagePane(queueName)
                step("Send a message and validate it is sent") {
                    fillSingleJBTextArea("message")
                    findAndClick("//div[@text='Send']")
                    // Make sure it shows a sent message
                    findByXpath("//div[contains(@accessiblename, 'Sent message ID')]")
                }
                step("Add 10 messages to the queue so poll will be guaranteed to work") {
                    client.sendMessageBatch {
                        it.queueUrl(queueUrl).entries(
                            (1..10).map { num ->
                                SendMessageBatchRequestEntry.builder().messageBody("bmessage$num").id(num.toString()).build()
                            }
                        )
                    }
                }
                openPollMessagePane(queueName)
                step("View messages") {
                    findAndClick("//div[@accessiblename='View Messages' and @class='JButton']")
                    recheckAssert(timeout = Duration.ofSeconds(10)) {
                        assertThat(find<JTableFixture>(byXpath("//div[@class='TableView']")).findAllText()).anySatisfy {
                            assertThat(it.text).contains("bmessage")
                        }
                    }
                }
            }

            step("FIFO queue") {
                openSendMessagePane(fifoQueueName)
                step("Send a message and validate it is sent") {
                    // fill the message box
                    fillSingleJBTextArea("message")
                    // Fill the rest of the fields (deduplication and group ids)
                    fillAllJBTextFields("message")
                    findAndClick("//div[@text='Send']")
                    // Make sure it shows a sent message
                    findByXpath("//div[contains(@accessiblename, 'Sent message ID')]")
                }
            }

            step("Edit queue parameters") {
                step("Open queue parameters and change visibility to a different value") {
                    awsExplorer {
                        openExplorerActionMenu(sqsNodeLabel, queueName)
                    }
                    actionMenuItem(editQueueAttributesAction).click()
                    dialog(editQueueAttributesTitle) {
                        step("change visibility") {
                            find<JTextFieldFixture>(byXpath("//div[@class='JTextField' and @visible_text='30']")).text = "24"
                            pressSave()
                        }
                    }
                }

                assertThat(findToastText()).anySatisfy {
                    assertThat(it).contains("Updated queue parameters")
                }

                step("Reopen the dialog to make sure the new value was saved") {
                    awsExplorer {
                        openExplorerActionMenu(sqsNodeLabel, queueName)
                    }
                    actionMenuItem(editQueueAttributesAction).click()
                    dialog(editQueueAttributesTitle) {
                        find<JTextFieldFixture>(byXpath("//div[@class='JTextField' and @visible_text='24']"))
                        close()
                    }
                }
            }

            step("Purge queue") {
                awsExplorer {
                    openExplorerActionMenu(sqsNodeLabel, queueName)
                }
                actionMenuItem(purgeQueueText).click()
                pressYes()
                reattemptAssert {
                    assertThat(findToastText()).anySatisfy {
                        assertThat(it).contains("Started purging queue")
                    }
                }
            }
            step("Subscribe queue to sns topic") {
                awsExplorer {
                    openExplorerActionMenu(sqsNodeLabel, queueName)
                }
                actionMenuItem(subscribeToSnsText).click()
                // Wait for the resource selector to load, we don't have a visual cue for this
                Thread.sleep(2000)
                comboBox(byXpath("//div[@class='ResourceSelector']")).selectItemContains(snsTopicName)
                step("Press subscribe") {
                    findAndClick("//div[@class='JButton' and @text='Subscribe']")
                }
                step("Add the policy") {
                    findAndClick("//div[@class='JButton' and @text='Add Policy']")
                }
                reattemptAssert {
                    assertThat(findToastText()).anySatisfy {
                        assertThat(it).contains("Subscribed successfully to topic")
                    }
                }
                step("Subscribe again, policy should not show again") {
                    awsExplorer {
                        openExplorerActionMenu(sqsNodeLabel, queueName)
                    }
                    actionMenuItem(subscribeToSnsText).click()
                    comboBox(byXpath("//div[@class='ResourceSelector']")).selectItemContains(snsTopicName)
                    step("Press subscribe") {
                        findAndClick("//div[@class='JButton' and @text='Subscribe']")
                    }
                    reattemptAssert {
                        assertThat(findToastText()).anySatisfy {
                            assertThat(it).contains("Subscribed successfully to topic")
                        }
                    }
                }
            }

            step("Delete queues") {
                step("Delete queue $queueName") {
                    awsExplorer {
                        openExplorerActionMenu(sqsNodeLabel, queueName)
                    }
                    actionMenuItem(deleteQueueText).click()
                    fillDeletionAndConfirm()
                    client.waitForDeletion(queueName)
                }
                step("Delete queue $fifoQueueName") {
                    awsExplorer {
                        openExplorerActionMenu(sqsNodeLabel, fifoQueueName)
                    }
                    actionMenuItem(deleteQueueText).click()
                    fillDeletionAndConfirm()
                    client.waitForDeletion(fifoQueueName)
                }
            }
        }
    }

    @AfterAll
    // Make sure the two queues and sns topic are deleted, and if not, delete them
    fun cleanup() {
        log.info("Running final cleanup")
        try {
            SqsClient.create().use { client ->
                client.verifyDeleted(queueName)
                client.verifyDeleted(fifoQueueName)
            }
        } catch (e: Exception) {
            log.error("Unable to verify the queues were removed", e)
        }
        try {
            SnsClient.create().use { client ->
                client.deleteTopic { it.topicArn(snsTopicArn) }
            }
            log.info("Deleted sns topic $snsTopicArn")
        } catch (e: Exception) {
            log.error("Unable to verify the topic was removed", e)
        }
    }

    private fun IdeaFrame.openSendMessagePane(queueName: String) = step("Open send message pane") {
        awsExplorer {
            openExplorerActionMenu(sqsNodeLabel, queueName)
            actionMenuItem("Send a Message").click()
        }
    }

    private fun IdeaFrame.openPollMessagePane(queueName: String) = step("Open view message pane") {
        awsExplorer {
            openExplorerActionMenu(sqsNodeLabel, queueName)
        }
        actionMenuItem("View Messages").click()
    }

    private fun SqsClient.verifyDeleted(queueName: String) {
        val queueUrl = try {
            getQueueUrl { it.queueName(queueName) }.queueUrl()
        } catch (e: QueueDoesNotExistException) {
            log.info("Queue $queueName is deleted")
            return
        } catch (e: Exception) {
            log.error("Get queue URL returned an error, cannot attempt deletion again", e)
            return
        }
        log.info("Deleting $queueUrl")
        try {
            deleteQueue { it.queueUrl(queueUrl) }
        } catch (e: Exception) {
            log.error("Trying to delete $queueUrl threw an exception, it might not be deleted!", e)
            return
        }

        waitForDeletion(queueName)
    }

    private fun SqsClient.waitForDeletion(queueName: String) = runBlocking {
        try {
            withTimeout(Duration.ofMinutes(5).toMillis()) {
                while (true) {
                    delay(2000)
                    try {
                        getQueueUrl { it.queueName(queueName) }
                    } catch (e: QueueDoesNotExistException) {
                        return@withTimeout
                    } catch (_: Exception) {
                    }
                }
            }
            log.info("Verified $queueName is deleted")
        } catch (e: Exception) {
            log.error("Exception thrown by waitForDeletion", e)
        }
    }

    private fun SqsClient.waitForCreation(queueName: String) = runBlocking {
        try {
            // getQueueUrl can get before list works, so we can't use it to check if it exists.
            // So, use getQueuesPaginator instead. This can also take more than 1 minute sometimes,
            // so give it a 5 min timeout
            withTimeout(Duration.ofMinutes(5).toMillis()) {
                while (true) {
                    delay(2000)
                    try {
                        if (listQueuesPaginator().queueUrls().toList().any { it.contains(queueName) }) {
                            return@withTimeout
                        }
                    } catch (_: Exception) {
                    }
                }
            }
            log.info("Verified $queueName is created")
        } catch (e: Exception) {
            log.error("Exception thrown by waitForDeletion", e)
        }
    }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.utils

import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import java.time.Duration

fun recheckAssert(
    timeout: Duration = Duration.ofSeconds(1),
    interval: Duration = Duration.ofMillis(100),
    assertion: () -> Unit
) {
    runBlocking {
        withTimeout(timeout.toMillis()) {
            while (true) {
                try {
                    assertion()
                    return@withTimeout
                } catch (e: AssertionError) { // deliberately narrowed to an AssertionError - this is intended to be used in a test assertion
                    delay(interval.toMillis())
                }
            }
        }
    }
}

fun reattemptAssert(
    maxAttempts: Int = 5,
    interval: Duration = Duration.ofSeconds(1),
    assertion: () -> Unit
) {
    for (i in 0..maxAttempts) {
        try {
            assertion()
            return
        } catch (e: AssertionError) { // deliberately narrowed to an AssertionError - this is intended to be used in a test assertion
            when {
                i >= maxAttempts -> throw e
                else -> Thread.sleep(interval.toMillis())
            }
        }
    }
}


// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package software.aws.toolkits.jetbrains.uitests.utils

import com.intellij.remoterobot.stepsProcessing.log
import com.intellij.remoterobot.stepsProcessing.step
import software.aws.toolkits.jetbrains.uitests.extensions.uiTest
import software.aws.toolkits.jetbrains.uitests.fixtures.preferencesDialog
import software.aws.toolkits.jetbrains.uitests.fixtures.welcomeFrame

/**
 * Setup SAM cli configuration from the welcome screen
 */
fun setupSamCli() {
    val samPath = System.getenv("SAM_CLI_EXEC")
    if (samPath.isNullOrEmpty()) {
        log.warn("No custom SAM set, skipping setup")
        return
    }

    uiTest {
        welcomeFrame {
            step("Open preferences page") {
                openPreferences()

                preferencesDialog {
                    // Search for AWS because sometimes it is off the screen
                    search("AWS")

                    selectPreferencePage("Tools", "AWS")

                    step("Set SAM CLI executable path to $samPath") {
                        textField("SAM CLI executable:").text = samPath
                    }

                    pressOk()
                }

                selectTab("Projects")
            }
        }
    }
}


package com.hivemq.extensions.helloworld

import com.hivemq.extension.sdk.api.annotations.NotNull
import com.hivemq.extension.sdk.api.annotations.Nullable
import com.hivemq.extension.sdk.api.auth.Authenticator
import com.hivemq.extension.sdk.api.auth.parameter.AuthenticatorProviderInput
import com.hivemq.extension.sdk.api.services.auth.provider.AuthenticatorProvider
import com.hivemq.extensions.helloworld.models.AzureAuthContext
import com.hivemq.extensions.helloworld.network.MSGraphClient
import com.hivemq.extensions.helloworld.network.AzureAuthenticator

class AADAuthenticationProvider : AuthenticatorProvider {
    private val graphClient: MSGraphClient = MSGraphClient()
    private val aadAuthenticator: AADAuthenticator = AADAuthenticator(this.graphClient)

    override fun getAuthenticator(authenticatorProviderInput: AuthenticatorProviderInput?): Authenticator {
        return this.aadAuthenticator
    }
}