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

package software.aws.toolkits.jetbrains.core

import com.github.tomakehurst.wiremock.client.WireMock.aResponse
import com.github.tomakehurst.wiremock.client.WireMock.any
import com.github.tomakehurst.wiremock.client.WireMock.anyRequestedFor
import com.github.tomakehurst.wiremock.client.WireMock.anyUrl
import com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching
import com.github.tomakehurst.wiremock.core.WireMockConfiguration
import com.github.tomakehurst.wiremock.junit.WireMockRule
import com.github.tomakehurst.wiremock.matching.ContainsPattern
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.use
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.ExtensionTestUtil
import com.intellij.testFramework.ProjectRule
import com.intellij.testFramework.RuleChain
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider
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.SdkAdvancedClientOption
import software.amazon.awssdk.core.client.config.SdkClientOption
import software.amazon.awssdk.core.signer.Signer
import software.amazon.awssdk.http.SdkHttpClient
import software.amazon.awssdk.regions.Region
import software.amazon.awssdk.services.lambda.LambdaClient
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.TokenConnectionSettings
import software.aws.toolkits.core.ToolkitClientCustomizer
import software.aws.toolkits.core.credentials.ToolkitBearerTokenProvider
import software.aws.toolkits.core.credentials.ToolkitBearerTokenProviderDelegate
import software.aws.toolkits.core.region.Endpoint
import software.aws.toolkits.core.region.Service
import software.aws.toolkits.core.region.anAwsRegion
import software.aws.toolkits.core.utils.test.aString
import software.aws.toolkits.jetbrains.core.AwsClientManager.Companion.CUSTOMIZER_EP
import software.aws.toolkits.jetbrains.core.credentials.CredentialManager
import software.aws.toolkits.jetbrains.core.credentials.MockAwsConnectionManager.ProjectAccountSettingsManagerRule
import software.aws.toolkits.jetbrains.core.credentials.MockCredentialManagerRule
import software.aws.toolkits.jetbrains.core.region.MockRegionProviderRule
import java.net.URI
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.jvm.isAccessible

class AwsClientManagerTest {
    private val projectRule = ProjectRule()
    private val disposableRule = DisposableRule()
    private val temporaryDirectory = TemporaryFolder()
    private val regionProvider = MockRegionProviderRule()
    private val credentialManager = MockCredentialManagerRule()
    private val projectSettingsRule = ProjectAccountSettingsManagerRule(projectRule)

    @Rule
    @JvmField
    val wireMockRule = WireMockRule(WireMockConfiguration.wireMockConfig().dynamicPort())

    @Rule
    @JvmField
    val ruleChain = RuleChain(
        projectRule,
        temporaryDirectory,
        credentialManager,
        regionProvider,
        projectSettingsRule,
        disposableRule
    )

    @Test
    fun canGetAnInstanceOfAClient() {
        val sut = getClientManager()
        val client = sut.getClient<DummyServiceClient>(credentialManager.createCredentialProvider(), regionProvider.createAwsRegion())
        assertThat(client.serviceName()).isEqualTo("dummyClient")
    }

    @Test
    fun clientsAreCached() {
        val sut = getClientManager()
        val credProvider = credentialManager.createCredentialProvider()
        val region = regionProvider.createAwsRegion()

        val fooClient = sut.getClient<DummyServiceClient>(credProvider, region)
        val barClient = sut.getClient<DummyServiceClient>(credProvider, region)

        assertThat(fooClient).isSameAs(barClient)
    }

    @Test
    fun oldClientsAreRemovedWhenCredentialsAreRemoved() {
        val sut = getClientManager()

        val credentialsIdentifier = credentialManager.addCredentials("profile:admin")
        val credentialProvider = credentialManager.getAwsCredentialProvider(credentialsIdentifier, regionProvider.defaultRegion())

        sut.getClient<DummyServiceClient>(credentialProvider, anAwsRegion())

        assertThat(sut.cachedClients().keys).anySatisfy {
            assertThat(it.providerId).isEqualTo("profile:admin")
        }

        ApplicationManager.getApplication().messageBus.syncPublisher(CredentialManager.CREDENTIALS_CHANGED).providerRemoved(credentialsIdentifier)

        assertThat(sut.cachedClients().keys).noneSatisfy {
            assertThat(it.providerId).isEqualTo("profile:admin")
        }
    }

    @Test
    fun clientsAreClosedWhenParentIsDisposed() {
        val sut = getClientManager()
        val client = Disposer.newDisposable().use { parent ->
            Disposer.register(parent, sut)

            sut.getClient<DummyServiceClient>(credentialManager.createCredentialProvider(), regionProvider.createAwsRegion()).also {
                assertThat(it.closed).isFalse()
            }
        }

        assertThat(client.closed).isTrue
        assertThat(sut.cachedClients()).isEmpty()
    }

    @Test
    fun clientsAreClosedWhenCredentialProviderIsRemoved() {
        val sut = getClientManager()
        val credentialProviderId = credentialManager.addCredentials()
        val credentialProvider = credentialManager.getAwsCredentialProvider(credentialProviderId, anAwsRegion())
        val client = sut.getClient<DummyServiceClient>(credentialProvider, regionProvider.createAwsRegion())

        assertThat(client.closed).isFalse
        credentialManager.removeCredentials(credentialProviderId)
        assertThat(client.closed).isTrue

        assertThat(sut.cachedClients()).isEmpty()
    }

    @Test
    fun httpClientIsSharedAcrossClients() {
        val sut = getClientManager()
        val dummy = sut.getClient<DummyServiceClient>(credentialManager.createCredentialProvider(), regionProvider.createAwsRegion())
        val secondDummy = sut.getClient<SecondDummyServiceClient>(credentialManager.createCredentialProvider(), regionProvider.createAwsRegion())

        assertThat(dummy.httpClient.delegate).isSameAs(secondDummy.httpClient.delegate)
    }

    @Test
    fun clientWithoutBuilderFailsDescriptively() {
        val sut = getClientManager()

        assertThatThrownBy { sut.getClient<InvalidServiceClient>(credentialManager.createCredentialProvider(), regionProvider.createAwsRegion()) }
            .isInstanceOf(IllegalArgumentException::class.java)
            .hasMessageContaining("builder()")
    }

    @Test
    fun clientInterfaceWithoutNameFieldFailsDescriptively() {
        val sut = getClientManager()

        assertThatThrownBy { sut.getClient<NoServiceNameClient>(credentialManager.createCredentialProvider(), regionProvider.createAwsRegion()) }
            .isInstanceOf(NoSuchFieldException::class.java)
            .hasMessageContaining("SERVICE_METADATA_ID")
    }

    @Test
    fun `tokenProvider can be passed to bearer clients`() {
        val sut = getClientManager()
        val client = sut.getClient<DummyBearerServiceClient>(TokenConnectionSettings(mockTokenProvider(), regionProvider.createAwsRegion()))

        assertThat(client.withTokenProvider).isTrue()
    }

    @Test
    fun `no error thrown when tokenProvider is passed to incompatible client`() {
        val sut = getClientManager()
        sut.getClient<DummyServiceClient>(TokenConnectionSettings(mockTokenProvider(), regionProvider.createAwsRegion()))
    }

    @Test
    fun clientsAreScopedToRegion() {
        val sut = getClientManager()
        val credProvider = credentialManager.createCredentialProvider()

        val firstRegion = sut.getClient<DummyServiceClient>(credProvider, regionProvider.createAwsRegion())
        val secondRegion = sut.getClient<DummyServiceClient>(credProvider, regionProvider.createAwsRegion())

        assertThat(secondRegion).isNotSameAs(firstRegion)
    }

    @Test
    fun globalServicesCanBeGivenAnyRegion() {
        val sut = getClientManager()
        regionProvider.addService(
            "DummyService",
            Service(
                endpoints = mapOf("global" to Endpoint()),
                isRegionalized = false,
                partitionEndpoint = "global"
            )
        )
        val credProvider = credentialManager.createCredentialProvider()

        val first = sut.getClient<DummyServiceClient>(credProvider, regionProvider.createAwsRegion(partitionId = "test"))
        val second = sut.getClient<DummyServiceClient>(credProvider, regionProvider.createAwsRegion(partitionId = "test"))

        assertThat(first.serviceName()).isEqualTo("dummyClient")
        assertThat(second).isSameAs(first)
    }

    @Test
    fun clientCustomizationIsAppliedToManagedClients() {
        wireMockRule.stubFor(any(anyUrl()).willReturn(aResponse().withStatus(200)))

        val aConnection = ConnectionSettings(credentialManager.createCredentialProvider(), regionProvider.createAwsRegion())
        val customizer = ToolkitClientCustomizer { credentialProvider, _, region, builder, _ ->
            assertThat(credentialProvider).isEqualTo(aConnection.credentials)
            assertThat(region).isEqualTo(aConnection.region.id)

            builder.endpointOverride(URI.create(wireMockRule.baseUrl()))
        }
        ExtensionTestUtil.maskExtensions(CUSTOMIZER_EP, listOf(customizer), disposableRule.disposable)

        getClientManager().getClient(LambdaClient::class, aConnection).use {
            it.listFunctions()
        }

        wireMockRule.verify(anyRequestedFor(anyUrl()))
    }

    @Test
    fun clientCustomizationIsAppliedToUnmanagedClients() {
        wireMockRule.stubFor(any(anyUrl()).willReturn(aResponse().withStatus(200)))

        val aConnection = ConnectionSettings(credentialManager.createCredentialProvider(), regionProvider.createAwsRegion())
        val customizer = ToolkitClientCustomizer { credentialProvider, _, region, builder, _ ->
            assertThat(credentialProvider).isEqualTo(aConnection.credentials)
            assertThat(region).isEqualTo(aConnection.region.id)

            builder.endpointOverride(URI.create(wireMockRule.baseUrl()))
        }
        ExtensionTestUtil.maskExtensions(CUSTOMIZER_EP, listOf(customizer), disposableRule.disposable)

        getClientManager().createUnmanagedClient<LambdaClient>(aConnection.credentials, Region.of(aConnection.region.id)).use {
            it.listFunctions()
        }

        wireMockRule.verify(anyRequestedFor(anyUrl()))
    }

    @Test
    fun userAgentIsPassedForManagedClients() {
        wireMockRule.stubFor(any(anyUrl()).willReturn(aResponse().withStatus(200)))

        val aConnection = ConnectionSettings(credentialManager.createCredentialProvider(), regionProvider.createAwsRegion())
        val customizer = ToolkitClientCustomizer { _, _, _, builder, _ ->
            builder.endpointOverride(URI.create(wireMockRule.baseUrl()))
        }
        ExtensionTestUtil.maskExtensions(CUSTOMIZER_EP, listOf(customizer), disposableRule.disposable)

        getClientManager().getClient<LambdaClient>(aConnection).use {
            it.listFunctions()
        }

        wireMockRule.verify(anyRequestedFor(urlPathMatching("(.*)/functions/")).withHeader("User-Agent", ContainsPattern("AWS-Toolkit-For-JetBrains/")))
    }

    @Test
    fun userAgentIsPassedForUnmanagedClients() {
        wireMockRule.stubFor(any(anyUrl()).willReturn(aResponse().withStatus(200)))

        val aConnection = ConnectionSettings(credentialManager.createCredentialProvider(), regionProvider.createAwsRegion())
        val customizer = ToolkitClientCustomizer { _, _, _, builder, _ ->
            builder.endpointOverride(URI.create(wireMockRule.baseUrl()))
        }
        ExtensionTestUtil.maskExtensions(CUSTOMIZER_EP, listOf(customizer), disposableRule.disposable)

        getClientManager().createUnmanagedClient<LambdaClient>(aConnection.credentials, Region.of(aConnection.region.id)).use {
            it.listFunctions()
        }

        wireMockRule.verify(anyRequestedFor(urlPathMatching("(.*)/functions/")).withHeader("User-Agent", ContainsPattern("AWS-Toolkit-For-JetBrains/")))
    }

    private fun mockTokenProvider() = mock<ToolkitBearerTokenProviderDelegate>().let {
        whenever(it.id).thenReturn(aString())

        ToolkitBearerTokenProvider(it)
    }

    // Test against real version so bypass ServiceManager for the client manager
    private fun getClientManager() = AwsClientManager()

    class DummyServiceClient(val httpClient: SdkHttpClient) : TestClient() {
        companion object {
            @Suppress("unused")
            @JvmStatic
            fun builder() = DummyServiceClientBuilder()
        }
    }

    class DummyServiceClientBuilder : TestClientBuilder<DummyServiceClientBuilder, DummyServiceClient>() {
        override fun serviceName(): String = "DummyService"

        override fun signingName(): String = serviceName()

        override fun buildClient() = DummyServiceClient(syncClientConfiguration().option(SdkClientOption.SYNC_HTTP_CLIENT))
    }

    class DummyBearerServiceClient(val httpClient: SdkHttpClient, val withTokenProvider: Boolean = false) : TestClient() {
        companion object {
            @Suppress("unused")
            @JvmStatic
            fun builder() = DummyBearerServiceClientBuilder()
        }
    }

    class DummyBearerServiceClientBuilder : TestClientBuilder<DummyBearerServiceClientBuilder, DummyBearerServiceClient>() {
        private var tokenProviderMethodInvoked = false

        fun tokenProvider(ignored: SdkTokenProvider): DummyBearerServiceClientBuilder {
            tokenProviderMethodInvoked = true

            return this
        }

        override fun serviceName(): String = "DummyBearerService"

        override fun signingName(): String = serviceName()

        override fun buildClient() = DummyBearerServiceClient(syncClientConfiguration().option(SdkClientOption.SYNC_HTTP_CLIENT), tokenProviderMethodInvoked)
    }

    class SecondDummyServiceClient(val httpClient: SdkHttpClient) : TestClient() {
        companion object {
            @Suppress("unused")
            @JvmStatic
            fun builder() = SecondDummyServiceClientBuilder()
        }
    }

    class SecondDummyServiceClientBuilder :
        TestClientBuilder<SecondDummyServiceClientBuilder, SecondDummyServiceClient>() {
        override fun serviceName(): String = "SecondDummyService"

        override fun signingName(): String = serviceName()

        override fun buildClient() = SecondDummyServiceClient(syncClientConfiguration().option(SdkClientOption.SYNC_HTTP_CLIENT))
    }

    class InvalidServiceClient : TestClient() {
        override fun close() {}

        override fun serviceName() = "invalidClient"
    }

    class NoServiceNameClient : SdkClient {
        override fun close() {}

        override fun serviceName() = "invalidClient"
    }

    abstract class TestClient : SdkClient, AutoCloseable {
        var closed = false

        override fun serviceName() = "dummyClient"

        override fun close() {
            closed = true
        }

        companion object {
            @Suppress("unused", "MayBeConstant")
            @JvmField
            val SERVICE_METADATA_ID = "DummyService"
        }
    }

    abstract class TestClientBuilder<B : AwsClientBuilder<B, C>, C> : AwsDefaultClientBuilder<B, C>() {
        init {
            overrideConfiguration {
                it.advancedOptions(mapOf(SdkAdvancedClientOption.SIGNER to Signer { _, _ -> throw NotImplementedError() }))
            }
        }

        override fun serviceEndpointPrefix() = "dummyClient"
    }

    private val SdkHttpClient.delegate: SdkHttpClient
        get() {
            val delegateProperty = this::class.declaredMemberProperties.find { it.name == "delegate" }
                ?: throw IllegalArgumentException(
                    "Expected instance of software.amazon.awssdk.core.client.builder.SdkDefaultClientBuilder.NonManagedSdkHttpClient"
                )
            delegateProperty.isAccessible = true
            return delegateProperty.call(this) as SdkHttpClient
        }
}


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

package software.aws.toolkits.jetbrains.core

import com.intellij.openapi.application.ApplicationManager
import com.intellij.testFramework.ProjectRule
import com.intellij.testFramework.RuleChain
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.jupiter.api.assertThrows
import org.mockito.kotlin.any
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
import org.mockito.kotlin.reset
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
import software.aws.toolkits.core.ClientConnectionSettings
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.credentials.CredentialIdentifier
import software.aws.toolkits.core.credentials.ToolkitCredentialsProvider
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.core.utils.test.aString
import software.aws.toolkits.core.utils.test.retryableAssert
import software.aws.toolkits.jetbrains.core.credentials.CredentialManager
import software.aws.toolkits.jetbrains.core.credentials.MockCredentialManagerRule
import software.aws.toolkits.jetbrains.core.region.MockRegionProviderRule
import software.aws.toolkits.jetbrains.core.utils.buildList
import software.aws.toolkits.jetbrains.utils.hasCauseWithMessage
import software.aws.toolkits.jetbrains.utils.hasException
import software.aws.toolkits.jetbrains.utils.hasValue
import software.aws.toolkits.jetbrains.utils.value
import software.aws.toolkits.jetbrains.utils.wait
import java.time.Clock
import java.time.Duration
import java.time.Instant
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CountDownLatch
import java.util.concurrent.ExecutionException
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicInteger

@ExperimentalCoroutinesApi
class AwsResourceCacheTest {
    private val projectRule = ProjectRule()
    private val credentialsManager = MockCredentialManagerRule()
    private val regionProvider = MockRegionProviderRule()

    // If we don't control the order manually, regionProvider can run before projectRule which causes a NPE
    @Rule
    @JvmField
    val ruleChain = RuleChain(
        projectRule,
        credentialsManager,
        regionProvider
    )

    private val mockClock = mock<Clock>()
    private val mockResource = mock<Resource.Cached<String>>()

    private lateinit var sut: AwsResourceCache

    private lateinit var cred1Identifier: CredentialIdentifier
    private lateinit var cred1Provider: ToolkitCredentialsProvider
    private lateinit var cred2Identifier: CredentialIdentifier
    private lateinit var cred2Provider: ToolkitCredentialsProvider
    private lateinit var connectionSettings: ConnectionSettings

    @Before
    fun setUp() {
        cred1Identifier = credentialsManager.addCredentials("Cred1")
        cred1Provider = credentialsManager.getAwsCredentialProvider(cred1Identifier, regionProvider.defaultRegion())

        cred2Identifier = credentialsManager.addCredentials("Cred2")
        cred2Provider = credentialsManager.getAwsCredentialProvider(cred2Identifier, regionProvider.defaultRegion())

        connectionSettings = ConnectionSettings(credentialsManager.createCredentialProvider(), regionProvider.createAwsRegion())

        if (this::sut.isInitialized) {
            (sut as DefaultAwsResourceCache).dispose()
        }
        sut = DefaultAwsResourceCache(mockClock, 1000, Duration.ofMinutes(1))

        reset(mockClock, mockResource)
        whenever(mockResource.expiry()).thenReturn(DEFAULT_EXPIRY)
        whenever(mockResource.id).thenReturn("mock")
        whenever(mockClock.instant()).thenReturn(Instant.now())
    }

    @Test
    fun basicCachingWorks() {
        whenever(mockResource.fetch(any())).thenReturn("hello")

        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("hello")
        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("hello")

        verifyResourceCalled(times = 1)
    }

    @Test
    fun expirationWorks() {
        whenever(mockResource.fetch(any())).thenReturn("hello")

        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("hello")
        whenever(mockClock.instant()).thenReturn(Instant.now().plus(DEFAULT_EXPIRY))
        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("hello")

        verifyResourceCalled(times = 2)
    }

    @Test
    fun exceptionsAreBubbledWhenNoEntry() {
        doAnswer { throw Throwable("Bang!") }.whenever(mockResource).fetch(any())
        assertThatThrownBy { sut.getResource(mockResource, connectionSettings).value }.hasCauseWithMessage("Bang!")
    }

    @Test
    fun exceptionsAreLoggedButStaleEntryReturnedByDefault() {
        whenever(mockResource.fetch(any())).thenReturn("hello").doThrow(RuntimeException("BOOM"))

        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("hello")
        whenever(mockClock.instant()).thenReturn(Instant.now().plus(DEFAULT_EXPIRY))
        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("hello")
    }

    @Test
    fun exceptionsAreBubbledWhenExistingEntryExpiredAndUseStaleIsFalse() {
        whenever(mockResource.fetch(any())).thenReturn("hello").doThrow(RuntimeException("BOOM"))

        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("hello")
        whenever(mockClock.instant()).thenReturn(Instant.now().plus(DEFAULT_EXPIRY))
        assertThat(sut.getResource(mockResource, connectionSettings, useStale = false)).hasException
    }

    @Test
    fun cacheEntriesAreSeparatedByRegionAndCredentials() {
        whenever(mockResource.fetch(any())).thenAnswer {
            val (cred, region) = it.getArgument<ConnectionSettings>(0)
            "${region.id}-${cred.id}"
        }

        assertThat(sut.getResource(mockResource, region = US_WEST_1, credentialProvider = cred1Provider)).hasValue("us-west-1-Cred1")
        assertThat(sut.getResource(mockResource, region = US_WEST_2, credentialProvider = cred1Provider)).hasValue("us-west-2-Cred1")
        assertThat(sut.getResource(mockResource, region = US_WEST_1, credentialProvider = cred2Provider)).hasValue("us-west-1-Cred2")
        assertThat(sut.getResource(mockResource, region = US_WEST_2, credentialProvider = cred2Provider)).hasValue("us-west-2-Cred2")
        assertThat(sut.getResource(mockResource, region = US_WEST_2, credentialProvider = cred2Provider)).hasValue("us-west-2-Cred2")

        verifyResourceCalled(times = 4)
    }

    @Test
    fun cacheCanBeCleared() {
        whenever(mockResource.fetch(any())).thenReturn("hello").thenReturn("goodbye")

        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("hello")
        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("hello")

        runBlocking { sut.clear() }

        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("goodbye")
        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("goodbye")

        verifyResourceCalled(times = 2)
    }

    @Test
    fun cacheCanBeClearedByKey() {
        whenever(mockResource.fetch(any())).thenReturn("hello").thenReturn("goodbye")

        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("hello")
        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("hello")

        sut.clear(mockResource, connectionSettings)

        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("goodbye")
        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("goodbye")

        verifyResourceCalled(times = 2)
    }

    @Test
    fun cacheCanBeClearedByKeyAndConnection() {
        val incrementer = AtomicInteger(0)
        whenever(mockResource.fetch(any())).thenAnswer {
            val (cred, region) = it.getArgument<ConnectionSettings>(0)
            "${region.id}-${cred.id}-${incrementer.getAndIncrement()}"
        }

        val usw1Cred1 = sut.getResource(mockResource, US_WEST_1, cred1Provider).value
        val usw1Cred2 = sut.getResource(mockResource, US_WEST_1, cred2Provider).value
        val usw2Cred1 = sut.getResource(mockResource, US_WEST_2, cred1Provider).value
        val usw2Cred2 = sut.getResource(mockResource, US_WEST_2, cred2Provider).value

        sut.clear(mockResource, ConnectionSettings(cred1Provider, US_WEST_1))

        assertThat(sut.getResource(mockResource, US_WEST_1, cred1Provider)).wait().isCompletedWithValueMatching { it != usw1Cred1 }
        assertThat(sut.getResource(mockResource, US_WEST_1, cred2Provider)).hasValue(usw1Cred2)
        assertThat(sut.getResource(mockResource, US_WEST_2, cred1Provider)).hasValue(usw2Cred1)
        assertThat(sut.getResource(mockResource, US_WEST_2, cred2Provider)).hasValue(usw2Cred2)
        verifyResourceCalled(times = 5)
    }

    @Test
    fun canForceCacheRefresh() {
        whenever(mockResource.fetch(any())).thenReturn("hello").thenReturn("goodbye")

        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("hello")
        assertThat(sut.getResource(mockResource, connectionSettings, forceFetch = true)).hasValue("goodbye")
        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("goodbye")

        verifyResourceCalled(times = 2)
    }

    @Test
    fun expirationOccursOnExpiryTime() {
        assertExpectedExpiryFunctions({ minusMillis(1) }, shouldExpire = false) // before expiry
        assertExpectedExpiryFunctions({ this }, shouldExpire = true) // on expiry
        assertExpectedExpiryFunctions({ plusMillis(1) }, shouldExpire = true) // after expiry
    }

    @Test
    fun viewsCanBeCreatedOnTopOfOtherCachedItems() {
        whenever(mockResource.fetch(any())).thenReturn("hello")
        val viewResource = Resource.view(mockResource) { toList() }

        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("hello")
        assertThat(sut.getResource(viewResource, connectionSettings)).hasValue(listOf('h', 'e', 'l', 'l', 'o'))
        verifyResourceCalled(times = 1)
    }

    @Test
    fun mapFilterAndFindExtensionsToEasilyCreateViews() {
        whenever(mockResource.fetch(any())).thenReturn("hello")
        val viewResource = Resource.view(mockResource) { toList() }

        val filteredAndMapped = viewResource.filter { it != 'l' }.map { it.toUpperCase() }
        assertThat(sut.getResource(filteredAndMapped, connectionSettings)).hasValue(listOf('H', 'E', 'O'))

        val find = viewResource.find { it == 'l' }
        assertThat(sut.getResource(find, connectionSettings)).hasValue('l')
    }

    @Test
    fun clearingViewsClearTheUnderlyingCachedResource() {
        whenever(mockResource.fetch(any())).thenReturn("hello")
        val viewResource = Resource.view(mockResource) { toList() }
        sut.getResource(viewResource, connectionSettings).value
        sut.clear(viewResource, connectionSettings)
        sut.getResource(viewResource, connectionSettings).value

        verifyResourceCalled(times = 2)
    }

    @Test
    fun viewsCanBeRegionAware() {
        whenever(mockResource.fetch(any())).thenReturn("hello")
        val viewResource = Resource.View(mockResource) { _, region -> region }

        assertThat(sut.getResource(viewResource, connectionSettings)).hasValue(connectionSettings.region)
    }

    @Test
    fun cacheIsRegularlyPrunedToEnsureItDoesntGrowTooLarge() {
        val localSut = DefaultAwsResourceCache(mockClock, 5, Duration.ofMillis(50))

        val now = Instant.now()
        whenever(mockClock.instant()).thenReturn(now)

        localSut.getResource(StringResource("1"), connectionSettings).value

        whenever(mockClock.instant()).thenReturn(now.plusMillis(10))

        localSut.getResource(StringResource("2"), connectionSettings).value
        localSut.getResource(StringResource("3"), connectionSettings).value
        localSut.getResource(StringResource("4"), connectionSettings).value
        localSut.getResource(StringResource("5"), connectionSettings).value
        localSut.getResource(StringResource("6"), connectionSettings).value

        retryableAssert {
            assertThat(localSut.getResourceIfPresent(StringResource("1"), connectionSettings)).isNull()
        }
    }

    @Test
    fun pruningConsidersCollectionEntriesBasedOnTheirSize() {
        val localSut = DefaultAwsResourceCache(mockClock, 5, Duration.ofMillis(50))

        val listResource = DummyResource("list", listOf("a", "b", "c", "d"))
        val now = Instant.now()
        whenever(mockClock.instant()).thenReturn(now)

        localSut.getResource(listResource, connectionSettings).value

        whenever(mockClock.instant()).thenReturn(now.plusMillis(10))

        localSut.getResource(StringResource("1"), connectionSettings).value
        localSut.getResource(StringResource("2"), connectionSettings).value

        retryableAssert {
            assertThat(localSut.getResourceIfPresent(listResource, connectionSettings)).isNull()
            assertThat(localSut.getResourceIfPresent(StringResource("1"), connectionSettings)).isNotEmpty()
            assertThat(localSut.getResourceIfPresent(StringResource("2"), connectionSettings)).isNotEmpty()
        }
    }

    @Test
    fun multipleCallsInDifferentThreadsStillOnlyCallTheUnderlyingResourceOnce() {
        whenever(mockResource.fetch(any())).thenReturn("hello")
        val concurrency = 200

        val latch = CountDownLatch(1)
        val executor = Executors.newFixedThreadPool(concurrency)
        try {
            val futures = (1 until concurrency).map {
                val future = CompletableFuture<String>()
                executor.submit {
                    latch.await()
                    sut.getResource(mockResource, connectionSettings).whenComplete { result, error ->
                        when {
                            result != null -> future.complete(result)
                            error != null -> future.completeExceptionally(error)
                        }
                    }
                }
                future
            }.toTypedArray()
            latch.countDown()
            CompletableFuture.allOf(*futures).get(10, TimeUnit.SECONDS)
        } finally {
            executor.shutdown()
        }

        verifyResourceCalled(times = 1)
    }

    @Test
    fun multipleCallsWhileFetchPendingCallTheUnderlyingResourceOnce() {
        val latch = CountDownLatch(1)
        whenever(mockResource.fetch(any())).thenAnswer {
            // don't allow the task to finish until all futures have been created
            latch.await()
            return@thenAnswer "hello"
        }

        val futures = buildList<CompletableFuture<String>> {
            repeat(20) {
                // simulate multiple calls to the same resource
                add(sut.getResource(mockResource, connectionSettings).toCompletableFuture())
            }
        }.toTypedArray()
        latch.countDown()

        // all futures should complete
        CompletableFuture.allOf(*futures).value

        // and we should have reused the same task for all of the requests
        verifyResourceCalled(times = 1)
    }

    @Test
    fun cachingShouldBeBasedOnResourceId() {
        val first = StringResource("first")
        val anotherFirst = StringResource("first")

        sut.getResource(first, connectionSettings).value
        sut.getResource(anotherFirst, connectionSettings).value

        assertThat(first.callCount).hasValue(1)
        assertThat(anotherFirst.callCount).hasValue(0)
    }

    @Test
    fun whenACredentialIdIsRemovedItsEntriesAreRemovedFromTheCache() {
        whenever(mockResource.fetch(any())).thenReturn("hello")
        getAllRegionAndCredPermutations()

        ApplicationManager.getApplication().messageBus.syncPublisher(CredentialManager.CREDENTIALS_CHANGED).providerRemoved(cred1Identifier)

        getAllRegionAndCredPermutations()

        verify(mockResource, times(2)).fetch(ConnectionSettings(cred1Provider, US_WEST_1))
        verify(mockResource, times(2)).fetch(ConnectionSettings(cred1Provider, US_WEST_2))
        verify(mockResource, times(1)).fetch(ConnectionSettings(cred2Provider, US_WEST_1))
        verify(mockResource, times(1)).fetch(ConnectionSettings(cred2Provider, US_WEST_2))
    }

    @Test
    fun whenACredentialIdIsModifiedItsEntriesAreRemovedFromTheCache() {
        whenever(mockResource.fetch(any())).thenReturn("hello")
        getAllRegionAndCredPermutations()

        ApplicationManager.getApplication().messageBus.syncPublisher(CredentialManager.CREDENTIALS_CHANGED).providerModified(cred1Identifier)

        getAllRegionAndCredPermutations()

        verify(mockResource, times(2)).fetch(ConnectionSettings(cred1Provider, US_WEST_1))
        verify(mockResource, times(2)).fetch(ConnectionSettings(cred1Provider, US_WEST_2))
        verify(mockResource, times(1)).fetch(ConnectionSettings(cred2Provider, US_WEST_1))
        verify(mockResource, times(1)).fetch(ConnectionSettings(cred2Provider, US_WEST_2))
    }

    @Test
    fun cacheExposesBlockingApi() {
        whenever(mockResource.fetch(any())).thenReturn("hello")
        assertThat(sut.getResourceNow(mockResource, connectionSettings)).isEqualTo("hello")
    }

    @Test
    fun cacheExposesBlockingApiWithRegionAndCred() {
        whenever(mockResource.fetch(any())).thenReturn("hello")
        assertThat(sut.getResourceNow(mockResource, US_WEST_1, cred1Provider)).isEqualTo("hello")
        verify(mockResource).fetch(ConnectionSettings(cred1Provider, US_WEST_1))
    }

    @Test
    fun cacheExposesBlockingApiWhereExecutionExceptionIsUnwrapped() {
        whenever(mockResource.fetch(any())).thenThrow(RuntimeException("boom"))
        assertThatThrownBy { sut.getResourceNow(mockResource, connectionSettings, timeout = Duration.ofSeconds(1)) }
            .isInstanceOf(RuntimeException::class.java)
            .hasMessage("boom")
    }

    @Test
    fun cacheExposesBlockingApiWithTimeout() {
        whenever(mockResource.fetch(any())).thenAnswer {
            Thread.sleep(50)
            "hello"
        }
        assertThatThrownBy { sut.getResourceNow(mockResource, connectionSettings, timeout = Duration.ofMillis(5)) }.isInstanceOf(TimeoutException::class.java)
    }

    @Test
    fun canConditionallyFetchOnlyIfAvailableInCache() {
        whenever(mockResource.fetch(any())).thenReturn("hello")

        assertThat(sut.getResourceIfPresent(mockResource, US_WEST_1, cred1Provider)).isNull()
        sut.getResource(mockResource, US_WEST_1, cred1Provider).value
        assertThat(sut.getResourceIfPresent(mockResource, US_WEST_1, cred1Provider)).isEqualTo("hello")
    }

    @Test
    fun canConditionallyFetchOnlyIfAvailableInCacheAndRespectExpiry() {
        whenever(mockResource.fetch(any())).thenReturn("hello")

        val now = Instant.now()
        whenever(mockClock.instant()).thenReturn(now)
        sut.getResource(mockResource, US_WEST_1, cred1Provider).value

        whenever(mockClock.instant()).thenReturn(now.plus(DEFAULT_EXPIRY).plusMillis(1))
        assertThat(sut.getResourceIfPresent(mockResource, US_WEST_1, cred1Provider, useStale = false)).isNull()
    }

    @Test
    fun canConditionallyFetchViewOnlyIfAvailableInCache() {
        whenever(mockResource.fetch(any())).thenReturn("hello")
        val viewResource = Resource.view(mockResource) { reversed() }

        assertThat(sut.getResourceIfPresent(viewResource, US_WEST_1, cred1Provider)).isNull()
        sut.getResource(viewResource, US_WEST_1, cred1Provider).value
        assertThat(sut.getResourceIfPresent(viewResource, US_WEST_1, cred1Provider)).isEqualTo("olleh")
    }

    @Test
    fun canConditionallyFetchOnlyIfAvailableWithoutExplicitCredentialsRegion() {
        whenever(mockResource.fetch(any())).thenReturn("hello")
        sut.getResource(mockResource, connectionSettings).value

        assertThat(sut.getResourceIfPresent(mockResource, connectionSettings)).isEqualTo("hello")
    }

    @Test
    fun concurrentlyRunningExceptionalResourcesGetTheSameException() {
        val latch = CountDownLatch(1)
        whenever(mockResource.fetch(any())).then {
            latch.await()
            // exception gets thrown fast enough where the second fetchIfNeeded check occurs after the first call throws
            runBlockingTest {
                delay(500)
            }
            throw RuntimeException("Boom")
        }

        val first = sut.getResource(mockResource, connectionSettings)
        val second = sut.getResource(mockResource, connectionSettings)
        latch.countDown()
        assertThatThrownBy { first.value }.hasCauseWithMessage("Boom")
        assertThatThrownBy { second.value }.hasCauseWithMessage("Boom")
        verifyResourceCalled(1, mockResource)
    }

    @Test
    fun canRecoverFromACachedException() {
        whenever(mockResource.fetch(any())).thenThrow(RuntimeException("Boom")).thenReturn("Success!")
        assertThrows<ExecutionException> { sut.getResource(mockResource, connectionSettings).value }
        assertThat(sut.getResource(mockResource, connectionSettings).value).isEqualTo("Success!")
    }

    @Test
    fun retriesReturnTheMostRecentException() {
        whenever(mockResource.fetch(any())).thenThrow(RuntimeException("Boom"), RuntimeException("Ouch"))
        assertThatThrownBy { sut.getResource(mockResource, connectionSettings).value }.hasCauseWithMessage("Boom")
        assertThatThrownBy { sut.getResource(mockResource, connectionSettings).value }.hasCauseWithMessage("Ouch")
    }

    @Test
    fun exceptionalEntriesAreRemovedFromTheCache() {
        whenever(mockResource.fetch(any())).thenThrow(RuntimeException("Boom"))
        assertThrows<ExecutionException> { sut.getResource(mockResource, connectionSettings).value }

        with(sut as DefaultAwsResourceCache) {
            doRunCacheMaintenance()
            assertThat(hasCacheEntry(mockResource.id)).isFalse
        }
    }

    private fun getAllRegionAndCredPermutations() {
        sut.getResource(mockResource, US_WEST_1, cred1Provider).value
        sut.getResource(mockResource, US_WEST_2, cred1Provider).value
        sut.getResource(mockResource, US_WEST_1, cred2Provider).value
        sut.getResource(mockResource, US_WEST_2, cred2Provider).value
    }

    private fun assertExpectedExpiryFunctions(expiryFunction: Instant.() -> Instant, shouldExpire: Boolean) {
        whenever(mockResource.fetch(any())).thenReturn("hello", "goodbye")
        whenever(mockResource.expiry()).thenReturn(Duration.ofSeconds(1))
        val now = Instant.now()
        val expiry = now.plus(Duration.ofSeconds(1))
        whenever(mockClock.instant()).thenReturn(now)
        assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("hello")

        whenever(mockClock.instant()).thenReturn(expiryFunction(expiry))
        when (shouldExpire) {
            true -> assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("goodbye")
            false -> assertThat(sut.getResource(mockResource, connectionSettings)).hasValue("hello")
        }

        runBlocking { sut.clear() }
    }

    private fun verifyResourceCalled(times: Int, resource: Resource.Cached<*> = mockResource) {
        verify(resource, times(times)).fetch(any())
        verify(resource, times(times)).expiry()
        verify(resource, atLeastOnce()).id
        verifyNoMoreInteractions(resource)
    }

    companion object {
        private val US_WEST_1 = AwsRegion("us-west-1", "USW1", "aws")
        private val US_WEST_2 = AwsRegion("us-west-2", "USW2", "aws")

        private val DEFAULT_EXPIRY = Duration.ofMinutes(10)

        private open class DummyResource<T>(override val id: String, private val value: T) : Resource.Cached<T>() {
            val callCount = AtomicInteger(0)

            override fun fetch(connectionSettings: ClientConnectionSettings<*>): T {
                callCount.getAndIncrement()
                return value
            }
        }

        private class StringResource(id: String) : DummyResource<String>(id, id)

        fun dummyResource(value: String = aString()): Resource.Cached<String> = StringResource(value)
    }
}

val Resource<*>.id: String
    get() = when (this) {
        is Resource.Cached -> this.id
        is Resource.View<*, *> -> this.underlying.id
    }


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

package software.aws.toolkits.jetbrains.core

import com.github.tomakehurst.wiremock.client.WireMock.aResponse
import com.github.tomakehurst.wiremock.client.WireMock.any
import com.github.tomakehurst.wiremock.client.WireMock.stubFor
import com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo
import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig
import com.github.tomakehurst.wiremock.junit.WireMockRule
import com.intellij.openapi.util.Disposer
import com.intellij.testFramework.ApplicationRule
import com.intellij.util.net.ssl.CertificateManager
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import software.amazon.awssdk.http.HttpExecuteRequest
import software.amazon.awssdk.http.SdkHttpFullRequest
import software.amazon.awssdk.http.SdkHttpMethod
import java.net.URI

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

    @Rule
    @JvmField
    val wireMock = createSelfSignedServer()

    @Before
    fun setUp() {
        stubFor(any(urlPathEqualTo("/")).willReturn(aResponse().withStatus(200)))
    }

    @After
    fun tearDown() {
        CertificateManager.getInstance().customTrustManager.removeCertificate("selfsign")
    }

    @Test
    fun testCertGetsTrusted() {
        val trustManager = CertificateManager.getInstance().customTrustManager
        val initialSize = trustManager.certificates.size

        val request = mockSdkRequest("https://localhost:" + wireMock.httpsPort())

        val awsSdkClient = AwsSdkClient()
        val response = try {
            awsSdkClient.sharedSdkClient().prepareRequest(
                HttpExecuteRequest.builder().request(request).build()
            ).call()
        } finally {
            Disposer.dispose(awsSdkClient)
        }

        assertThat(response.httpResponse().isSuccessful).isTrue()

        assertThat(trustManager.certificates).hasSize(initialSize + 1)
        assertThat(trustManager.containsCertificate("selfsign")).isTrue()
    }

    private fun mockSdkRequest(uriString: String): SdkHttpFullRequest? {
        val uri = URI.create(uriString)
        return SdkHttpFullRequest.builder()
            .uri(uri)
            .method(SdkHttpMethod.GET)
            .build()
    }

    private fun createSelfSignedServer(): WireMockRule {
        val selfSignedJks = AwsSdkClientTest::class.java.getResource("/selfSigned.jks")
        return WireMockRule(
            wireMockConfig()
                .dynamicHttpsPort()
                .keystorePath(selfSignedJks.toString())
                .keystorePassword("changeit")
                .keyManagerPassword("changeit")
                .keystoreType("jks")
        )
    }
}


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

package software.aws.toolkits.jetbrains.core

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.ExtensionTestUtil
import com.intellij.testFramework.ProjectRule
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.credentials.ToolkitCredentialsProvider
import software.aws.toolkits.jetbrains.core.credentials.MockCredentialManagerRule
import software.aws.toolkits.jetbrains.core.executables.ExecutableManager
import software.aws.toolkits.jetbrains.core.executables.ExecutableType
import software.aws.toolkits.jetbrains.core.executables.Validatable
import software.aws.toolkits.jetbrains.core.region.getDefaultRegion
import java.nio.file.Path
import java.util.concurrent.TimeUnit

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

    @Rule
    @JvmField
    val tempFolder = TemporaryFolder()

    @Rule
    @JvmField
    val disposableRule = DisposableRule()

    @Rule
    @JvmField
    val credentialManager = MockCredentialManagerRule()

    @Before
    fun setUp() {
        ExtensionTestUtil.maskExtensions(ExecutableType.EP_NAME, listOf(MockExecutable), disposableRule.disposable)
    }

    @Test
    fun testExecutableIsNotInstalledCausesException() {
        createMockExecutable("invalidBinary")

        assertThatThrownBy {
            executeCacheResource {}
        }.isInstanceOf(IllegalStateException::class.java)
    }

    @Test
    fun executablePathIsSet() {
        val path = createMockExecutable("validBinary")

        executeCacheResource {
            assertThat(this.exePath).isEqualTo(path.toAbsolutePath().toString())
        }
    }

    @Test
    fun regionIsSet() {
        createMockExecutable("validBinary")

        executeCacheResource {
            assertThat(this.environment).containsKey("AWS_REGION")
        }
    }

    @Test
    fun credentialsAreSet() {
        createMockExecutable("validBinary")

        executeCacheResource {
            assertThat(this.environment).containsKey("AWS_ACCESS_KEY_ID").containsKey("AWS_SECRET_ACCESS_KEY")
        }
    }

    @Test
    fun resultsAreReturned() {
        createMockExecutable("validBinary")

        assertThat(executeCacheResource { "Hello" }).isEqualTo("Hello")
    }

    private fun createMockExecutable(name: String): Path {
        val path = tempFolder.newFile(name).toPath()

        ExecutableManager.getInstance().setExecutablePath(MockExecutable, path).toCompletableFuture()
            .get(1, TimeUnit.SECONDS)

        return path
    }

    private fun <T> executeCacheResource(assertionBlock: GeneralCommandLine.() -> T): T {
        val cacheResource = ExecutableBackedCacheResource(MockExecutable::class, "mock", null) {
            assertionBlock.invoke(this)
        }

        return cacheResource.fetch(ConnectionSettings(mockCredentials(), getDefaultRegion()))
    }

    private fun mockCredentials(): ToolkitCredentialsProvider =
        credentialManager.getAwsCredentialProvider(credentialManager.addCredentials("Cred2"), getDefaultRegion())

    private object MockExecutable : ExecutableType<String>, Validatable {
        override val id: String = "Mock"
        override val displayName: String = "Mock Executable"
        override fun version(path: Path): String = "1.0"

        override fun validate(path: Path) {
            check(path.fileName.toString() == "validBinary")
        }
    }
}


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

package software.aws.toolkits.jetbrains.core

import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.testFramework.common.ThreadLeakTracker
import com.intellij.testFramework.replaceService
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.rules.ExternalResource
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.amazon.awssdk.auth.token.credentials.SdkTokenProvider
import software.amazon.awssdk.core.SdkClient
import software.amazon.awssdk.regions.Region
import software.aws.toolkits.core.ToolkitClientCustomizer
import software.aws.toolkits.core.ToolkitClientManager
import software.aws.toolkits.core.clients.SdkClientProvider
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.delegateMock
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
import kotlin.reflect.KClass

class MockClientManager : AwsClientManager() {
    private data class Key(
        val clazz: KClass<out SdkClient>,
        val regionId: String? = null,
        val credProviderId: AwsCredentialsProvider? = null
    )

    private val mockClients = mutableMapOf<Key, SdkClient>()

    @Suppress("UNCHECKED_CAST")
    override fun <T : SdkClient> constructAwsClient(
        sdkClass: KClass<T>,
        credProvider: AwsCredentialsProvider?,
        tokenProvider: SdkTokenProvider?,
        region: Region,
        endpointOverride: String?,
        clientCustomizer: ToolkitClientCustomizer?
    ): T = mockClients[Key(sdkClass, region.id(), credProvider)] as? T
        ?: mockClients[Key(sdkClass)] as? T
        ?: throw IllegalStateException("No mock registered for $sdkClass")

    override fun dispose() {
        super.dispose()
        mockClients.clear()
    }

    // Note: You must pass KClass of the interface, since we do not do instanceof checks, but == on the classes
    // This will lead to comparing the anonymous mock proxy to the interface and it will fail
    @Deprecated("Do not use, use MockClientManagerRule")
    fun register(clazz: KClass<out SdkClient>, sdkClient: SdkClient) {
        mockClients[Key(clazz)] = sdkClient
    }

    @Deprecated("Do not use, use MockClientManagerRule")
    fun <T : SdkClient> register(clazz: KClass<out SdkClient>, sdkClient: T, region: AwsRegion, credProvider: ToolkitCredentialsProvider) {
        mockClients[Key(clazz, region.id, credProvider)] = sdkClient
    }

    companion object {
        /**
         * Replaces all required test services with the real version for the life of the [Disposable] to allow calls to AWS to succeed
         */
        fun useRealImplementations(disposable: Disposable) {
            val clientManager = AwsClientManager()
            ApplicationManager.getApplication().replaceService(ToolkitClientManager::class.java, clientManager, disposable)

            // Need to use real region provider to know about global services
            val regionProvider = AwsRegionProvider()
            ApplicationManager.getApplication().replaceService(ToolkitRegionProvider::class.java, regionProvider, disposable)

            // Make a new http client that is scoped to the disposable and replace the global one with it, otherwise the apache connection reaper thread
            // is detected as leaking threads and fails the tests
            // TODO: We aren't closing cred providers and sdks when they are removed, we need to see what ramifications that has
            ThreadLeakTracker.longRunningThreadCreated(disposable, "idle-connection-reaper")

            val httpClient = AwsSdkClient()
            ApplicationManager.getApplication().replaceService(SdkClientProvider::class.java, httpClient, disposable)
        }
    }
}

// Scoped to this file only, users should be using MockClientManagerRule to enforce cleanup correctly
private fun getMockInstance(): MockClientManager = service<ToolkitClientManager>() as MockClientManager

sealed class MockClientManagerBase : ExternalResource() {
    private lateinit var mockClientManager: MockClientManager

    override fun before() {
        mockClientManager = getMockInstance()
    }

    override fun after() {
        mockClientManager.dispose()
    }

    @PublishedApi
    @Deprecated("Do not use, visible for inline")
    internal fun manager() = mockClientManager

    inline fun <reified T : SdkClient> create(): T = delegateMock<T>(verboseLogging = true).also {
        @Suppress("DEPRECATION")
        manager().register(T::class, it)
    }

    inline fun <reified T : SdkClient> create(region: AwsRegion, credProvider: ToolkitCredentialsProvider): T = delegateMock<T>().also {
        @Suppress("DEPRECATION")
        manager().register(T::class, it, region, credProvider)
    }

    inline fun <reified T : SdkClient> register(mock: T): T = mock.also {
        @Suppress("DEPRECATION")
        manager().register(T::class, it)
    }
}

class MockClientManagerRule : MockClientManagerBase()

class MockClientManagerExtension : MockClientManagerBase(), BeforeEachCallback, AfterEachCallback {
    override fun beforeEach(context: ExtensionContext?) {
        before()
    }

    override fun afterEach(context: ExtensionContext?) {
        after()
    }
}


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

package software.aws.toolkits.jetbrains.core

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.service
import com.intellij.openapi.project.Project
import com.intellij.testFramework.ApplicationRule
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.extension.AfterEachCallback
import org.junit.jupiter.api.extension.BeforeEachCallback
import org.junit.jupiter.api.extension.ExtensionContext
import org.junit.runner.Description
import software.aws.toolkits.core.ClientConnectionSettings
import software.aws.toolkits.core.ConnectionSettings
import software.aws.toolkits.core.credentials.ToolkitAuthenticationProvider
import software.aws.toolkits.core.credentials.ToolkitBearerTokenProvider
import software.aws.toolkits.core.credentials.ToolkitCredentialsProvider
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.jetbrains.core.credentials.AwsConnectionManager
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import java.util.concurrent.ConcurrentHashMap

@Suppress("UNCHECKED_CAST")
class MockResourceCache : AwsResourceCache {
    private val map = ConcurrentHashMap<CacheKey, Any>()

    override fun <T> getResourceIfPresent(
        resource: Resource<T>,
        region: AwsRegion,
        credentialProvider: ToolkitCredentialsProvider,
        useStale: Boolean
    ): T? = when (resource) {
        is Resource.View<*, T> -> getResourceIfPresent(resource.underlying, region, credentialProvider)?.let { resource.doMap(it, region) }
        is Resource.Cached<T> -> mockResourceIfPresent(resource, region, credentialProvider)
    }

    override fun <T> getResource(
        resource: Resource<T>,
        region: AwsRegion,
        credentialProvider: ToolkitCredentialsProvider,
        useStale: Boolean,
        forceFetch: Boolean
    ): CompletionStage<T> = when (resource) {
        is Resource.View<*, T> -> getResource(resource.underlying, region, credentialProvider, useStale, forceFetch).thenApply {
            resource.doMap(it as Any, region)
        }
        is Resource.Cached<T> -> {
            mockResource(resource, region, credentialProvider)
        }
    }

    override fun <T> getResourceIfPresent(
        resource: Resource<T>,
        region: AwsRegion,
        tokenProvider: ToolkitBearerTokenProvider,
        useStale: Boolean
    ): T? = when (resource) {
        is Resource.View<*, T> -> getResourceIfPresent(resource.underlying, region, tokenProvider)?.let { resource.doMap(it, region) }
        is Resource.Cached<T> -> mockResourceIfPresent(resource, region, tokenProvider)
    }

    override fun <T> getResource(
        resource: Resource<T>,
        region: AwsRegion,
        tokenProvider: ToolkitBearerTokenProvider,
        useStale: Boolean,
        forceFetch: Boolean
    ): CompletionStage<T> = when (resource) {
        is Resource.View<*, T> -> getResource(resource.underlying, region, tokenProvider, useStale, forceFetch).thenApply {
            resource.doMap(it as Any, region)
        }
        is Resource.Cached<T> -> {
            mockResource(resource, region, tokenProvider)
        }
    }

    private fun <T> mockResourceIfPresent(
        resource: Resource.Cached<T>,
        region: AwsRegion,
        credentials: ToolkitAuthenticationProvider
    ): T? = when (val value = map[CacheKey(resource.id, region.id, credentials.id)]) {
        is CompletableFuture<*> -> if (value.isDone) value.get() as T else null
        else -> value as? T?
    }

    private fun <T> mockResource(
        resource: Resource.Cached<T>,
        region: AwsRegion,
        credentials: ToolkitAuthenticationProvider
    ) = when (val value = map[CacheKey(resource.id, region.id, credentials.id)]) {
        is CompletableFuture<*> -> value as CompletionStage<T>
        else -> {
            val future = CompletableFuture<T>()
            ApplicationManager.getApplication().executeOnPooledThread {
                value?.also { future.complete(it as T) }
                    ?: future.completeExceptionally(IllegalStateException("No value found for $resource ${region.id} ${credentials.id} in mock"))
            }
            future
        }
    }

    override fun clear(resource: Resource<*>, connectionSettings: ClientConnectionSettings<*>) {
        when (resource) {
            is Resource.Cached<*> -> map.remove(CacheKey(resource.id, connectionSettings.region.id, connectionSettings.providerId))
            is Resource.View<*, *> -> clear(resource.underlying, connectionSettings)
        }
    }

    override fun clear(connectionSettings: ClientConnectionSettings<*>) {
        map.keys.removeIf { it.credentialsId == connectionSettings.providerId && it.regionId == connectionSettings.region.id }
    }

    override suspend fun clear() {
        map.clear()
    }

    fun entryCount() = map.size

    fun addEntry(resourceId: String, regionId: String, credentialsId: String, value: Any) {
        map[CacheKey(resourceId, regionId, credentialsId)] = value
    }

    companion object {
        @JvmStatic
        fun getInstance(): MockResourceCache = service<AwsResourceCache>() as MockResourceCache

        private data class CacheKey(val resourceId: String, val regionId: String, val credentialsId: String)
    }
}

class MockResourceCacheRule : ApplicationRule(), MockResourceCacheInterface by MockResourceCacheInterface.delegate() {
    public override fun before(description: Description) {
        super.before(description)
    }

    public override fun after() {
        runBlocking { cache.clear() }
    }
}

class MockResourceCacheExtension : BeforeEachCallback, AfterEachCallback, MockResourceCacheInterface by MockResourceCacheInterface.delegate() {
    private val rule = MockResourceCacheRule()

    override fun beforeEach(context: ExtensionContext) {
        rule.before(Description.EMPTY)
    }

    override fun afterEach(context: ExtensionContext) {
        rule.after()
    }
}

interface MockResourceCacheInterface {
    val cache: MockResourceCache

    fun addEntry(resourceId: String, regionId: String, credentialsId: String, value: Any) {
        cache.addEntry(resourceId, regionId, credentialsId, value)
    }

    fun addEntry(project: Project, resourceId: String, value: Any) {
        val connectionManager = AwsConnectionManager.getInstance(project)
        addEntry(resourceId, connectionManager.selectedRegion!!.id, connectionManager.selectedCredentialIdentifier!!.id, value)
    }

    fun <T> addEntry(project: Project, resource: Resource.Cached<T>, value: T) {
        addEntry(project, resource.id, value as Any)
    }

    fun <T> addEntry(project: Project, resourceId: String, value: CompletableFuture<T>) {
        val connectionManager = AwsConnectionManager.getInstance(project)
        addEntry(resourceId, connectionManager.selectedRegion!!.id, connectionManager.selectedCredentialIdentifier!!.id, value)
    }

    fun <T> addEntry(project: Project, resource: Resource.Cached<T>, value: CompletableFuture<T>) {
        addEntry(project, resource.id, value)
    }

    fun addEntry(project: Project, resourceId: String, throws: Exception) {
        addEntry(project, resourceId, CompletableFuture.failedFuture<Any>(throws))
    }

    fun <T> addEntry(connectionSettings: ConnectionSettings, resource: Resource.Cached<T>, value: CompletableFuture<T>) {
        addEntry(resource, connectionSettings.region.id, connectionSettings.credentials.id, value)
    }

    fun <T> addEntry(resource: Resource.Cached<T>, regionId: String, credentialsId: String, value: T) {
        addEntry(resource.id, regionId, credentialsId, value as Any)
    }

    fun <T> addEntry(resource: Resource.Cached<T>, regionId: String, credentialsId: String, value: CompletableFuture<T>) {
        addEntry(resource.id, regionId, credentialsId, value)
    }

    fun size() = cache.entryCount()

    companion object {
        fun delegate() = object : MockResourceCacheInterface {
            override val cache by lazy { MockResourceCache.getInstance() }
        }
    }
}


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

package software.aws.toolkits.jetbrains.core

typealias ToolWindowHeadlessManagerImpl = com.intellij.toolWindow.ToolWindowHeadlessManagerImpl
typealias MockToolWindow = com.intellij.toolWindow.ToolWindowHeadlessManagerImpl.MockToolWindow


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

package software.aws.toolkits.jetbrains.core

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.compiler.CompileContext
import com.intellij.openapi.compiler.CompileStatusNotification
import com.intellij.openapi.compiler.CompilerManager
import com.intellij.openapi.compiler.CompilerMessageCategory
import com.intellij.openapi.project.Project
import org.mockito.kotlin.mock
import software.amazon.awssdk.services.apprunner.model.ServiceStatus
import software.amazon.awssdk.services.apprunner.model.ServiceSummary
import software.amazon.awssdk.services.cloudformation.model.StackStatus
import software.amazon.awssdk.services.cloudformation.model.StackSummary
import software.amazon.awssdk.services.cloudwatchlogs.model.LogGroup
import software.amazon.awssdk.services.lambda.model.FunctionConfiguration
import software.amazon.awssdk.services.lambda.model.Runtime
import software.amazon.awssdk.services.lambda.model.TracingConfigResponse
import software.amazon.awssdk.services.lambda.model.TracingMode
import software.amazon.awssdk.services.s3.model.Bucket
import software.amazon.awssdk.services.schemas.model.RegistrySummary
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.jetbrains.services.apprunner.resources.AppRunnerResources
import software.aws.toolkits.jetbrains.services.cloudformation.resources.CloudFormationResources
import software.aws.toolkits.jetbrains.services.cloudwatch.logs.resources.CloudWatchResources
import software.aws.toolkits.jetbrains.services.dynamic.CloudControlApiResources
import software.aws.toolkits.jetbrains.services.ecr.resources.EcrResources
import software.aws.toolkits.jetbrains.services.ecr.resources.Repository
import software.aws.toolkits.jetbrains.services.ecs.resources.EcsResources
import software.aws.toolkits.jetbrains.services.lambda.resources.LambdaResources
import software.aws.toolkits.jetbrains.services.s3.resources.S3Resources
import software.aws.toolkits.jetbrains.services.schemas.resources.SchemasResources
import software.aws.toolkits.jetbrains.services.sqs.resources.SqsResources
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit

fun MockResourceCacheRule.fillResourceCache(project: Project) {
    this.addEntry(
        project,
        EcsResources.LIST_CLUSTER_ARNS,
        listOf("arn2", "arn3")
    )

    this.addEntry(
        project,
        S3Resources.LIST_REGIONALIZED_BUCKETS,
        listOf(S3Resources.RegionalizedBucket(Bucket.builder().name("abc").build(), AwsRegion.GLOBAL))
    )

    this.addEntry(
        project,
        makeMockList("arn2"),
        listOf("service1", "service2")
    )

    this.addEntry(
        project,
        makeMockList("arn3"),
        listOf("service1", "service2")
    )

    this.addEntry(
        project,
        CloudControlApiResources.listTypes(),
        CompletableFuture.completedFuture(listOf("Aws::Sample::Resource"))
    )

    this.addEntry(
        project,
        AppRunnerResources.LIST_SERVICES,
        listOf(ServiceSummary.builder().serviceName("sample-service").status(ServiceStatus.OPERATION_IN_PROGRESS).build())
    )

    this.addEntry(
        project,
        CloudFormationResources.ACTIVE_STACKS,
        listOf(StackSummary.builder().stackName("sample-stack").stackId("sample-stack-ID").stackStatus(StackStatus.CREATE_COMPLETE).build())
    )

    this.addEntry(
        project,
        CloudWatchResources.LIST_LOG_GROUPS,
        listOf(LogGroup.builder().arn("sample-arn").logGroupName("sample-lg-name").build())
    )

    this.addEntry(
        project,
        EcrResources.LIST_REPOS,
        listOf(Repository("sample-repo-name", "sample-repo-arn", "sample-repo-uri"))
    )

    this.addEntry(
        project,
        LambdaResources.LIST_FUNCTIONS,
        listOf(
            FunctionConfiguration.builder()
                .functionName("sample-function")
                .functionArn("arn:aws:lambda:us-west-2:0123456789:function:sample-function")
                .lastModified("A ways back")
                .handler("blah:blah")
                .runtime(Runtime.JAVA8)
                .role("SomeRoleArn")
                .environment { it.variables(emptyMap()) }
                .timeout(60)
                .memorySize(128)
                .tracingConfig(TracingConfigResponse.builder().mode(TracingMode.PASS_THROUGH).build())
                .build()
        )
    )

    this.addEntry(
        project,
        SchemasResources.LIST_REGISTRIES,
        listOf(RegistrySummary.builder().registryName("sample-registry-name").build())
    )

    this.addEntry(
        project,
        SqsResources.LIST_QUEUE_URLS,
        listOf("https://sqs.us-east-1.amazonaws.com/123456789012/test1")
    )
}

fun makeMockList(clusterArn: String): Resource.Cached<List<String>> = mock {
    on { id }.thenReturn("ecs.list_services.$clusterArn")
}

fun compileProjectAndWait(project: Project) {
    val compileFuture = CompletableFuture<CompileContext>()
    ApplicationManager.getApplication().invokeAndWait {
        @Suppress("ObjectLiteralToLambda")
        CompilerManager.getInstance(project).rebuild(
            object : CompileStatusNotification {
                override fun finished(aborted: Boolean, errors: Int, warnings: Int, compileContext: CompileContext) {
                    if (!aborted && errors == 0) {
                        compileFuture.complete(compileContext)
                    } else {
                        compileFuture.completeExceptionally(
                            RuntimeException(
                                "Compilation error: ${compileContext.getMessages(CompilerMessageCategory.ERROR).map { it.message }}"
                            )
                        )
                    }
                }
            }
        )
    }
    compileFuture.get(30, TimeUnit.SECONDS)
}


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

package software.aws.toolkits.jetbrains.core.coroutines

import com.intellij.openapi.application.ApplicationManager
import com.intellij.testFramework.ApplicationRule
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.runInEdtAndWait
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test

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

    @Rule
    @JvmField
    val disposableRule = DisposableRule()

    @Test
    fun `getCoroutineUiContext context runs on UI thread`() {
        runBlocking {
            assertThat(ApplicationManager.getApplication().isDispatchThread).isFalse
            withContext(getCoroutineUiContext()) {
                assertThat(ApplicationManager.getApplication().isDispatchThread).isTrue
            }
        }
    }

    @Test
    fun `getCoroutineBgContext context runs not on UI thread`() {
        runInEdtAndWait {
            assertThat(ApplicationManager.getApplication().isDispatchThread).isTrue
            runBlocking {
                withContext(getCoroutineBgContext()) {
                    assertThat(ApplicationManager.getApplication().isDispatchThread).isFalse
                }
            }
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.coroutines

import com.intellij.ide.highlighter.ProjectFileType
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.ComponentManager
import com.intellij.openapi.project.ex.ProjectManagerEx
import com.intellij.openapi.util.Disposer
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.PlatformTestUtil
import com.intellij.testFramework.ProjectRule
import com.intellij.testFramework.TestApplicationManager
import com.intellij.testFramework.createTestOpenProjectOptions
import com.intellij.testFramework.replaceService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
import kotlinx.coroutines.future.asCompletableFuture
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.junit.rules.TestName
import software.aws.toolkits.jetbrains.utils.isInstanceOf
import java.time.Duration
import java.util.concurrent.CancellationException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean

class ScopeTest {
    @Rule
    @JvmField
    val tempDir = TemporaryFolder()

    @Rule
    @JvmField
    val projectRule = ProjectRule()

    @Rule
    @JvmField
    val disposableRule = DisposableRule()

    @Rule
    @JvmField
    val testName = TestName()

    @Test
    fun `plugin being uploaded cancels application scope`() {
        val fakePluginScope = createFakePluginScope()

        assertScopeIsCanceled(applicationCoroutineScope()) {
            Disposer.dispose(fakePluginScope)
        }
    }

    @Test
    fun `plugin being uploaded cancels project scope`() {
        val fakePluginScope = createFakePluginScope(projectRule.project)

        assertScopeIsCanceled(projectCoroutineScope(projectRule.project)) {
            Disposer.dispose(fakePluginScope)
        }
    }

    @Test
    fun `plugin being uploaded cancels disposable scope`() {
        val fakePluginScope = createFakePluginScope()

        // Use fake disposable so we dont accidentally trigger false positive, nor disposable leak detector
        val fakeDisposable = Disposable { }
        assertScopeIsCanceled(disposableCoroutineScope(fakeDisposable)) {
            Disposer.dispose(fakePluginScope)
        }
    }

    @Test
    @Ignore("Disposing the application leads to the AppExecutorUtil.getAppExecutorService being shutdown and no way to restart and thus fails all future tests")
    fun `application being disposed cancels application scope`() {
        assertScopeIsCanceled(applicationCoroutineScope()) {
            TestApplicationManager.getInstance().dispose()
        }
    }

    @Test
    fun `project being disposed cancels project scope`() {
        val projectFile = tempDir.newFile("${testName.methodName}${ProjectFileType.DOT_DEFAULT_EXTENSION}").toPath()
        val options = createTestOpenProjectOptions(runPostStartUpActivities = false)
        val project = ProjectManagerEx.getInstanceEx().openProject(projectFile, options)!!

        assertScopeIsCanceled(projectCoroutineScope(project)) {
            PlatformTestUtil.forceCloseProjectWithoutSaving(project)
        }
    }

    @Test
    fun `disposable being disposed cancels disposable scope`() {
        val disposable = Disposer.newDisposable()

        assertScopeIsCanceled(disposableCoroutineScope(disposable)) {
            Disposer.dispose(disposable)
        }
    }

    @Test
    fun `applicationCoroutineScope launches on background thread`() {
        assertScopeIsCorrectThread(applicationCoroutineScope())
    }

    @Test
    fun `projectCoroutineScope launches on background thread`() {
        assertScopeIsCorrectThread(projectCoroutineScope(projectRule.project))
    }

    @Test
    fun `disposableCoroutineScope launches on background thread`() {
        assertScopeIsCorrectThread(disposableCoroutineScope(disposableRule.disposable))
    }

    @Test
    fun `application and project trackers are different`() {
        val projectFile = tempDir.newFile("${testName.methodName}${ProjectFileType.DOT_DEFAULT_EXTENSION}").toPath()
        val options = createTestOpenProjectOptions(runPostStartUpActivities = false)
        val project2 = ProjectManagerEx.getInstanceEx().openProject(projectFile, options)!!

        try {
            assertThat(
                listOf(
                    PluginCoroutineScopeTracker.getInstance(),
                    PluginCoroutineScopeTracker.getInstance(projectRule.project),
                    PluginCoroutineScopeTracker.getInstance(project2)
                )
            ).doesNotHaveDuplicates()
        } finally {
            PlatformTestUtil.forceCloseProjectWithoutSaving(project2)
        }
    }

    @Test
    fun `disposableCoroutineScope can't take a project`() {
        assertThatThrownBy { disposableCoroutineScope(projectRule.project) }.isInstanceOf<IllegalStateException>()
    }

    @Test
    fun `disposableCoroutineScope can't take an application`() {
        assertThatThrownBy { disposableCoroutineScope(ApplicationManager.getApplication()) }.isInstanceOf<IllegalStateException>()
    }

    private fun createFakePluginScope(componentManager: ComponentManager = ApplicationManager.getApplication()): Disposable {
        // We can't unload the real plugin in tests, so make another instance of the service and replace it for the tests
        val tracker = PluginCoroutineScopeTracker()
        componentManager.replaceService(PluginCoroutineScopeTracker::class.java, tracker, disposableRule.disposable)
        return tracker
    }

    private fun assertScopeIsCorrectThread(scope: CoroutineScope) {
        val ran = AtomicBoolean(false)
        runBlocking(scope.coroutineContext) {
            assertThat(ApplicationManager.getApplication().isDispatchThread).isFalse
            ran.set(true)
        }
        assertThat(ran).isTrue
    }

    private fun assertScopeIsCanceled(scope: CoroutineScope, cancellationTask: () -> Unit) {
        val testTarget = TestTarget(scope)
        val future = testTarget.computeAsync().asCompletableFuture()
        assertThat(testTarget.computationStarted.await(10, TimeUnit.SECONDS)).isTrue

        cancellationTask()

        testTarget.cancelFired.countDown()

        assertThat(future).failsWithin(Duration.ofSeconds(10)).withThrowableOfType(CancellationException::class.java)
        assertThat(testTarget.bgTaskDone.get()).isFalse
    }

    @Suppress("BlockingMethodInNonBlockingContext")
    private class TestTarget(private val scope: CoroutineScope) {
        val computationStarted = CountDownLatch(1)
        val cancelFired = CountDownLatch(1)
        val bgTaskDone = AtomicBoolean(false)

        fun computeAsync() = scope.async {
            computationStarted.countDown()
            cancelFired.await()
            doTask()
        }

        suspend fun doTask() = withContext(getCoroutineBgContext()) {
            bgTaskDone.set(true)
        }
    }
}


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

package software.aws.toolkits.jetbrains.core.credentials

import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.Separator
import com.intellij.openapi.util.Ref
import com.intellij.testFramework.ProjectRule
import com.intellij.testFramework.RuleChain
import com.intellij.testFramework.TestActionEvent
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import software.aws.toolkits.core.credentials.CredentialIdentifier
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.core.utils.test.aString
import software.aws.toolkits.jetbrains.core.credentials.ConnectionSettingsMenuBuilder.Companion.connectionSettingsMenuBuilder
import software.aws.toolkits.jetbrains.core.credentials.ConnectionSettingsMenuBuilder.SwitchCredentialsAction
import software.aws.toolkits.jetbrains.core.credentials.ConnectionSettingsMenuBuilder.SwitchRegionAction
import software.aws.toolkits.jetbrains.core.credentials.MockAwsConnectionManager.ProjectAccountSettingsManagerRule
import software.aws.toolkits.jetbrains.core.region.AwsRegionProvider
import software.aws.toolkits.jetbrains.core.region.MockRegionProviderRule
import software.aws.toolkits.resources.message

class ConnectionSettingsMenuBuilderTest {
    private val projectRule = ProjectRule()
    private val regionProviderRule = MockRegionProviderRule()
    private val credentialManagerRule = MockCredentialManagerRule()
    private val settingsManagerRule = ProjectAccountSettingsManagerRule(projectRule)

    @Rule
    @JvmField
    val ruleChain = RuleChain(
        projectRule,
        regionProviderRule,
        credentialManagerRule,
        settingsManagerRule
    )

    @Test
    fun `all regions are shown if no previous selection`() {
        val partition = aString()
        regionProviderRule.createAwsRegion(partitionId = partition)
        regionProviderRule.createAwsRegion(partitionId = partition)
        regionProviderRule.createAwsRegion(partitionId = aString())
        regionProviderRule.createAwsRegion(partitionId = aString())

        val group = connectionSettingsMenuBuilder()
            .withRegions(currentSelection = null) {}
            .build()
        val actions = group.getChildren().filterIsInstance<SwitchRegionAction>()

        assertThat(actions).hasSize(AwsRegionProvider.getInstance().allRegions().size)
    }

    @Test
    fun `only regions from same partition is shown if has a selection`() {
        val partition = aString()
        val selected = regionProviderRule.createAwsRegion(partitionId = partition)
        regionProviderRule.createAwsRegion(partitionId = partition)
        regionProviderRule.createAwsRegion(partitionId = aString())
        regionProviderRule.createAwsRegion(partitionId = aString())

        val group = connectionSettingsMenuBuilder()
            .withRegions(currentSelection = selected) {}
            .build()
        val actions = group.getChildren().filterIsInstance<SwitchRegionAction>()

        assertThat(actions).hasSize(AwsRegionProvider.getInstance().regions(selected.partitionId).size)
        assertThat(actions).allMatch { it.value.partitionId == selected.partitionId }
    }

    @Test
    fun `other partitions are a sub-menu if has a selection`() {
        val partition = aString()
        val selected = regionProviderRule.createAwsRegion(partitionId = partition)
        regionProviderRule.createAwsRegion(partitionId = partition)
        regionProviderRule.createAwsRegion(partitionId = aString())
        regionProviderRule.createAwsRegion(partitionId = aString())

        val otherPartitions = AwsRegionProvider.getInstance().partitions().keys.filterNot { partitionId -> partitionId == selected.partitionId }

        val group = connectionSettingsMenuBuilder()
            .withRegions(currentSelection = selected) {}
            .build()
        val actions = group.getChildren().filterIsInstance<ActionGroup>()

        assertThat(actions).singleElement().satisfies {
            assertThat(it.isPopup).isTrue
            assertThat(it.templatePresentation.text).isEqualTo(message("settings.partitions"))
            assertThat(it.getChildren()).hasSize(otherPartitions.size)
        }
    }

    @Test
    fun `other partitions sub-menu is hidden if only 1 partition`() {
        val partition = regionProviderRule.defaultPartition().id
        val selected = regionProviderRule.createAwsRegion(partitionId = regionProviderRule.defaultRegion().partitionId)
        regionProviderRule.createAwsRegion(partitionId = partition)
        regionProviderRule.createAwsRegion(partitionId = partition)

        val otherPartitions = AwsRegionProvider.getInstance().partitions().keys.filterNot { partitionId -> partitionId == selected.partitionId }
        assertThat(otherPartitions).isEmpty()

        val group = connectionSettingsMenuBuilder()
            .withRegions(currentSelection = selected) {}
            .build()
        val actions = group.getChildren().filterIsInstance<ActionGroup>()
        assertThat(actions).isEmpty()
    }

    @Test
    fun `can change regions`() {
        val holder = Ref.create<AwsRegion>()

        val group = connectionSettingsMenuBuilder()
            .withRegions(currentSelection = null, holder::set)
            .build()
        val actions = group.getChildren().filterIsInstance<SwitchRegionAction>()

        assertThat(actions).hasSizeGreaterThanOrEqualTo(1)
        val action = actions.first()

        action.actionPerformed(TestActionEvent())

        assertThat(holder.get()).isEqualTo(action.value)
    }

    @Test
    fun `can change region only if not same as selected`() {
        val holder = Ref.create<AwsRegion>()

        val partition = aString()
        val selected = regionProviderRule.createAwsRegion(partitionId = partition)
        val notSelected = regionProviderRule.createAwsRegion(partitionId = partition)

        val group = connectionSettingsMenuBuilder()
            .withRegions(currentSelection = selected, holder::set)
            .build()
        val actions = group.getChildren().filterIsInstance<SwitchRegionAction>()

        val selectedAction = actions.first { it.value == selected }
        selectedAction.actionPerformed(TestActionEvent())
        assertThat(holder.isNull).isTrue

        val notSelectedAction = actions.first { it.value == notSelected }
        notSelectedAction.actionPerformed(TestActionEvent())
        assertThat(holder.get()).isEqualTo(notSelected)
    }

    @Test
    fun `all credentials are shown`() {
        credentialManagerRule.addCredentials()
        credentialManagerRule.addCredentials()
        credentialManagerRule.addCredentials()
        credentialManagerRule.addCredentials()
        credentialManagerRule.addCredentials()

        val group = connectionSettingsMenuBuilder()
            .withCredentials(currentSelection = null) {}
            .build()
        val actions = group.getChildren().filterIsInstance<SwitchCredentialsAction>()

        assertThat(actions).hasSize(CredentialManager.getInstance().getCredentialIdentifiers().size)
    }

    @Test
    fun `can change credentials`() {
        val holder = Ref.create<CredentialIdentifier>()

        credentialManagerRule.addCredentials()

        val group = connectionSettingsMenuBuilder()
            .withCredentials(currentSelection = null, holder::set)
            .build()
        val actions = group.getChildren().filterIsInstance<SwitchCredentialsAction>()

        assertThat(actions).hasSizeGreaterThan(1)
        val action = actions.first()

        action.actionPerformed(TestActionEvent())

        assertThat(holder.get()).isEqualTo(action.value)
    }

    @Test
    fun `can change credentials only if not same as selected`() {
        val holder = Ref.create<CredentialIdentifier>()

        val selected = credentialManagerRule.addCredentials()
        val notSelected = credentialManagerRule.addCredentials()

        val group = connectionSettingsMenuBuilder()
            .withCredentials(currentSelection = selected, holder::set)
            .build()
        val actions = group.getChildren().filterIsInstance<SwitchCredentialsAction>()

        val selectedAction = actions.first { it.value == selected }
        selectedAction.actionPerformed(TestActionEvent())
        assertThat(holder.isNull).isTrue

        val notSelectedAction = actions.first { it.value == notSelected }
        notSelectedAction.actionPerformed(TestActionEvent())
        assertThat(holder.get()).isEqualTo(notSelected)
    }

    @Test
    fun `both credentials and regions can be in the same menu`() {
        regionProviderRule.createAwsRegion()
        regionProviderRule.createAwsRegion()
        regionProviderRule.createAwsRegion()

        credentialManagerRule.addCredentials()
        credentialManagerRule.addCredentials()
        credentialManagerRule.addCredentials()

        val group = connectionSettingsMenuBuilder()
            .withCredentials(currentSelection = null) {}
            .withRegions(currentSelection = null) {}
            .build()

        val regionActions = group.getChildren().filterIsInstance<SwitchRegionAction>()
        val credentialActions = group.getChildren().filterIsInstance<SwitchCredentialsAction>()

        assertThat(regionActions).hasSize(AwsRegionProvider.getInstance().allRegions().size)
        assertThat(credentialActions).hasSize(CredentialManager.getInstance().getCredentialIdentifiers().size)
    }

    @Test
    fun `recent regions are shown, with all in a sub-menu`() {
        val settingsManager = settingsManagerRule.settingsManager

        settingsManager.addRecentRegion(regionProviderRule.createAwsRegion())
        settingsManager.addRecentRegion(regionProviderRule.createAwsRegion())
        regionProviderRule.createAwsRegion()
        regionProviderRule.createAwsRegion()

        val group = connectionSettingsMenuBuilder()
            .withRegions(currentSelection = null) {}
            .withRecentChoices(projectRule.project)
            .build()

        val recentActions = group.getChildren().filterIsInstance<SwitchRegionAction>()
        val allRegions = group.getChildren().filterIsInstance<ActionGroup>()
            .first { it.templatePresentation.text == message("settings.regions.region_sub_menu") }
            .getChildren()
            .filterIsInstance<SwitchRegionAction>()

        assertThat(recentActions).hasSize(settingsManager.recentlyUsedRegions().size)
        assertThat(allRegions).hasSize(AwsRegionProvider.getInstance().allRegions().size)
    }

    @Test
    fun `if no recent regions, all are shown`() {
        settingsManagerRule.settingsManager.clearRecentRegions()

        regionProviderRule.createAwsRegion()
        regionProviderRule.createAwsRegion()
        regionProviderRule.createAwsRegion()
        regionProviderRule.createAwsRegion()

        val group = connectionSettingsMenuBuilder()
            .withRegions(currentSelection = null) {}
            .withRecentChoices(projectRule.project)
            .build()

        val titleAction = group.getChildren().filterIsInstance<Separator>().filter { it.text == message("settings.regions.recent") }
        val regionActions = group.getChildren().filterIsInstance<SwitchRegionAction>()

        assertThat(titleAction).isEmpty()
        assertThat(regionActions).hasSize(AwsRegionProvider.getInstance().allRegions().size)
    }

    @Test
    fun `recent credentials are shown, with all in a sub-menu`() {
        val settingsManager = settingsManagerRule.settingsManager

        settingsManager.addRecentCredentials(credentialManagerRule.addCredentials())
        settingsManager.addRecentCredentials(credentialManagerRule.addCredentials())
        credentialManagerRule.addCredentials()
        credentialManagerRule.addCredentials()

        val group = connectionSettingsMenuBuilder()
            .withCredentials(currentSelection = null) {}
            .withRecentChoices(projectRule.project)
            .build()

        val titleAction = group.getChildren().filterIsInstance<Separator>().filter { it.text == message("settings.credentials.iam") }
        val recentActions = group.getChildren().filterIsInstance<SwitchCredentialsAction>()
        val allCredentials = group.getChildren().filterIsInstance<ActionGroup>()
            .first { it.templatePresentation.text == message("settings.credentials.profile_sub_menu") }
            .getChildren()
            .filterIsInstance<SwitchCredentialsAction>()

        assertThat(titleAction).singleElement()
        assertThat(recentActions).hasSize(settingsManager.recentlyUsedCredentials().size)
        assertThat(allCredentials).hasSize(CredentialManager.getInstance().getCredentialIdentifiers().size)
    }

    @Test
    fun `if no recent credentials, all are shown`() {
        settingsManagerRule.settingsManager.clearRecentCredentials()

        credentialManagerRule.addCredentials()
        credentialManagerRule.addCredentials()
        credentialManagerRule.addCredentials()

        val group = connectionSettingsMenuBuilder()
            .withCredentials(currentSelection = null) {}
            .withRecentChoices(projectRule.project)
            .build()

        val titleAction = group.getChildren().filterIsInstance<Separator>().filter { it.text == message("settings.credentials.recent") }
        val credentialsActions = group.getChildren().filterIsInstance<SwitchCredentialsAction>()

        assertThat(titleAction).isEmpty()
        assertThat(credentialsActions).hasSize(CredentialManager.getInstance().getCredentialIdentifiers().size)
    }

    private fun ActionGroup.getChildren(): Array<AnAction> = this.getChildren(TestActionEvent())
}


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

package software.aws.toolkits.jetbrains.core.credentials

import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileTypes.FileTypes
import com.intellij.openapi.fileTypes.ex.FileTypeManagerEx
import com.intellij.openapi.ui.TestDialog
import com.intellij.openapi.ui.TestDialogManager
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.testFramework.ProjectRule
import com.intellij.testFramework.TestActionEvent
import com.intellij.testFramework.runInEdtAndWait
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import java.io.File

class CreateOrUpdateCredentialProfilesActionTest {

    @Rule
    @JvmField
    val folderRule = TemporaryFolder()

    @Rule
    @JvmField
    val projectRule = ProjectRule()

    private lateinit var fileEditorManager: FileEditorManager
    private lateinit var localFileSystem: LocalFileSystem

    @Before
    fun setUp() {
        fileEditorManager = FileEditorManager.getInstance(projectRule.project)
        localFileSystem = LocalFileSystem.getInstance()
    }

    @After
    fun cleanUp() {
        runInEdtAndWait {
            fileEditorManager.openFiles.forEach { fileEditorManager.closeFile(it) }
        }
    }

    @Test
    fun confirmConfigFileCreated_bothFilesDoNotExist() {
        val writer = mock<ConfigFileWriter> {
            on { createFile(any()) }.doAnswer { it.getArgument<File>(0).writeText("hello") }
        }

        val configFile = File(folderRule.newFolder(), "config")
        val credFile = File(folderRule.newFolder(), "credentials")

        val sut = CreateOrUpdateCredentialProfilesAction(writer, configFile, credFile)
        TestDialogManager.setTestDialog(TestDialog.OK)

        sut.actionPerformed(TestActionEvent { projectRule.project })

        verify(writer).createFile(configFile)

        assertThat(fileEditorManager.openFiles).hasOnlyOneElementSatisfying { assertThat(it.name).isEqualTo("config") }
    }

    @Test
    fun bothFilesOpened_bothFilesExists() {
        val writer = mock<ConfigFileWriter>()

        val configFile = folderRule.newFile("config")
        val credFile = folderRule.newFile("credentials")
        // IDE interprets blank files with no extension as binary
        configFile.writeText("config")
        credFile.writeText("cred")

        val sut = CreateOrUpdateCredentialProfilesAction(writer, configFile, credFile)
        TestDialogManager.setTestDialog(TestDialog.OK)

        sut.actionPerformed(TestActionEvent { projectRule.project })

        verifyNoMoreInteractions(writer)

        assertThat(fileEditorManager.openFiles).hasSize(2)
            .anySatisfy { assertThat(it.name).isEqualTo("config") }
            .anySatisfy { assertThat(it.name).isEqualTo("credentials") }
    }

    @Test
    fun configFileOpened_onlyConfigExists() {
        val writer = mock<ConfigFileWriter>()

        val configFile = folderRule.newFile("config")
        val credFile = File(folderRule.newFolder(), "credentials")
        configFile.writeText("config")

        val sut = CreateOrUpdateCredentialProfilesAction(writer, configFile, credFile)
        TestDialogManager.setTestDialog(TestDialog.OK)

        sut.actionPerformed(TestActionEvent { projectRule.project })

        verifyNoMoreInteractions(writer)

        assertThat(fileEditorManager.openFiles).hasOnlyOneElementSatisfying { assertThat(it.name).isEqualTo("config") }
    }

    @Test
    fun credentialFileOpened_onlyCredentialsExists() {
        val writer = mock<ConfigFileWriter>()

        val configFile = File(folderRule.newFolder(), "config")
        val credFile = folderRule.newFile("credentials")
        credFile.writeText("cred")

        val sut = CreateOrUpdateCredentialProfilesAction(writer, configFile, credFile)
        TestDialogManager.setTestDialog(TestDialog.OK)

        sut.actionPerformed(TestActionEvent { projectRule.project })

        verifyNoMoreInteractions(writer)

        assertThat(fileEditorManager.openFiles).hasOnlyOneElementSatisfying { assertThat(it.name).isEqualTo("credentials") }
    }

    @Test
    fun emptyFileCanBeOpenedAsPlainText() {
        val writer = mock<ConfigFileWriter>()

        val configFile = File(folderRule.newFolder(), "config")
        val credFile = folderRule.newFile("credentials")

        // Mark the file as unknown for the purpose of the test. This is needed because some
        // other extensions can have weird file type association patterns (like Docker having
        // *. (?)) which makes this test fail because it is not file type unknown
        localFileSystem.refreshAndFindFileByIoFile(credFile)
        runInEdtAndWait {
            ApplicationManager.getApplication().runWriteAction {
                FileTypeManagerEx.getInstanceEx().associatePattern(
                    FileTypes.UNKNOWN,
                    "credentials"
                )
            }
        }

        val sut = CreateOrUpdateCredentialProfilesAction(writer, configFile, credFile)
        TestDialogManager.setTestDialog(TestDialog.OK)

        sut.actionPerformed(TestActionEvent { projectRule.project })

        verifyNoMoreInteractions(writer)

        assertThat(fileEditorManager.openFiles).hasOnlyOneElementSatisfying {
            assertThat(it.name).isEqualTo("credentials")
            // FIX_WHEN_MIN_IS_212: assert that type is `FileTypes.PLAIN_TEXT` or `DetectedByContentFileType`
            assertThat(it.fileType).isNotNull()
            assertThat(it.fileType).isNotEqualTo(FileTypes.UNKNOWN)
        }
    }

    @Test
    fun negativeConfirmationDoesNotCreateFile() {
        val writer = mock<ConfigFileWriter>()

        val configFile = File(folderRule.newFolder(), "config")
        val credFile = File(folderRule.newFolder(), "credentials")

        val sut = CreateOrUpdateCredentialProfilesAction(writer, configFile, credFile)
        TestDialogManager.setTestDialog(TestDialog.NO)

        sut.actionPerformed(TestActionEvent { projectRule.project })

        verifyNoMoreInteractions(writer)
    }
}