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

package software.aws.toolkits.jetbrains.utils

import com.intellij.testFramework.ProjectRule
import com.intellij.util.containers.ContainerUtil
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.jetbrains.concurrency.Promise
import org.jetbrains.concurrency.all
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import software.aws.toolkits.resources.message
import java.io.File
import java.time.Instant

class FileInfoCacheTest {

    @Rule
    @JvmField
    val tempFolder = TemporaryFolder()

    @Rule
    @JvmField
    val projectRule = ProjectRule()

    @Test
    fun cachedResultsAreReturned() {
        var callCount = 0
        val tempFile = tempFolder.newFile()
        val filePath = tempFile.absolutePath
        val info = "v1"
        tempFile.writeText(info)

        val infoProvider = object : FileInfoCache<String>() {
            override fun getFileInfo(path: String): String {
                callCount++
                return File(filePath).readText()
            }
        }

        assertThat(infoProvider.evaluateBlocking(filePath).result).isEqualTo(info)
        assertThat(infoProvider.evaluateBlocking(filePath).result).isEqualTo(info)
        assertThat(callCount).isEqualTo(1)
    }

    @Test
    fun exceptionLeadsToCheckingAgain() {
        var callCount = 0
        val tempFile = tempFolder.newFile()
        val filePath = tempFile.absolutePath
        val info = "v1"
        tempFile.writeText(info)

        val infoProvider = object : FileInfoCache<String>() {
            override fun getFileInfo(path: String): String {
                try {
                    if (callCount == 0) {
                        throw RuntimeException("Simulated exception")
                    } else {
                        return File(filePath).readText()
                    }
                } finally {
                    callCount++
                }
            }
        }

        assertThatThrownBy { infoProvider.evaluateBlocking(filePath) }

        Thread.sleep(1000)
        tempFile.setLastModified(Instant.now().toEpochMilli())

        assertThat(infoProvider.evaluateBlocking(filePath).result).isEqualTo(info)
        assertThat(callCount).isEqualTo(2)
    }

    @Test
    fun updatingAFileLeadsToCheckingAgain() {
        var callCount = 0
        val tempFile = tempFolder.newFile()
        val filePath = tempFile.absolutePath
        var info = "v1"
        tempFile.writeText(info)

        val infoProvider = object : FileInfoCache<String>() {
            override fun getFileInfo(path: String): String {
                callCount++
                return File(filePath).readText()
            }
        }

        assertThat(infoProvider.evaluateBlocking(filePath).result).isEqualTo(info)

        // Mac timestamp granularity is 1 sec
        Thread.sleep(1000)

        info = "v2"
        tempFile.writeText(info)

        assertThat(infoProvider.evaluateBlocking(filePath).result).isEqualTo(info)
        assertThat(callCount).isEqualTo(2)
    }

    @Test
    fun emptyCache_SingleExecutableRequest() {
        val tempFile = tempFolder.newFile().also { it.writeText("tempFile") }
        val infoProvider = TestFileInfoCache()
        val pathPromise = infoProvider.evaluate(tempFile.absolutePath)
        waitAll(listOf(pathPromise))

        assertThat(pathPromise.blockingGet(0)!!.result).isEqualTo("tempFile")

        assertThat(infoProvider.testOnlyGetRequestCache())
            .describedAs("Cache size does not match expected value")
            .hasSize(1)
    }

    @Test
    fun nonEmptyCache_SingleExecutableRequest() {
        val tempFile = tempFolder.newFile().also { it.writeText("tempFile") }
        val infoProvider = TestFileInfoCache()
        val pathPromise = infoProvider.evaluate(tempFile.absolutePath)
        waitAll(listOf(pathPromise))

        // Get the value with no wait because the value should be already cached
        val samePathPromise = infoProvider.evaluate(tempFile.absolutePath).blockingGet(0)!!.result
        assertThat(samePathPromise).isEqualTo("tempFile")

        assertThat(infoProvider.testOnlyGetRequestCache())
            .describedAs("Cache size does not match expected value")
            .hasSize(1)
    }

    @Test
    fun differentExecutableRequests() {
        val tempFile1 = tempFolder.newFile().also { it.writeText("tempFile1") }
        val tempFile2 = tempFolder.newFile().also { it.writeText("tempFile2") }
        val infoProvider = TestFileInfoCache()

        val pathTempFile1Promise = infoProvider.evaluate(tempFile1.absolutePath)
        val pathTempFile2Promise = infoProvider.evaluate(tempFile2.absolutePath)
        waitAll(listOf(pathTempFile1Promise, pathTempFile2Promise))

        assertThat(pathTempFile1Promise.blockingGet(0)!!.result).isEqualTo("tempFile1")
        assertThat(pathTempFile2Promise.blockingGet(0)!!.result).isEqualTo("tempFile2")

        assertThat(infoProvider.testOnlyGetRequestCache())
            .describedAs("Cache size does not match expected value")
            .hasSize(2)
    }

    @Test
    fun multipleThreads_SameSamPath() {
        val threadsCount = 20
        val tempFile = tempFolder.newFile()
        val results = ContainerUtil.newConcurrentSet<Promise<FileInfoCache.InfoResult<String>>>()
        val infoProvider = TestFileInfoCache()

        val info = "v1"
        tempFile.writeText(info)

        fun retrieveVersion() {
            val promise = infoProvider.evaluate(tempFile.absolutePath)
            results.add(promise)
        }

        val threads = (1..threadsCount).map { Thread(::retrieveVersion).apply { start() } }.toList()
        for (thread in threads) {
            thread.join()
        }

        waitAll(results)

        assertThat(results).describedAs("Number of threads does not match expected value").hasSize(1)

        for (result in results) {
            assertThat(result.blockingGet(0)!!.result).isEqualTo(info)
        }

        assertThat(infoProvider.testOnlyGetRequestCache())
            .describedAs("Cache size does not match expected value")
            .hasSize(1)
    }

    @Test
    fun invalidExecutablePath() {
        val invalidPath = "invalid_path"

        assertThatThrownBy { TestFileInfoCache().evaluateBlocking(invalidPath) }
            .isInstanceOf(IllegalStateException::class.java)
            .hasMessage(message("general.file_not_found", invalidPath))
    }

    @Test
    fun testDeleteFileAfterReadingFromIt() {
        val tempFile = tempFolder.newFile()
        val path = tempFile.absolutePath

        val testFileInfoCache = object : FileInfoCache<String>() {
            override fun getFileInfo(path: String): String {
                val file = File(path)
                return try {
                    file.readText()
                } finally {
                    file.delete()
                }
            }
        }

        assertThatThrownBy { testFileInfoCache.evaluateBlocking(path) }
            .isInstanceOf(IllegalStateException::class.java)
            .hasMessage(message("general.file_not_found", path))
    }

    private class TestFileInfoCache : FileInfoCache<String>() {
        var callCount = 0
            private set

        override fun getFileInfo(path: String): String {
            try {
                return File(path).readText()
            } finally {
                callCount++
            }
        }
    }

    private fun waitAll(promises: Collection<Promise<*>>) {
        val all = promises.all(null, ignoreErrors = true)
        all.blockingGet(3000)
    }
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.runWriteActionAndWait
import com.intellij.openapi.fileTypes.FileType
import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.util.Disposer

fun associateFilePattern(fileType: FileType, pattern: String, parentDisposable: Disposable) {
    val fileTypeManager = FileTypeManager.getInstance()
    runWriteActionAndWait {
        fileTypeManager.associatePattern(fileType, pattern)
    }
    Disposer.register(parentDisposable) {
        runWriteActionAndWait {
            fileTypeManager.removeAssociation(fileType, FileTypeManager.parseFromString(pattern))
        }
    }
}


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

package software.aws.toolkits.jetbrains.utils

import org.jetbrains.ide.BuiltInServerManager
import java.util.concurrent.atomic.AtomicReference

object FrameworkTestUtils {
    private val serverStarted = AtomicReference(false)

    // Make sure the built in server is started. This is not done in unit test mode, so it must be done
    // for several integration tests in ultimate/gateway
    fun ensureBuiltInServerStarted() {
        serverStarted.getAndUpdate { started ->
            if (!started) {
                BuiltInServerManager.getInstance().waitForStart()
            }
            true
        }
    }
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.application.runWriteActionAndWait
import com.intellij.openapi.externalSystem.importing.ImportSpecBuilder
import com.intellij.openapi.externalSystem.model.DataNode
import com.intellij.openapi.externalSystem.model.project.ProjectData
import com.intellij.openapi.externalSystem.service.execution.ProgressExecutionMode
import com.intellij.openapi.externalSystem.service.project.ExternalProjectRefreshCallback
import com.intellij.openapi.externalSystem.service.project.ProjectDataManager
import com.intellij.openapi.externalSystem.settings.ExternalSystemSettingsListenerAdapter
import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil
import com.intellij.openapi.externalSystem.util.ExternalSystemUtil
import com.intellij.openapi.projectRoots.JavaSdk
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.impl.JavaAwareProjectJdkTableImpl
import com.intellij.openapi.projectRoots.impl.SdkConfigurationUtil
import com.intellij.openapi.roots.ModuleRootModificationUtil
import com.intellij.openapi.util.BuildNumber
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Ref
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiJavaFile
import com.intellij.testFramework.IdeaTestUtil
import com.intellij.testFramework.RunAll
import com.intellij.testFramework.runInEdtAndWait
import com.intellij.util.io.isDirectory
import com.intellij.xdebugger.XDebuggerUtil
import org.jetbrains.idea.maven.model.MavenExplicitProfiles
import org.jetbrains.idea.maven.project.MavenProjectsManager
import org.jetbrains.idea.maven.server.MavenServerManager
import org.jetbrains.plugins.gradle.settings.GradleProjectSettings
import org.jetbrains.plugins.gradle.util.GradleConstants
import org.junit.Assert.fail
import software.aws.toolkits.core.utils.exists
import software.aws.toolkits.core.utils.inputStream
import software.aws.toolkits.jetbrains.utils.rules.HeavyJavaCodeInsightTestFixtureRule
import software.aws.toolkits.jetbrains.utils.rules.addFileToModule
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

fun HeavyJavaCodeInsightTestFixtureRule.setUpJdk(jdkName: String = "Real JDK"): String {
    val jdkHome = IdeaTestUtil.requireRealJdkHome()

    runInEdtAndWait {
        runWriteAction {
            VfsRootAccess.allowRootAccess(this.fixture.testRootDisposable, jdkHome)
            val jdkHomeDir = LocalFileSystem.getInstance().refreshAndFindFileByPath(jdkHome)!!
            val jdk = SdkConfigurationUtil.setupSdk(emptyArray(), jdkHomeDir, JavaSdk.getInstance(), false, null, jdkName)!!

            ProjectJdkTable.getInstance().addJdk(jdk, this.fixture.testRootDisposable)
            ModuleRootModificationUtil.setModuleSdk(this.module, jdk)
        }
    }

    return jdkHome
}

fun HeavyJavaCodeInsightTestFixtureRule.setUpGradleProject(compatibility: String = "1.8"): PsiClass {
    val fixture = this.fixture
    val buildFile = fixture.addFileToModule(
        this.module,
        "build.gradle",
        """
            plugins {
                id 'java'
            }

            sourceCompatibility = '$compatibility'
            targetCompatibility = '$compatibility'
        """.trimIndent()
    ).virtualFile

    // Use our project's own Gradle version
    this.copyGradleFiles()

    val lambdaClass = fixture.addClass(
        """
            package com.example;

            public class SomeClass {
                public static String upperCase(String input) {
                    return input.toUpperCase();
                }
            }
        """.trimIndent()
    )

    val jdkName = "Gradle JDK"
    setUpJdk(jdkName)

    ExternalSystemApiUtil.subscribe(
        project,
        GradleConstants.SYSTEM_ID,
        object : ExternalSystemSettingsListenerAdapter<GradleProjectSettings>() {
            override fun onProjectsLinked(settings: Collection<GradleProjectSettings>) {
                super.onProjectsLinked(settings)
                settings.first().gradleJvm = jdkName
            }
        }
    )

    val gradleProjectSettings = GradleProjectSettings().apply {
        withQualifiedModuleNames()
        externalProjectPath = buildFile.path
    }

    val externalSystemSettings = ExternalSystemApiUtil.getSettings(project, GradleConstants.SYSTEM_ID)
    externalSystemSettings.setLinkedProjectsSettings(setOf(gradleProjectSettings))

    val error = Ref.create<String>()

    val refreshCallback = object : ExternalProjectRefreshCallback {
        override fun onSuccess(externalProject: DataNode<ProjectData>?) {
            if (externalProject == null) {
                System.err.println("Got null External project after import")
                return
            }
            ProjectDataManager.getInstance().importData(externalProject, project, true)
            println("External project was successfully imported")
        }

        override fun onFailure(errorMessage: String, errorDetails: String?) {
            error.set(errorMessage)
        }
    }

    val importSpecBuilder = ImportSpecBuilder(project, GradleConstants.SYSTEM_ID)
        .callback(refreshCallback)
        .use(ProgressExecutionMode.MODAL_SYNC)

    ExternalSystemUtil.refreshProjects(importSpecBuilder)

    if (!error.isNull) {
        fail("Import failed: " + error.get())
    }

    return lambdaClass
}

fun HeavyJavaCodeInsightTestFixtureRule.addBreakpoint() {
    runInEdtAndWait {
        val document = fixture.editor.document
        val psiFile = fixture.file as PsiJavaFile
        val body = psiFile.classes[0].allMethods[0].body!!.statements[0]
        val lineNumber = document.getLineNumber(body.textOffset)

        XDebuggerUtil.getInstance().toggleLineBreakpoint(
            project,
            fixture.file.virtualFile,
            lineNumber
        )
    }
}

private fun HeavyJavaCodeInsightTestFixtureRule.copyGradleFiles() {
    val gradleRoot = findGradlew()
    val gradleFiles = setOf("gradle/wrapper", "gradlew.bat", "gradlew")

    gradleFiles.forEach {
        val gradleFile = gradleRoot.resolve(it)
        if (gradleFile.exists()) {
            copyPath(gradleRoot, gradleFile)
        } else {
            throw IllegalStateException("Failed to locate $it")
        }
    }
}

private fun HeavyJavaCodeInsightTestFixtureRule.copyPath(root: Path, path: Path) {
    if (path.isDirectory()) {
        Files.list(path).forEach {
            // Skip over files like .DS_Store. No gradlew related files start with a "." so safe to skip
            if (it.fileName.toString().startsWith(".")) {
                return@forEach
            }
            this@copyPath.copyPath(root, it)
        }
    } else {
        fixture.addFileToModule(module, root.relativize(path).toString(), "").also { newFile ->
            runInEdtAndWait {
                runWriteAction {
                    newFile.virtualFile.getOutputStream(null).use { out ->
                        path.inputStream().use { it.copyTo(out) }
                    }
                }
            }
            if (SystemInfo.isUnix) {
                val newPath = Paths.get(newFile.virtualFile.path)
                Files.setPosixFilePermissions(newPath, Files.getPosixFilePermissions(path))
            }
        }
    }
}

private fun findGradlew(): Path {
    var root = Paths.get("").toAbsolutePath()
    while (root.parent != null) {
        if (root.resolve("gradlew").exists()) {
            return root
        } else {
            root = root.parent
        }
    }

    throw IllegalStateException("Failed to locate gradlew")
}

internal fun HeavyJavaCodeInsightTestFixtureRule.setUpMavenProject(): PsiClass {
    val fixture = this.fixture
    val pomFile = fixture.addFileToModule(
        this.module,
        "pom.xml",
        """
            <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
                <modelVersion>4.0.0</modelVersion>
                <groupId>helloworld</groupId>
                <artifactId>HelloWorld</artifactId>
                <version>1.0</version>
                <packaging>jar</packaging>
                <name>A sample Hello World created for SAM CLI.</name>
                <properties>
                    <maven.compiler.source>1.8</maven.compiler.source>
                    <maven.compiler.target>1.8</maven.compiler.target>
                </properties>
            </project>
        """.trimIndent()
    ).virtualFile

    val lambdaClass = fixture.addClass(
        """
            package com.example;

            public class SomeClass {
                public static String upperCase(String input) {
                    return input.toUpperCase();
                }
            }
        """.trimIndent()
    )

    Disposer.register(this.fixture.testRootDisposable) {
        RunAll.runAll(
            { runWriteActionAndWait { JavaAwareProjectJdkTableImpl.removeInternalJdkInTests() } },
            { MavenServerManager.getInstance().shutdown(true) }
        )
    }

    val projectsManager = MavenProjectsManager.getInstance(project)
    projectsManager.initForTests()

    val poms = listOf(pomFile)
    projectsManager.resetManagedFilesAndProfilesInTests(poms, MavenExplicitProfiles(emptyList()))

    runInEdtAndWait {
        projectsManager.waitForReadingCompletion()
        check(ApplicationInfo.getInstance().build.compareTo(BuildNumber.fromString("232.6734.9")) <= 0) {
            "FIXME: uncomment waitForResolvingCompletion"
        }
        // will be re-added in next EAP
        // https://github.com/JetBrains/intellij-community/commit/0488a28a89b661c2cbf0706060bee2f7bd367237
        // projectsManager.waitForResolvingCompletion()
        projectsManager.scheduleImportInTests(poms)
        projectsManager.importProjects()
    }

    return lambdaClass
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.fileChooser.FileChooserDialog
import com.intellij.openapi.fileChooser.FileChooserFactory
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.testFramework.replaceService
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.mock
import java.nio.file.Path

fun createMockFileChooser(disposable: Disposable, vararg files: Path) {
    val dialog = object : FileChooserDialog {
        @Deprecated("needs to be implemented, but interface doesn't provide default impl")
        override fun choose(toSelect: VirtualFile?, project: Project?): Array<VirtualFile> = toSelect?.let {
            choose(project, it)
        } ?: choose(project)

        override fun choose(project: Project?, vararg toSelect: VirtualFile): Array<VirtualFile> {
            val lfs = LocalFileSystem.getInstance()
            return files.mapNotNull {
                lfs.refreshAndFindFileByIoFile(it.toFile())
            }.toTypedArray()
        }
    }

    val mock = mock<FileChooserFactory> {
        on { createFileChooser(any(), anyOrNull(), anyOrNull()) }.thenReturn(dialog)
    }

    ApplicationManager.getApplication().replaceService(FileChooserFactory::class.java, mock, disposable)
}


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

package software.aws.toolkits.jetbrains.utils

import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test

class MRUListTest {
    private lateinit var list: MRUList<String>

    @Before
    fun setUp() {
        list = MRUList(3)
    }

    @Test
    fun testEvictionOfOldest() {
        list.add(FOO)
        list.add(BAR)
        list.add(BAZ)
        list.add(FIZ)

        assertThat(list.elements()).containsExactly(FIZ, BAZ, BAR)
    }

    @Test
    fun testReAddingMovesUp() {
        list.add(FOO)
        list.add(BAR)
        list.add(BAZ)
        list.add(FOO)

        assertThat(list.elements()).containsExactly(FOO, BAZ, BAR)
    }

    private companion object {
        const val FOO = "Foo"
        const val BAR = "Bar"
        const val BAZ = "Baz"
        const val FIZ = "Fiz"
    }
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.ProjectRule
import org.assertj.core.api.Assertions.STRING
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import software.aws.toolkits.jetbrains.utils.rules.NotificationListenerRule
import java.util.function.Function

class NotificationUtilsTest {
    @Rule
    @JvmField
    val projectRule = ProjectRule()

    @Rule
    @JvmField
    val disposableRule = DisposableRule()

    @Rule
    @JvmField
    val notificationListener = NotificationListenerRule(projectRule, disposableRule.disposable)

    @Test
    fun `Notifications show stack traces for exceptions`() {
        NullPointerException().notifyError("ooops", project = projectRule.project)

        assertThat(notificationListener.notifications)
            .extracting(Function { t -> t.content })
            .singleElement(STRING)
            .startsWith("java.lang.NullPointerException")
            .contains("NotificationUtilsTest.kt")
    }
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.openapi.progress.ProgressIndicator
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify

class ProgressMonitorInputStreamTest {

    @JvmField
    @Rule
    val folder = TemporaryFolder()

    val mock = mock<ProgressIndicator>()

    @Test
    fun canReportProgressAsInputStreamIsRead() {
        val file = folder.newFile()
        file.writeBytes(ByteArray(100))

        ProgressMonitorInputStream.fromFile(mock, file.toPath()).use { sut ->
            sut.read()
        }

        verify(mock).fraction = 0.01
    }

    @Test
    fun canReportProgressWhenByteArrayIsUsed() {
        val file = folder.newFile()
        file.writeBytes(ByteArray(100))

        ProgressMonitorInputStream.fromFile(mock, file.toPath()).use { sut ->
            var read = 10
            while (read > 0) {
                read = -sut.read(ByteArray(read))
            }
        }

        verify(mock).fraction = 0.10
    }
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.openapi.progress.ProgressIndicator
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import java.io.FileOutputStream
import java.nio.charset.StandardCharsets

class ProgressMonitorOutputStreamTest {
    @JvmField
    @Rule
    val folder = TemporaryFolder()

    val mock = mock<ProgressIndicator>()

    @Test
    fun canReportProgressAsOutputStreamIsWritten() {
        val file = folder.newFile()

        ProgressMonitorOutputStream(mock, FileOutputStream(file), 10).use { sut ->
            sut.write(Byte.MIN_VALUE.toInt())
        }

        verify(mock).fraction = 0.1
    }

    @Test
    fun canReportProgressAsOutputStreamIsWrittenWithAnArray() {
        val file = folder.newFile()

        val helloWorld = "hello world".toByteArray(StandardCharsets.UTF_8)

        ProgressMonitorOutputStream(mock, FileOutputStream(file), 100).use { sut ->
            sut.write(helloWorld)
        }

        verify(mock).fraction = 0.11
        assertThat(file).hasContent("hello world")
    }
}


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

package software.aws.toolkits.jetbrains.utils

import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.intellij.execution.ExecutionListener
import com.intellij.execution.ExecutionManager
import com.intellij.execution.ExecutorRegistry
import com.intellij.execution.Output
import com.intellij.execution.OutputListener
import com.intellij.execution.configurations.RunConfiguration
import com.intellij.execution.configurations.RunProfileState
import com.intellij.execution.executors.DefaultDebugExecutor
import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.runners.ExecutionEnvironmentBuilder
import com.intellij.execution.runners.ProgramRunner
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Ref
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.testFramework.runInEdtAndWait
import com.intellij.util.ReflectionUtil
import com.intellij.util.text.SemVer
import com.intellij.xdebugger.XDebugProcess
import com.intellij.xdebugger.XDebugSessionListener
import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.XDebuggerManagerListener
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assume.assumeTrue
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.core.executables.ExecutableManager
import software.aws.toolkits.jetbrains.core.executables.getExecutableIfPresent
import software.aws.toolkits.jetbrains.services.lambda.execution.local.createTemplateRunConfiguration
import software.aws.toolkits.jetbrains.services.lambda.sam.SamCommon.minImageVersion
import software.aws.toolkits.jetbrains.services.lambda.sam.SamExecutable
import software.aws.toolkits.jetbrains.utils.rules.CodeInsightTestFixtureRule
import software.aws.toolkits.jetbrains.utils.rules.addFileToModule
import java.io.File
import java.nio.file.Paths
import java.util.concurrent.CompletableFuture
import java.util.concurrent.DelayQueue
import java.util.concurrent.TimeUnit
import javax.swing.Timer
import kotlin.test.assertNotNull

fun executeRunConfiguration(runConfiguration: RunConfiguration, executorId: String): CompletableFuture<Output> {
    val executor = ExecutorRegistry.getInstance().getExecutorById(executorId)
    assertNotNull(executor)
    val executionFuture = CompletableFuture<Output>()
    // In the real world create and execute runs on EDT
    runInEdt {
        try {
            val runner = ProgramRunner.getRunner(executorId, runConfiguration)!!
            val executionEnvironmentBuilder = ExecutionEnvironmentBuilder.create(executor, runConfiguration)
                .runner(runner)
            val executionEnvironment = executionEnvironmentBuilder.build()

            val listener = object : OutputListener() {
                override fun processWillTerminate(event: ProcessEvent, willBeDestroyed: Boolean) {
                    println("Received processWillTerminate for ${event.processHandler}")
                    super.processWillTerminate(event, willBeDestroyed)
                }

                override fun processTerminated(event: ProcessEvent) {
                    println("Received processTerminated for ${event.processHandler}")
                    super.processTerminated(event)
                    executionFuture.complete(this.output)
                }
            }

            runConfiguration.project.messageBus.connect(executionEnvironment).subscribe(
                ExecutionManager.EXECUTION_TOPIC,
                object : ExecutionListener {
                    override fun processTerminating(executorId: String, env: ExecutionEnvironment, handler: ProcessHandler) {
                        println("Execution topic listener called processTerminating for $handler")
                        super.processTerminating(executorId, env, handler)
                    }

                    override fun processTerminated(executorId: String, env: ExecutionEnvironment, handler: ProcessHandler, exitCode: Int) {
                        println("Execution topic listener called processTerminated with exit code $exitCode for $handler")
                        super.processTerminated(executorId, env, handler, exitCode)
                    }

                    override fun processStarting(executorId: String, env: ExecutionEnvironment, handler: ProcessHandler) {
                        handler.addProcessListener(listener)
                    }
                }
            )

            // TODO: exception isn't propagated out and test is forced to wait to timeout instead of exiting immediately
            executionEnvironment.runner.execute(executionEnvironment)
        } catch (e: Throwable) {
            executionFuture.completeExceptionally(e)
        }
    }
    return executionFuture
}

fun getState(runConfiguration: RunConfiguration, executorId: String = DefaultRunExecutor.EXECUTOR_ID): RunProfileState? {
    val executor = ExecutorRegistry.getInstance().getExecutorById(executorId)
    assertNotNull(executor)

    val environment = ExecutionEnvironmentBuilder.create(
        ExecutorRegistry.getInstance().getExecutorById(executorId)!!,
        runConfiguration
    )
        .runner(ProgramRunner.getRunner(executorId, runConfiguration)!!)
        .build()

    return runConfiguration.getState(executor, environment)
}

fun executeRunConfigurationAndWait(runConfiguration: RunConfiguration, executorId: String = DefaultRunExecutor.EXECUTOR_ID): Output {
    val executionFuture = executeRunConfiguration(runConfiguration, executorId)
    // 4 is arbitrary, but Image-based functions can take > 3 min on first build/run, so 4 is a safe number
    return try {
        executionFuture.get(4, TimeUnit.MINUTES)
    } finally {
        try {
            checkJavaSwingTimersAreDisposed()
        } catch (e: Throwable) {
            println("Exception attempting to dispose of Java Swing Timers: ${e.message}")
            e.printStackTrace()
        }
    }
}

// Adapted from https://github.com/JetBrains/intellij-community/blob/2926d8e27e2e0d8120a63089d1173b7e461e3dd1/platform/testFramework/src/com/intellij/testFramework/TestApplicationManager.kt#L338
// See https://intellij-support.jetbrains.com/hc/en-us/community/posts/360006918780-Tests-Fail-due-to-Java-Swing-Timers-Not-Disposed
// Seems to have been removed in 223: https://github.com/JetBrains/intellij-community/blob/223.7126/platform/testFramework/src/com/intellij/testFramework/TestApplicationManager.kt
private fun checkJavaSwingTimersAreDisposed() {
    val timerQueueClass = Class.forName("javax.swing.TimerQueue")
    val sharedInstance = timerQueueClass.getMethod("sharedInstance")
    sharedInstance.isAccessible = true
    val timerQueue = sharedInstance.invoke(null)
    val delayQueue = ReflectionUtil.getField(timerQueueClass, timerQueue, DelayQueue::class.java, "queue")
    while (true) {
        val timer = delayQueue.peek() ?: return
        val delay = timer.getDelay(TimeUnit.MILLISECONDS)
        val getTimer = ReflectionUtil.getDeclaredMethod(timer.javaClass, "getTimer") ?: throw NullPointerException()
        val swingTimer = getTimer.invoke(timer) as Timer
        println("Not disposed javax.swing.Timer: (listeners: ${listOf(*swingTimer.actionListeners)}) (delayed for ${delay}ms)")
        swingTimer.stop()
    }
}

fun stopOnPause(project: Project) {
    project.messageBus.connect().subscribe(
        XDebuggerManager.TOPIC,
        object : XDebuggerManagerListener {
            override fun processStarted(debugProcess: XDebugProcess) {
                println("Debugger attached: $debugProcess")

                debugProcess.session.addSessionListener(
                    object : XDebugSessionListener {
                        override fun sessionPaused() {
                            runInEdt {
                                val suspendContext = debugProcess.session.suspendContext
                                println("Stopping: $suspendContext")
                                debugProcess.stop()
                            }
                        }
                    }
                )
            }
        }
    )
}

fun checkBreakPointHit(project: Project, callback: () -> Unit = {}): Ref<Boolean> {
    val debuggerIsHit = Ref(false)

    val messageBusConnection = project.messageBus.connect()
    messageBusConnection.subscribe(
        XDebuggerManager.TOPIC,
        object : XDebuggerManagerListener {
            override fun processStarted(debugProcess: XDebugProcess) {
                println("Debugger attached: $debugProcess")

                debugProcess.session.addSessionListener(
                    object : XDebugSessionListener {
                        override fun sessionPaused() {
                            ApplicationManager.getApplication().executeOnPooledThread {
                                val stopLocation = debugProcess.session.suspendContext
                                println("Session paused: $stopLocation")

                                callback()
                                debuggerIsHit.set(true)

                                runInEdt {
                                    println("Resuming: $stopLocation")
                                    debugProcess.session.resume()
                                }
                            }
                        }

                        override fun sessionStopped() {
                            debuggerIsHit.setIfNull(false) // Used to prevent having to wait for max timeout
                        }
                    }
                )
            }
        }
    )

    return debuggerIsHit
}

fun readProject(
    relativePath: String,
    sourceFileName: String,
    projectRule: CodeInsightTestFixtureRule,
    templatePatches: Map<String, String> = emptyMap()
): Pair<VirtualFile, VirtualFile> {
    val testDataPath = Paths.get(System.getProperty("testDataPath"), relativePath).toFile()
    val (source, template) = testDataPath.walk().fold<File, Pair<VirtualFile?, VirtualFile?>>(Pair(null, null)) { acc, file ->
        // skip directories which are part of the walk
        if (!file.isFile) {
            return@fold acc
        }

        var fileText = file.readText()
        templatePatches.forEach { (search, replace) -> fileText = fileText.replace(search, replace) }

        val virtualFile = projectRule.fixture.addFileToModule(projectRule.module, file.relativeTo(testDataPath).path, fileText).virtualFile
        when (virtualFile.name) {
            "template.yaml" -> {
                acc.first to virtualFile
            }
            sourceFileName -> {
                virtualFile to acc.second
            }
            else -> {
                acc
            }
        }
    }

    assertNotNull(source)
    assertNotNull(template)

    // open the file so we can do stuff like set breakpoints on it
    runInEdtAndWait {
        projectRule.fixture.openFileInEditor(source)
    }

    return source to template
}

fun samImageRunDebugTest(
    projectRule: CodeInsightTestFixtureRule,
    relativePath: String,
    templatePatches: Map<String, String> = emptyMap(),
    sourceFileName: String,
    runtime: LambdaRuntime,
    mockCredentialsId: String,
    input: String,
    expectedOutput: String? = input.uppercase(),
    addBreakpoint: (() -> Unit)? = null
) {
    assumeImageSupport()
    val (_, template) = readProject(relativePath, sourceFileName, projectRule, templatePatches)

    addBreakpoint?.let { it() }

    val runConfiguration = createTemplateRunConfiguration(
        project = projectRule.project,
        runtime = runtime,
        templateFile = template.path,
        logicalId = "SomeFunction",
        input = "\"$input\"",
        credentialsProviderId = mockCredentialsId,
        isImage = true
    )

    assertNotNull(runConfiguration)

    val debuggerIsHit = checkBreakPointHit(projectRule.project)

    val executeLambda = if (addBreakpoint != null) {
        executeRunConfigurationAndWait(runConfiguration, DefaultDebugExecutor.EXECUTOR_ID)
    } else {
        executeRunConfigurationAndWait(runConfiguration)
    }

    assertThat(executeLambda.exitCode).isEqualTo(0)
    if (expectedOutput != null) {
        assertThat(executeLambda.stdout).contains(expectedOutput)
    }

    if (addBreakpoint != null) {
        assertThat(debuggerIsHit.get()).isTrue
    }
}

fun assumeImageSupport() {
    val samVersion = ExecutableManager.getInstance().getExecutableIfPresent<SamExecutable>().version?.let {
        SemVer.parseFromText(it)
    }
    assumeTrue(samVersion?.isGreaterOrEqualThan(minImageVersion) == true)
}

// Extracts the last json structure. Needed since output has all build output and sam cli messages
// TODO: handle JSON with nested objects
fun jsonToMap(data: String) = data.substringAfterLast("{").substringBefore("}").let {
    jacksonObjectMapper().readValue<Map<String, String>>("{$it}")
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.configurationStore.deserializeAndLoadState
import com.intellij.configurationStore.serializeStateInto
import com.intellij.openapi.components.PersistentStateComponent
import org.intellij.lang.annotations.Language
import org.jdom.Element
import org.jdom.input.SAXBuilder
import org.jdom.output.XMLOutputter
import java.io.ByteArrayInputStream

fun xmlElement(@Language("XML") str: String): Element {
    val stream = ByteArrayInputStream(str.toByteArray())
    val builder = SAXBuilder()
    return builder.build(stream).rootElement
}

fun serializeState(rootTag: String, state: PersistentStateComponent<*>): String {
    val element = Element(rootTag)
    serializeStateInto(state, element)
    return XMLOutputter().outputString(element)
}

fun deserializeState(@Language("XML") str: String, state: PersistentStateComponent<*>) {
    deserializeAndLoadState(state, xmlElement(str))
}


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

package software.aws.toolkits.jetbrains.utils

import software.amazon.awssdk.awscore.exception.AwsServiceException
import software.amazon.awssdk.core.exception.SdkException

// https://github.com/aws/aws-toolkit-vscode/blob/bf612d8c7477316a5f2e4bd591966b78d449846d/src/test/setupUtil.ts#L139-L150
val ARN_REGEX = "arn:(aws|aws-cn|aws-us-gov):(?:.*?):(.*?):(.*?):.".toRegex()

fun scrubException(e: Exception): Exception {
    if (e is AwsServiceException && e.awsErrorDetails() != null) {
        return e.toBuilder()
            .message(scrubArn(e.message))
            .awsErrorDetails(e.awsErrorDetails().toBuilder().errorMessage(scrubArn(e.awsErrorDetails().errorMessage())).build())
            .build().apply {
                stackTrace = e.stackTrace
            }
    }

    if (e is SdkException) {
        return e.toBuilder()
            .message(scrubArn(e.message))
            .build().apply {
                stackTrace = e.stackTrace
            }
    }

    return e
}

private fun scrubArn(s: String?) = s?.let { message ->
    ARN_REGEX.replace(message) {
        val (_partition, region, account) = it.destructured

        var ret = it.value
        if (region.isNotBlank()) {
            ret = ret.replace(region, "***")
        }
        if (account.isNotBlank()) {
            ret = ret.replace(account, "***")
        }

        ret
    }
}


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

package software.aws.toolkits.jetbrains.utils

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import software.aws.toolkits.core.utils.htmlWrap
import software.aws.toolkits.core.utils.splitNoBlank

class StringUtilsTest {
    @Test
    fun splitNoBlank() {
        assertThat("a\nb\nc\n".split('\n')).isEqualTo(listOf("a", "b", "c", ""))
        assertThat("a\nb\nc\n".splitNoBlank('\n')).isEqualTo(listOf("a", "b", "c"))
        assertThat("a\nb\nc".splitNoBlank('\n')).isEqualTo(listOf("a", "b", "c"))
        assertThat("a\nb\nc\n   ".splitNoBlank('\n')).isEqualTo(listOf("a", "b", "c"))
        assertThat("a\nb\nc\n   \n".splitNoBlank('\n')).isEqualTo(listOf("a", "b", "c"))
    }

    @Test
    fun htmlWrap() {
        assertThat("thing".htmlWrap()).isEqualTo("<html>thing</html>")
    }
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.json.JsonLanguage
import com.intellij.testFramework.ProjectRule
import com.intellij.testFramework.runInEdtAndWait
import org.assertj.core.api.Assertions.assertThat
import org.intellij.lang.annotations.Language
import org.junit.Rule
import org.junit.Test

class TextUtilsTest {
    @Rule
    @JvmField
    val projectRule = ProjectRule()

    @Test
    fun textGetsFormatted() {
        @Language("JSON")
        val actual =
            """
            {
              "hello":
                      "world"}
            """.trimIndent()

        @Language("JSON")
        val expected =
            """
            {
              "hello": "world"
            }
            """.trimIndent()

        lateinit var formatted: String
        runInEdtAndWait {
            formatted = formatText(projectRule.project, JsonLanguage.INSTANCE, actual)
        }
        assertThat(formatted).isEqualTo(expected)
    }

    @Test
    fun canConvertToTitleHumanReadable() {
        assertThat("CREATE_COMPLETE".toHumanReadable()).isEqualTo("Create Complete")
        assertThat("UPDATE_IN_PROGRESS".toHumanReadable()).isEqualTo("Update In Progress")
    }
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressManager
import com.intellij.testFramework.ApplicationRule
import com.intellij.util.concurrency.AppExecutorUtil
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import java.time.Duration
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

class ThreadingUtilsKtTest {
    @Rule
    @JvmField
    val application = ApplicationRule()

    @Test
    fun `computeOnEdt runs on edt`() {
        computeOnEdt {
            ApplicationManager.getApplication().assertIsDispatchThread()
        }
    }

    @Test
    fun `computeOnEdt bubbles out errors`() {
        assertThatThrownBy {
            computeOnEdt {
                throw IllegalStateException("Dummy error")
            }
        }.isInstanceOf<IllegalStateException>()
    }

    @Test
    fun `computeOnEdt respects cancellation`() {
        val latch = CountDownLatch(1)
        try {
            assertThatThrownBy {
                val indicator = EmptyProgressIndicator()
                ProgressManager.getInstance().runProcess(
                    {
                        computeOnEdt {
                            indicator.cancel()
                            latch.await()
                        }
                    },
                    indicator
                )
            }.isInstanceOf<ProcessCanceledException>()
        } finally {
            latch.countDown()
        }
    }

    @Test
    fun `sleepWithCancellation respects cancellation`() {
        val indicator = EmptyProgressIndicator()
        AppExecutorUtil.getAppScheduledExecutorService().schedule(
            { indicator.cancel() },
            100,
            TimeUnit.MILLISECONDS
        )
        assertThatThrownBy {
            sleepWithCancellation(Duration.ofHours(3), indicator)
        }.isInstanceOf<ProcessCanceledException>()
    }
}


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

package software.aws.toolkits.jetbrains.utils

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class YamlWriterTest {
    @Test
    fun testYamlWriter() {
        val text = yaml {
            mapping("Foo") {
                mapping("Bar") {
                    keyValue("Hello", "World")
                    mapping("Some") {
                        keyValue("More", "Keys")
                    }
                    mapping("EvenSome") {
                        keyValue("More2", "Keys2")
                    }
                    mapping("SomeList") {
                        listValue("Foo")
                        listValue("Bar")
                    }
                }
            }
        }
        assertThat(text).isEqualTo(
            """
            Foo:
              Bar:
                Hello: World
                Some:
                  More: Keys
                EvenSome:
                  More2: Keys2
                SomeList:
                  - Foo
                  - Bar
            """.trimIndent()
        )
    }
}


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

package software.aws.toolkits.jetbrains.utils.execution.steps

import com.intellij.build.BuildView
import com.intellij.build.events.BuildEvent
import com.intellij.build.events.FailureResult
import com.intellij.build.events.FinishEvent
import com.intellij.build.events.OutputBuildEvent
import com.intellij.build.events.SkippedResult
import com.intellij.build.events.StartEvent
import com.intellij.build.events.SuccessResult
import com.intellij.openapi.progress.ProcessCanceledException
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify

class BuildViewStepEmitterTest {
    @Rule
    @JvmField
    val mockRule: MockitoRule = MockitoJUnit.rule()

    private val buildView = mock<BuildView>()
    private val rootEmitter = BuildViewStepEmitter.createRoot(buildView, PARENT_ID)

    @Test
    fun startEventIsWritten() {
        val stepId = "ChildStep"

        val messageEmitter = rootEmitter.createChildEmitter(stepId, hidden = false)

        messageEmitter.stepStarted()

        argumentCaptor<BuildEvent>().apply {
            verify(buildView).onEvent(eq(PARENT_ID), capture())

            assertThat(allValues).hasSize(1)
            assertThat(firstValue).satisfies {
                assertThat(it.parentId).isEqualTo(PARENT_ID)
            }.isInstanceOfSatisfying(StartEvent::class.java) {
                assertThat(it.message).isEqualTo(stepId)
            }
        }
    }

    @Test
    fun finishEventIsWritten() {
        val parentId = "ParentStep"
        val stepId = "ChildStep"

        val messageEmitter = rootEmitter.createChildEmitter(stepId, hidden = false)

        messageEmitter.stepFinishSuccessfully()

        argumentCaptor<BuildEvent>().apply {
            verify(buildView).onEvent(eq(PARENT_ID), capture())

            assertThat(allValues).hasSize(1)
            assertThat(firstValue).satisfies {
                assertThat(it.parentId).isEqualTo(parentId)
                assertThat(it.id).isEqualTo(stepId)
            }.isInstanceOfSatisfying(FinishEvent::class.java) {
                assertThat(it.message).isEqualTo(stepId)
                assertThat(it.result).isInstanceOf(SuccessResult::class.java)
            }
        }
    }

    @Test
    fun finishWithErrorEventIsWritten() {
        val parentId = "ParentStep"
        val stepId = "ChildStep"

        val messageEmitter = rootEmitter.createChildEmitter(stepId, hidden = false)

        messageEmitter.stepFinishExceptionally(NullPointerException("Test exception"))

        argumentCaptor<BuildEvent>().apply {
            verify(buildView, times(3)).onEvent(eq(PARENT_ID), capture())

            assertThat(allValues).hasSize(3)

            assertThat(firstValue).satisfies {
                assertThat(it.parentId).isEqualTo(parentId)
            }.isInstanceOfSatisfying(OutputBuildEvent::class.java) {
                assertThat(it.message).isEqualTo("ChildStep has failed: Test exception")
                assertThat(it.isStdOut).isFalse()
            }

            assertThat(secondValue).satisfies {
                assertThat(it.parentId).isEqualTo(stepId)
            }.isInstanceOfSatisfying(OutputBuildEvent::class.java) {
                assertThat(it.message).isEqualTo("ChildStep has failed: Test exception")
                assertThat(it.isStdOut).isFalse()
            }

            assertThat(thirdValue).satisfies {
                assertThat(it.parentId).isEqualTo(parentId)
                assertThat(it.id).isEqualTo(stepId)
            }.isInstanceOfSatisfying(FinishEvent::class.java) {
                assertThat(it.message).isEqualTo(stepId)
                assertThat(it.result).isInstanceOf(FailureResult::class.java)
            }
        }
    }

    @Test
    fun exceptionsWithoutAMessagePrintsStacktrace() {
        val parentId = "ParentStep"

        rootEmitter.stepFinishExceptionally(NullPointerException())

        argumentCaptor<BuildEvent>().apply {
            verify(buildView, times(2)).onEvent(eq(PARENT_ID), capture())

            assertThat(firstValue).satisfies {
                assertThat(it.parentId).isEqualTo(parentId)
            }.isInstanceOfSatisfying(OutputBuildEvent::class.java) {
                assertThat(it.message).contains("ParentStep has failed: java.lang.NullPointerException", "at")
                assertThat(it.isStdOut).isFalse()
            }
        }
    }

    @Test
    fun cancelledExceptionsPrintHelpfulMessage() {
        val parentId = "ParentStep"

        rootEmitter.stepFinishExceptionally(ProcessCanceledException())

        argumentCaptor<BuildEvent>().apply {
            verify(buildView, times(2)).onEvent(eq(PARENT_ID), capture())

            assertThat(firstValue).satisfies {
                assertThat(it.parentId).isEqualTo(parentId)
            }.isInstanceOfSatisfying(OutputBuildEvent::class.java) {
                assertThat(it.message).contains("ParentStep has been canceled")
                assertThat(it.isStdOut).isFalse()
            }
        }
    }

    @Test
    fun skippedTestIsReported() {
        val parentId = "ParentStep"
        val stepId = "ChildStep"

        val messageEmitter = rootEmitter.createChildEmitter(stepId, hidden = false)

        messageEmitter.stepSkipped()

        argumentCaptor<BuildEvent>().apply {
            verify(buildView).onEvent(eq(PARENT_ID), capture())

            assertThat(allValues).hasSize(1)
            assertThat(firstValue).satisfies {
                assertThat(it.parentId).isEqualTo(parentId)
                assertThat(it.id).isEqualTo(stepId)
            }.isInstanceOfSatisfying(FinishEvent::class.java) {
                assertThat(it.message).isEqualTo(stepId)
                assertThat(it.result).isInstanceOf(SkippedResult::class.java)
            }
        }
    }

    @Test
    fun messageIsWrittenToChildAndParent() {
        val stepId = "ChildStep"
        val message = "A message"

        val messageEmitter = rootEmitter.createChildEmitter(stepId, hidden = false)

        messageEmitter.emitMessage(message, false)

        argumentCaptor<BuildEvent>().apply {
            verify(buildView, times(2)).onEvent(eq(PARENT_ID), capture())

            assertThat(firstValue).satisfies {
                assertThat(it.parentId).isEqualTo(PARENT_ID)
            }.isInstanceOfSatisfying(OutputBuildEvent::class.java) {
                assertThat(it.message).isEqualTo(message)
                assertThat(it.isStdOut).isTrue()
            }

            assertThat(secondValue).satisfies {
                assertThat(it.parentId).isEqualTo(stepId)
            }.isInstanceOfSatisfying(OutputBuildEvent::class.java) {
                assertThat(it.message).isEqualTo(message)
                assertThat(it.isStdOut).isTrue()
            }
        }
    }

    @Test
    fun messageIsWrittenAsError() {
        val stepId = "ChildStep"
        val message = "A message"

        val messageEmitter = rootEmitter.createChildEmitter(stepId, hidden = false)

        messageEmitter.emitMessage(message, true)

        argumentCaptor<BuildEvent>().apply {
            verify(buildView, times(2)).onEvent(eq(PARENT_ID), capture())

            assertThat(firstValue).satisfies {
                assertThat(it.parentId).isEqualTo(PARENT_ID)
            }.isInstanceOfSatisfying(OutputBuildEvent::class.java) {
                assertThat(it.message).isEqualTo(message)
                assertThat(it.isStdOut).isFalse()
            }

            assertThat(secondValue).satisfies {
                assertThat(it.parentId).isEqualTo(stepId)
            }.isInstanceOfSatisfying(OutputBuildEvent::class.java) {
                assertThat(it.message).isEqualTo(message)
                assertThat(it.isStdOut).isFalse()
            }
        }
    }

    @Test
    fun parentIdIsCorrect() {
        val childId1 = "ChildStep1"
        val childId2 = "ChildStep2"

        val messageEmitter = rootEmitter.createChildEmitter(childId1, hidden = false).createChildEmitter(childId2, hidden = false)

        messageEmitter.stepStarted()

        argumentCaptor<BuildEvent>().apply {
            verify(buildView).onEvent(eq(PARENT_ID), capture())

            assertThat(allValues).hasSize(1)
            assertThat(firstValue).satisfies {
                assertThat(it.parentId).isEqualTo(childId1)
            }.isInstanceOfSatisfying(StartEvent::class.java) {
                assertThat(it.message).isEqualTo(childId2)
            }
        }
    }

    @Test
    fun hiddenMessageEmittersDontPublishEvents() {
        val hidden = "HiddenStep"
        val message = "A message"
        val hiddenEmitter = rootEmitter.createChildEmitter(hidden, hidden = true)

        hiddenEmitter.emitMessage(message, false)

        argumentCaptor<BuildEvent>().apply {
            verify(buildView).onEvent(eq(PARENT_ID), capture())

            assertThat(allValues).hasSize(1)
            assertThat(firstValue).satisfies {
                assertThat(it.parentId).isEqualTo(PARENT_ID)
            }.isInstanceOfSatisfying(OutputBuildEvent::class.java) {
                assertThat(it.message).isEqualTo(message)
            }
        }
    }

    companion object {
        private const val PARENT_ID = "ParentStep"
    }
}


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

package software.aws.toolkits.jetbrains.utils.execution.steps

import com.intellij.util.ExceptionUtil

class ConsoleViewWorkflowEmitter private constructor(private val workflowTitle: String) : WorkflowEmitter {
    override fun createStepEmitter(): StepEmitter = ConsoleMessageEmitter(workflowTitle)

    override fun workflowStarted() {
        println("Workflow '$workflowTitle' started")
    }

    override fun workflowCompleted() {
        println("Workflow '$workflowTitle' completed")
    }

    override fun workflowFailed(e: Throwable) {
        println("Workflow '$workflowTitle' failed: ${ExceptionUtil.getThrowableText(e)}")
    }

    companion object {
        fun createEmitter(workflowTitle: String) = ConsoleViewWorkflowEmitter(workflowTitle)
    }
}

class ConsoleMessageEmitter(private val stepName: String) : StepEmitter {
    override fun createChildEmitter(stepName: String, hidden: Boolean): StepEmitter = ConsoleMessageEmitter(stepName)

    override fun stepStarted() {
        println("[$stepName] [Start Event]")
    }

    override fun stepFinishSuccessfully() {
        println("[$stepName] [Finish Event] Success")
    }

    override fun stepFinishExceptionally(e: Throwable) {
        println("[$stepName] [Finished Exceptionally] ${ExceptionUtil.getNonEmptyMessage(e, e.javaClass.simpleName)}")
    }

    override fun emitMessage(message: String, isError: Boolean) {
        println("[$stepName] $message")
    }
}


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

package software.aws.toolkits.jetbrains.utils.execution.steps

import com.intellij.testFramework.ProjectRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

class StepExecutorTest {
    @Rule
    @JvmField
    val projectRule = ProjectRule()

    private lateinit var workflowEmitter: WorkflowEmitter
    private lateinit var successCallback: (Context) -> Unit
    private lateinit var errorCallback: (Throwable) -> Unit

    @Before
    fun setUp() {
        workflowEmitter = mock {
            on { createStepEmitter() } doAnswer { createMockEmitter() }
        }
        successCallback = mock()
        errorCallback = mock()
    }

    @Test
    fun `success callback is invoked when no steps fail`() {
        val step1 = mock<Step>()
        val step2 = mock<Step>()

        createExecutor(step1, step2).startExecution().waitFor(2_000)

        verify(step1).run(any(), any(), any())
        verify(step2).run(any(), any(), any())
        verify(successCallback).invoke(any())
        verifyNoMoreInteractions(errorCallback)

        verify(workflowEmitter).workflowStarted()
        verify(workflowEmitter).workflowCompleted()
    }

    @Test
    fun `an error in a step skips rest of the steps and invokes error callback`() {
        val step1 = mock<Step> {
            on { run(any(), any(), any()) }.thenThrow(IllegalStateException("Simulated"))
        }
        val step2 = mock<Step>()

        createExecutor(step1, step2).startExecution().waitFor(2_000)

        verify(step1).run(any(), any(), any())
        verifyNoMoreInteractions(step2)
        verifyNoMoreInteractions(successCallback)
        verify(errorCallback).invoke(any())

        verify(workflowEmitter).workflowStarted()
        verify(workflowEmitter).workflowFailed(any())
    }

    @Test
    fun `an error in success callback leads to error callback`() {
        successCallback.stub {
            on { invoke(any()) }.thenThrow(IllegalStateException("Simulated"))
        }

        createExecutor().startExecution().waitFor(2_000)

        verify(successCallback).invoke(any())
        verify(errorCallback).invoke(any())

        verify(workflowEmitter).workflowStarted()
        verify(workflowEmitter).workflowFailed(any())
    }

    @Test
    fun `stopping the process handler will cancel the workflow steps and skip success callback`() {
        val stepStarted = CountDownLatch(1)
        val stepPaused = CountDownLatch(1)
        val step1 = mock<Step> {
            on { run(any(), any(), any()) }.thenAnswer {
                stepStarted.countDown()
                stepPaused.await()
            }
        }
        val step2 = mock<Step>()

        val execution = createExecutor(step1, step2).startExecution()

        assertThat(stepStarted.await(3, TimeUnit.SECONDS)).isTrue

        execution.destroyProcess()
        stepPaused.countDown()
        execution.waitFor(2_000)

        verifyNoMoreInteractions(successCallback)
        verify(errorCallback).invoke(any())

        verify(workflowEmitter).workflowStarted()
        verify(workflowEmitter).workflowFailed(any())
    }

    @Test
    fun `error in error callback still finishes the workflow`() {
        successCallback.stub {
            on { invoke(any()) }.thenThrow(IllegalStateException("Simulated"))
        }

        errorCallback.stub {
            on { invoke(any()) }.thenThrow(IllegalStateException("Simulated 2"))
        }

        createExecutor().startExecution().waitFor(2_000)

        verify(successCallback).invoke(any())
        verify(errorCallback).invoke(any())

        verify(workflowEmitter).workflowStarted()
        verify(workflowEmitter).workflowFailed(any())
    }

    private fun createMockEmitter(): StepEmitter = mock {
        on { createChildEmitter(any(), any()) } doAnswer { createMockEmitter() }
    }

    private fun createExecutor(vararg steps: Step): StepExecutor {
        val executor = StepExecutor(projectRule.project, StepWorkflow(*steps), workflowEmitter)
        executor.onSuccess = successCallback
        executor.onError = errorCallback
        return executor
    }
}


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

package software.aws.toolkits.jetbrains.utils.execution.steps

import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify

class StepTest {
    @Test
    fun `skipping completes step and reports no error`() {
        val skipStep = object : Step() {
            override val stepName: String = "SkipStep"

            override fun execute(context: Context, stepEmitter: StepEmitter, ignoreCancellation: Boolean) {
                throw SkipStepException()
            }
        }

        val (parentEmitter, childEmitter) = createEmitters()

        skipStep.run(Context(), parentEmitter)

        verify(parentEmitter).createChildEmitter(any(), any())

        verify(childEmitter).stepStarted()
        verify(childEmitter).stepSkipped()
    }

    private fun createEmitters(): Pair<StepEmitter, StepEmitter> {
        val childEmitter = createMockEmitter()
        val parentEmitter = mock<StepEmitter> {
            on { createChildEmitter(any(), any()) } doReturn childEmitter
        }
        return parentEmitter to childEmitter
    }

    private fun createMockEmitter(): StepEmitter = mock {
        on { createChildEmitter(any(), any()) } doAnswer { createMockEmitter() }
    }
}


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

package software.aws.toolkits.jetbrains.utils.extensions

import org.junit.jupiter.api.extension.AfterAllCallback
import org.junit.jupiter.api.extension.ExtensionContext
import software.amazon.awssdk.services.codecatalyst.CodeCatalystClient
import software.amazon.awssdk.services.codecatalyst.model.CreateDevEnvironmentRequest
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.credentials.ToolkitConnection

class DevEnvironmentExtension(
    connection: () -> ToolkitConnection,
    environmentBuilder: (CodeCatalystClient, CreateDevEnvironmentRequest.Builder) -> Unit
) : AfterAllCallback {
    private val client: CodeCatalystClient by lazy {
        AwsClientManager.getInstance().getClient(connection().getConnectionSettings())
    }

    private val lazyEnvironment = lazy {
        client.createDevEnvironment {
            environmentBuilder(client, it)
        }.let {
            DevEnvironment(
                spaceName = it.spaceName(),
                projectName = it.projectName(),
                id = it.id()
            )
        }
    }
    val environment: DevEnvironment by lazyEnvironment

    override fun afterAll(context: ExtensionContext) {
        if (lazyEnvironment.isInitialized()) {
            client.deleteDevEnvironment {
                it.spaceName(environment.spaceName)
                it.projectName(environment.projectName)
                it.id(environment.id)
            }
        }
    }
}

data class DevEnvironment(
    val spaceName: String,
    val projectName: String,
    val id: String
)


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

package software.aws.toolkits.jetbrains.utils.extensions

import com.intellij.testFramework.DisposableRule
import org.junit.jupiter.api.extension.AfterAllCallback
import org.junit.jupiter.api.extension.ExtensionContext

/**
 * Same as [com.intellij.testFramework.DisposableExtension], but AfterAll instead of AfterEach
 */
@Deprecated("Should be injected with annotations available in 223 instead")
class DisposerAfterAllExtension : DisposableRule(), AfterAllCallback {
    override fun afterAll(context: ExtensionContext) {
        after()
    }
}


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

package software.aws.toolkits.jetbrains.utils.extensions

import com.intellij.testFramework.DisposableRule
import org.jetbrains.annotations.TestOnly
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.jupiter.api.extension.ExtensionContext
import software.aws.toolkits.jetbrains.core.MockClientManager
import software.aws.toolkits.jetbrains.core.credentials.sso.MockSsoLoginCallbackProvider
import software.aws.toolkits.jetbrains.core.credentials.sso.TestSsoPrompt

class SsoLoginExtension : DisposableRule(), BeforeEachCallback, AfterEachCallback {
    override fun beforeEach(context: ExtensionContext) {
        val ssoSecret = getAnnotation(context) ?: return

        MockClientManager.useRealImplementations(disposable)
        MockSsoLoginCallbackProvider.getInstance().provider = TestSsoPrompt(ssoSecret)
    }

    override fun afterEach(context: ExtensionContext?) {
        MockSsoLoginCallbackProvider.getInstance().provider = null

        // todo: is there a better way to do this
        after()
    }

    private fun getAnnotation(context: ExtensionContext?): String? {
        if (context == null || context == context.root) {
            return null
        }

        return context.element.orElse(null)
            ?.annotations
            ?.filterIsInstance<SsoLogin>()
            ?.firstOrNull()
            ?.secretName
            ?: getAnnotation(context.parent.orElse(null))
    }
}

@TestOnly
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@ExtendWith(SsoLoginExtension::class)
annotation class SsoLogin(val secretName: String)


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

package software.aws.toolkits.jetbrains.utils.rules

import org.junit.rules.ExternalResource
import software.amazon.awssdk.services.cloudformation.CloudFormationClient
import software.amazon.awssdk.services.cloudformation.model.Capability
import software.amazon.awssdk.services.cloudformation.model.ChangeSetType
import software.amazon.awssdk.services.cloudformation.model.CloudFormationException
import software.amazon.awssdk.services.cloudformation.model.Parameter
import software.aws.toolkits.jetbrains.services.cloudformation.executeChangeSetAndWait
import software.aws.toolkits.jetbrains.services.cloudformation.waitForChangeSetCreateComplete
import java.util.UUID

class CloudFormationLazyInitRule(
    private val stackName: String,
    private val templateBody: String,
    private val parameters: List<Parameter>,
    private val cloudformationClient: CloudFormationClient
) : ExternalResource() {
    val outputs: Map<String, String> by lazy {
        cloudformationClient.describeStacks {
            it.stackName(stackName)
        }.stacks()
            .first()
            .outputs()
            .map {
                it.outputKey() to it.outputValue()
            }.toMap()
    }

    override fun before() {
        val type = if (stackExists()) {
            println("Ensuring $stackName is up-to-date...")
            ChangeSetType.UPDATE
        } else {
            println("Creating stack $stackName because it does not exist. This may take a while.")
            ChangeSetType.CREATE
        }

        val changeSetArn = cloudformationClient.createChangeSet {
            it.stackName(stackName)
            it.changeSetName("changeset-${UUID.randomUUID()}")
            it.changeSetType(type)
            it.capabilities(
                Capability.CAPABILITY_AUTO_EXPAND,
                Capability.CAPABILITY_NAMED_IAM
            )
            it.parameters(parameters)
            it.templateBody(templateBody)
        }.id()

        // wait for changeset creation to complete
        try {
            cloudformationClient.waitForChangeSetCreateComplete(stackName, changeSetArn)
        } catch (e: Exception) {
            if (e.message?.contains("The submitted information didn't contain changes") == true) {
                cloudformationClient.deleteChangeSet {
                    it.stackName(stackName)
                    it.changeSetName(changeSetArn)
                }
                return
            } else {
                throw e
            }
        }

        cloudformationClient.executeChangeSetAndWait(stackName, changeSetArn)
    }

    private fun stackExists(): Boolean =
        try {
            cloudformationClient.describeStacks {
                it.stackName(stackName)
            }
            true
        } catch (e: CloudFormationException) {
            false
        }
}


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

package software.aws.toolkits.jetbrains.utils.rules

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.testFramework.ApplicationRule
import com.intellij.testFramework.LightProjectDescriptor
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.testFramework.common.ThreadLeakTracker
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory
import com.intellij.testFramework.fixtures.impl.LightTempDirTestFixtureImpl
import com.intellij.testFramework.runInEdtAndGet
import com.intellij.testFramework.runInEdtAndWait
import com.intellij.testFramework.writeChild
import org.junit.runner.Description
import org.mockito.Mockito
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import java.nio.file.Paths

/**
 * JUnit test Rule that will create a Light [Project] and [CodeInsightTestFixture]. Projects are lazily created and are
 * torn down after each test.
 *
 * If you wish to have just a [Project], you may use Intellij's [com.intellij.testFramework.ProjectRule]
 */
open class CodeInsightTestFixtureRule(protected val testDescription: LightProjectDescriptor = LightProjectDescriptor.EMPTY_PROJECT_DESCRIPTOR) :
    ApplicationRule() {
    private lateinit var description: Description
    protected val lazyFixture = ClearableLazy {
        invokeAndWait {
            createTestFixture()
        }
    }

    protected open fun createTestFixture(): CodeInsightTestFixture {
        val fixtureBuilder = IdeaTestFixtureFactory.getFixtureFactory().createLightFixtureBuilder(testDescription, testName)
        val newFixture = IdeaTestFixtureFactory.getFixtureFactory()
            .createCodeInsightFixture(fixtureBuilder.fixture, LightTempDirTestFixtureImpl(true))
        newFixture.setUp()
        newFixture.testDataPath = testDataPath
        return newFixture
    }

    override fun before(description: Description) {
        super.before(description)
        this.description = description
        // This timer is cancelled but it still continues running when the test is over since it cancels lazily. This is fine, so suppress the leak
        ThreadLeakTracker.longRunningThreadCreated(ApplicationManager.getApplication(), "Debugger Worker launch timer")
    }

    override fun after() {
        super.after()
        // Hack: Runs often enough that we keep our leaks down. https://github.com/mockito/mockito/pull/1619
        // TODO: Investigate Mockk and remove this
        Mockito.framework().clearInlineMocks()

        lazyFixture.ifSet {
            try {
                fixture.tearDown()
            } catch (e: Exception) {
                LOG.warn(e) { "Exception during tear-down" }
            }
            lazyFixture.clear()
        }
    }

    val project: Project
        get() = fixture.project

    val testName: String
        get() = PlatformTestUtil.getTestName(description.methodName, true)

    private val testClass: Class<*>
        get() = description.testClass

    val module: Module
        get() = fixture.module

    open val fixture: CodeInsightTestFixture
        get() = lazyFixture.value

    protected val testDataPath: String
        get() = Paths.get("testdata", testClass.simpleName, testName).toString()

    private companion object {
        val LOG = getLogger<CodeInsightTestFixtureRule>()
    }
}

class ClearableLazy<out T>(private val initializer: () -> T) {
    private var _value: T? = null
    private var isSet = false

    val value: T
        get() {
            synchronized(this) {
                if (!isSet) {
                    _value = initializer()
                    isSet = true
                }
                return _value!!
            }
        }

    fun clear() {
        synchronized(this) {
            isSet = false
            _value = null
        }
    }

    fun ifSet(function: () -> Unit) {
        synchronized(this) {
            if (isSet) function()
        }
    }
}

internal fun <T> invokeAndWait(action: () -> T): T {
    val application = ApplicationManager.getApplication()

    return if (application.isDispatchThread) {
        action()
    } else {
        var ref: T? = null
        application.invokeAndWait({ ref = action() }, ModalityState.NON_MODAL)
        ref!!
    }
}

fun CodeInsightTestFixture.openFile(relativePath: String, fileText: String): VirtualFile {
    val file = this.addFileToProject(relativePath, fileText).virtualFile
    runInEdtAndWait {
        this.openFileInEditor(file)
    }

    return file
}

fun CodeInsightTestFixture.addFileToModule(
    module: Module,
    relativePath: String,
    fileText: String
): PsiFile = runInEdtAndGet {
    val file = try {
        val contentRoot = ModuleRootManager.getInstance(module).contentRoots[0]
        runWriteAction {
            contentRoot.writeChild(FileUtil.toSystemIndependentName(relativePath), fileText)
        }
    } finally {
        PsiManager.getInstance(project).dropPsiCaches()
    }

    runReadAction {
        PsiManager.getInstance(project).findFile(file)!!
    }
}


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

package software.aws.toolkits.jetbrains.utils.rules

import com.intellij.testFramework.ApplicationRule
import org.junit.runner.Description
import software.aws.toolkits.jetbrains.core.experiments.ToolkitExperiment
import software.aws.toolkits.jetbrains.core.experiments.isEnabled
import software.aws.toolkits.jetbrains.core.experiments.setState

class ExperimentRule(private val experiment: ToolkitExperiment, private val desiredEnabledState: Boolean = true) : ApplicationRule() {
    private var originalState: Boolean = false

    override fun before(description: Description) {
        super.before(description)
        originalState = experiment.isEnabled()
        if (originalState != desiredEnabledState) {
            experiment.setState(desiredEnabledState)
        }
    }

    override fun after() {
        if (experiment.isEnabled() != originalState) {
            experiment.setState(originalState)
        }
    }
}


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

package software.aws.toolkits.jetbrains.utils.rules

import com.intellij.ide.highlighter.JavaFileType
import com.intellij.openapi.application.ReadAction
import com.intellij.openapi.application.WriteAction
import com.intellij.openapi.module.JavaModuleType
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiFileFactory
import com.intellij.psi.PsiJavaFile
import com.intellij.testFramework.IdeaTestUtil
import com.intellij.testFramework.PsiTestUtil
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
import com.intellij.testFramework.fixtures.DefaultLightProjectDescriptor
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory
import com.intellij.testFramework.fixtures.JavaCodeInsightTestFixture
import com.intellij.testFramework.fixtures.JavaTestFixtureFactory
import com.intellij.testFramework.fixtures.impl.LightTempDirTestFixtureImpl
import com.intellij.testFramework.runInEdtAndWait
import org.intellij.lang.annotations.Language
import org.jetbrains.jps.model.java.JavaResourceRootType
import org.jetbrains.jps.model.java.JavaSourceRootType
import org.jetbrains.jps.model.module.JpsModuleSourceRootType
import java.io.File
import java.nio.file.Paths

/**
 * JUnit test Rule that will create a Light [Project] and [JavaCodeInsightTestFixture]. Projects are lazily created
 * and are torn down after each test.
 *
 * If you wish to have just a [Project], you may use Intellij's [com.intellij.testFramework.ProjectRule]
 */
class JavaCodeInsightTestFixtureRule(testDescription: DefaultLightProjectDescriptor = DefaultLightProjectDescriptor()) :
    CodeInsightTestFixtureRule(testDescription) {

    override fun createTestFixture(): CodeInsightTestFixture {
        val fixtureBuilder = IdeaTestFixtureFactory.getFixtureFactory().createLightFixtureBuilder(testDescription, testName)
        val newFixture = JavaTestFixtureFactory.getFixtureFactory()
            .createCodeInsightFixture(fixtureBuilder.fixture, LightTempDirTestFixtureImpl(true))
        newFixture.setUp()
        newFixture.testDataPath = testDataPath
        return newFixture
    }

    override val fixture: JavaCodeInsightTestFixture
        get() = lazyFixture.value as JavaCodeInsightTestFixture
}

/**
 * JUnit test Rule that will create a Heavy [Project] and [JavaCodeInsightTestFixture]. Projects are lazily created
 * and are torn down after each test.
 *
 * If you wish to have just a [Project], you may use Intellij's [com.intellij.testFramework.ProjectRule]
 */
class HeavyJavaCodeInsightTestFixtureRule : CodeInsightTestFixtureRule() {
    override fun createTestFixture(): CodeInsightTestFixture {
        val fixtureBuilder = IdeaTestFixtureFactory.getFixtureFactory().createFixtureBuilder(testName)
        val newFixture = JavaTestFixtureFactory.getFixtureFactory().createCodeInsightFixture(fixtureBuilder.fixture)
        newFixture.setUp()
        newFixture.testDataPath = testDataPath
        return newFixture
    }

    override val fixture: JavaCodeInsightTestFixture
        get() = lazyFixture.value as JavaCodeInsightTestFixture
}

/**
 * Add a JDK1.8 module named [moduleName] to the test fixture project with 'src', 'src-resources' and 'tst roots setup.
 * @return the created [Module]
 */
fun JavaCodeInsightTestFixture.addModule(moduleName: String): Module {
    val root = this.tempDirFixture.findOrCreateDir(moduleName)
    val module = PsiTestUtil.addModule(project, JavaModuleType.getModuleType(), moduleName, root)
    PsiTestUtil.removeAllRoots(module, IdeaTestUtil.getMockJdk18())
    runInEdtAndWait {
        WriteAction.run<Exception> {
            PsiTestUtil.addContentRoot(module, root)
            PsiTestUtil.addSourceRoot(module, createChildDirectories(root, "src/main/java"), false)
            PsiTestUtil.addSourceRoot(
                module,
                createChildDirectories(root, "src/main/resources"),
                JavaResourceRootType.RESOURCE
            )
            PsiTestUtil.addSourceRoot(module, createChildDirectories(root, "tst/main/java"), true)
        }
    }
    return module
}

private fun createChildDirectories(root: VirtualFile, path: String): VirtualFile {
    var parent = root
    path.split("/").forEach {
        val childDirectory = parent.findChild(it) ?: parent.createChildDirectory(null, it)
        parent = childDirectory
    }

    return parent
}

fun JavaCodeInsightTestFixture.openClass(@Language("JAVA") javaClass: String): PsiClass {
    val psiClass = this.addClass(javaClass)
    runInEdtAndWait {
        this.openFileInEditor(psiClass.containingFile.virtualFile)
    }
    return psiClass
}

/**
 * Add a Java class to the given [module] in the [JavaSourceRootType.SOURCE] root at the in the appropriate directory
 * determined by the definition in the [classText] content.
 *
 * @see JavaCodeInsightTestFixture.addClass
 */
fun JavaCodeInsightTestFixture.addClass(module: Module, @Language("JAVA") classText: String): PsiClass {
    val qName = determineQualifiedName(module.project, classText)
    val fileName = qName.replace('.', File.separatorChar) + ".java"
    val psiFile = addFile(module, JavaSourceRootType.SOURCE, fileName, classText) as PsiJavaFile
    return ReadAction.compute<PsiClass, RuntimeException> { psiFile.classes[0] }
}

/**
 * Add a Java test class to the given [module] in the [JavaSourceRootType.TEST_SOURCE] root at the in the appropriate directory
 * determined by the definition in the [classText] content.
 *
 * @see JavaCodeInsightTestFixture.addClass
 */
fun JavaCodeInsightTestFixture.addTestClass(module: Module, @Language("JAVA") classText: String): PsiClass {
    val qName = determineQualifiedName(module.project, classText)
    val fileName = qName.replace('.', File.separatorChar) + ".java"
    val psiFile = addFile(module, JavaSourceRootType.TEST_SOURCE, fileName, classText) as PsiJavaFile
    return ReadAction.compute<PsiClass, RuntimeException> { psiFile.classes[0] }
}

/**
 * Add a resource file to the given [module] in the [JavaResourceRootType.RESOURCE] root at the path specified in [fileName]
 * with [content].
 */
fun JavaCodeInsightTestFixture.addResourceFile(module: Module, fileName: String, content: String): PsiFile =
    addFile(module, JavaResourceRootType.RESOURCE, fileName, content)

private fun JavaCodeInsightTestFixture.addFile(
    module: Module,
    type: JpsModuleSourceRootType<*>,
    fileName: String,
    content: String
): PsiFile {
    val sourceRoot = ModuleRootManager.getInstance(module).getSourceRoots(type).first()
    val fullPath = Paths.get(sourceRoot.path, fileName).toString()
    val projectRelativePath = FileUtil.getRelativePath(tempDirPath, fullPath, File.separatorChar)
        ?: throw RuntimeException("Cannot determine relative path")
    return addFileToProject(projectRelativePath.replace('\\', '/'), content)
}

private fun determineQualifiedName(project: Project, classText: String): String =
    ReadAction.compute<String, RuntimeException> {
        val factory = PsiFileFactory.getInstance(project)
        val javaFile = factory.createFileFromText("a.java", JavaFileType.INSTANCE, classText) as PsiJavaFile
        javaFile.classes[0].qualifiedName
    } ?: throw RuntimeException("Cannot determine fully qualified name")


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

package software.aws.toolkits.jetbrains.utils.rules

import com.intellij.notification.Notification
import com.intellij.notification.Notifications
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.ComponentManager
import com.intellij.testFramework.ProjectRule
import org.junit.rules.ExternalResource
import java.util.concurrent.CopyOnWriteArrayList

class NotificationListenerRule : ExternalResource {
    private val messageBusSupplier: () -> ComponentManager
    private val disposable: Disposable

    constructor(disposable: Disposable) : super() {
        this.messageBusSupplier = { ApplicationManager.getApplication() }
        this.disposable = disposable
        this.notifications = CopyOnWriteArrayList<Notification>()
    }

    constructor(projectRule: ProjectRule, disposable: Disposable) : super() {
        this.messageBusSupplier = { projectRule.project }
        this.disposable = disposable
        this.notifications = CopyOnWriteArrayList<Notification>()
    }

    constructor(projectRule: CodeInsightTestFixtureRule, disposable: Disposable) : super() {
        this.messageBusSupplier = { projectRule.project }
        this.disposable = disposable
        this.notifications = CopyOnWriteArrayList<Notification>()
    }

    val notifications: CopyOnWriteArrayList<Notification>

    override fun before() {
        messageBusSupplier().messageBus.connect(disposable).subscribe(
            Notifications.TOPIC,
            object : Notifications {
                override fun notify(notification: Notification) {
                    notifications.add(notification)
                }
            }
        )
        notifications.clear()
    }
}


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

package software.aws.toolkits.jetbrains.utils.rules

import com.intellij.ide.util.projectWizard.EmptyModuleBuilder
import com.intellij.openapi.application.runWriteActionAndWait
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleType
import com.intellij.openapi.module.ModuleTypeManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.projectRoots.impl.ProjectJdkImpl
import com.intellij.openapi.roots.ModuleRootModificationUtil
import com.intellij.openapi.util.SystemInfo
import com.intellij.testFramework.PsiTestUtil
import com.intellij.testFramework.builders.ModuleFixtureBuilder
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
import com.intellij.testFramework.fixtures.IdeaProjectTestFixture
import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory
import com.intellij.testFramework.fixtures.ModuleFixture
import com.intellij.testFramework.fixtures.TestFixtureBuilder
import com.intellij.testFramework.fixtures.impl.ModuleFixtureBuilderImpl
import com.intellij.testFramework.fixtures.impl.ModuleFixtureImpl
import com.intellij.testFramework.runInEdtAndWait
import com.intellij.xdebugger.XDebuggerUtil
import com.jetbrains.python.PythonModuleTypeBase
import com.jetbrains.python.psi.PyFile
import com.jetbrains.python.sdk.PythonSdkAdditionalData
import com.jetbrains.python.sdk.PythonSdkType
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.attribute.PosixFilePermission

/**
 * JUnit test Rule that will create a Light [Project] and [CodeInsightTestFixture] with Python support. Projects are
 * lazily created and are torn down after each test.
 *
 * If you wish to have just a [Project], you may use Intellij's [com.intellij.testFramework.ProjectRule]
 */
class PythonCodeInsightTestFixtureRule : CodeInsightTestFixtureRule() {
    override fun createTestFixture(): CodeInsightTestFixture {
        val fixtureFactory = IdeaTestFixtureFactory.getFixtureFactory()
        fixtureFactory.registerFixtureBuilder(
            PythonModuleFixtureBuilder::class.java,
            PythonModuleFixtureBuilder::class.java
        )
        val fixtureBuilder = fixtureFactory.createFixtureBuilder(testName)
        fixtureBuilder.addModule(PythonModuleFixtureBuilder::class.java)
        val newFixture = fixtureFactory.createCodeInsightFixture(fixtureBuilder.fixture)
        newFixture.testDataPath = testDataPath
        newFixture.setUp()

        val module = newFixture.module

        val projectRoot = newFixture.tempDirFixture.getFile(".")!!

        if (SystemInfo.isUnix) {
            val path = Paths.get(projectRoot.path)

            // TODO: Investigate this more. On 2020.1 this folder has strict permissions
            // on code build (due to root?) that prevents it from mounting into docker for sam
            Files.setPosixFilePermissions(
                path,
                setOf(
                    PosixFilePermission.OWNER_EXECUTE,
                    PosixFilePermission.OWNER_WRITE,
                    PosixFilePermission.OWNER_READ,
                    PosixFilePermission.GROUP_READ,
                    PosixFilePermission.GROUP_WRITE,
                    PosixFilePermission.GROUP_EXECUTE,
                    PosixFilePermission.OTHERS_READ,
                    PosixFilePermission.OTHERS_WRITE,
                    PosixFilePermission.OTHERS_EXECUTE
                )
            )
        }

        PsiTestUtil.addContentRoot(module, projectRoot)
        val sdk = PyTestSdk("3.6.0")
        setModuleSdk(module, sdk)

        return newFixture
    }

    override val fixture: CodeInsightTestFixture
        get() = lazyFixture.value

    fun setModuleSdk(module: Module, sdk: Sdk) {
        runWriteActionAndWait {
            ProjectJdkTable.getInstance().addJdk(sdk, module)
            ModuleRootModificationUtil.setModuleSdk(module, sdk)
        }
    }
}

internal class PythonModuleFixtureBuilder(fixtureBuilder: TestFixtureBuilder<out IdeaProjectTestFixture>) :
    ModuleFixtureBuilderImpl<ModuleFixture>(PlatformPythonModuleType(), fixtureBuilder),
    ModuleFixtureBuilder<ModuleFixture> {

    override fun instantiateFixture(): ModuleFixture = ModuleFixtureImpl(this)
}

internal class PlatformPythonModuleType : PythonModuleTypeBase<EmptyModuleBuilder>() {
    override fun createModuleBuilder(): EmptyModuleBuilder = object : EmptyModuleBuilder() {
        override fun getModuleType(): ModuleType<EmptyModuleBuilder> = instance
    }

    companion object {
        val instance: PlatformPythonModuleType
            get() = ModuleTypeManager.getInstance().findByID("PYTHON_MODULE") as PlatformPythonModuleType
    }
}

class PyTestSdk(private val version: String) : ProjectJdkImpl("PySdk $version", PythonSdkType.getInstance()) {
    init {
        sdkAdditionalData = PythonSdkAdditionalData(FakeCPython())
    }

    override fun getVersionString(): String = "FakeCPython $version"
}

fun PythonCodeInsightTestFixtureRule.addBreakpoint() {
    runInEdtAndWait {
        val document = fixture.editor.document
        val lambdaClass = fixture.file as PyFile
        val lambdaBody = lambdaClass.topLevelFunctions[0].statementList.statements[0]
        val lineNumber = document.getLineNumber(lambdaBody.textOffset)

        XDebuggerUtil.getInstance().toggleLineBreakpoint(
            project,
            fixture.file.virtualFile,
            lineNumber
        )
    }
}


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

package software.aws.toolkits.jetbrains.utils.rules

import com.intellij.openapi.util.registry.Registry
import com.intellij.testFramework.ApplicationRule
import org.junit.runner.Description

/**
 * Allows a test run to have an experiment enabled, and then restore previous state
 */
class RegistryRule(private val featureId: String, private val desiredEnabledState: Boolean = true) : ApplicationRule() {

    private var originalState: Boolean = false

    override fun before(description: Description) {
        super.before(description)
        originalState = Registry.`is`(featureId)
        if (originalState != desiredEnabledState) {
            Registry.get(featureId).setValue(desiredEnabledState)
        }
    }

    fun setState(state: Boolean) {
        if (Registry.get(featureId).asBoolean() != state) {
            Registry.get(featureId).setValue(state)
        }
    }

    override fun after() {
        if (Registry.`is`(featureId) != originalState) {
            Registry.get(featureId).setValue(originalState)
        }
    }
}


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

package software.aws.toolkits.jetbrains.utils.rules

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.testFramework.ExtensionTestUtil
import com.intellij.testFramework.ProjectRule
import com.intellij.testFramework.replaceService
import org.assertj.core.api.Assertions.assertThat
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.AwsResourceCache
import software.aws.toolkits.jetbrains.core.DefaultAwsResourceCache
import software.aws.toolkits.jetbrains.core.MockClientManager
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.CredentialManager
import software.aws.toolkits.jetbrains.core.credentials.DefaultAwsConnectionManager
import software.aws.toolkits.jetbrains.core.credentials.DefaultCredentialManager
import software.aws.toolkits.jetbrains.core.credentials.profiles.DEFAULT_PROFILE_ID
import software.aws.toolkits.jetbrains.core.credentials.profiles.ProfileCredentialProviderFactory
import software.aws.toolkits.jetbrains.core.credentials.waitUntilConnectionStateIsStable
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider

class RunWithRealCredentials : TestRule {
    /**
     * Marks a unit test, or unit test class, as requiring AWS credentials to run. Will always use the default AWS profile, and by default us-west-2
     */
    annotation class RequiresRealCredentials

    private val projectSupplier: () -> Project

    constructor(projectRule: ProjectRule) : super() {
        this.projectSupplier = { projectRule.project }
    }

    constructor(projectRule: CodeInsightTestFixtureRule) : super() {
        this.projectSupplier = { projectRule.project }
    }

    override fun apply(base: Statement, description: Description): Statement = if (description.getAnnotation(RequiresRealCredentials::class.java) == null &&
        description.testClass.getAnnotation(RequiresRealCredentials::class.java) == null
    ) {
        base
    } else {
        object : Statement() {
            override fun evaluate() {
                val disposable = Disposer.newDisposable()
                try {
                    val project = projectSupplier.invoke()

                    MockClientManager.useRealImplementations(disposable)

                    // ProfileCredentialProviderFactory is designed to be a singleton, so replace it with a new instance
                    ExtensionTestUtil.maskExtensions(DefaultCredentialManager.EP_NAME, listOf(ProfileCredentialProviderFactory()), disposable)

                    ApplicationManager.getApplication().replaceService(CredentialManager::class.java, DefaultCredentialManager(), disposable)
                    // suppress beacuse otherwise we have to declare experimental coroutines on every single test where we use the rule
                    @Suppress("EXPERIMENTAL_API_USAGE")
                    ApplicationManager.getApplication().replaceService(AwsResourceCache::class.java, DefaultAwsResourceCache(), disposable)
                    project.replaceService(AwsConnectionManager::class.java, DefaultAwsConnectionManager(project), disposable)

                    val credentialIdentifier = CredentialManager.getInstance().getCredentialIdentifierById(DEFAULT_PROFILE_ID)
                        ?: throw IllegalStateException("RunWithRealCredentials requires a default AWS profile!")
                    val regionId = System.getenv().getOrDefault("AWS_DEFAULT_REGION", "us-west-2")
                    val region = AwsRegionProvider.getInstance()[regionId] ?: throw IllegalStateException("Can't locate us-west-2")

                    getLogger<RunWithRealCredentials>().warn { "WARNING! Running test with real AWS credentials!" }

                    val connectionManager = AwsConnectionManager.getInstance(project)
                    connectionManager.changeRegion(region)
                    connectionManager.changeCredentialProvider(credentialIdentifier)
                    connectionManager.waitUntilConnectionStateIsStable()

                    assertThat(connectionManager.isValidConnectionSettings()).isTrue

                    base.evaluate()
                } finally {
                    Disposer.dispose(disposable)
                }
            }
        }
    }
}