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

package software.aws.toolkits.gradle.intellij

import org.gradle.api.Project
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory

enum class IdeFlavor { GW, IC, IU, RD }

object IdeVersions {
    private val commonPlugins = arrayOf(
        "git4idea",
        "org.jetbrains.plugins.terminal",
        "org.jetbrains.plugins.yaml"
    )

    // FIX_WHEN_MIN_IS_223
    private val commonPlugins223 = commonPlugins.map {
        when (it) {
            "git4idea" -> "vcs-git"
            else -> it
        }
    }.toTypedArray()

    private val ideProfiles = listOf(
        Profile(
            name = "2022.2",
            community = ProductProfile(
                sdkFlavor = IdeFlavor.IC,
                sdkVersion = "2022.2",
                plugins = commonPlugins + listOf(
                    "java",
                    "com.intellij.gradle",
                    "org.jetbrains.idea.maven",
                    "PythonCore:222.3345.118",
                    "Docker:222.3345.118"
                )
            ),
            ultimate = ProductProfile(
                sdkFlavor = IdeFlavor.IU,
                sdkVersion = "2022.2",
                plugins = commonPlugins + listOf(
                    "JavaScript",
                    // Transitive dependency needed for javascript
                    // Can remove when https://github.com/JetBrains/gradle-intellij-plugin/issues/608 is fixed
                    "com.intellij.css",
                    "JavaScriptDebugger",
                    "com.intellij.database",
                    "com.jetbrains.codeWithMe",
                    "Pythonid:222.3345.118",
                    "org.jetbrains.plugins.go:222.3345.118",
                    // https://github.com/JetBrains/gradle-intellij-plugin/issues/1056
                    "org.intellij.intelliLang"
                )
            ),
            rider = RiderProfile(
                sdkVersion = "2022.2",
                plugins = commonPlugins + listOf(
                    "rider-plugins-appender" // Workaround for https://youtrack.jetbrains.com/issue/IDEA-179607
                ),
                netFrameworkTarget = "net472",
                rdGenVersion = "2022.2.4",
                nugetVersion = "2022.2.0"
            )
        ),
        Profile(
            name = "2022.3",
            community = ProductProfile(
                sdkFlavor = IdeFlavor.IC,
                // test failure related to null notification contexts in 2022.3
                sdkVersion = "2022.3.1",
                plugins = commonPlugins223 + listOf(
                    "java",
                    "com.intellij.gradle",
                    "org.jetbrains.idea.maven",
                    "PythonCore:223.8214.16",
                    "Docker:223.8214.64"
                )
            ),
            ultimate = ProductProfile(
                sdkFlavor = IdeFlavor.IU,
                sdkVersion = "2022.3.1",
                plugins = commonPlugins223 + listOf(
                    "JavaScript",
                    // Transitive dependency needed for javascript
                    // Can remove when https://github.com/JetBrains/gradle-intellij-plugin/issues/608 is fixed
                    "com.intellij.css",
                    "JavaScriptDebugger",
                    "com.intellij.database",
                    "com.jetbrains.codeWithMe",
                    "Pythonid:223.8214.52",
                    "org.jetbrains.plugins.go:223.8214.52",
                    // https://github.com/JetBrains/gradle-intellij-plugin/issues/1056
                    "org.intellij.intelliLang"
                )
            ),
            rider = RiderProfile(
                sdkVersion = "2022.3.1",
                plugins = commonPlugins223 + listOf(
                    "rider-plugins-appender" // Workaround for https://youtrack.jetbrains.com/issue/IDEA-179607
                ),
                netFrameworkTarget = "net472",
                rdGenVersion = "2022.3.4",
                nugetVersion = "2022.3.1"
            )
        ),
        Profile(
            name = "2023.1",
            gateway = ProductProfile(
                sdkFlavor = IdeFlavor.GW,
                sdkVersion = "231.8109-EAP-CANDIDATE-SNAPSHOT",
                plugins = arrayOf("org.jetbrains.plugins.terminal")
            ),
            community = ProductProfile(
                sdkFlavor = IdeFlavor.IC,
                sdkVersion = "2023.1",
                plugins = commonPlugins223 + listOf(
                    "java",
                    "com.intellij.gradle",
                    "org.jetbrains.idea.maven",
                    "PythonCore:231.8109.144",
                    "Docker:231.8109.217"
                )
            ),
            ultimate = ProductProfile(
                sdkFlavor = IdeFlavor.IU,
                sdkVersion = "2023.1",
                plugins = commonPlugins223 + listOf(
                    "JavaScript",
                    // Transitive dependency needed for javascript
                    // Can remove when https://github.com/JetBrains/gradle-intellij-plugin/issues/608 is fixed
                    "com.intellij.css",
                    "JavaScriptDebugger",
                    "com.intellij.database",
                    "com.jetbrains.codeWithMe",
                    "Pythonid:231.8109.175",
                    "org.jetbrains.plugins.go:231.8109.175",
                    // https://github.com/JetBrains/gradle-intellij-plugin/issues/1056
                    "org.intellij.intelliLang"
                )
            ),
            rider = RiderProfile(
                sdkVersion = "2023.1",
                plugins = commonPlugins223 + listOf(
                    "rider-plugins-appender" // Workaround for https://youtrack.jetbrains.com/issue/IDEA-179607
                ),
                netFrameworkTarget = "net472",
                rdGenVersion = "2023.1.2",
                nugetVersion = "2023.1.0"
            )
        ),
        Profile(
            name = "2023.2",
            gateway = ProductProfile(
                sdkFlavor = IdeFlavor.GW,
                sdkVersion = "232.6734-EAP-CANDIDATE-SNAPSHOT",
                plugins = arrayOf("org.jetbrains.plugins.terminal")
            ),
            community = ProductProfile(
                sdkFlavor = IdeFlavor.IC,
                sdkVersion = "232.6734-EAP-CANDIDATE-SNAPSHOT",
                plugins = commonPlugins223 + listOf(
                    "java",
                    "com.intellij.gradle",
                    "org.jetbrains.idea.maven",
                    "PythonCore:232.6734.9",
                    "Docker:232.6734.4"
                )
            ),
            ultimate = ProductProfile(
                sdkFlavor = IdeFlavor.IU,
                sdkVersion = "232.6734-EAP-CANDIDATE-SNAPSHOT",
                plugins = commonPlugins223 + listOf(
                    "JavaScript",
                    // Transitive dependency needed for javascript
                    // Can remove when https://github.com/JetBrains/gradle-intellij-plugin/issues/608 is fixed
                    "com.intellij.css",
                    "JavaScriptDebugger",
                    "com.intellij.database",
                    "com.jetbrains.codeWithMe",
                    "Pythonid:232.6734.9",
                    "org.jetbrains.plugins.go:232.6734.9",
                    // https://github.com/JetBrains/gradle-intellij-plugin/issues/1056
                    "org.intellij.intelliLang"
                )
            ),
            rider = RiderProfile(
                sdkVersion = "2023.2-EAP2-SNAPSHOT",
                plugins = commonPlugins223 + listOf(
                    "rider-plugins-appender" // Workaround for https://youtrack.jetbrains.com/issue/IDEA-179607
                ),
                netFrameworkTarget = "net472",
                rdGenVersion = "2023.2.1",
                nugetVersion = "2023.2.0-eap02"
            )
        ),

    ).associateBy { it.name }

    fun ideProfile(project: Project): Profile = ideProfile(project.providers).get()

    fun ideProfile(providers: ProviderFactory): Provider<Profile> = resolveIdeProfileName(providers).map {
        ideProfiles[it] ?: throw IllegalStateException("Can't find profile for $it")
    }

    private fun resolveIdeProfileName(providers: ProviderFactory): Provider<String> = providers.gradleProperty("ideProfileName")
}

open class ProductProfile(
    val sdkFlavor: IdeFlavor,
    val sdkVersion: String,
    val plugins: Array<String> = emptyArray()
) {
    fun version(): String? = if (!isLocalPath(sdkVersion)) {
        sdkFlavor.name + "-" + sdkVersion
    } else {
        null
    }

    fun localPath(): String? = sdkVersion.takeIf {
        isLocalPath(it)
    }

    private fun isLocalPath(str: String) = str.startsWith("/") || str.getOrNull(1) == ':'
}

class RiderProfile(
    sdkVersion: String,
    plugins: Array<String>,
    val netFrameworkTarget: String,
    val rdGenVersion: String, // https://www.myget.org/feed/rd-snapshots/package/maven/com.jetbrains.rd/rd-gen
    val nugetVersion: String // https://www.nuget.org/packages/JetBrains.Rider.SDK/
) : ProductProfile(IdeFlavor.RD, sdkVersion, plugins)

class Profile(
    val name: String,
    val shortName: String = shortenedIdeProfileName(name),
    val sinceVersion: String = shortName,
    val untilVersion: String = "$sinceVersion.*",
    val gateway: ProductProfile? = null,
    val community: ProductProfile,
    val ultimate: ProductProfile,
    val rider: RiderProfile,
)

private fun shortenedIdeProfileName(sdkName: String): String {
    val parts = sdkName.trim().split(".")
    return parts[0].substring(2) + parts[1]
}


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

package software.aws.toolkits.gradle.intellij

import org.gradle.api.provider.Property
import org.gradle.api.provider.Provider
import org.gradle.api.provider.ProviderFactory

abstract class ToolkitIntelliJExtension(private val providers: ProviderFactory) {
    abstract val ideFlavor: Property<IdeFlavor>

    fun ideProfile() = IdeVersions.ideProfile(providers)

    fun version(): Provider<String?> = productProfile().flatMap { profile ->
        providers.provider { profile.version() }
    }

    fun localPath(): Provider<String?> = productProfile().flatMap { profile ->
        providers.provider { profile.localPath() }
    }

    fun productProfile(): Provider<out ProductProfile> = ideFlavor.flatMap { flavor ->
        when (flavor) {
            IdeFlavor.IC -> ideProfile().map { it.community }
            IdeFlavor.IU -> ideProfile().map { it.ultimate }
            IdeFlavor.RD -> ideProfile().map { it.rider }
            IdeFlavor.GW -> ideProfile().map { it.gateway!! }
        }
    }
}


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

package software.aws.toolkits.gradle.jacoco

import org.gradle.BuildAdapter
import org.gradle.BuildResult
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.services.BuildService
import org.gradle.api.services.BuildServiceParameters
import org.gradle.api.tasks.testing.Test
import org.gradle.testing.jacoco.plugins.JacocoTaskExtension
import org.jacoco.core.data.ExecutionData
import org.jacoco.core.data.ExecutionDataWriter
import org.jacoco.core.data.IExecutionDataVisitor
import org.jacoco.core.data.ISessionInfoVisitor
import org.jacoco.core.data.SessionInfo
import org.jacoco.core.runtime.RemoteControlReader
import org.jacoco.core.runtime.RemoteControlWriter
import java.io.FileOutputStream
import java.net.ServerSocket
import java.net.Socket
import java.util.concurrent.atomic.AtomicBoolean

class RemoteCoverage private constructor(task: Test) {
    companion object {
        fun enableRemoteCoverage(task: Test) = RemoteCoverage(task)

        private const val DEFAULT_JACOCO_PORT = 6300
    }

    init {
        task.extensions.findByType(JacocoTaskExtension::class.java)?.let {
            val execFile = it.destinationFile ?: return@let

            // Use a shared service since it does not block task execution
            val jacocoServer = task.project.gradle.sharedServices.registerIfAbsent("jacocoServer", JacocoServer::class.java) {
                if (!execFile.exists()) {
                    task.project.mkdir(execFile.parentFile)
                    execFile.createNewFile()
                }

                parameters.execFile.set(execFile)
            }

            task.doFirst {
                jacocoServer.get().start()
                task.project.gradle.addBuildListener(object : BuildAdapter() {
                    override fun buildFinished(result: BuildResult) {
                        task.project.gradle.removeListener(this)
                        runCatching {
                            jacocoServer.get().close()
                        }
                    }
                })
            }
        } ?: task.logger.warn("$task does not have Jacoco enabled on it")
    }

    abstract class JacocoServer : BuildService<JacocoServer.Params>, AutoCloseable {
        interface Params : BuildServiceParameters {
            val execFile: RegularFileProperty
        }

        private val serverSocket = ServerSocket(DEFAULT_JACOCO_PORT)
        private val isRunning = AtomicBoolean(false)

        private val serverRunnable = Runnable {
            parameters.execFile.asFile.get().outputStream().use {
                while (isRunning.get()) {
                    val clientSocket = serverSocket.accept()
                    JacocoHandler(clientSocket, it).run()
                }
            }
        }
        private lateinit var serverThread: Thread

        fun start() {
            if (!isRunning.getAndSet(true)) {
                serverThread = Thread(serverRunnable)
                serverThread.start()
            } else {
                throw IllegalStateException("Jacoco server is already running!")
            }
        }

        override fun close() {
            if (isRunning.getAndSet(false)) {
                serverThread.interrupt()
            }
        }
    }

    private class JacocoHandler(private val socket: Socket, private val outputFile: FileOutputStream) : ISessionInfoVisitor, IExecutionDataVisitor {
        private val fileWriter = ExecutionDataWriter(outputFile)

        fun run() {
            socket.use {
                socket.getInputStream().use { input ->
                    socket.getOutputStream().use { output ->
                        val reader = RemoteControlReader(input)
                        reader.setSessionInfoVisitor(this)
                        reader.setExecutionDataVisitor(this)

                        RemoteControlWriter(output)

                        @Suppress("ControlFlowWithEmptyBody")
                        // Read all the data from jacoco
                        while (reader.read()) {
                        }
                        synchronized(fileWriter) { fileWriter.flush() }
                    }
                }
            }
        }

        override fun visitSessionInfo(info: SessionInfo) {
            synchronized(fileWriter) { fileWriter.visitSessionInfo(info) }
        }

        override fun visitClassExecution(data: ExecutionData) {
            synchronized(fileWriter) { fileWriter.visitClassExecution(data) }
        }
    }
}


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

package software.aws.toolkits.gradle.resources

import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.language.base.plugins.LifecycleBasePlugin.VERIFICATION_GROUP
import java.time.Instant

open class ValidateMessages : DefaultTask() {
    private companion object {
        const val COPYRIGHT_HEADER_LINES = 2
    }
    @InputFiles
    val paths: ConfigurableFileCollection = project.objects.fileCollection()

    @OutputFile
    val output: RegularFileProperty = project.objects.fileProperty().convention {
        project.buildDir.resolve("validateMessages")
    }

    init {
        group = VERIFICATION_GROUP
    }

    @TaskAction
    fun validateMessage() {
        var hasError = false
        paths
            .map { it.absolutePath to it.readLines() }
            .forEach { (filePath, fileLines) ->
                fileLines
                    // filter out blank lines and comments
                    .filter { it.isNotBlank() && it.trim().firstOrNull() != '#' }
                    .mapIndexed { lineNumber, it ->
                        if (it.contains("=")) {
                            it
                        } else {
                            logger.error(""""$filePath:${lineNumber + COPYRIGHT_HEADER_LINES} contains invalid message missing a '=': "$it"""")
                            hasError = true
                            null
                        }
                    }
                    .filterNotNull()
                    .map { it.split("=").first() }
                    .reduceIndexed { lineNumber, item1, item2 ->
                        if (item1 > item2) {
                            logger.error("""$filePath:${lineNumber + COPYRIGHT_HEADER_LINES} is not sorted:"$item1" > "$item2"""")
                            hasError = true
                        }

                        item2
                    }
                if (hasError) {
                    throw GradleException("$filePath has one or more out of order items!")
                }
            }

        // Write the current time to the file so it will be cacheable (gradle can only use files to determine up to date checks)
        output.asFile.get().writeText(Instant.now().toString())
    }
}


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

package software.aws.toolkits.gradle.sdk

import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import software.amazon.awssdk.codegen.C2jModels
import software.amazon.awssdk.codegen.CodeGenerator
import software.amazon.awssdk.codegen.model.config.customization.CustomizationConfig
import software.amazon.awssdk.codegen.model.service.ServiceModel
import software.amazon.awssdk.codegen.utils.ModelLoaderUtils
import java.io.File

open class GenerateSdk : DefaultTask() {
    @InputDirectory
    val c2jFolder: DirectoryProperty = project.objects.directoryProperty().convention(project.extensions.getByType(GenerateSdkExtension::class.java).c2jFolder)

    @OutputDirectory
    val srcDir: DirectoryProperty = project.objects.directoryProperty().convention(project.extensions.getByType(GenerateSdkExtension::class.java).srcDir())

    @Internal
    val testDir: DirectoryProperty = project.objects.directoryProperty().convention(project.extensions.getByType(GenerateSdkExtension::class.java).testDir())

    @TaskAction
    fun generate() {
        val srcDir = srcDir.asFile.get()
        val testDir = testDir.asFile.get()
        srcDir.deleteRecursively()
        testDir.deleteRecursively()

        c2jFolder.get().asFileTree.visit {
            if (isDirectory) {
                with(file) {
                    logger.info("Generating SDK from $this")
                    val models = C2jModels.builder()
                        .serviceModel(loadServiceModel())
                        .paginatorsModel(loadOptionalModel("paginators-1.json"))
                        .customizationConfig(loadOptionalModel("customization.config") ?: CustomizationConfig.create())
                        .waitersModel(loadOptionalModel("waiters-2.json"))
                        .build()

                    CodeGenerator.builder()
                        .models(models)
                        .sourcesDirectory(srcDir.absolutePath)
                        .testsDirectory(testDir.absolutePath)
                        .build()
                        .execute()
                }
            }
        }
    }

    private fun File.loadServiceModel(): ServiceModel? = ModelLoaderUtils.loadModel(ServiceModel::class.java, resolve("service-2.json"))

    private inline fun <reified T> File.loadOptionalModel(fileName: String): T? = resolve(fileName).takeIf { it.exists() }?.let {
        ModelLoaderUtils.loadModel(T::class.java, it)
    }
}


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

package software.aws.toolkits.gradle.sdk

import org.gradle.api.file.Directory
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.Provider

open class GenerateSdkExtension(objects: ObjectFactory) {
    val c2jFolder: DirectoryProperty = objects.directoryProperty()

    val outputDir: DirectoryProperty = objects.directoryProperty()

    fun srcDir(): Provider<Directory> = outputDir.dir("src")
    fun testDir(): Provider<Directory> = outputDir.dir("tst")
}


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

package software.aws.toolkits.gradle

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized

@RunWith(Parameterized::class)
class SourceUtilsTest(private val folderName: String, private val expected: Boolean) {

    companion object {
        @JvmStatic
        @Parameterized.Parameters(name = "{0} -> {1}")
        fun data(): Collection<Array<Any>> = listOf(
            arrayOf("tst", true),
            arrayOf("tst-201", true),
            arrayOf("tst-190+", true),
            arrayOf("tst-201+", true),
            arrayOf("tst-201-202", true),
            arrayOf("tst-193-201", true),
            arrayOf("tst-193-202", true),

            arrayOf("tst-resources", false),
            arrayOf("tst-resources-201", false),
            arrayOf("tst-192", false),
            arrayOf("tst-202", false),
            arrayOf("tst-202+", false),
            arrayOf("src-201", false),
            arrayOf("random", false),
            arrayOf("src-tst", false),
            arrayOf("tst-192-193", false)
        )
    }

    @Test
    fun `correctly includes folder`() {
        assertThat(includeFolder("tst", "201", folderName)).isEqualTo(expected)
    }
}


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

package software.aws.toolkits.gradle.changelog

import org.assertj.core.api.Assertions.assertThat
import org.intellij.lang.annotations.Language
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.mockito.ArgumentMatchers.anyString
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.inOrder
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import software.aws.toolkits.gradle.changelog.ChangeLogGenerator.Companion.renderEntry
import software.aws.toolkits.gradle.changelog.ChangeType.BUGFIX
import software.aws.toolkits.gradle.changelog.ChangeType.FEATURE
import java.nio.file.Path
import java.time.LocalDate

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

    @Test
    fun entriesAreReverseOrderedByDate() {
        val first = createFile(
            """
                {
                  "date": "2017-01-01",
                  "version": "2.0.0-preview-1",
                  "entries": [
                    {
                      "type": "feature",
                      "description": "Some feature"
                    }
                  ]
                }
            """
        )

        val second = createFile(
            """
                {
                  "date": "2017-01-03",
                  "version": "2.0.0-preview-2",
                  "entries": [
                    {
                      "type": "bugfix",
                      "description": "Some bugfix"
                    },
                    {
                      "type": "feature",
                      "description": "Another feature"
                    }
                  ]
                }
            """
        )

        val third = createFile(
            """
                {
                  "date": "2017-02-01",
                  "version": "2.0.0-preview-3",
                  "entries": [
                    {
                      "type": "feature",
                      "description": "Third feature"
                    }
                  ]
                }
            """
        )

        val writer = mock<ChangeLogWriter>()
        val sut = ChangeLogGenerator(listOf(writer), mock())
        sut.addReleasedChanges(listOf(first, third, second))
        sut.close()

        writer.inOrder {
            verify().writeLine(
                renderEntry(
                    ReleaseEntry(
                        LocalDate.of(2017, 2, 1),
                        "2.0.0-preview-3",
                        listOf(Entry(FEATURE, "Third feature"))
                    )
                )
            )
            verify().writeLine(
                renderEntry(
                    ReleaseEntry(
                        LocalDate.of(2017, 1, 3),
                        "2.0.0-preview-2",
                        listOf(
                            Entry(BUGFIX, "Some bugfix"),
                            Entry(FEATURE, "Another feature")
                        )
                    )
                )
            )
            verify().writeLine(
                renderEntry(
                    ReleaseEntry(
                        LocalDate.of(2017, 1, 1),
                        "2.0.0-preview-1",
                        listOf(Entry(FEATURE, "Some feature"))
                    )
                )
            )
            verify().close()
        }
    }

    @Test(expected = RuntimeException::class)
    fun versionNumbersMustBeUnique() {
        val first = createFile(
            """
                {
                  "date": "2017-01-01",
                  "version": "2.0.0-preview-1",
                  "entries": [
                    {
                      "type": "feature",
                      "description": "Some feature"
                    }
                  ]
                }
            """
        )

        val second = createFile(
            """
                {
                  "date": "2017-01-03",
                  "version": "2.0.0-preview-1",
                  "entries": [
                    {
                      "type": "bugfix",
                      "description": "Some bugfix"
                    }
                  ]
                }
            """
        )

        val sut = ChangeLogGenerator(mock(), mock())
        sut.addReleasedChanges(listOf(first, second))
    }

    @Test
    fun canWriteToMultipleWriters() {
        val entry = createFile(
            """
                {
                  "date": "2017-01-01",
                  "version": "2.0.0-preview-1",
                  "entries": [
                    {
                      "type": "feature",
                      "description": "Some feature"
                    }
                  ]
                }
            """
        )

        val firstWriter = mock<ChangeLogWriter>()
        val secondWriter = mock<ChangeLogWriter>()
        val sut = ChangeLogGenerator(listOf(firstWriter, secondWriter), mock())
        sut.addReleasedChanges(listOf(entry))
        sut.close()

        verify(firstWriter).writeLine(anyString())
        verify(firstWriter).close()
        verify(secondWriter).writeLine(anyString())
        verify(secondWriter).close()
    }

    @Test
    fun basicWrite() {
        val writer = mock<ChangeLogWriter>()
        val sut = ChangeLogGenerator(listOf(writer), mock())

        val first = createFile(
            """
                {
                  "date": "2017-02-01",
                  "version": "2.0.0-preview-3",
                  "entries": [
                    {
                      "type": "feature",
                      "description": "Third feature"
                    }
                  ]
                }
            """
        )

        val second = createFile(
            """
                {
                  "date": "2017-01-03",
                  "version": "2.0.0-preview-2",
                  "entries": [
                    {
                      "type": "feature",
                      "description": "Another feature"
                    },
                    {
                      "type": "bugfix",
                      "description": "Some bugfix"
                    }
                  ]
                }
            """
        )

        sut.addReleasedChanges(listOf(first, second))
        sut.close()

        argumentCaptor<String>().apply {
            verify(writer, times(2)).writeLine(capture())

            assertThat(firstValue.trim()).isEqualTo(
                """
                # _2.0.0-preview-3_ (2017-02-01)
                - **(Feature)** Third feature
                """.trimIndent()
            )

            assertThat(secondValue.trim()).isEqualTo(
                """
                # _2.0.0-preview-2_ (2017-01-03)
                - **(Feature)** Another feature
                - **(Bug Fix)** Some bugfix
                """.trimIndent()
            )
        }
    }

    @Test
    fun canHandleMarkdown() {
        val writer = mock<ChangeLogWriter>()
        val sut = ChangeLogGenerator(listOf(writer), mock())

        val first = createFile(
            """
                {
                  "date": "2017-02-01",
                  "version": "2.0.0-preview-3",
                  "entries": [
                    {
                      "type": "feature",
                      "description": "A feature with some *code* sample\n```java\nhello();\n```"
                    },
                    {
                      "type": "feature",
                      "description": "A feature with [links](http://linkme.com)"
                    }
                  ]
                }
            """
        )

        sut.addReleasedChanges(listOf(first))
        sut.close()

        argumentCaptor<String>().apply {
            verify(writer).writeLine(capture())

            assertThat(firstValue.trim()).isEqualTo(
                """
                # _2.0.0-preview-3_ (2017-02-01)
                - **(Feature)** A feature with some *code* sample
                  ```java
                  hello();
                  ```
                - **(Feature)** A feature with [links](http://linkme.com)
                """.trimIndent()
            )
        }
    }

    @Test
    fun canHandleMultiLine() {
        val writer = mock<ChangeLogWriter>()
        val sut = ChangeLogGenerator(listOf(writer), mock())

        val first = createFile(
            """
                {
                  "date": "2017-02-01",
                  "version": "2.0.0-preview-3",
                  "entries": [
                    {
                      "type": "feature",
                      "description": "A feature what includes\nmultiple lines of text where\nwe need to ensure it works"
                    },
                    {
                      "type": "deprecation",
                      "description": "Extra blank lines \n\n\n causes Markdown to space our lists so strip them out"
                    }
                  ]
                }
            """
        )

        sut.addReleasedChanges(listOf(first))
        sut.close()

        argumentCaptor<String>().apply {
            verify(writer).writeLine(capture())

            assertThat(firstValue.trim()).isEqualTo(
                """
                # _2.0.0-preview-3_ (2017-02-01)
                - **(Feature)** A feature what includes
                  multiple lines of text where
                  we need to ensure it works
                - **(Deprecation)** Extra blank lines
                  causes Markdown to space our lists so strip them out
                """.trimIndent()
            )
        }
    }

    private fun createFile(@Language("JSON") input: String): Path = folder.newFile().apply {
        writeText(input.trimIndent())
    }.toPath()
}


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

package software.aws.toolkits.gradle.changelog

import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import software.aws.toolkits.gradle.changelog.ChangeLogGenerator.Companion.renderEntry
import java.time.LocalDate

class GithubWriterTest {

    @Rule
    @JvmField
    val folder = TemporaryFolder()

    @Test
    fun basicWrite() {
        val file = folder.newFile()
        val sut = GithubWriter(file.toPath(), null)

        sut.writeLine(
            renderEntry(
                ReleaseEntry(
                    LocalDate.of(2017, 2, 1),
                    "2.0.0-preview-3",
                    listOf(Entry(ChangeType.FEATURE, "Third feature"))
                )
            )
        )
        sut.writeLine(
            renderEntry(
                ReleaseEntry(
                    LocalDate.of(2017, 1, 3),
                    "2.0.0-preview-2",
                    listOf(
                        Entry(ChangeType.FEATURE, "Another feature"),
                        Entry(ChangeType.BUGFIX, "Some bugfix")
                    )
                )
            )
        )
        sut.close()

        assertThat(file.readText().trim()).isEqualToNormalizingNewlines(
            """
            # _2.0.0-preview-3_ (2017-02-01)
            - **(Feature)** Third feature

            # _2.0.0-preview-2_ (2017-01-03)
            - **(Feature)** Another feature
            - **(Bug Fix)** Some bugfix
            """.trimIndent()
        )
    }

    @Test
    fun canHandleReplaceGithubIssueLinks() {
        val file = folder.newFile()
        val sut = GithubWriter(file.toPath(), issueUrl = "http://github.com/org/repo/issues/")

        sut.writeLine(
            renderEntry(
                ReleaseEntry(
                    LocalDate.of(2017, 2, 1),
                    "2.0.0-preview-3",
                    listOf(
                        Entry(
                            ChangeType.FEATURE,
                            "A feature with some an issue link #45 or (#12) but not regular #hash"
                        )
                    )
                )
            )
        )
        sut.close()

        assertThat(file.readText().trim()).isEqualToNormalizingNewlines(
            """
            # _2.0.0-preview-3_ (2017-02-01)
            - **(Feature)** A feature with some an issue link [#45](http://github.com/org/repo/issues/45) or ([#12](http://github.com/org/repo/issues/12)) but not regular #hash
            """.trimIndent()
        )
    }
}


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

package software.aws.toolkits.gradle.changelog

import org.assertj.core.api.Assertions.assertThat
import org.eclipse.jgit.api.Git
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File

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

    lateinit var baseDirectory: File

    lateinit var sut: GitStager

    @Before
    fun initGitRepo() {
        baseDirectory = folder.newFolder()
        Git.init().setDirectory(baseDirectory).call()
        sut = GitStager(baseDirectory)
    }

    @Test
    fun canStageSingleFile() {
        val file = File(baseDirectory, "newFile").apply { createNewFile() }

        sut.stage(file)

        assertThat(Git.open(baseDirectory).status().call().added).contains(file.name)
    }

    @Test(expected = RuntimeException::class)
    fun canNotStageFilesOutsideOfBase() {
        val file = folder.newFile()

        sut.stage(file)
    }

    @Test
    fun canStageSingleFileInSubdirectories() {
        val file = File(File(baseDirectory, "folder").apply { mkdir() }, "file").apply { createNewFile() }

        sut.stage(file)

        assertThat(Git.open(baseDirectory).status().call().added).contains(file.relativizedPath)
    }

    @Test
    fun canStageAFolder() {
        val folder = File(baseDirectory, "folder").apply { mkdir() }
        val firstFile = File(folder, "first").apply { createNewFile() }
        val secondFile = File(folder, "second").apply { createNewFile() }

        sut.stage(folder)

        assertThat(Git.open(baseDirectory).status().call().added).contains(firstFile.relativizedPath, secondFile.relativizedPath)
    }

    @Test
    fun stagingIncludesRemovals() {
        val folder = File(baseDirectory, "folder").apply { mkdir() }
        val firstFile = File(folder, "first").apply { createNewFile() }
        Git.open(baseDirectory).apply {
            add().addFilepattern(folder.name).call()
            commit().setMessage("First commit").setAuthor("Bob", "bob@smith.com").call()
        }

        firstFile.delete()

        sut.stage(folder)

        assertThat(Git.open(baseDirectory).status().call().removed).contains(firstFile.relativizedPath)
    }

    @Test
    fun nullWhenNotGit() {
        assertThat(GitStager.create(folder.newFolder())).isNull()
    }

    private val File.relativizedPath: String get() = this.relativeTo(baseDirectory).path.replace('\\', '/')
}


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

package software.aws.toolkits.gradle.changelog

import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import software.aws.toolkits.gradle.changelog.ChangeLogGenerator.Companion.renderEntry
import java.time.LocalDate

class JetBrainsWriterTest {

    @Rule
    @JvmField
    val folder = TemporaryFolder()

    @Test
    fun basicWrite() {
        val file = folder.newFile()
        val sut = JetBrainsWriter(file)

        sut.writeLine(
            renderEntry(
                ReleaseEntry(
                    LocalDate.of(2017, 2, 1),
                    "2.0.0-preview-3",
                    listOf(Entry(ChangeType.FEATURE, "Third feature"))
                )
            )
        )
        sut.writeLine(
            renderEntry(
                ReleaseEntry(
                    LocalDate.of(2017, 1, 3),
                    "2.0.0-preview-2",
                    listOf(
                        Entry(ChangeType.FEATURE, "Another feature"),
                        Entry(ChangeType.BUGFIX, "Some bugfix")
                    )
                )
            )
        )
        sut.close()

        assertThat(file.readText().trim()).isEqualToIgnoringWhitespace(
            """

2.0.0-preview-3 (2017-02-01)

(Feature) Third feature

2.0.0-preview-2 (2017-01-03)

(Feature) Another feature

(Bug Fix) Some bugfix

            """.trimIndent().wrappedInCData()
        )
    }

    @Test
    fun canHandleMultiLine() {
        val file = folder.newFile()
        val sut = JetBrainsWriter(file)

        sut.writeLine(
            renderEntry(
                ReleaseEntry(
                    LocalDate.of(2017, 2, 1),
                    "2.0.0-preview-3",
                    listOf(
                        Entry(
                            ChangeType.FEATURE,
                            "A feature what includes\nmultiple lines of text where\nwe need to ensure it works"
                        ),
                        Entry(
                            ChangeType.DEPRECATION,
                            "Extra blank lines \n\n\n causes Markdown to space our lists so strip them out"
                        )
                    )
                )
            )
        )
        sut.close()

        assertThat(file.readText().trim()).isEqualToIgnoringWhitespace(
            """

2.0.0-preview-3 (2017-02-01)

(Feature) A feature what includes
multiple lines of text where
we need to ensure it works

(Deprecation) Extra blank lines
causes Markdown to space our lists so strip them out

            """.trimIndent().wrappedInCData()
        )
    }

    @Test
    fun canHandleMarkdown() {
        val file = folder.newFile()
        val sut = JetBrainsWriter(file)

        sut.writeLine(
            renderEntry(
                ReleaseEntry(
                    LocalDate.of(2017, 2, 1),
                    "2.0.0-preview-3",
                    listOf(
                        Entry(
                            ChangeType.FEATURE,
                            "A feature with some *code* sample\n```java\nhello();\n```"
                        ),
                        Entry(
                            ChangeType.FEATURE,
                            "A feature with [links](http://linkme.com)"
                        )
                    )
                )
            )
        )
        sut.close()
        assertThat(file.readText().trim()).isEqualToIgnoringWhitespace(
            """

2.0.0-preview-3 (2017-02-01)

(Feature) A feature with some code sample
hello();

(Feature) A feature with links

            """.trimIndent().wrappedInCData()
        )
    }

    @Test
    fun canHandleReplaceGithubIssueLinks() {
        val file = folder.newFile()
        val sut = JetBrainsWriter(file, issueUrl = "http://github.com/org/repo/issues/")

        sut.writeLine(
            renderEntry(
                ReleaseEntry(
                    LocalDate.of(2017, 2, 1),
                    "2.0.0-preview-3",
                    listOf(
                        Entry(
                            ChangeType.FEATURE,
                            "A feature with some an issue link #45 or (#12) but not regular #hash"
                        )
                    )
                )
            )
        )
        sut.close()

        assertThat(file.readText().trim()).isEqualToIgnoringWhitespace(
            """

2.0.0-preview-3 (2017-02-01)

(Feature) A feature with some an issue link #45 or (#12) but not regular #hash

            """.trimIndent().wrappedInCData()
        )
    }

    private fun String.wrappedInCData() =
        """
        <idea-plugin>
            <change-notes>
            <![CDATA[
                $this
            ]]>
            </change-notes>
        </idea-plugin>
        """.trimIndent()
}


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

package software.aws.toolkits.gradle.changelog

import org.mockito.kotlin.mock
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.io.File
import java.time.LocalDate

class ReleaseCreatorTest {

    private val date = LocalDate.of(2018, 8, 27)

    @Rule
    @JvmField
    val folder = TemporaryFolder()

    @Test
    fun basicReleaseAggregation() {
        val nextRelease = folder.newFolder()
        val nextReleaseFile = folder.newFolder().resolve("2.0.0.json")

        val firstFile = File(nextRelease, "firstChange.json").apply {
            writeText(
                """
            {
                "type": "bugfix",
                "description": "Some bugfix"
            }
                """.trimIndent()
            )
        }

        val secondFile = File(nextRelease, "secondChange.json").apply {
            writeText(
                """
            {
                "type": "feature",
                "description": "Some feature"
            }
                """.trimIndent()
            )
        }

        val sut = ReleaseCreator(listOf(firstFile, secondFile), nextReleaseFile, mock())

        sut.create("2.0.0", date)

        assertThat(nextReleaseFile).exists().hasContent(
            """
            {
              "date" : "2018-08-27",
              "version" : "2.0.0",
              "entries" : [ {
                "type" : "feature",
                "description" : "Some feature"
              }, {
                "type" : "bugfix",
                "description" : "Some bugfix"
              } ]
            }
            """.trimIndent()
        )

        assertThat(firstFile).doesNotExist()
        assertThat(secondFile).doesNotExist()
    }

    @Test(expected = RuntimeException::class)
    fun exitingReleaseVersionThrows() {
        val nextReleaseFile = folder.newFolder().resolve("2.0.0.json")
        nextReleaseFile.createNewFile()
        ReleaseCreator(listOf(folder.newFile()), nextReleaseFile, mock())
    }
}


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

package software.aws.toolkits.core.s3

import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import software.amazon.awssdk.core.sync.RequestBody
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.BucketVersioningStatus
import software.amazon.awssdk.services.s3.model.PutObjectRequest
import software.aws.toolkits.core.rules.S3TemporaryBucketRule

class BucketUtilsTest {
    private val usEast1Client = S3Client.builder().region(Region.US_EAST_1).build()
    private val euWest2Client = S3Client.builder().region(Region.EU_WEST_2).build()

    @Rule
    @JvmField
    val usEast1TempBucket = S3TemporaryBucketRule(usEast1Client)

    @Rule
    @JvmField
    val euWest2TempBucket = S3TemporaryBucketRule(euWest2Client)

    @Test
    fun deleteAnEmptyBucket() {
        createAndDeleteBucket {}
    }

    @Test
    fun deleteABucketWithObjects() {
        createAndDeleteBucket { bucket ->
            usEast1Client.putObject(PutObjectRequest.builder().bucket(bucket).key("hello").build(), RequestBody.fromString(""))
        }
    }

    @Test
    fun deleteABucketWithVersionedObjects() {
        createAndDeleteBucket { bucket ->
            usEast1Client.putBucketVersioning { it.bucket(bucket).versioningConfiguration { it.status(BucketVersioningStatus.ENABLED) } }
            usEast1Client.putObject(PutObjectRequest.builder().bucket(bucket).key("hello").build(), RequestBody.fromString(""))
            usEast1Client.putObject(PutObjectRequest.builder().bucket(bucket).key("hello").build(), RequestBody.fromString(""))
        }
    }

    @Test
    fun canGetRegionBucketWithRegionNotSameAsClient() {
        val bucket = euWest2TempBucket.createBucket()

        assertThat(usEast1Client.regionForBucket(bucket)).isEqualTo("eu-west-2")
    }

    @Test
    fun canGetRegionInSameRegionAsClient() {
        val bucket = usEast1TempBucket.createBucket()

        assertThat(usEast1Client.regionForBucket(bucket)).isEqualTo("us-east-1")
    }

    private fun createAndDeleteBucket(populateBucket: (String) -> Unit) {
        val bucket = usEast1TempBucket.createBucket()
        populateBucket(bucket)
        usEast1Client.deleteBucketAndContents(bucket)
        assertThat(usEast1Client.listBuckets().buckets().map { it.name() }).doesNotContain(bucket)
    }
}


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

package software.aws.toolkits.core

import software.aws.toolkits.core.credentials.ToolkitBearerTokenProvider
import software.aws.toolkits.core.credentials.ToolkitCredentialsProvider
import software.aws.toolkits.core.credentials.toEnvironmentVariables
import software.aws.toolkits.core.region.AwsRegion

sealed interface ClientConnectionSettings<out T> {
    val region: AwsRegion
    val providerId: String

    /**
     * Copies bean with the region replaced
     */
    fun withRegion(region: AwsRegion): ClientConnectionSettings<T>
}

data class ConnectionSettings(val credentials: ToolkitCredentialsProvider, override val region: AwsRegion) : ClientConnectionSettings<ConnectionSettings> {
    override val providerId: String
        get() = credentials.id

    override fun withRegion(region: AwsRegion) = copy(region = region)
}

data class TokenConnectionSettings(
    val tokenProvider: ToolkitBearerTokenProvider,
    override val region: AwsRegion
) : ClientConnectionSettings<TokenConnectionSettings> {
    override val providerId: String
        get() = tokenProvider.id

    override fun withRegion(region: AwsRegion) = copy(region = region)
}

val ConnectionSettings.shortName get() = "${credentials.shortName}@${region.id}"

fun ConnectionSettings.toEnvironmentVariables(): Map<String, String> = region.toEnvironmentVariables() +
    credentials.resolveCredentials().toEnvironmentVariables()


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

package software.aws.toolkits.core

import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider
import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration

/**
 * Used to override/add behavior during AWS SDK Client creation.
 *
 * Example usage to add a local development endpoint for a particular service:
 *
 * ```
 * class MyDevEndpointCustomizer : AwsClientCustomizer {
 *   override fun customize(credentialProvider: AwsCredentialsProvider, regionId: String, builder: AwsClientBuilder<*, *>) {
 *     if (builder is LambdaClientBuilder && connection.region.id == "us-west-2") {
 *       builder.endpointOverride(URI.create("http://localhost:8888"))
 *     }
 *   }
 * }
 * ```
 */
fun interface ToolkitClientCustomizer {
    fun customize(
        credentialProvider: AwsCredentialsProvider?,
        tokenProvider: SdkTokenProvider?,
        regionId: String,
        builder: AwsClientBuilder<*, *>,
        clientOverrideConfiguration: ClientOverrideConfiguration.Builder
    )
}


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

package software.aws.toolkits.core

import org.jetbrains.annotations.TestOnly
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider
import software.amazon.awssdk.auth.token.signer.aws.BearerTokenSigner
import software.amazon.awssdk.awscore.client.builder.AwsClientBuilder
import software.amazon.awssdk.awscore.client.builder.AwsDefaultClientBuilder
import software.amazon.awssdk.core.SdkClient
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration
import software.amazon.awssdk.core.client.config.SdkAdvancedClientOption
import software.amazon.awssdk.core.retry.RetryMode
import software.amazon.awssdk.http.SdkHttpClient
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.utils.SdkAutoCloseable
import software.aws.toolkits.core.clients.nullDefaultProfileFile
import software.aws.toolkits.core.credentials.ToolkitCredentialsProvider
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.core.region.ToolkitRegionProvider
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import java.lang.reflect.Modifier
import java.net.URI
import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KClass

/**
 * An SPI for caching of AWS clients inside of a toolkit
 */
abstract class ToolkitClientManager {
    data class AwsClientKey(
        val providerId: String,
        val region: AwsRegion,
        val serviceClass: KClass<out SdkClient>
    )

    private val cachedClients = ConcurrentHashMap<AwsClientKey, SdkClient>()

    protected abstract val userAgent: String

    protected abstract fun sdkHttpClient(): SdkHttpClient

    inline fun <reified T : SdkClient> getClient(credProvider: ToolkitCredentialsProvider, region: AwsRegion): T =
        this.getClient(T::class, ConnectionSettings(credProvider, region))

    inline fun <reified T : SdkClient> getClient(connection: ClientConnectionSettings<*>): T = this.getClient(T::class, connection)

    fun <T : SdkClient> getClient(sdkClass: KClass<T>, connection: ClientConnectionSettings<*>): T {
        val key = AwsClientKey(
            providerId = connection.providerId,
            region = connection.region,
            serviceClass = sdkClass
        )

        val serviceId = key.serviceClass.java.getField("SERVICE_METADATA_ID").get(null) as String
        if (serviceId !in GLOBAL_SERVICE_DENY_LIST && getRegionProvider().isServiceGlobal(connection.region, serviceId)) {
            val globalRegion = getRegionProvider().getGlobalRegionForService(connection.region, serviceId)
            @Suppress("UNCHECKED_CAST")
            return cachedClients.computeIfAbsent(key.copy(region = globalRegion)) {
                createNewClient(sdkClass, connection.withRegion(region = globalRegion))
            } as T
        }

        @Suppress("UNCHECKED_CAST")
        return cachedClients.computeIfAbsent(key) { createNewClient(sdkClass, connection) } as T
    }

    private fun <T : SdkClient> createNewClient(sdkClass: KClass<T>, connection: ClientConnectionSettings<*>): T = when (connection) {
        is ConnectionSettings -> constructAwsClient(
            sdkClass = sdkClass,
            credProvider = connection.credentials,
            region = Region.of(connection.region.id),
        )

        is TokenConnectionSettings -> constructAwsClient(
            sdkClass = sdkClass,
            tokenProvider = connection.tokenProvider,
            region = Region.of(connection.region.id),
        )
    }

    /**
     * Constructs a new low-level AWS client whose lifecycle is **NOT** managed centrally. Caller is responsible for shutting down the client
     */
    inline fun <reified T : SdkClient> createUnmanagedClient(
        credProvider: AwsCredentialsProvider,
        region: Region,
        endpointOverride: String? = null,
        clientCustomizer: ToolkitClientCustomizer? = null
    ): T = createUnmanagedClient(T::class, credProvider, region, endpointOverride, clientCustomizer)

    /**
     * Constructs a new low-level AWS client whose lifecycle is **NOT** managed centrally. Caller is responsible for shutting down the client
     */
    fun <T : SdkClient> createUnmanagedClient(
        sdkClass: KClass<T>,
        credProvider: AwsCredentialsProvider,
        region: Region,
        endpointOverride: String?,
        clientCustomizer: ToolkitClientCustomizer? = null
    ): T = constructAwsClient(sdkClass, credProvider = credProvider, region = region, endpointOverride = endpointOverride, clientCustomizer = clientCustomizer)

    protected abstract fun getRegionProvider(): ToolkitRegionProvider

    /**
     * Allow implementations to apply customizations to clients before they are built
     */
    protected open fun globalClientCustomizer(
        credentialProvider: AwsCredentialsProvider?,
        tokenProvider: SdkTokenProvider?,
        regionId: String,
        builder: AwsClientBuilder<*, *>,
        clientOverrideConfiguration: ClientOverrideConfiguration.Builder
    ) {}

    /**
     * Calls [SdkAutoCloseable.close] on all managed clients and clears the cache
     */
    protected fun shutdown() {
        cachedClients.values.forEach { it.close() }
        cachedClients.clear()
    }

    protected fun invalidateSdks(providerId: String) {
        val invalidClients = cachedClients.entries.filter { it.key.providerId == providerId }.toSet()
        cachedClients.entries.removeAll(invalidClients)
        invalidClients.forEach { it.value.close() }
    }

    protected open fun <T : SdkClient> constructAwsClient(
        sdkClass: KClass<T>,
        credProvider: AwsCredentialsProvider? = null,
        tokenProvider: SdkTokenProvider? = null,
        region: Region,
        endpointOverride: String? = null,
        clientCustomizer: ToolkitClientCustomizer? = null
    ): T {
        checkNotNull(credProvider ?: tokenProvider) { "Either a credential provider or a bearer token provider must be provided" }

        val builderMethod = sdkClass.java.methods.find {
            it.name == "builder" && Modifier.isStatic(it.modifiers) && Modifier.isPublic(it.modifiers)
        } ?: throw IllegalArgumentException("Expected service interface to have a public static `builder()` method.")

        val builder = builderMethod.invoke(null) as AwsDefaultClientBuilder<*, *>

        @Suppress("UNCHECKED_CAST")
        return builder
            .httpClient(sdkHttpClient())
            .region(region)
            .apply {
                val clientOverrideConfig = ClientOverrideConfiguration.builder()

                if (credProvider != null) {
                    credentialsProvider(credProvider)
                }

                if (tokenProvider != null) {
                    val tokenMethod = builderMethod.returnType.methods.find {
                        it.name == "tokenProvider" &&
                            it.parameterCount == 1 &&
                            it.parameters[0].type.name == "software.amazon.awssdk.auth.token.credentials.SdkTokenProvider"
                    }

                    if (tokenMethod == null) {
                        LOG.warn { "Ignoring bearer provider parameter for ${sdkClass.qualifiedName} since it's not a supported client attribute" }
                    } else {
                        tokenMethod.invoke(this, tokenProvider)
                        clientOverrideConfig.nullDefaultProfileFile()
                        // TODO: why do we need this?
                        clientOverrideConfig.putAdvancedOption(SdkAdvancedClientOption.SIGNER, BearerTokenSigner())
                    }
                }

                clientOverrideConfig.let { configuration ->
                    configuration.putAdvancedOption(SdkAdvancedClientOption.USER_AGENT_PREFIX, userAgent)
                    configuration.retryPolicy(RetryMode.STANDARD)
                }

                endpointOverride?.let {
                    endpointOverride(URI.create(it))
                }

                globalClientCustomizer(credProvider, tokenProvider, region.id(), this, clientOverrideConfig)

                clientCustomizer?.let {
                    it.customize(credProvider, tokenProvider, region.id(), this, clientOverrideConfig)
                }

                // TODO: ban overrideConfiguration outside of here
                overrideConfiguration(clientOverrideConfig.build())
            }
            .build() as T
    }

    @TestOnly
    fun cachedClients() = cachedClients

    companion object {
        private val LOG = getLogger<ToolkitClientManager>()
        private val GLOBAL_SERVICE_DENY_LIST = setOf(
            // sts is regionalized but does not identify as such in metadata
            "sts"
        )
    }
}


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

package software.aws.toolkits.core.clients

import software.amazon.awssdk.core.client.builder.SdkClientBuilder
import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration
import software.amazon.awssdk.profiles.ProfileFile
import java.io.InputStream

fun ClientOverrideConfiguration.Builder.nullDefaultProfileFile() = defaultProfileFile(
    ProfileFile.builder()
        .content(InputStream.nullInputStream())
        .type(ProfileFile.Type.CONFIGURATION)
        .build()
)

/**
 * Only use if this is the only [overrideConfiguration] block used by the [SdkClientBuilder]
 */
fun<C> SdkClientBuilder<*, C>.nullDefaultProfileFile() = apply {
    overrideConfiguration {
        it.nullDefaultProfileFile()
    }
}


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

package software.aws.toolkits.core.clients

import software.amazon.awssdk.http.SdkHttpClient

interface SdkClientProvider {
    fun sharedSdkClient(): SdkHttpClient
}


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

package software.aws.toolkits.core.credentials

import software.amazon.awssdk.auth.credentials.AwsCredentials
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials
import software.amazon.awssdk.core.SdkSystemSetting

private val CREDENTIAL_ENVIRONMENT_VARIABLES = setOf(
    SdkSystemSetting.AWS_ACCESS_KEY_ID.environmentVariable(),
    SdkSystemSetting.AWS_SECRET_ACCESS_KEY.environmentVariable(),
    SdkSystemSetting.AWS_SESSION_TOKEN.environmentVariable()
)

fun AwsCredentials.toEnvironmentVariables(): Map<String, String> {
    val map = mutableMapOf<String, String>()
    map[SdkSystemSetting.AWS_ACCESS_KEY_ID.environmentVariable()] = this.accessKeyId()
    map[SdkSystemSetting.AWS_SECRET_ACCESS_KEY.environmentVariable()] = this.secretAccessKey()

    if (this is AwsSessionCredentials) {
        map[SdkSystemSetting.AWS_SESSION_TOKEN.environmentVariable()] = this.sessionToken()
    }

    return map
}

fun AwsCredentials.mergeWithExistingEnvironmentVariables(existing: MutableMap<String, String>, replace: Boolean = false) {
    mergeWithExistingEnvironmentVariables(existing.keys, existing::remove, existing::putAll, replace)
}

fun AwsCredentials.mergeWithExistingEnvironmentVariables(
    existingKeys: Collection<String>,
    removeKey: (String) -> Unit,
    putValues: (Map<String, String>) -> Unit,
    replace: Boolean = false
) {
    val envVars = toEnvironmentVariables()
    if (replace || existingKeys.none { it in CREDENTIAL_ENVIRONMENT_VARIABLES }) {
        CREDENTIAL_ENVIRONMENT_VARIABLES.forEach { removeKey(it) }
        putValues(envVars)
    }
}


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

package software.aws.toolkits.core.credentials

import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.aws.toolkits.core.region.AwsRegion

/**
 * Factory for adding new credential providers to the central credential management system
 */
interface CredentialProviderFactory {
    /**
     * ID used to uniquely identify this factory
     */
    val id: String

    /**
     * ID used to indicate where credentials are stored or retrieved from
     */
    val credentialSourceId: CredentialSourceId

    /**
     * Invoked on creation of the factory to update the credential system with what [CredentialIdentifier] this factory
     * is capable of creating. The provided [credentialLoadCallback] is capable of being invoked multiple times in the case that
     * the credentials this factory creates is modified in some way.
     */
    fun setUp(credentialLoadCallback: CredentialsChangeListener)

    /**
     * Creates an [AwsCredentialsProvider] for the specified [CredentialIdentifier] scoped to the specified [region]
     */
    fun createAwsCredentialProvider(
        providerId: CredentialIdentifier,
        region: AwsRegion
    ): AwsCredentialsProvider
}


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

package software.aws.toolkits.core.credentials

/**
 * Event that indicates that credentials were manipulated in a way that the toolkit needs to be notified so state can be updated
 * to give an accurate representation of the state of the credentials system
 */
data class CredentialsChangeEvent(
    val added: List<CredentialIdentifier>,
    val modified: List<CredentialIdentifier>,
    val removed: List<CredentialIdentifier>
)

typealias CredentialsChangeListener = (changeEvent: CredentialsChangeEvent) -> Unit


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

package software.aws.toolkits.core.credentials

import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider
import software.aws.toolkits.resources.message

enum class CredentialType {
    StaticProfile,
    StaticSessionProfile,
    CredentialProcessProfile,
    AssumeRoleProfile,
    AssumeMfaRoleProfile,
    SsoProfile,
    Ec2Metadata,
    EcsMetadata
}

enum class CredentialSourceId {
    SharedCredentials,
    SdkStore,
    Ecs,
    Ec2,
    EnvVars
}

/**
 * Represents a possible credential provider that can be used within the toolkit.
 *
 * Implementers should extend [CredentialIdentifierBase] instead of directly implementing this interface.
 */
interface CredentialIdentifier {
    /**
     * The ID must be unique across all [CredentialIdentifier] instances.
     * It is recommended to concatenate the factory ID into this field to help enforce this requirement.
     */
    val id: String

    /**
     * A user friendly display name shown in the UI.
     */
    val displayName: String

    /**
     * An optional shortened version of the name to display in the UI where space is at a premium
     */
    val shortName: String get() = displayName

    /**
     * The ID of the corresponding [CredentialProviderFactory] so that the credential manager knows which factory to invoke in order
     * to resolve this into a [ToolkitCredentialsProvider]
     */
    val factoryId: String

    /**
     * The type of credential
     */
    val credentialType: CredentialType?

    /**
     * Some ID types (e.g. Profile) have a concept of a default region, this is optional.
     */
    val defaultRegionId: String? get() = null
}

abstract class CredentialIdentifierBase(override val credentialType: CredentialType?) : CredentialIdentifier {
    final override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as CredentialIdentifierBase

        if (id != other.id) return false

        return true
    }

    final override fun hashCode(): Int = id.hashCode()

    final override fun toString(): String = "${this::class.simpleName}(id='$id')"
}

interface ToolkitAuthenticationProvider {
    val id: String
    val displayName: String
}

class ToolkitCredentialsProvider(
    val identifier: CredentialIdentifier,
    val delegate: AwsCredentialsProvider
) : ToolkitAuthenticationProvider, AwsCredentialsProvider by delegate {
    override val id: String = identifier.id
    override val displayName = identifier.displayName
    val shortName = identifier.shortName

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as ToolkitCredentialsProvider

        if (identifier != other.identifier) return false

        return true
    }

    override fun hashCode(): Int = identifier.hashCode()

    override fun toString(): String = "${this::class.simpleName}(identifier='$identifier')"
}

// TODO: try to get rid of this because it's really annoying casting the delegate everywhere
interface ToolkitBearerTokenProviderDelegate : SdkTokenProvider, ToolkitAuthenticationProvider

class ToolkitBearerTokenProvider(val delegate: ToolkitBearerTokenProviderDelegate) : SdkTokenProvider by delegate, ToolkitAuthenticationProvider by delegate {
    companion object {
        // TODO: is there a better place for this
        fun ssoIdentifier(startUrl: String, region: String = DEFAULT_SSO_REGION) = "sso;$region;$startUrl"

        // TODO: For AWS Builder ID, we only have startUrl for now instead of each users' metadata data i.e. Email address
        fun ssoDisplayName(startUrl: String) = if (startUrl == SONO_URL) {
            message("aws_builder_id.service_name")
        } else {
            message("iam_identity_center.service_name", extractOrgID(startUrl))
        }

        fun diskSessionIdentifier(profileName: String) = "diskSessionProfile;$profileName"
        fun diskSessionDisplayName(profileName: String) = "IAM Identity Center Session ($profileName)"
    }
}

private const val SONO_URL = "https://view.awsapps.com/start"

const val DEFAULT_SSO_REGION = "us-east-1"

private fun extractOrgID(startUrl: String) = startUrl.removePrefix("https://").removeSuffix(".awsapps.com/start")


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

package software.aws.toolkits.core.credentials

/**
 * TODO: Deprecate and remove this since it is less efficient than [CredentialsChangeEvent]
 */
interface ToolkitCredentialsChangeListener {
    fun providerAdded(identifier: CredentialIdentifier) {}
    fun providerModified(identifier: CredentialIdentifier) {}
    fun providerRemoved(identifier: CredentialIdentifier) {}
    fun providerRemoved(providerId: String) {}
}

class CredentialProviderNotFoundException : RuntimeException {
    constructor(msg: String) : super(msg)
    constructor(msg: String, exception: Exception) : super(msg, exception)
}


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

package software.aws.toolkits.core.lambda

import software.amazon.awssdk.services.lambda.model.Architecture

enum class LambdaArchitecture(
    private val architecture: Architecture,
    val minSam: String? = null,
) {
    X86_64(Architecture.X86_64),
    ARM64(Architecture.ARM64, minSam = "1.33.0");

    override fun toString() = architecture.toString()

    fun toSdkArchitecture() = architecture.validOrNull

    companion object {
        fun fromValue(value: String?): LambdaArchitecture? = if (value == null) {
            null
        } else {
            values().find { it.toString() == value }
        }

        fun fromValue(value: Architecture): LambdaArchitecture? = values().find { it.architecture == value }

        val DEFAULT = X86_64
        val ARM_COMPATIBLE = listOf(X86_64, ARM64)
    }
}


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

package software.aws.toolkits.core.lambda

import software.amazon.awssdk.services.lambda.model.Runtime
import software.aws.toolkits.core.lambda.LambdaArchitecture.Companion.ARM_COMPATIBLE

enum class LambdaRuntime(
    private val runtime: Runtime?,
    val minSamInit: String? = null,
    val minSamDebugging: String? = null,
    val architectures: List<LambdaArchitecture>? = listOf(LambdaArchitecture.DEFAULT),
    private val runtimeOverride: String? = null
) {
    GO1_X(
        Runtime.GO1_X,
        // Although go sam debugging was supported before 1.18.1, it does not work on 1.13.0-1.16.0
        // and 1.17.0 broke the arguments
        minSamDebugging = "1.18.1"
    ),
    NODEJS14_X(Runtime.NODEJS14_X, minSamDebugging = "1.17.0", minSamInit = "1.17.0", architectures = ARM_COMPATIBLE),
    NODEJS16_X(Runtime.NODEJS16_X, minSamDebugging = "1.49.0", minSamInit = "1.49.0", architectures = ARM_COMPATIBLE),
    NODEJS18_X(Runtime.NODEJS18_X, minSamDebugging = "1.65.0", minSamInit = "1.65.0", architectures = ARM_COMPATIBLE),
    JAVA8(Runtime.JAVA8),
    JAVA8_AL2(Runtime.JAVA8_AL2, minSamDebugging = "1.2.0", architectures = ARM_COMPATIBLE),
    JAVA11(Runtime.JAVA11, architectures = ARM_COMPATIBLE),
    JAVA17(Runtime.JAVA17, minSamDebugging = "1.81.0", minSamInit = "1.81.0", architectures = ARM_COMPATIBLE),
    PYTHON3_7(Runtime.PYTHON3_7),
    PYTHON3_8(Runtime.PYTHON3_8, architectures = ARM_COMPATIBLE),
    PYTHON3_9(Runtime.PYTHON3_9, minSamDebugging = "1.28.0", minSamInit = "1.28.0", architectures = ARM_COMPATIBLE),
    PYTHON3_10(Runtime.PYTHON3_10, minSamDebugging = "1.78.0", minSamInit = "1.78.0", architectures = ARM_COMPATIBLE),
    DOTNET5_0(null, minSamInit = "1.16.0", runtimeOverride = "dotnet5.0"),
    DOTNET6_0(Runtime.DOTNET6, minSamDebugging = "1.40.1", minSamInit = "1.40.1", architectures = ARM_COMPATIBLE);

    override fun toString() = runtime?.toString() ?: runtimeOverride ?: throw IllegalStateException("LambdaRuntime has no runtime or override string")

    fun toSdkRuntime() = runtime.validOrNull

    companion object {
        fun fromValue(value: String?): LambdaRuntime? = if (value == null) {
            null
        } else {
            values().find { it.toString() == value }
        }

        fun fromValue(value: Runtime): LambdaRuntime? = values().find { it.runtime == value }
    }
}


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

package software.aws.toolkits.core.lambda

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.dataformat.xml.XmlMapper
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import org.slf4j.LoggerFactory
import software.aws.toolkits.core.utils.RemoteResolveParser
import software.aws.toolkits.core.utils.RemoteResource
import software.aws.toolkits.core.utils.RemoteResourceResolver
import software.aws.toolkits.core.utils.inputStream
import software.aws.toolkits.core.utils.readText
import software.aws.toolkits.core.utils.tryOrNull
import java.io.InputStream
import java.time.Duration
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import java.util.concurrent.atomic.AtomicReference

class LambdaSampleEventProvider(private val resourceResolver: RemoteResourceResolver) {
    private val mapper = XmlMapper().registerKotlinModule().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)

    private val manifest = AtomicReference<List<LambdaSampleEvent>?>(null)

    fun get(): CompletionStage<List<LambdaSampleEvent>> {
        val value = manifest.get()
        if (value != null) {
            return CompletableFuture.completedFuture(value)
        } else {
            return resourceResolver.resolve(LambdaSampleEventManifestResource).thenApply { resource ->
                val resolved = mapper.readValue<LambdaSampleEventManifest>(resource.inputStream()).requests.map { request ->
                    LambdaSampleEvent(request.name) {
                        resourceResolver.resolve(LambdaSampleEventResource(request.filename))
                            .thenApply { it?.readText() }
                    }
                }
                manifest.set(resolved)
                resolved
            }
        }
    }
}

open class LambdaSampleEvent(val name: String, private val contentProvider: () -> CompletionStage<String>) {
    val content: CompletionStage<String> by lazy { contentProvider() }
    override fun toString() = name
}

data class LambdaSampleEventManifest(
    @JsonProperty(value = "request")
    @JacksonXmlElementWrapper(useWrapping = false)
    val requests: List<LambdaSampleEventRequest>
)

data class LambdaSampleEventRequest(
    val filename: String,
    val name: String
)

internal val LambdaSampleEventManifestResource = LambdaSampleEventResource("manifest.xml")
object LambdaManifestValidator : RemoteResolveParser {

    private val LOG = LoggerFactory.getLogger(LambdaManifestValidator::class.java)
    override fun canBeParsed(data: InputStream): Boolean {
        val mapper = XmlMapper().registerKotlinModule().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
        val result = LOG.tryOrNull("Failed to parse Requests") {
            mapper.readValue<LambdaSampleEventManifest>(data)
        }

        return result?.requests?.isNotEmpty() ?: false
    }
}
internal data class LambdaSampleEventResource(val filename: String) : RemoteResource {
    override val urls: List<String> = listOf(
        "https://aws-vs-toolkit.s3.amazonaws.com/LambdaSampleFunctions/SampleRequests/$filename"
    )

    override val name: String = "lambda-sample-event-$filename"
    override val ttl: Duration = Duration.ofDays(7)
    override val remoteResolveParser: RemoteResolveParser? = resolveParserForGivenFile(filename.substringAfterLast('.', ""))
}
object LambdaSampleEventJsonValidator : RemoteResolveParser {
    private val LOG = LoggerFactory.getLogger(LambdaSampleEventJsonValidator::class.java)

    private val mapper = jacksonObjectMapper()
        .disable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS)
        .disable(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS)
        .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
        .enable(JsonParser.Feature.ALLOW_COMMENTS)

    override fun canBeParsed(data: InputStream): Boolean {
        val jsonMapper: Map<String, Any> = HashMap()
        val result = LOG.tryOrNull("Failed to parse Lambda Sample Event request") {
            this.mapper.readValue(data, jsonMapper.javaClass)
        }
        return result?.isNotEmpty() ?: false
    }
}
fun resolveParserForGivenFile(extension: String): RemoteResolveParser? =
    when (extension) {
        "xml" -> LambdaManifestValidator
        "json" -> LambdaSampleEventJsonValidator
        else -> null
    }


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

package software.aws.toolkits.core.lambda

import software.amazon.awssdk.services.lambda.model.Architecture
import software.amazon.awssdk.services.lambda.model.Runtime

val Runtime?.validOrNull: Runtime? get() = this?.takeUnless { it == Runtime.UNKNOWN_TO_SDK_VERSION }
val Architecture?.validOrNull: Architecture? get() = this?.takeUnless { it == Architecture.UNKNOWN_TO_SDK_VERSION }


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

package software.aws.toolkits.core.region

data class AwsPartition(val id: String, val description: String, val regions: Collection<AwsRegion>) {
    val displayName = "$description ($id)"
}


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

package software.aws.toolkits.core.region

import software.amazon.awssdk.regions.Region

data class AwsRegion(val id: String, val name: String, val partitionId: String) {
    val category: String? = when {
        id.startsWith("af") -> "Africa"
        id.startsWith("us") -> "North America"
        id.startsWith("ca") -> "North America"
        id.startsWith("eu") -> "Europe"
        id.startsWith("ap") -> "Asia Pacific"
        id.startsWith("sa") -> "South America"
        id.startsWith("cn") -> "China"
        id.startsWith("me") -> "Middle East"
        else -> null
    }

    val displayName: String = when {
        category == "Europe" -> "${name.removePrefix("Europe").trimPrefixAndRemoveBrackets("EU")} ($id)"
        category == "North America" -> "${name.removePrefix("US West").trimPrefixAndRemoveBrackets("US East")} ($id)"
        category != null && name.startsWith(category) -> "${name.trimPrefixAndRemoveBrackets(category)} ($id)"
        else -> name
    }

    fun toEnvironmentVariables() = mapOf(
        "AWS_REGION" to id,
        "AWS_DEFAULT_REGION" to id
    )

    companion object {
        val GLOBAL = AwsRegion(Region.AWS_GLOBAL.id(), "Global", "Global")
        private fun String.trimPrefixAndRemoveBrackets(prefix: String) = this.removePrefix(prefix).replace("(", "").replace(")", "").trim()
    }
}

fun AwsRegion.mergeWithExistingEnvironmentVariables(existing: MutableMap<String, String>, replace: Boolean = false) {
    mergeWithExistingEnvironmentVariables(existing.keys, existing::putAll, replace)
}

fun AwsRegion.mergeWithExistingEnvironmentVariables(
    existingKeys: Collection<String>,
    putValues: (Map<String, String>) -> Unit,
    replace: Boolean = false
) {
    val regionEnvs = this.toEnvironmentVariables()
    if (replace || regionEnvs.keys.none { it in existingKeys }) {
        putValues(regionEnvs)
    }
}


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

package software.aws.toolkits.core.region

import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import org.slf4j.LoggerFactory
import software.aws.toolkits.core.utils.RemoteResolveParser
import software.aws.toolkits.core.utils.RemoteResource
import software.aws.toolkits.core.utils.tryOrNull
import software.aws.toolkits.resources.BundledResources
import java.io.InputStream
import java.time.Duration

data class Partitions(val partitions: List<Partition>) {
    fun getPartition(name: String): Partition =
        partitions.find { it.partition == name } ?: throw RuntimeException("Partition named '$name' not found")
}

data class Partition(
    val partition: String,
    val partitionName: String,
    val regions: Map<String, PartitionRegion>,
    val services: Map<String, Service>
)

data class PartitionRegion(val description: String)

data class Service(val endpoints: Map<String, Endpoint>, val isRegionalized: Boolean?, val partitionEndpoint: String?) {
    val isGlobal = isRegionalized != true && partitionEndpoint != null
}

class Endpoint

object PartitionParser {
    private val LOG = LoggerFactory.getLogger(PartitionParser::class.java)

    private val mapper = jacksonObjectMapper()
        .disable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS)
        .disable(MapperFeature.ALLOW_FINAL_FIELDS_AS_MUTATORS)
        .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
        .enable(JsonParser.Feature.ALLOW_COMMENTS)

    fun parse(inputStream: InputStream): Partitions? = LOG.tryOrNull("Failed to parse Partitions") {
        mapper.readValue(inputStream, Partitions::class.java)
    }
}
object EndpointsJsonValidator : RemoteResolveParser {
    override fun canBeParsed(data: InputStream): Boolean {
        return PartitionParser.parse(data)?.partitions?.isNotEmpty() ?: return false
    }
}
object ServiceEndpointResource : RemoteResource {
    override val urls: List<String> = listOf("https://idetoolkits.amazonwebservices.com/endpoints.json")
    override val name: String = "service-endpoints.json"
    override val ttl: Duration? = Duration.ofHours(24)
    override val initialValue: (() -> InputStream)? = { BundledResources.ENDPOINTS_FILE }
    override val remoteResolveParser: EndpointsJsonValidator = EndpointsJsonValidator
}


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

package software.aws.toolkits.core.region

import java.util.concurrent.ConcurrentHashMap

/**
 * An SPI to provide regions supported by this toolkit
 */
abstract class ToolkitRegionProvider {
    protected data class PartitionData(
        val description: String,
        val services: Map<String, Service>,
        val regions: Map<String, AwsRegion>
    )

    protected abstract fun partitionData(): Map<String, PartitionData>

    private val globalCache = ConcurrentHashMap<Pair<AwsRegion, String>, AwsRegion>()

    /**
     * Returns a map of region ID([AwsRegion.id]) to [AwsRegion]
     */
    fun allRegions(): Map<String, AwsRegion> = partitionData().flatMap { it.value.regions.asIterable() }.associate { it.key to it.value }

    /**
     * Returns a map of region ID([AwsRegion.id]) to [AwsRegion], filtering by if the service is supported
     */
    fun allRegionsForService(serviceId: String): Map<String, AwsRegion> = allRegions().filter { isServiceSupported(it.value, serviceId) }

    /**
     * Returns a map of region ID([AwsRegion.id]) to [AwsRegion] for the specified partition
     */
    fun regions(partitionId: String): Map<String, AwsRegion> = partitionData()[partitionId]?.regions
        ?: throw IllegalArgumentException("Unknown partition $partitionId")

    /**
     * Returns a map of partition ID([AwsPartition.id]) to [AwsPartition]
     */
    fun partitions(): Map<String, AwsPartition> = partitionData().asSequence()
        .associate { it.key to AwsPartition(it.key, it.value.description, it.value.regions.values) }

    /**
     * Returns the default region to use based on the environment
     */
    abstract fun defaultRegion(): AwsRegion

    /**
     * Returns the default partition to use based on the environment
     */
    abstract fun defaultPartition(): AwsPartition

    operator fun get(regionId: String): AwsRegion? = allRegions()[regionId]

    open fun isServiceGlobal(region: AwsRegion, serviceId: String): Boolean {
        val partition = partitionData()[region.partitionId] ?: throw IllegalStateException("Partition data is missing for ${region.partitionId}")
        return partition.services[serviceId]?.isGlobal == true
    }

    fun getGlobalRegionForService(region: AwsRegion, serviceId: String): AwsRegion {
        val cacheKey = region to serviceId
        globalCache[cacheKey]?.let {
            return it
        }

        val partition = partitionData()[region.partitionId] ?: throw IllegalStateException("Partition data is missing for ${region.partitionId}")
        val service = partition.services[serviceId] ?: throw IllegalStateException("Unknown service $serviceId in ${region.partitionId}")
        if (!service.isGlobal) {
            throw IllegalStateException("$serviceId is not global in ${region.partitionId}")
        }

        // TODO: A few services lack partition endpoint like Shield and MTurk, how should that be handled?
        val partitionEndpoint = service.partitionEndpoint ?: throw IllegalStateException("$serviceId in ${region.partitionId} lacks a partitionEndpoint")
        return globalCache.computeIfAbsent(cacheKey) {
            AwsRegion(partitionEndpoint, partitionEndpoint, region.partitionId)
        }
    }

    open fun isServiceSupported(region: AwsRegion, serviceName: String): Boolean {
        val currentPartition = partitionData()[region.partitionId] ?: return false
        val service = currentPartition.services[serviceName] ?: return false
        return service.isGlobal || service.endpoints.containsKey(region.id)
    }
}


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

package software.aws.toolkits.core.s3

import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.ListObjectVersionsRequest
import software.amazon.awssdk.services.s3.model.ObjectIdentifier

fun S3Client.deleteBucketAndContents(bucket: String) {
    this.listObjectVersionsPaginator(ListObjectVersionsRequest.builder().bucket(bucket).build()).forEach { resp ->
        val versions = resp.versions().map {
            ObjectIdentifier.builder()
                .key(it.key())
                .versionId(it.versionId()).build()
        }
        if (versions.isEmpty()) {
            return@forEach
        }
        this.deleteObjects { it.bucket(bucket).delete { obj -> obj.objects(versions) } }
    }

    this.deleteBucket { it.bucket(bucket) }
}

fun S3Client.regionForBucket(bucketName: String): String = this.getBucketLocation {
    it.bucket(bucketName)
}.locationConstraintAsString()
    .takeIf { it.isNotEmpty() } // getBucketLocation returns an explicit empty string location contraint for us-east-1
    ?: Region.US_EAST_1.id()


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

package software.aws.toolkits.core.telemetry

/**
 * Used to provide a way to cache an identity ID in order to prevent creating additional unneeded identities.
 */
interface CachedIdentityStorage {
    /**
     * Saves the identity ID to the backing storage.
     *
     * @param identityPoolId The pool the identity belongs to
     * @param identityId The generated ID
     */
    fun storeIdentity(identityPoolId: String, identityId: String)

    /**
     * Attempts to retrieve the identity ID from the backing storage. If no ID exists for the specified pool,
     * `null` should be returned in order to generate a new ID.
     *
     * @param identityPoolId The ID of the pool we are requested the ID for
     * @return The ID for the specified pool, else null
     */
    fun loadIdentity(identityPoolId: String): String?
}


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

package software.aws.toolkits.core.telemetry

import software.aws.toolkits.core.telemetry.MetricEvent.Companion.illegalCharsRegex
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import java.time.Instant
import software.amazon.awssdk.services.toolkittelemetry.model.Unit as MetricUnit

interface MetricEvent {
    val createTime: Instant
    val awsAccount: String
    val awsRegion: String
    val data: Iterable<Datum>

    interface Builder {
        fun createTime(createTime: Instant): Builder

        fun awsAccount(awsAccount: String): Builder

        fun awsRegion(awsRegion: String): Builder

        fun datum(name: String, buildDatum: Datum.Builder.() -> Unit = {}): Builder

        fun build(): MetricEvent
    }

    interface Datum {
        val name: String
        val value: Double
        val unit: MetricUnit
        val passive: Boolean
        val metadata: Map<String, String>

        interface Builder {
            fun name(name: String): Builder

            fun value(value: Double): Builder

            fun unit(unit: MetricUnit): Builder

            fun passive(value: Boolean): Builder

            fun metadata(key: String, value: String): Builder

            fun metadata(key: String, value: Boolean): Builder = metadata(key, value.toString())

            fun count(value: Double = 1.0): Builder {
                value(value)
                unit(MetricUnit.COUNT)
                return this
            }

            fun build(): Datum
        }
    }

    companion object {
        val illegalCharsRegex = "[^\\w+-.:]".toRegex()
    }
}

fun String.replaceIllegal(replacement: String = "") = this.replace(illegalCharsRegex, replacement)

data class DefaultMetricEvent internal constructor(
    override val createTime: Instant,
    override val awsAccount: String,
    override val awsRegion: String,
    override val data: Iterable<MetricEvent.Datum>
) : MetricEvent {

    class BuilderImpl : MetricEvent.Builder {
        private var createTime: Instant = Instant.now()
        private var awsAccount: String = METADATA_NA
        private var awsRegion: String = METADATA_NA
        private val data: MutableCollection<MetricEvent.Datum> = mutableListOf()

        override fun createTime(createTime: Instant): MetricEvent.Builder {
            this.createTime = createTime
            return this
        }

        override fun awsAccount(awsAccount: String): MetricEvent.Builder {
            this.awsAccount = awsAccount
            return this
        }

        override fun awsRegion(awsRegion: String): MetricEvent.Builder {
            this.awsRegion = awsRegion
            return this
        }

        override fun datum(name: String, buildDatum: MetricEvent.Datum.Builder.() -> Unit): MetricEvent.Builder {
            val builder = DefaultDatum.builder(name)
            buildDatum(builder)
            data.add(builder.build())
            return this
        }

        override fun build(): MetricEvent = DefaultMetricEvent(createTime, awsAccount, awsRegion, data)
    }

    companion object {
        fun builder() = BuilderImpl()

        const val METADATA_NA = "n/a"
        const val METADATA_NOT_SET = "not-set"
        const val METADATA_INVALID = "invalid"
    }

    data class DefaultDatum(
        override val name: String,
        override val value: Double,
        override val unit: MetricUnit,
        override val passive: Boolean,
        override val metadata: Map<String, String>
    ) : MetricEvent.Datum {
        class BuilderImpl(private var name: String) : MetricEvent.Datum.Builder {
            private var value: Double = 0.0
            private var unit: MetricUnit = MetricUnit.NONE
            private var passive: Boolean = false
            private val metadata: MutableMap<String, String> = HashMap()

            override fun name(name: String): MetricEvent.Datum.Builder {
                this.name = name
                return this
            }

            override fun value(value: Double): MetricEvent.Datum.Builder {
                this.value = value
                return this
            }

            override fun unit(unit: MetricUnit): MetricEvent.Datum.Builder {
                this.unit = unit
                return this
            }

            override fun passive(value: Boolean): MetricEvent.Datum.Builder {
                this.passive = value
                return this
            }

            override fun metadata(key: String, value: String): MetricEvent.Datum.Builder {
                if (metadata.containsKey(key)) {
                    LOG.warn { "Attempted to add multiple pieces of metadata with the same key" }
                    return this
                }

                metadata[key] = value
                return this
            }

            override fun build(): MetricEvent.Datum = DefaultDatum(
                name.replaceIllegal(),
                this.value,
                this.unit,
                this.passive,
                this.metadata
            )
        }

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

            fun builder(name: String): MetricEvent.Datum.Builder = BuilderImpl(name)
        }
    }
}


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

package software.aws.toolkits.core.telemetry

import kotlinx.coroutines.runBlocking
import org.jetbrains.annotations.TestOnly
import software.amazon.awssdk.core.exception.SdkServiceException
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import java.util.concurrent.Executors
import java.util.concurrent.LinkedBlockingDeque
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean

interface TelemetryBatcher {
    fun enqueue(event: MetricEvent)

    fun flush(retry: Boolean)

    fun onTelemetryEnabledChanged(isEnabled: Boolean)

    fun shutdown()
}

class DefaultTelemetryBatcher(
    private val publisher: TelemetryPublisher,
    private val maxBatchSize: Int = DEFAULT_MAX_BATCH_SIZE,
    maxQueueSize: Int = DEFAULT_MAX_QUEUE_SIZE,
    private val executor: ScheduledExecutorService = createDefaultExecutor()
) : TelemetryBatcher {
    private val isTelemetryEnabled: AtomicBoolean = AtomicBoolean(false)
    private val isShuttingDown: AtomicBoolean = AtomicBoolean(false)

    val eventQueue: LinkedBlockingDeque<MetricEvent> = LinkedBlockingDeque(maxQueueSize)
        @TestOnly get

    init {
        executor.scheduleWithFixedDelay(
            {
                if (!isShuttingDown.get()) {
                    try {
                        flush(true)
                    } catch (e: Exception) {
                        LOG.warn(e) { "Unexpected exception while publishing telemetry" }
                    }
                }
            },
            DEFAULT_PUBLISH_INTERVAL,
            DEFAULT_PUBLISH_INTERVAL,
            DEFAULT_PUBLISH_INTERVAL_UNIT
        )
    }

    override fun shutdown() {
        if (!isShuttingDown.compareAndSet(false, true)) {
            return
        }

        executor.shutdown()
        flush(false)
    }

    override fun enqueue(event: MetricEvent) {
        if (!isTelemetryEnabled.get()) {
            return
        }

        try {
            eventQueue.add(event)
        } catch (e: Exception) {
            LOG.warn(e) { "Failed to add metric to queue" }
        }
    }

    @Synchronized
    override fun flush(retry: Boolean) {
        if (!isTelemetryEnabled.get()) {
            return
        }

        while (!eventQueue.isEmpty()) {
            val batch: ArrayList<MetricEvent> = arrayListOf()

            while (!eventQueue.isEmpty() && batch.size < maxBatchSize) {
                batch.add(eventQueue.pop())
            }

            val stop = runBlocking {
                try {
                    publisher.publish(batch)
                } catch (e: Exception) {
                    LOG.warn(e) { "Failed to publish metrics" }
                    val shouldRetry = retry && when (e) {
                        is SdkServiceException -> e.statusCode() !in 400..499
                        else -> true
                    }
                    if (shouldRetry) {
                        LOG.warn { "Telemetry metrics failed to publish, retrying later..." }
                        eventQueue.addAll(batch)
                        // don't want an infinite loop...
                        return@runBlocking true
                    }
                }
                return@runBlocking false
            }
            if (stop) {
                return
            }
        }
    }

    override fun onTelemetryEnabledChanged(isEnabled: Boolean) {
        isTelemetryEnabled.set(isEnabled)
        if (!isEnabled) {
            eventQueue.clear()
        }
    }

    companion object {
        private val LOG = getLogger<DefaultTelemetryBatcher>()
        private const val DEFAULT_MAX_BATCH_SIZE = 20
        private const val DEFAULT_MAX_QUEUE_SIZE = 10000
        private const val DEFAULT_PUBLISH_INTERVAL = 5L
        private val DEFAULT_PUBLISH_INTERVAL_UNIT = TimeUnit.MINUTES

        private fun createDefaultExecutor() = Executors.newSingleThreadScheduledExecutor {
            val daemonThread = Thread(it)
            daemonThread.isDaemon = true
            daemonThread.name = "AWS-Toolkit-Metrics-Publisher"
            daemonThread
        }
    }
}


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

package software.aws.toolkits.core.telemetry

import software.amazon.awssdk.services.toolkittelemetry.model.Sentiment

interface TelemetryPublisher : AutoCloseable {
    suspend fun publish(metricEvents: Collection<MetricEvent>)

    suspend fun sendFeedback(sentiment: Sentiment, comment: String, metadata: Map<String, String>)
}