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

package software.aws.toolkits.core.utils

import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentMap

data class AttributeBagKey<T> private constructor(val key: String) {
    companion object {
        private val map: ConcurrentMap<String, AttributeBagKey<*>> = ConcurrentHashMap()

        @Suppress("UNCHECKED_CAST")
        fun <T> create(name: String): AttributeBagKey<T> = map.computeIfAbsent(name) {
            AttributeBagKey<T>(name)
        } as AttributeBagKey<T>
    }
}

class AttributeBag {
    private val data = mutableMapOf<String, Any>()

    fun <T : Any> putData(key: AttributeBagKey<T>, value: T) {
        data.put(key.key, value)
    }

    @Suppress("UNCHECKED_CAST")
    fun <T> get(key: AttributeBagKey<T>): T? = data[key.key]?.let { it as T }

    fun <T> getOrThrow(key: AttributeBagKey<T>): T = get(key)
        ?: throw NoSuchElementException("Required element $key not found in AttributeBag")
}


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

package software.aws.toolkits.core.utils

/**
 * Removes all items in this collection and replaces them with the items in the [other] collection
 */
fun <T> MutableCollection<T>.replace(other: Collection<T>) {
    clear()
    addAll(other)
}

/**
 * Removes all items in this map and replaces them with the items in the [other] map
 */
fun <K, V> MutableMap<K, V>.replace(other: Map<K, V>) {
    clear()
    putAll(other)
}


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

package software.aws.toolkits.core.utils

sealed class Either<out L, out R> {
    data class Left<L>(val value: L) : Either<L, Nothing>()
    data class Right<R>(val value: R) : Either<Nothing, R>()
}

infix fun <L, R> L?.xor(other: R?): Either<L, R> = when {
    this != null && other == null -> Either.Left(this)
    this == null && other != null -> Either.Right(other)
    else -> throw IllegalArgumentException("Exactly one of $this or $other must be null")
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
@file:JvmName("ExceptionUtils")

package software.aws.toolkits.core.utils

/**
 * Convert exceptions raised from [block] to null
 */
fun <T> tryOrNull(block: () -> T): T? = try {
    block()
} catch (_: Exception) {
    null
}


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

@file:Suppress("LazyLog")
package software.aws.toolkits.core.utils

import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.slf4j.event.Level
import kotlin.reflect.KClass

inline fun <reified T : Any> getLogger(): Logger = getLogger(T::class)
fun getLogger(clazz: KClass<*>): Logger = LoggerFactory.getLogger(clazz.java)

/**
 * Execute the given [block] and log any exception that occurs at the [level] with the provided [message].
 *
 * Defaults to [Level.ERROR] if none specified.
 */
fun <T> Logger.tryOrNull(message: String, level: Level = Level.ERROR, block: () -> T?): T? = try {
    block()
} catch (e: Exception) {
    log(level, e) { message }
    null
}

/**
 * Execute the given block, log at the given [level] and then bubble any exception that occurs.
 *
 * A [block] that returns null bubbles an exception
 */
fun <T> Logger.tryOrThrow(message: String, level: Level = Level.ERROR, block: () -> T?): T = try {
    block() ?: throw NullPointerException()
} catch (e: Exception) {
    log(level, e) { message }
    throw e
}

/**
 * Execute the given block, log an error and then bubble any exception that occurs.
 *
 * A [block] that returns null is legal
 */
fun <T> Logger.tryOrThrowNullable(message: String, level: Level = Level.ERROR, block: () -> T?) = try {
    block()
} catch (e: Exception) {
    log(level, e) { message }
    throw e
}

/**
 * Execute the given block and return the result. Log a warning when the result is null
 */
fun <T> Logger.logWhenNull(message: String, level: Level = Level.WARN, block: () -> T?): T? {
    val value = block()
    if (value == null) {
        log(level) { message }
    }
    return value
}

fun Logger.log(level: Level, exception: Throwable? = null, block: () -> String) {
    when (level) {
        Level.ERROR -> error(exception, block)
        Level.WARN -> warn(exception, block)
        Level.INFO -> info(exception, block)
        Level.DEBUG -> debug(exception, block)
        Level.TRACE -> trace(exception, block)
    }
}

fun Logger.debug(exception: Throwable? = null, block: () -> String) {
    if (isDebugEnabled) {
        debug(block(), exception)
    }
}

fun Logger.info(exception: Throwable? = null, block: () -> String) {
    if (isInfoEnabled) {
        info(block(), exception)
    }
}

fun Logger.error(exception: Throwable? = null, block: () -> String) {
    if (isErrorEnabled) {
        error(block(), exception)
    }
}

fun Logger.warn(exception: Throwable? = null, block: () -> String) {
    if (isWarnEnabled) {
        warn(block(), exception)
    }
}

fun Logger.trace(exception: Throwable? = null, block: () -> String) {
    if (isTraceEnabled) {
        trace(block(), exception)
    }
}

fun Logger.assertTrue(value: Boolean, block: () -> String): Boolean {
    if (!value) {
        val resultMessage = "Assertion failed: ${block.invoke()}"
        error(resultMessage, Throwable(resultMessage))
    }

    return value
}


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

package software.aws.toolkits.core.utils

import org.slf4j.Logger
import java.io.InputStream
import java.io.OutputStream
import java.nio.charset.Charset
import java.nio.file.AccessMode
import java.nio.file.FileAlreadyExistsException
import java.nio.file.Files
import java.nio.file.NoSuchFileException
import java.nio.file.Path
import java.nio.file.attribute.AclEntry
import java.nio.file.attribute.AclFileAttributeView
import java.nio.file.attribute.FileTime
import java.nio.file.attribute.PosixFilePermission
import java.nio.file.attribute.PosixFilePermissions
import java.nio.file.attribute.UserPrincipal
import kotlin.io.path.getPosixFilePermissions
import kotlin.io.path.isRegularFile

val POSIX_OWNER_ONLY_FILE = setOf(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE)
val POSIX_OWNER_ONLY_DIR = setOf(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE)

fun Path.inputStream(): InputStream = Files.newInputStream(this)
fun Path.inputStreamIfExists(): InputStream? = try {
    inputStream()
} catch (e: NoSuchFileException) {
    null
}

fun Path.touch(restrictToOwner: Boolean = false) {
    try {
        if (!restrictToOwner || !hasPosixFilePermissions()) {
            Files.createFile(this)
        } else {
            Files.createFile(this, PosixFilePermissions.asFileAttribute(POSIX_OWNER_ONLY_FILE))
        }
    } catch (_: FileAlreadyExistsException) {}
}

fun Path.outputStream(): OutputStream {
    this.createParentDirectories()
    return Files.newOutputStream(this)
}

fun Path.createParentDirectories(restrictToOwner: Boolean = false) = if (!restrictToOwner || !hasPosixFilePermissions()) {
    Files.createDirectories(this.parent)
} else {
    Files.createDirectories(this.parent, PosixFilePermissions.asFileAttribute(POSIX_OWNER_ONLY_DIR))
}

fun Path.exists() = Files.exists(this)
fun Path.deleteIfExists() = Files.deleteIfExists(this)
fun Path.lastModified(): FileTime = Files.getLastModifiedTime(this)
fun Path.readText(charset: Charset = Charsets.UTF_8) = toFile().readText(charset)
fun Path.writeText(text: String, charset: Charset = Charsets.UTF_8) = toFile().writeText(text, charset)

// Comes from PosixFileAttributeView#name()
fun Path.hasPosixFilePermissions() = "posix" in this.fileSystem.supportedFileAttributeViews()
fun Path.filePermissions(permissions: Set<PosixFilePermission>) {
    if (hasPosixFilePermissions()) {
        Files.setPosixFilePermissions(this, permissions)
    }
}

fun Path.tryDirOp(log: Logger, block: Path.() -> Unit) {
    try {
        log.debug { "dir op on $this" }
        block(this)
    } catch (e: Exception) {
        if (e !is java.nio.file.AccessDeniedException && e !is kotlin.io.AccessDeniedException) {
            throw e
        }

        if (!hasPosixFilePermissions()) {
            throw tryAugmentExceptionMessage(e, this)
        }

        log.info(e) { "Attempting to handle ADE for directory operation" }
        try {
            var parent = if (this.isRegularFile()) { parent } else { this }

            while (parent != null) {
                if (!parent.exists()) {
                    log.info { "${parent.toAbsolutePath()}: does not exist yet" }
                } else {
                    if (tryOrNull { parent.fileSystem.provider().checkAccess(parent, AccessMode.READ, AccessMode.WRITE, AccessMode.EXECUTE) } != null) {
                        log.debug { "$parent has rwx, exiting" }
                        // can assume parent permissions are correct
                        break
                    }

                    log.debug { "fixing perms for $parent" }
                    parent.tryFixPerms(log, POSIX_OWNER_ONLY_DIR)
                }

                parent = parent.parent
            }
        } catch (e2: Exception) {
            log.warn(e2) { "Encountered error while handling ADE for ${e.message}" }

            throw tryAugmentExceptionMessage(e, this)
        }

        log.info { "Done attempting to handle ADE for directory operation" }
        block(this)
    }
}

fun<T> Path.tryFileOp(log: Logger, block: Path.() -> T) =
    try {
        log.debug { "file op on $this" }
        block(this)
    } catch (e: Exception) {
        if (e !is java.nio.file.AccessDeniedException && e !is kotlin.io.AccessDeniedException) {
            throw e
        }

        if (!hasPosixFilePermissions()) {
            throw tryAugmentExceptionMessage(e, this)
        }

        log.info(e) { "Attempting to handle ADE for file operation" }
        try {
            log.debug { "fixing perms for $this" }
            tryFixPerms(log, POSIX_OWNER_ONLY_FILE)
        } catch (e2: Exception) {
            log.warn(e2) { "Encountered error while handling ADE for ${e.message}" }

            throw tryAugmentExceptionMessage(e, this)
        }

        log.info { "Done attempting to handle ADE for file operation" }
        block(this)
    }

private fun Path.tryFixPerms(log: Logger, desiredPermissions: Set<PosixFilePermission>) {
    // TODO: consider handling linux ACLs
    // only try ops if we own the file
    // (ab)use invariant that chmod only works if you are root or the file owner
    val perms = tryOrLogShortException(log) { Files.getPosixFilePermissions(this) }
    val ownership = tryOrLogShortException(log) { Files.getOwner(this) }

    log.info { "Permissions for ${toAbsolutePath()}: $ownership, $perms" }
    if (perms != null && ownership != null) {
        if (ownership.name != "root" && tryOrNull { filePermissions(perms) } != null) {
            val permissions = perms + desiredPermissions
            log.info { "Setting perms for ${toAbsolutePath()}: $permissions" }
            filePermissions(permissions)
        }
    }
}

private fun tryAugmentExceptionMessage(e: Exception, path: Path): Exception {
    if (e !is java.nio.file.AccessDeniedException && e !is kotlin.io.AccessDeniedException) {
        return e
    }

    var potentialProblem = if (path.exists()) { path } else { path.parent }
    var acls: List<AclEntry>? = null
    var ownership: UserPrincipal? = null
    while (potentialProblem != null) {
        acls = tryOrNull { Files.getFileAttributeView(potentialProblem, AclFileAttributeView::class.java).acl }
        ownership = tryOrNull { Files.getOwner(potentialProblem) }

        if (acls != null || ownership != null) {
            break
        }

        potentialProblem = potentialProblem.parent
    }

    val message = buildString {
        // $path is automatically added to the front of the exception message
        appendLine("Exception trying to perform operation")

        if (potentialProblem != null) {
            append("Potential issue is with $potentialProblem")

            if (ownership != null) {
                append(", which has owner: $ownership")
            }

            if (acls != null) {
                append(", and ACL entries for: ${acls.map { it.principal() }}")
            }

            val posixPermissions = tryOrNull { PosixFilePermissions.toString(potentialProblem.getPosixFilePermissions()) }
            if (posixPermissions != null) {
                append(", and POSIX permissions: $posixPermissions")
            }
        }
    }

    return when (e) {
        is kotlin.io.AccessDeniedException -> kotlin.io.AccessDeniedException(e.file, e.other, message)
        is java.nio.file.AccessDeniedException -> java.nio.file.AccessDeniedException(e.file, e.otherFile, message)
        // should never happen
        else -> e
    }.also {
        it.stackTrace = e.stackTrace
    }
}

private fun<T> tryOrLogShortException(log: Logger, block: () -> T) = try {
    block()
} catch (e: Exception) {
    log.warn { "${e::class.simpleName}: ${e.message}" }
    null
}


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

package software.aws.toolkits.core.utils

import java.io.FileInputStream
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.time.Duration
import java.time.Instant
import java.util.UUID
import java.util.concurrent.Callable
import java.util.concurrent.CompletionStage

interface RemoteResourceResolver {
    fun resolve(resource: RemoteResource): CompletionStage<Path>
}
interface RemoteResolveParser {
    fun canBeParsed(data: InputStream): Boolean
}

class DefaultRemoteResourceResolver(
    private val urlFetcher: UrlFetcher,
    private val cacheBasePath: Path,
    private val executor: (Callable<Path>) -> CompletionStage<Path>
) : RemoteResourceResolver {

    override fun resolve(resource: RemoteResource): CompletionStage<Path> = executor(Callable { internalResolve(resource) })

    private fun internalResolve(resource: RemoteResource): Path {
        val expectedLocation = cacheBasePath.resolve(resource.name)
        val current = expectedLocation.existsOrNull()
        if (current != null && !isExpired(current, resource)) {
            LOG.debug { "Existing file ($current) for ${resource.name} is present and not expired - using it." }
            return current
        }

        LOG.debug { "Current file for ${resource.name} does not exist or is expired. Attempting to fetch from ${resource.urls}" }
        var lastException: Exception? = null
        val downloaded = resource.urls.asSequence().mapNotNull { url ->
            val tmpFile = try {
                Files.createTempFile("${resource.name}-${UUID.randomUUID()}", ".tmp")
            } catch (e: Exception) {
                LOG.warn(e) { "Error creating temporary file" }
                lastException = e
                return@mapNotNull null
            }
            LOG.debug { "Beginning file download from $url to $tmpFile" }

            try {
                urlFetcher.fetch(url, tmpFile)
            } catch (e: Exception) {
                LOG.warn(e) { "Exception occurred downloading" }
                lastException = e
                tmpFile.deleteIfExists()
                return@mapNotNull null
            }
            tmpFile
        }.firstOrNull()

        val initialValue = resource.initialValue

        when {
            downloaded != null -> {
                LOG.debug { "Downloaded new file $downloaded, replacing old file $expectedLocation" }
                val isParsingSuccess = FileInputStream(downloaded.toFile()).use {
                    resource.remoteResolveParser?.canBeParsed(it) ?: true
                }
                if (isParsingSuccess) {
                    Files.move(downloaded, expectedLocation, StandardCopyOption.REPLACE_EXISTING)
                }
            }
            current != null -> LOG.debug { "No new file available - re-using current file $current" }
            initialValue != null -> {
                Files.copy(initialValue(), expectedLocation, StandardCopyOption.REPLACE_EXISTING)
            }
            else -> throw RuntimeException("Unable to resolve file $resource", lastException)
        }

        return expectedLocation
    }

    private companion object {
        val LOG = getLogger<RemoteResourceResolver>()
        fun Path.existsOrNull() = if (this.exists()) {
            this
        } else {
            null
        }

        fun isExpired(file: Path, resource: RemoteResource): Boolean {
            val ttl = resource.ttl ?: return false
            return (Duration.between(file.lastModified().toInstant(), Instant.now()) > ttl).also {
                if (it) {
                    LOG.debug { "TTL for file $file has expired." }
                }
            }
        }
    }
}

interface UrlFetcher {
    fun fetch(url: String, file: Path)
}

/**
 * A resource that can be resolved remotely from a list of [urls], falling back to an [initialValue] if specified.
 * The resource is cached at [name] until the [ttl].
 */
interface RemoteResource {
    val urls: List<String>
    val name: String
    val ttl: Duration? get() = null
    val initialValue: (() -> InputStream)? get() = null
    val remoteResolveParser: RemoteResolveParser? get() = null
}


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

package software.aws.toolkits.core.utils

import kotlin.reflect.full.hasAnnotation
import kotlin.reflect.full.memberProperties

@Target(AnnotationTarget.PROPERTY)
annotation class SensitiveField

fun redactedString(o: Any): String {
    val clazz = o::class
    if (!clazz.isData) {
        error("Only supports redacting data classes")
    }

    return buildString {
        append(clazz.simpleName)
        append("(")

        val properties = o::class.memberProperties
        properties.forEachIndexed { i, prop ->
            append(prop.name)
            append("=")
            if (prop.hasAnnotation<SensitiveField>()) {
                if (prop.getter.call(o) == null) {
                    append("null")
                } else {
                    append("<redacted>")
                }
            } else {
                append(prop.getter.call(o))
            }

            if (i != properties.size - 1) {
                append(", ")
            }
        }

        append(")")
    }
}


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

package software.aws.toolkits.core.utils

/**
 * Like String.split(), but discards blank (whitespace-only) results.
 */
fun String.splitNoBlank(vararg delimiters: Char, ignoreCase: Boolean = false, limit: Int = 0): List<String> =
    split(*delimiters, ignoreCase = ignoreCase, limit = limit).filter { it.isNotBlank() }

fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }

fun String.htmlWrap() = """${this.replace("\n", "
")}"""


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

package software.aws.toolkits.core.utils

import kotlinx.coroutines.delay
import software.amazon.awssdk.awscore.exception.AwsServiceException
import java.time.Duration
import java.time.Instant
import java.util.concurrent.CompletionException
import kotlin.math.min
import kotlin.reflect.KClass

object Waiters {
    private val LOG = getLogger<Waiters>()

    /**
     * Creates a waiter that attempts executing the provided [call] until the specified conditions are met.
     *
     * @param T The response type of the [call]
     * @param succeedOn The condition on the response under which the thing we are trying is complete. Defaults to if the call succeeds, we stop wating
     * @param failOn The condition on the response under which the thing we are trying has already failed and further attempts are pointless. Defaults to always try again
     * @param exceptionsToStopOn The exception types that should be considered a success and stop waiting. Default to never stop on any exception
     * @param exceptionsToIgnore The exception types that should be ignored if the thing we are trying throws them. Default to not ignoring any exceptions and let it bubble out
     * @param maxDuration The max amount of time we want to wait for
     * @param call The function we want to keep retrying
     */
    suspend fun <T> waitUntil(
        succeedOn: (T) -> Boolean = { true },
        failOn: (T) -> Boolean = { false },
        exceptionsToStopOn: Set<KClass<out Exception>> = emptySet(),
        exceptionsToIgnore: Set<KClass<out Exception>> = emptySet(),
        maxDuration: Duration = Duration.ofMinutes(1),
        // The status pulling method to get the latest resource
        call: () -> T
    ): T? {
        val start = Instant.now()
        var attempt = 0

        while (Duration.between(start, Instant.now()) < maxDuration) {
            attempt++

            try {
                val result = call()
                if (succeedOn.invoke(result)) {
                    LOG.info { "Got expected response: $result" }
                    return result
                }

                if (failOn.invoke(result)) {
                    throw WaiterUnrecoverableException("Received a response that matched the failOn predicate: $result")
                }

                LOG.info { "Attempt $attempt failed predicate." }
            } catch (e: WaiterUnrecoverableException) {
                throw e
            } catch (e: Exception) {
                val cause = if (e is CompletionException) {
                    e.cause ?: e
                } else {
                    e
                }

                if (cause::class in exceptionsToStopOn) {
                    LOG.info { "Got expected exception: ${cause::class}" }
                    return null
                }

                if (cause::class in exceptionsToIgnore) {
                    LOG.info { "Attempt $attempt failed with an expected exception (${cause::class})" }
                } else {
                    throw e
                }
            }

            delay(calculateBackOffMs(attempt))
        }

        throw WaiterTimeoutException("Condition was not met after $attempt attempts (${Duration.between(start, Instant.now()).seconds} seconds)")
    }

    private fun calculateBackOffMs(attempt: Int) = 250L shl min(attempt - 1, 4) // Exponential backoff: Max = 250 * 2^4 = 4_000
}

/**
 * Generic waiter method to wait for a certain AWS resource to be in a steady status until it reaches in a failed status
 * or time out for pulling.
 * @param T The AWS resource type for querying the status from it
 */
// TODO: Migrate off of, this should not expose strings on errors
@Deprecated("Exposes localized strings, use waitUntil")
fun <T> wait(
    // The status pulling method to get the latest resource
    call: () -> T,
    // The success predicate based on the returned resource
    success: (T) -> Boolean,
    // The fail predicate based on the returned resource. Return an error message if it fails, null otherwise
    fail: (T) -> String?,
    // The success predicate based on the exception thrown
    successByException: (AwsServiceException) -> Boolean = { false },
    // The fail predicate based on the exception thrown. Return an error message if it fails, null otherwise.
    failByException: (AwsServiceException) -> String? = { null },
    // The error message for timeout this pulling process.
    timeoutErrorMessage: String = "Timeout for transitioning the resource to be in the desired state.",
    // The maximum attempt for pulling the resource
    attempts: Int,
    // A fixed time interval between the pulling
    delay: Duration
) {
    repeat(attempts) {
        try {
            val result = call()
            val errorMessage = fail(result)
            when {
                success(result) -> return
                errorMessage != null -> throw WaiterUnrecoverableException(errorMessage)
                else -> Thread.sleep(delay.toMillis())
            }
        } catch (e: AwsServiceException) {
            val errorMessage = failByException(e)
            when {
                successByException(e) -> return
                errorMessage != null -> throw WaiterUnrecoverableException(errorMessage)
                else -> throw e
            }
        }
    }
    throw WaiterTimeoutException(timeoutErrorMessage)
}

class WaiterTimeoutException(message: String) : Exception(message)

class WaiterUnrecoverableException(message: String) : Exception(message)


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

package software.aws.toolkits.core.utils

import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Path
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream

/**
 * Adds a new [ZipEntry] with the contents of [file] to the [ZipOutputStream].
 */
fun ZipOutputStream.putNextEntry(entryName: String, file: Path) {
    this.putNextEntry(ZipEntry(entryName))
    val bytes = Files.readAllBytes(file)
    this.write(bytes, 0, bytes.size)
    this.closeEntry()
}

/**
 * Adds a new [ZipEntry] with the contents of [inputStream] to the [ZipOutputStream].
 */
fun ZipOutputStream.putNextEntry(entryName: String, inputStream: InputStream) {
    this.putNextEntry(ZipEntry(entryName))
    inputStream.copyTo(this)
    this.closeEntry()
}

/**
 * Create a zip file in a temporary location.
 *
 * Statements included in [block] populate the zip file with entries.
 *
 * @return the [Path] of the temporary file
 */
fun createTemporaryZipFile(block: (ZipOutputStream) -> Unit): Path {
    val file = Files.createTempFile(null, ".zip")
    ZipOutputStream(Files.newOutputStream(file)).use(block)
    return file
}


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

package software.aws.toolkits.core.credentials

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials
import software.aws.toolkits.core.utils.test.aString

class AwsCredentialsExtensionsTest {

    @Test
    fun `can convert basic credentials to environment variables`() {
        val credentials = AwsBasicCredentials.create(aString(), aString())
        assertThat(credentials.toEnvironmentVariables()).hasSize(2)
            .containsEntry("AWS_ACCESS_KEY_ID", credentials.accessKeyId())
            .containsEntry("AWS_SECRET_ACCESS_KEY", credentials.secretAccessKey())
    }

    @Test
    fun `can convert session credentials to environment variables`() {
        val credentials = AwsSessionCredentials.create(aString(), aString(), aString())
        assertThat(credentials.toEnvironmentVariables()).hasSize(3)
            .containsEntry("AWS_ACCESS_KEY_ID", credentials.accessKeyId())
            .containsEntry("AWS_SECRET_ACCESS_KEY", credentials.secretAccessKey())
            .containsEntry("AWS_SESSION_TOKEN", credentials.sessionToken())
    }

    @Test
    fun `can add environment variables to an existing env map`() {
        val credentials = AwsSessionCredentials.create(aString(), aString(), aString())
        val env = mutableMapOf<String, String>()

        credentials.mergeWithExistingEnvironmentVariables(env)

        assertThat(env).containsOnlyKeys("AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN")
    }

    @Test
    fun `existing credentials are not replaced by default`() {
        val credentials = AwsBasicCredentials.create(aString(), aString())
        val existingToken = aString()
        val env = mutableMapOf("AWS_SESSION_TOKEN" to existingToken)

        credentials.mergeWithExistingEnvironmentVariables(env)

        assertThat(env).hasSize(1).containsEntry("AWS_SESSION_TOKEN", existingToken)
    }

    @Test
    fun `existing credentials can be replaced`() {
        val credentials = AwsBasicCredentials.create(aString(), aString())
        val existingToken = aString()
        val env = mutableMapOf("AWS_SESSION_TOKEN" to existingToken)

        credentials.mergeWithExistingEnvironmentVariables(env, replace = true)

        assertThat(env).hasSize(2)
            .containsEntry("AWS_ACCESS_KEY_ID", credentials.accessKeyId())
            .containsEntry("AWS_SECRET_ACCESS_KEY", credentials.secretAccessKey())
    }
}


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

package software.aws.toolkits.core.credentials

import org.mockito.kotlin.mock
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.aws.toolkits.core.utils.test.aString

fun aToolkitCredentialsProvider(
    identifier: CredentialIdentifier = aCredentialsIdentifier(),
    delegate: AwsCredentialsProvider = mock()
) = ToolkitCredentialsProvider(identifier, delegate)

fun aCredentialsIdentifier(
    id: String = aString(),
    displayName: String = aString(),
    factoryId: String = aString(),
    defaultRegionId: String? = null
) = object : CredentialIdentifierBase(CredentialType.StaticProfile) {
    override val id: String = id
    override val displayName: String = displayName
    override val factoryId: String = factoryId
    override val defaultRegionId: String? = defaultRegionId
}


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

package software.aws.toolkits.core.lambda

import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import software.aws.toolkits.core.utils.RemoteResourceResolver
import java.util.concurrent.CompletableFuture

class LambdaSampleEventProviderTest {

    @Rule
    @JvmField
    val tempPath = TemporaryFolder()

    @Test
    fun canParseManifestFileAndLoadContent() {
        val manifestFile = tempPath.newFile()
        val firstFile = tempPath.newFile()
        val secondFile = tempPath.newFile()

        manifestFile.writeText(
            """
            <requests>
                <request category="AWS">
                    <name>First Sample</name>
                    <filename>first.json</filename>
                </request>
                <request category="AWS">
                    <name>Second Sample</name>
                    <filename>second.json</filename>
                </request>
            </requests>
            """.trimIndent()
        )

        val firstContent =
            """
            {
                "hello": "world"
            }
            """.trimIndent()

        firstFile.writeText(firstContent)

        val secondContent =
            """
            ["hello"]
            """.trimIndent()

        secondFile.writeText(secondContent)

        val resourceResolver = mock<RemoteResourceResolver> {
            on { resolve(LambdaSampleEventManifestResource) }.thenReturn(CompletableFuture.completedFuture(manifestFile.toPath()))
            on { resolve(LambdaSampleEventResource("first.json")) }.thenReturn(CompletableFuture.completedFuture(firstFile.toPath()))
            on { resolve(LambdaSampleEventResource("second.json")) }.thenReturn(CompletableFuture.completedFuture(secondFile.toPath()))
        }
        val sut = LambdaSampleEventProvider(resourceResolver)

        val samples = sut.get().toCompletableFuture().get()

        assertThat(samples).hasSize(2)

        val first = samples[0]
        assertThat(first.name).isEqualTo("First Sample")
        assertThat(first.content.toCompletableFuture().get()).isEqualTo(firstContent)

        val second = samples[1]
        assertThat(second.name).isEqualTo("Second Sample")
        assertThat(second.content.toCompletableFuture().get()).isEqualTo(secondContent)
    }

    @Test
    fun manifestResultsAreCached() {
        val manifestFile = tempPath.newFile()

        manifestFile.writeText(
            """
            <requests>
                <request category="AWS">
                    <name>First Sample</name>
                    <filename>first.json</filename>
                </request>
            </requests>
            """.trimIndent()
        )

        val resourceResolver = mock<RemoteResourceResolver> {
            on { resolve(LambdaSampleEventManifestResource) }.thenReturn(CompletableFuture.completedFuture(manifestFile.toPath()))
        }
        val sut = LambdaSampleEventProvider(resourceResolver)

        sut.get().toCompletableFuture().get()
        sut.get().toCompletableFuture().get()

        verify(resourceResolver, times(1)).resolve(LambdaSampleEventManifestResource)
    }
}


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

package software.aws.toolkits.core.parser

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import software.aws.toolkits.core.region.EndpointsJsonValidator

class EndpointsJsonValidatorTest {
    @Test
    fun `endpoints json file parsing succeeds`() {
        EndpointsJsonValidatorTest::class.java.getResourceAsStream("/jsonSampleSuccess.json").use {
            assertThat(EndpointsJsonValidator.canBeParsed(it)).isTrue
        }
    }

    @Test
    fun `endpoints json file parsing fails`() {
        EndpointsJsonValidatorTest::class.java.getResourceAsStream("/jsonSampleFailure.json").use {
            assertThat(EndpointsJsonValidator.canBeParsed(it)).isFalse
        }
    }
}


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

package software.aws.toolkits.core.parser

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import software.aws.toolkits.core.lambda.LambdaManifestValidator

class LambdaManifestValidatorTest {

    @Test
    fun `manifest xml file parsing succeeds`() {
        LambdaManifestValidatorTest::class.java.getResourceAsStream("/xmlSampleSuccess.xml").use {
            assertThat(LambdaManifestValidator.canBeParsed(it)).isTrue
        }
    }

    @Test
    fun `manifest xml file parsing fails`() {
        LambdaManifestValidatorTest::class.java.getResourceAsStream("/xmlSampleFailure.xml").use {
            assertThat(LambdaManifestValidator.canBeParsed(it)).isFalse
        }
    }
}


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

package software.aws.toolkits.core.parser

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import software.aws.toolkits.core.lambda.LambdaSampleEventJsonValidator
class LambdaSampleEventJsonValidatorTest {
    @Test
    fun `lambda sample event json file parsing succeeds`() {
        LambdaSampleEventJsonValidatorTest::class.java.getResourceAsStream("/sampleLambdaEvent.json").use {
            assertThat(LambdaSampleEventJsonValidator.canBeParsed(it)).isTrue
        }
    }

    @Test
    fun `lambda sample event json file parsing fails`() {
        LambdaSampleEventJsonValidatorTest::class.java.getResourceAsStream("/jsonSampleFailure.json").use {
            assertThat(LambdaSampleEventJsonValidator.canBeParsed(it)).isFalse
        }
    }
}


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

package software.aws.toolkits.core.region

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.junit.experimental.runners.Enclosed
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import software.aws.toolkits.core.utils.test.aString
import kotlin.random.Random

@RunWith(Enclosed::class)
class AwsRegionTest {

    @RunWith(Parameterized::class)
    class NameAndCategorizationTest(private val region: AwsRegion, private val expectedCategory: String, private val expectedDisplayName: String) {
        @Test
        fun `display name should be correct`() {
            assertThat(region.displayName).isEqualTo(expectedDisplayName)
        }

        @Test
        fun `category should match`() {
            assertThat(region.category).isEqualTo(expectedCategory)
        }

        companion object {
            @JvmStatic
            @Parameterized.Parameters(name = "{2}")
            fun data(): Collection<Array<Any>> = listOf(
                arrayOf(AwsRegion("ap-northeast-1", "Asia Pacific (Tokyo)", "aws"), "Asia Pacific", "Tokyo (ap-northeast-1)"),
                arrayOf(AwsRegion("ca-central-1", "Canada (Central)", "aws"), "North America", "Canada Central (ca-central-1)"),
                arrayOf(AwsRegion("eu-central-1", "EU (Frankfurt)", "aws"), "Europe", "Frankfurt (eu-central-1)"),
                arrayOf(AwsRegion("eu-south-1", "Europe (Milan)", "aws"), "Europe", "Milan (eu-south-1)"),
                arrayOf(AwsRegion("sa-east-1", "South America (Sao Paulo)", "aws"), "South America", "Sao Paulo (sa-east-1)"),
                arrayOf(AwsRegion("us-east-1", "US East (N. Virginia)", "aws"), "North America", "N. Virginia (us-east-1)"),
                arrayOf(AwsRegion("us-west-1", "US West (N. California)", "aws"), "North America", "N. California (us-west-1)"),
                arrayOf(AwsRegion("cn-north-1", "China (Beijing)", "aws"), "China", "Beijing (cn-north-1)"),
                arrayOf(AwsRegion("us-gov-west-1", "AWS GovCloud (US)", "aws"), "North America", "AWS GovCloud US (us-gov-west-1)"),
                arrayOf(AwsRegion("me-south-1", "Middle East (Bahrain)", "aws"), "Middle East", "Bahrain (me-south-1)"),
                arrayOf(AwsRegion("af-south-1", "Africa (Cape Town)", "aws"), "Africa", "Cape Town (af-south-1)")
            )
        }
    }

    class ExtensionFunctionTests {

        private val region = anAwsRegion()

        @Test
        fun `mergeWithExistingEnvironmentVariables puts basic settings in the map`() {
            val env = mutableMapOf<String, String>()

            region.mergeWithExistingEnvironmentVariables(env)

            assertThat(env).hasSize(2)
                .containsEntry("AWS_REGION", region.id)
                .containsEntry("AWS_DEFAULT_REGION", region.id)
        }

        @Test
        fun `mergeWithExistingEnvironmentVariables does not replace existing AWS_REGION`() {
            val existing = aString()
            val env = mutableMapOf(
                "AWS_REGION" to existing
            )

            region.mergeWithExistingEnvironmentVariables(env)

            assertThat(env).hasSize(1).containsEntry("AWS_REGION", existing)
        }

        @Test
        fun `mergeWithExistingEnvironmentVariables does not replace existing AWS_DEFAULT_REGION`() {
            val existing = aString()
            val env = mutableMapOf(
                "AWS_DEFAULT_REGION" to existing
            )

            region.mergeWithExistingEnvironmentVariables(env)

            assertThat(env).hasSize(1).containsEntry("AWS_DEFAULT_REGION", existing)
        }

        @Test
        fun `mergeWithExistingEnvironmentVariables can force replace existing`() {
            val existing = aString()
            val env = mutableMapOf(
                "AWS_REGION" to existing,
                "AWS_DEFAULT_REGION" to existing
            )

            region.mergeWithExistingEnvironmentVariables(env, replace = true)

            assertThat(env).hasSize(2)
                .containsEntry("AWS_REGION", region.id)
                .containsEntry("AWS_DEFAULT_REGION", region.id)
        }
    }
}

fun anAwsRegion(id: String = aRegionId(), name: String = aString(), partitionId: String = aString()) = AwsRegion(id, name, partitionId)

fun aRegionId(): String {
    val prefix = arrayOf("af", "us", "ca", "eu", "ap", "me", "cn").random()
    val compass = arrayOf("north", "south", "east", "west", "central").random()
    val count = Random.nextInt(1, 100)
    return "$prefix-$compass-$count"
}


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

package software.aws.toolkits.core.region

import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import software.aws.toolkits.resources.BundledResources

class PartitionParserTest {
    @Test
    fun canLoadPartitionsFromEndpointsFile() {
        val partitions = PartitionParser.parse(BundledResources.ENDPOINTS_FILE)!!
        val awsPartition = partitions.getPartition("aws")

        val iam = awsPartition.services.getValue("iam")
        val s3 = awsPartition.services.getValue("s3")
        val lambda = awsPartition.services.getValue("lambda")

        assertThat(iam.isGlobal).isTrue()
        assertThat(s3.isGlobal).isFalse()
        assertThat(lambda.isGlobal).isFalse()
    }
}


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

package software.aws.toolkits.core.rules

import org.junit.rules.ExternalResource
import software.amazon.awssdk.services.ecr.EcrClient
import software.amazon.awssdk.services.ecr.model.Repository
import software.amazon.awssdk.services.ecr.model.RepositoryNotFoundException
import software.aws.toolkits.core.utils.RuleUtils

class EcrTemporaryRepositoryRule(private val ecrClientSupplier: () -> EcrClient) : ExternalResource() {
    constructor(ecrClient: EcrClient) : this({ ecrClient })

    private val repositories = mutableListOf<String>()

    /**
     * Creates a temporary repository with the optional prefix (or calling class if prefix is omitted)
     */
    fun createRepository(prefix: String = RuleUtils.prefixFromCallingClass()): Repository {
        val repositoryName: String = RuleUtils.randomName(prefix).lowercase()
        val client = ecrClientSupplier()

        // note there is no waiter for this
        val repo = client.createRepository { it.repositoryName(repositoryName) }

        repositories.add(repositoryName)

        return repo.repository()
    }

    override fun after() {
        val exceptions = repositories.mapNotNull { deleteRepository(it) }
        if (exceptions.isNotEmpty()) {
            throw RuntimeException("Failed to delete all repositories. \n\t- ${exceptions.map { it.message }.joinToString("\n\t- ")}")
        }
    }

    private fun deleteRepository(repository: String): Exception? = try {
        ecrClientSupplier().deleteRepository { it.repositoryName(repository).force(true) }
        null
    } catch (e: Exception) {
        when (e) {
            is RepositoryNotFoundException -> null
            else -> RuntimeException("Failed to delete repository: $repository - ${e.message}", e)
        }
    }
}


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

package software.aws.toolkits.core.rules

import org.junit.rules.ExternalResource
import software.amazon.awssdk.services.ecs.EcsClient
import software.amazon.awssdk.services.ecs.model.CreateServiceRequest
import software.amazon.awssdk.services.ecs.model.Service
import software.amazon.awssdk.services.ecs.model.ServiceNotFoundException
import software.aws.toolkits.core.utils.RuleUtils

class ECSTemporaryServiceRule(val ecsClient: EcsClient) : ExternalResource() {
    private val services = mutableListOf<Service>()

    /**
     * Creates a temporary service. A random name with the generated prefix will be generated if a name is not provided in the request.
     */
    fun createService(prefix: String = RuleUtils.prefixFromCallingClass(), serviceBuilder: (CreateServiceRequest.Builder) -> Unit): Service {
        val service = ecsClient.createService {
            it.serviceName(RuleUtils.randomName(prefix))
            serviceBuilder(it)
        }.service()

        services.add(service)
        return service
    }

    override fun after() {
        val exceptions = services.mapNotNull { service ->
            val clusterArn = service.clusterArn()
            val serviceArn = service.serviceArn()
            deleteService(clusterArn, serviceArn)
        }
        if (exceptions.isNotEmpty()) {
            throw RuntimeException("Failed to delete all services. \n\t- ${exceptions.map { it.message }.joinToString("\n\t- ")}")
        }
    }

    private fun deleteService(cluster: String, service: String): Exception? = try {
        ecsClient.deleteService {
            it.cluster(cluster)
            it.service(service)
            it.force(true)
        }
        null
    } catch (e: Exception) {
        when (e) {
            is ServiceNotFoundException -> null
            else -> RuntimeException("Failed to delete service: $service - ${e.message}", e)
        }
    }
}


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

package software.aws.toolkits.core.rules

import org.junit.rules.ExternalResource
import java.security.AccessController
import java.security.PrivilegedAction

/**
 * A utility that can temporarily forcibly set environment variables and
 * then allows resetting them to the original values.
 */
class EnvironmentVariableHelper : ExternalResource() {
    private val originalEnvironmentVariables = System.getenv().toMap()
    private val modifiableMap = getProcessEnvMap() ?: getEnvMap()

    @Volatile
    private var mutated = false

    fun remove(vararg keys: String) {
        mutated = true
        keys.forEach { modifiableMap.remove(it) }
    }

    operator fun set(key: String, value: String) {
        mutated = true
        modifiableMap[key] = value
    }

    private fun getEnvMap(): MutableMap<String, String> = getField(System.getenv().javaClass, System.getenv(), "m")!!

    private fun getProcessEnvMap(): MutableMap<String, String>? {
        val processEnvironment = Class.forName("java.lang.ProcessEnvironment")
        return getField(processEnvironment, null, "theCaseInsensitiveEnvironment")
    }

    private fun getField(processEnvironment: Class<*>, obj: Any?, fieldName: String): MutableMap<String, String>? = try {
        val declaredField = processEnvironment.getDeclaredField(fieldName)
        AccessController.doPrivileged(
            PrivilegedAction<Unit> {
                declaredField.isAccessible = true
            }
        )
        @Suppress("UNCHECKED_CAST")
        declaredField.get(obj) as MutableMap<String, String>
    } catch (_: NoSuchFieldException) {
        null
    }

    private fun reset() {
        if (mutated) {
            synchronized(this) {
                if (mutated) {
                    modifiableMap.clear()
                    modifiableMap.putAll(originalEnvironmentVariables)
                    mutated = false
                }
            }
        }
    }

    override fun after() {
        reset()
    }
}


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

package software.aws.toolkits.core.rules

import org.junit.rules.ExternalResource
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.NoSuchBucketException
import software.aws.toolkits.core.s3.deleteBucketAndContents
import software.aws.toolkits.core.utils.RuleUtils

class S3TemporaryBucketRule(private val s3ClientSupplier: () -> S3Client) : ExternalResource() {
    constructor(s3Client: S3Client) : this({ s3Client })

    private val buckets = mutableListOf<String>()

    /**
     * Creates a temporary bucket with the optional prefix (or calling class if prefix is omitted)
     */
    fun createBucket(prefix: String = RuleUtils.prefixFromCallingClass()): String {
        val bucketName: String = RuleUtils.randomName(prefix).lowercase()
        val client = s3ClientSupplier()

        client.createBucket { it.bucket(bucketName) }

        // Wait for bucket to be ready
        client.waiter().waitUntilBucketExists { it.bucket(bucketName) }

        buckets.add(bucketName)

        return bucketName
    }

    override fun after() {
        val exceptions = buckets.mapNotNull { deleteBucketAndContents(it) }
        if (exceptions.isNotEmpty()) {
            throw RuntimeException("Failed to delete all buckets. \n\t- ${exceptions.map { it.message }.joinToString("\n\t- ")}")
        }
    }

    private fun deleteBucketAndContents(bucket: String): Exception? = try {
        s3ClientSupplier().deleteBucketAndContents(bucket)
        null
    } catch (e: Exception) {
        when (e) {
            is NoSuchBucketException -> null
            else -> RuntimeException("Failed to delete bucket: $bucket - ${e.message}", e)
        }
    }
}


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

package software.aws.toolkits.core.rules

import org.junit.rules.ExternalResource
import java.util.Properties

/**
 * A utility that can temporarily forcibly set system properties and
 * then allows resetting them to the original values.
 */
class SystemPropertyHelper : ExternalResource() {
    private lateinit var originalProperties: Properties

    override fun before() {
        originalProperties = Properties().apply {
            this.putAll(System.getProperties())
        }
    }

    override fun after() {
        System.setProperties(originalProperties)
    }
}


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

package software.aws.toolkits.core.telemetry

import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers.anyCollection
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.mock
import org.mockito.kotlin.stub
import org.mockito.kotlin.times
import org.mockito.kotlin.verifyBlocking
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.stubbing.Answer
import software.amazon.awssdk.core.exception.SdkServiceException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

class TelemetryBatcherTest {
    private var publisher: TelemetryPublisher = mock()
    private var batcher = DefaultTelemetryBatcher(publisher, MAX_BATCH_SIZE, MAX_QUEUE_SIZE)

    @Before
    fun setUp() {
        batcher.onTelemetryEnabledChanged(true)
    }

    @Test
    fun testSingleBatch() {
        val publishCountDown = CountDownLatch(1)
        val publishCaptor = argumentCaptor<Collection<MetricEvent>>()

        publisher.stub {
            onBlocking { publisher.publish(publishCaptor.capture()) }
                .doAnswer(createPublishAnswer(publishCountDown))
        }

        batcher.enqueue(DefaultMetricEvent.builder().build())
        batcher.flush(false)

        waitForPublish(publishCountDown)

        verifyBlocking(publisher) { publish(anyCollection()) }

        assertThat(publishCaptor.firstValue).hasSize(1)
    }

    @Test
    fun testSplitBatch() {
        val publishCaptor = argumentCaptor<Collection<MetricEvent>>()

        val totalEvents = MAX_BATCH_SIZE + 1
        repeat(totalEvents) {
            batcher.enqueue(createEmptyMetricEvent())
        }
        batcher.flush(false)

        verifyBlocking(publisher, times(2)) { publish(publishCaptor.capture()) }

        assertThat(publishCaptor.allValues).hasSize(2)
        assertThat(publishCaptor.firstValue).hasSize(MAX_BATCH_SIZE)
        assertThat(publishCaptor.secondValue).hasSize(1)
    }

    @Test
    fun testRetryException() {
        val publishCaptor = argumentCaptor<Collection<MetricEvent>>()

        publisher.stub {
            onBlocking { publisher.publish(anyCollection()) }
                .doThrow(RuntimeException("Mock exception"))
        }

        batcher.enqueue(createEmptyMetricEvent())
        batcher.flush(true)

        verifyBlocking(publisher, times(1)) { publish(publishCaptor.capture()) }

        assertThat(publishCaptor.allValues).hasSize(1)
        assertThat(batcher.eventQueue).hasSize(1)
    }

    @Test
    fun testDontRetry400Exception() {
        val publishCaptor = argumentCaptor<Collection<MetricEvent>>()

        publisher.stub {
            onBlocking { publisher.publish(anyCollection()) }
                .doThrow(SdkServiceException.builder().statusCode(400).build())
                .doAnswer(Answer {})
        }

        batcher.enqueue(createEmptyMetricEvent())
        batcher.flush(true)

        verifyBlocking(publisher, times(1)) { publish(publishCaptor.capture()) }

        assertThat(publishCaptor.allValues).hasSize(1)
        assertThat(batcher.eventQueue).hasSize(0)
    }

    @Test
    fun testDispose() {
        val publishCaptor = argumentCaptor<Collection<MetricEvent>>()

        batcher.enqueue(createEmptyMetricEvent())
        batcher.shutdown()
        batcher.enqueue(createEmptyMetricEvent())
        batcher.shutdown()

        verifyBlocking(publisher) { publish(publishCaptor.capture()) }

        assertThat(publishCaptor.allValues).hasSize(1)
        assertThat(publishCaptor.firstValue.toList()).hasSize(1)
    }

    @Test
    fun testNotSendingWhileDisabled() {
        batcher.onTelemetryEnabledChanged(false)

        batcher.enqueue(createEmptyMetricEvent())
        batcher.flush(false)

        verifyNoMoreInteractions(publisher)

        assertThat(batcher.eventQueue).isEmpty()
    }

    @Test
    fun verifyOptOut() {
        batcher.enqueue(createEmptyMetricEvent())
        assertThat(batcher.eventQueue).isNotEmpty

        batcher.onTelemetryEnabledChanged(false)

        batcher.flush(false)

        verifyNoMoreInteractions(publisher)

        assertThat(batcher.eventQueue).isEmpty()
    }

    private fun createEmptyMetricEvent(): MetricEvent = DefaultMetricEvent.builder().build()

    private fun waitForPublish(publishCountDown: CountDownLatch) {
        // Wait for maximum of 5 secs before thread continues, may not reach final count though
        publishCountDown.await(5, TimeUnit.SECONDS)
    }

    private fun createPublishAnswer(publishCountDown: CountDownLatch): Answer<Unit> = Answer {
        publishCountDown.countDown()
    }

    companion object {
        private const val MAX_BATCH_SIZE = 5
        private const val MAX_QUEUE_SIZE = 10
    }
}


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

package software.aws.toolkits.core.utils

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

class CollectionUtilsTest {

    @Test
    fun `collection items are replaced`() {
        val source = mutableListOf("hello")

        source.replace(listOf("world"))

        assertThat(source).containsOnly("world")
    }

    @Test
    fun `map entries are replaced`() {
        val source = mutableMapOf("foo" to "bar")
        source.replace(mapOf("hello" to "world"))

        assertThat(source).containsOnlyKeys("hello")
    }
}


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

package software.aws.toolkits.core.utils

import org.jetbrains.annotations.TestOnly
import java.util.concurrent.CompletionStage
import java.util.concurrent.ExecutionException

// Wait for a completion stage to end, and throw the exception that caused it to fail
// if it fails.
@TestOnly
fun <T> CompletionStage<T>.unwrap(): T = try {
    this.toCompletableFuture().get()
} catch (e: ExecutionException) {
    throw e.cause ?: e
}


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

package software.aws.toolkits.core.utils

import org.mockito.Mockito
import org.mockito.invocation.InvocationOnMock
import org.mockito.kotlin.KStubbing
import org.mockito.kotlin.withSettings
import org.mockito.stubbing.Answer
import software.amazon.awssdk.core.SdkClient
import software.amazon.awssdk.core.SdkRequest
import kotlin.reflect.full.isSubclassOf

/**
 * Answer that inspects the target invocation and determines if it should call the real method or respond with a mock answer.
 * This is tied to the implementation of the Java SDK V2's generated client interfaces where lambda based calls use a default implementation to delegate
 * eventually down to a method that takes a built SdkRequest type. The final concrete method is coded to always throw an exception so that is the method that we
 * will mock.
 *
 * This will handle "simple" methods that they generate as well such as ListBuckets that takes 0 arguments.
 */
class DelegateSdkConsumers(private val sdkClass: Class<*>) : Answer<Any> {
    override fun answer(invocation: InvocationOnMock): Any? {
        val method = invocation.method
        return if (method.name == "waiter") {
            createWaiter(invocation.method.returnType, invocation)
        } else if (method.isDefault && method?.parameters?.getOrNull(0)?.type?.kotlin?.isSubclassOf(SdkRequest::class) != true) {
            invocation.callRealMethod()
        } else {
            Mockito.RETURNS_DEFAULTS.answer(invocation)
        }
    }

    private fun createWaiter(waiterType: Class<*>, invocation: InvocationOnMock): Any? {
        val builder = waiterType.getDeclaredMethod("builder").invoke(null)
        with(builder::class.java) {
            getDeclaredMethod("client", sdkClass).invoke(builder, invocation.mock)
            return getDeclaredMethod("build").invoke(builder)
        }
    }
}

inline fun <reified T : SdkClient> delegateMock(verboseLogging: Boolean = false): T = Mockito.mock(
    T::class.java,
    withSettings(
        verboseLogging = verboseLogging,
        defaultAnswer = DelegateSdkConsumers(T::class.java)
    )
)

inline fun <reified T : SdkClient> delegateMock(stubbing: KStubbing<T>.(T) -> Unit): T = delegateMock<T>().apply {
    KStubbing(this).stubbing(this)
}


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

package software.aws.toolkits.core.utils

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

class EitherTest {
    @Test
    fun basicLeftFunctionality() {
        val result: Either<String, Int> = maybeString(false) xor maybeInt(true)
        assertThat(result).isInstanceOfSatisfying(Either.Left::class.java) {
            assertThat(it.value).isEqualTo("foo")
        }
    }

    @Test
    fun basicRightFunctionality() {
        val result: Either<String, Int> = maybeString(true) xor maybeInt(false)
        assertThat(result).isInstanceOfSatisfying(Either.Right::class.java) {
            assertThat(it.value).isEqualTo(50)
        }
    }

    @Test
    fun cantBothBeNonNull() {
        assertThatThrownBy { maybeString(false) xor maybeInt(false) }.isInstanceOf(IllegalArgumentException::class.java)
    }

    @Test
    fun cantBothBeNull() {
        assertThatThrownBy { maybeString(true) xor maybeInt(true) }.isInstanceOf(IllegalArgumentException::class.java)
    }

    private fun maybeString(isNull: Boolean): String? = if (isNull) null else "foo"
    private fun maybeInt(isNull: Boolean): Int? = if (isNull) null else 50
}


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

package software.aws.toolkits.core.utils

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

class ExceptionUtilsTest {
    @Test
    fun exceptionsAreNotBubbled() {
        @Suppress("DIVISION_BY_ZERO")
        val result = tryOrNull { 1 / 0 }
        assertThat(result).isNull()
    }
}


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

package software.aws.toolkits.core.utils

import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider
import software.amazon.awssdk.auth.credentials.ContainerCredentialsProvider
import software.amazon.awssdk.services.sts.StsClient
import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider
import software.amazon.awssdk.services.sts.model.AssumeRoleRequest
import software.amazon.awssdk.utils.SdkAutoCloseable
import java.util.UUID

/**
 * Creates an [AwsCredentialsProvider] meant to be used in integration tests
 *
 * If the environment variable `ASSUME_ROLE_ARN` is set, it will be assumed using the default credential chain as the source credentials.
 * If it is not set, we will just use the default credential provider chain
 */
fun createIntegrationTestCredentialProvider(): AwsCredentialsProvider {
    // TODO: Finish https://github.com/aws/aws-toolkit-jetbrains/pull/2193 and revert back to Default Chain
    val defaultCredentials = ContainerCredentialsProvider.builder().build()

    return System.getenv("ASSUME_ROLE_ARN")?.takeIf { it.isNotEmpty() }?.let { role ->
        val sessionName = UUID.randomUUID().toString()
        val stsClient = StsClient.builder().credentialsProvider(defaultCredentials).build()
        val credentialsProvider = StsAssumeRoleCredentialsProvider.builder()
            .stsClient(stsClient)
            .refreshRequest(AssumeRoleRequest.builder().roleArn(role).roleSessionName(sessionName).build())
            .build()

        // Wrap this in SdkAutoClosable so we have a hook to close this STS client else IntelliJ will say we thread leak
        return object : AwsCredentialsProvider by credentialsProvider, SdkAutoCloseable {
            override fun close() {
                stsClient.close()
            }
        }
    } ?: defaultCredentials
}


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

@file:Suppress("LazyLog")
package software.aws.toolkits.core.utils

import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.reset
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
import org.slf4j.Logger
import org.slf4j.event.Level

class LogUtilsTest {

    private val log = mock<Logger>()

    @Test
    fun exceptionIsLoggedAndSuppressedInTryOrNull() {
        val expectedException = RuntimeException("Boom")
        log.tryOrNull("message", level = Level.WARN) { throw expectedException }
        verify(log).warn(any(), eq(expectedException))
    }

    @Test
    fun exceptionIsLoggedAndBubbledInTryOrThrowNullable() {
        val expectedException = RuntimeException("Boom")
        val exception = catch { log.tryOrThrowNullable("message") { throw expectedException } }
        verify(log).error(any(), eq(expectedException))
        assertThat(exception).isEqualTo(expectedException)
    }

    @Test
    fun exceptionIsLoggedAndBubbledInTryOrThrow() {
        val expectedException = RuntimeException("Boom")
        val exception = catch { log.tryOrThrow<Unit>("message") { throw expectedException } }
        verify(log).error(any(), eq(expectedException))
        assertThat(exception).isEqualTo(expectedException)
    }

    @Test
    fun nullableIsNotOkInTryOrThrow() {
        val exception = catch { log.tryOrThrow<String?>("message") { mightBeNull(shouldBeNull = true) } }
        verify(log).error(any(), eq(exception))
    }

    @Test
    fun smartCastToNonNullOnTryOrThrow() {
        val nullableValue: String = log.tryOrThrow("message") { mightBeNull(shouldBeNull = false) }
        val nonNullableValue: String = log.tryOrThrow("message") { willNeverBeNull() }
        assertThat(nullableValue).isEqualTo(nonNullableValue)
    }

    @Test
    fun nullableIsOkInTryOrThrowNullable() {
        log.tryOrThrowNullable("message") { null }
        verifyNoMoreInteractions(log)
    }

    @Test
    fun nullIsOkInTryOrNull() {
        log.tryOrNull("message") { null }
        verifyNoMoreInteractions(log)
    }

    @Test
    fun conditionalLazyLoggingInfo() {
        val exception = RuntimeException("Boom")

        log.info(exception) { "message" }

        whenever(log.isInfoEnabled).thenReturn(false)
        log.info(exception) { "message" }

        verify(log).info("message", exception)
    }

    @Test
    fun conditionalLazyLoggingDebug() {
        val exception = RuntimeException("Boom")

        log.debug(exception) { "message" }

        whenever(log.isDebugEnabled).thenReturn(false)
        log.debug(exception) { "message" }

        verify(log).debug("message", exception)
    }

    @Test
    fun conditionalLazyLoggingWarn() {
        val exception = RuntimeException("Boom")

        log.warn(exception) { "message" }

        whenever(log.isWarnEnabled).thenReturn(false)
        log.warn(exception) { "message" }

        verify(log).warn("message", exception)
    }

    @Test
    fun conditionalLazyLoggingError() {
        val exception = RuntimeException("Boom")

        log.error(exception) { "message" }

        whenever(log.isErrorEnabled).thenReturn(false)
        log.error(exception) { "message" }

        verify(log).error("message", exception)
    }

    @Test
    fun conditionalLazyLoggingTrace() {
        val exception = RuntimeException("Boom")

        log.trace(exception) { "message" }

        whenever(log.isTraceEnabled).thenReturn(false)
        log.trace(exception) { "message" }

        verify(log).trace("message", exception)
    }

    @Test
    fun canLogAtDifferentLevels() {
        val exception = RuntimeException("Boom")

        log.log(Level.TRACE) { "trace" }
        log.log(Level.INFO) { "info" }
        log.log(Level.ERROR, exception = exception) { "error" }
        log.log(Level.WARN) { "warn" }
        log.log(Level.DEBUG) { "debug" }

        verify(log).trace("trace", null)
        verify(log).info("info", null)
        verify(log).error("error", exception)
        verify(log).warn("warn", null)
        verify(log).debug("debug", null)
    }

    @Test
    fun logWhenNull() {
        log.logWhenNull("message", level = Level.WARN) { null }
        verify(log).warn("message", null)
    }

    @Before
    fun setup() {
        reset(log)
        whenever(log.isInfoEnabled).thenReturn(true)
        whenever(log.isWarnEnabled).thenReturn(true)
        whenever(log.isTraceEnabled).thenReturn(true)
        whenever(log.isErrorEnabled).thenReturn(true)
        whenever(log.isDebugEnabled).thenReturn(true)
    }

    private fun catch(block: () -> Unit): Exception = try {
        block()
        throw AssertionError("Expected exception")
    } catch (e: Exception) {
        e
    }

    private fun mightBeNull(shouldBeNull: Boolean): String? = if (shouldBeNull) {
        null
    } else {
        "hello"
    }

    @Suppress("FunctionOnlyReturningConstant")
    private fun willNeverBeNull(): String = "hello"
}


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

package software.aws.toolkits.core.utils

import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.mockito.invocation.InvocationOnMock
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import software.aws.toolkits.core.lambda.LambdaManifestValidator
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Path
import java.time.Duration
import java.util.concurrent.Callable
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletionStage
import kotlin.streams.toList

class RemoteResourceResolverTest {

    @Rule
    @JvmField
    val tempPath = TemporaryFolder()

    @Test
    fun canDownloadAFileOnce() {
        val urlFetcher = mock<UrlFetcher> {
            on { fetch(eq(PRIMARY_URL), any()) }.doAnswer(writeDataToFile("data"))
        }

        val cachePath = tempPath.newFolder().toPath()
        val sut = DefaultRemoteResourceResolver(urlFetcher, cachePath, immediateExecutor)

        val resource = resource()

        val firstCall = sut.resolve(resource).unwrap()
        val secondCall = sut.resolve(resource).unwrap()

        assertThat(firstCall).isEqualTo(secondCall)
        assertThat(firstCall).hasContent("data")
        verify(urlFetcher).fetch(eq(PRIMARY_URL), any())
        assertThat(Files.list(cachePath).use { it.toList() }).hasSize(1)
    }

    @Test
    fun expiredFileIsDownloadedAgain() {
        val urlFetcher = mock<UrlFetcher> {
            on { fetch(eq(PRIMARY_URL), any()) }.doAnswer(writeDataToFile("data1")).doAnswer(writeDataToFile("data2"))
        }

        val sut = DefaultRemoteResourceResolver(urlFetcher, tempPath.newFolder().toPath(), immediateExecutor)

        val resource = resource(ttl = Duration.ofMillis(1))

        val firstCall = sut.resolve(resource).toCompletableFuture().get()
        Thread.sleep(100)
        val secondCall = sut.resolve(resource).toCompletableFuture().get()

        assertThat(firstCall).isEqualTo(secondCall)

        assertThat(secondCall).hasContent("data2")
        verify(urlFetcher, times(2)).fetch(eq(PRIMARY_URL), any())
    }

    @Test
    fun downloadedParseFailedSkippedMove() {
        val urlFetcher = mock<UrlFetcher> {
            on { fetch(eq(PRIMARY_URL), any()) }.doAnswer(writeDataToFile(FAIL))
        }

        val cachePath = tempPath.newFolder().toPath()
        val sut = DefaultRemoteResourceResolver(urlFetcher, cachePath, immediateExecutor)

        val resource = xmlResource()

        val firstCall = sut.resolve(resource).unwrap()
        val secondCall = sut.resolve(resource).unwrap()

        assertThat(firstCall).isEqualTo(secondCall)
        assertThat(firstCall.exists()).isFalse()
    }

    @Test
    fun failureToHitUrlFallsBackToCurrentCopy() {
        val urlFetcher = mock<UrlFetcher> {
            on { fetch(eq(PRIMARY_URL), any()) }.doAnswer(writeDataToFile("data")).thenThrow(RuntimeException("BOOM!"))
        }

        val sut = DefaultRemoteResourceResolver(urlFetcher, tempPath.newFolder().toPath(), immediateExecutor)

        val resource = resource(ttl = Duration.ofMillis(1))

        val firstCall = sut.resolve(resource).toCompletableFuture().get()
        val secondCall = sut.resolve(resource).toCompletableFuture().get()

        assertThat(firstCall).isEqualTo(secondCall)
        assertThat(firstCall).hasContent("data")
    }

    @Test
    fun usesInitialValueIfNoOtherValueAvailable() {
        val initialValue = "initialValue".byteInputStream()

        val urlFetcher = mock<UrlFetcher> {
            on { fetch(eq(PRIMARY_URL), any()) }.thenThrow(RuntimeException("BOOM!"))
        }

        val sut = DefaultRemoteResourceResolver(urlFetcher, tempPath.newFolder().toPath(), immediateExecutor)

        val resource = resource(initialValue = initialValue)

        val result = sut.resolve(resource).toCompletableFuture().get()
        assertThat(result).hasContent("initialValue")
    }

    @Test(expected = RuntimeException::class)
    fun noCurrentOrInitialValueAndExceptionHittingUrlBubbles() {
        val urlFetcher = mock<UrlFetcher> {
            on { fetch(eq(PRIMARY_URL), any()) }.thenThrow(RuntimeException("BOOM!"))
        }

        val sut = DefaultRemoteResourceResolver(urlFetcher, tempPath.newFolder().toPath(), immediateExecutor)

        sut.resolve(resource()).toCompletableFuture().get()
    }

    @Test
    fun canFallbackDownListOfUrls() {
        val urlFetcher = mock<UrlFetcher> {
            on { fetch(eq(PRIMARY_URL), any()) }.thenThrow(RuntimeException("BOOM!"))
            on { fetch(eq(SECONDARY_URL), any()) }.doAnswer(writeDataToFile("data"))
        }

        val sut = DefaultRemoteResourceResolver(urlFetcher, tempPath.newFolder().toPath(), immediateExecutor)

        assertThat(sut.resolve(resource(urls = listOf(PRIMARY_URL, SECONDARY_URL))).toCompletableFuture().get()).hasContent("data")
    }

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

        fun resource(
            name: String = "resource",
            urls: List<String> = listOf(PRIMARY_URL),
            ttl: Duration? = Duration.ofMillis(1000),
            initialValue: InputStream? = null,
        ) = object : RemoteResource {
            override val urls: List<String> = urls
            override val name: String = name
            override val ttl: Duration? = ttl
            override val initialValue = initialValue?.let { { it } }
        }

        fun xmlResource(
            name: String = "resource",
            urls: List<String> = listOf(PRIMARY_URL),
            ttl: Duration? = Duration.ofMillis(1000),
            initialValue: InputStream? = null,
            remoteResolveParser: RemoteResolveParser? = LambdaManifestValidator
        ) = object : RemoteResource {
            override val urls: List<String> = urls
            override val name: String = name
            override val ttl: Duration? = ttl
            override val initialValue = initialValue?.let { { it } }
            override val remoteResolveParser: RemoteResolveParser = remoteResolveParser as LambdaManifestValidator
        }

        fun writeDataToFile(data: String): (InvocationOnMock) -> Unit = { invocation ->
            val path = invocation.arguments[1] as Path
            path.writeText(data)
            // It's possible for it to be done writing but path.exists to not work yet which
            // makes the canDownloadAFileOnce test fail (on CodeBuild).
            while (!path.exists()) {
                LOG.debug { "writeDataToFile path does not exist yet: $path" }
                Thread.sleep(10)
            }
        }

        val immediateExecutor: (Callable<Path>) -> CompletionStage<Path> = { CompletableFuture.completedFuture(it.call()) }

        const val PRIMARY_URL = "http://example.com"
        const val SECONDARY_URL = "http://example2.com"
        const val FAIL = "<aws>" +
            "data" +
            "<>"
    }
}


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

package software.aws.toolkits.core.utils

import java.util.Random

object RuleUtils {
    fun randomName(prefix: String = "a", length: Int = 63): String {
        val characters = ('0'..'9') + ('A'..'Z') + ('a'..'Z')
        val userName = System.getProperty("user.name", "unknown")
        return "${prefix.lowercase()}-${userName.lowercase()}-${List(length) { characters.random() }.joinToString("")}".take(length)
    }

    fun prefixFromCallingClass(): String {
        val callingClass = StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).callerClass
        return callingClass.simpleName
    }

    fun randomNumber(min: Int = 0, max: Int = 65535): Int = Random().nextInt(max - min + 1) + min
}


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

package software.aws.toolkits.core.utils

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

class RuleUtilsTest {
    private lateinit var callingClass: String

    @Before
    fun setUp() {
        callingClass = RuleUtils.prefixFromCallingClass()
    }

    @Test
    fun `late init before works`() {
        assertThat(callingClass).isEqualTo("RuleUtilsTest")
    }

    @Test
    fun `inline works`() {
        assertThat(RuleUtils.prefixFromCallingClass()).isEqualTo("RuleUtilsTest")
    }
}


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

package software.aws.toolkits.core.utils

import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.fail
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream

class ZipUtilsTest {

    @Rule
    @JvmField
    val tmpFolder = TemporaryFolder()
    var zipFile: Path? = null

    @After fun cleanup() {
        if (zipFile != null) {
            Files.delete(zipFile)
        }
    }

    @Test fun fileCanBeAddedToAZip() {
        val fileToAdd = tmpFolder.newFile()
        fileToAdd.writeText("hello world", StandardCharsets.UTF_8)
        val zipFile = tmpFolder.newFile("blah.zip")?.toPath() ?: return fail("Couldn't create new file")

        ZipOutputStream(Files.newOutputStream(zipFile)).use {
            it.putNextEntry("file.txt", fileToAdd.toPath())
        }

        assertZipContainsHelloWorldFile(zipFile)
    }

    @Test fun inputStreamCanBeAddedToAZip() {
        val zipFile = tmpFolder.newFile("blah.zip")?.toPath() ?: return fail("Couldn't create new file")
        ZipOutputStream(Files.newOutputStream(zipFile)).use {
            it.putNextEntry("file.txt", "hello world".byteInputStream(StandardCharsets.UTF_8))
        }

        assertZipContainsHelloWorldFile(zipFile)
    }

    @Test fun shortcutToCreateATemporaryZip() {
        zipFile = createTemporaryZipFile {
            it.putNextEntry("file.txt", "hello world".byteInputStream(StandardCharsets.UTF_8))
        }

        assertZipContainsHelloWorldFile(zipFile!!)
    }

    private fun assertZipContainsHelloWorldFile(zipFile: Path) {
        ZipFile(zipFile.toFile()).use { actualZip ->
            val actualEntry = actualZip.entries().toList().find { it.name == "file.txt" }

            assertThat(actualEntry).isNotNull
            val contents = actualZip.getInputStream(actualEntry).bufferedReader(StandardCharsets.UTF_8).use { it.readText() }
            assertThat(contents).isEqualTo("hello world")
        }
    }
}


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

package software.aws.toolkits.core.utils.test

import org.assertj.core.api.IterableAssert
import org.assertj.core.api.ListAssert
import org.assertj.core.api.ObjectAssert

@Suppress("UNCHECKED_CAST")
val <T : Any> ObjectAssert<T?>.notNull: ObjectAssert<T>
    get() = this.isNotNull as ObjectAssert<T>

@Suppress("UNCHECKED_CAST")
inline fun <reified SubType : Any> IterableAssert<*>.hasOnlyElementsOfType(): IterableAssert<SubType> =
    hasOnlyElementsOfType(SubType::class.java) as IterableAssert<SubType>

@Suppress("UNCHECKED_CAST")
inline fun <reified SubType : Any> ListAssert<*>.hasOnlyOneElementOfType(): ObjectAssert<SubType> =
    (hasOnlyElementsOfType(SubType::class.java) as ListAssert<SubType>).singleElement()


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

package software.aws.toolkits.core.utils.test

import java.time.Duration
import java.time.Instant
import java.util.UUID
import java.util.concurrent.atomic.AtomicInteger
import kotlin.random.Random

fun aString(length: Int = Random.nextInt(5, 30)): String = UUID.randomUUID().toString().substring(length)

fun retryableAssert(
    timeout: Duration? = null,
    maxAttempts: Int? = null,
    interval: Duration = Duration.ofMillis(100),
    assertion: () -> Unit
) {
    val calculatedTimeout = timeout ?: maxAttempts?.let { Duration.ofMillis(it * (interval.toMillis() * 2)) } ?: Duration.ofSeconds(1)
    val expiry = Instant.now().plus(calculatedTimeout)
    val attempts = AtomicInteger(0)
    while (true) {
        try {
            assertion()
            return
        } catch (e: AssertionError) { // deliberately narrowed to an AssertionError - this is intended to be used in a test assertion
            when {
                Instant.now().isAfter(expiry) -> throw e
                maxAttempts != null && attempts.incrementAndGet() >= maxAttempts -> throw e
                else -> Thread.sleep(interval.toMillis())
            }
        }
    }
}


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

package software.aws.toolkits.core.utils.test

import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import java.time.Duration

class TestUtilsTest {

    @Test
    fun retryableAssertionErrorsBubbleAfterMaxDuration() {
        assertThatThrownBy {
            retryableAssert(timeout = Duration.ofMillis(50), interval = Duration.ofMillis(5)) {
                throw AssertionError("Boom")
            }
        }.isInstanceOf(AssertionError::class.java)
    }

    @Test
    fun nonAssertionErrorsBubbleImmediately() {
        val mock = mock<Runnable> {
            on { run() }.thenThrow(RuntimeException("Boom"))
        }
        assertThatThrownBy {
            retryableAssert(maxAttempts = 3, interval = Duration.ofMillis(5)) {
                mock.run()
            }
        }.isInstanceOf(RuntimeException::class.java)
        verify(mock, times(1)).run()
    }
}


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

package software.aws.toolkits.gradle.detekt.rules

import io.gitlab.arturbosch.detekt.api.CodeSmell
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.Entity
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
import org.jetbrains.kotlin.psi.KtImportList

class BannedImportsRule : Rule() {
    override val issue = Issue("BannedImports", Severity.Defect, "Imports banned by the project", Debt.FIVE_MINS)

    override fun visitImportList(importList: KtImportList) {
        super.visitImportList(importList)
        importList.imports.forEach { element ->
            val importedFqName = element.importedFqName?.asString()
            if (importedFqName == "org.assertj.core.api.Assertions") {
                report(
                    CodeSmell(
                        issue,
                        Entity.from(element),
                        message = "Import the assertion you want to use directly instead of importing the top level Assertions"
                    )
                )
            }

            if (importedFqName?.startsWith("org.hamcrest") == true) {
                report(
                    CodeSmell(
                        issue,
                        Entity.from(element),
                        message = "Use AssertJ instead of Hamcrest assertions"
                    )
                )
            }

            if (importedFqName?.startsWith("kotlin.test.assert") == true &&
                importedFqName.startsWith("kotlin.test.assertNotNull") == false
            ) {
                report(
                    CodeSmell(
                        issue,
                        Entity.from(element),
                        message = "Use AssertJ instead of Kotlin test assertions"
                    )
                )
            }

            if (importedFqName?.contains("kotlinx.coroutines.Dispatchers") == true) {
                report(
                    CodeSmell(
                        issue,
                        Entity.from(element),
                        message = "Use contexts from CoroutineUtils.kt instead of Dispatchers"
                    )
                )
            }

            if (importedFqName == "com.intellij.ui.layout.panel") {
                report(
                    CodeSmell(
                        issue,
                        Entity.from(element),
                        message = "Use com.intellij.ui.dsl.builder.panel from Kotlin UI DSL Version 2"
                    )
                )
            }
        }
    }
}


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

@file:Suppress("BannedPattern")
package software.aws.toolkits.gradle.detekt.rules

import io.gitlab.arturbosch.detekt.api.CodeSmell
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.Entity
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
import org.jetbrains.kotlin.psi.KtFile

class BannedPatternRule(private val patterns: List<BannedPattern>) : Rule() {
    override val issue = Issue("BannedPattern", Severity.Defect, "Banned calls", Debt.FIVE_MINS)

    override fun visitKtFile(file: KtFile) {
        var offset = 0
        file.text.split("\n").forEachIndexed { _, text ->
            patterns.forEach { pattern ->
                val match = pattern.regex.find(text) ?: return@forEach
                report(
                    CodeSmell(
                        issue,
                        Entity.from(file, offset + match.range.first),
                        message = pattern.message
                    )
                )
            }
            // account for delimiter
            offset += text.length + 1
        }
    }

    companion object {
        val DEFAULT_PATTERNS = listOf(
            BannedPattern("Runtime\\.valueOf".toRegex(), "Runtime.valueOf is banned, use Runtime.fromValue instead."),
            BannedPattern(
                """com\.intellij\.openapi\.actionSystem\.DataKeys""".toRegex(),
                "DataKeys is not available in all IDEs, use LangDataKeys instead"
            ),
            BannedPattern(
                """PsiUtil\.getPsiFile""".toRegex(),
                "PsiUtil (java-api.jar) is not available in all IDEs, use PsiManager.getInstance(project).findFile() instead"
            ),
            BannedPattern(
                """com\.intellij\.psi\.util\.PsiUtil$""".toRegex(),
                "PsiUtil (java-api.jar) is not available in all IDEs, use PsiUtilCore or PsiManager instead (platform-api.jar)"
            )
        )
    }
}

data class BannedPattern(val regex: Regex, val message: String)


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

package software.aws.toolkits.gradle.detekt.rules

import io.gitlab.arturbosch.detekt.api.Config
import io.gitlab.arturbosch.detekt.api.RuleSet
import io.gitlab.arturbosch.detekt.api.RuleSetProvider

class CustomRuleSetProvider : RuleSetProvider {
    override val ruleSetId: String = "CustomDetektRules"
    override fun instance(config: Config): RuleSet = RuleSet(
        ruleSetId,
        listOf(
            BannedPatternRule(BannedPatternRule.DEFAULT_PATTERNS),
            LazyLogRule(),
            DialogModalityRule(),
            BannedImportsRule()
        )
    )
}


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

package software.aws.toolkits.gradle.detekt.rules

import io.gitlab.arturbosch.detekt.api.CodeSmell
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.Entity
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.KtNameReferenceExpression
import org.jetbrains.kotlin.psi.psiUtil.containingClass
import org.jetbrains.kotlin.psi.psiUtil.getSuperNames

class DialogModalityRule : Rule() {
    override val issue = Issue("RunInEdtWithoutModalityInDialog", Severity.Defect, "Use ModalityState when calling runInEdt in dialogs", Debt.FIVE_MINS)

    override fun visitCallExpression(element: KtCallExpression) {
        super.visitCallExpression(element)
        val callee = element.calleeExpression as? KtNameReferenceExpression ?: return
        if (callee.getReferencedName() != "runInEdt") return
        val clz = element.containingClass() ?: return
        if (clz.getSuperNames().none { it in KNOWN_DIALOG_SUPER_TYPES }) return

        if (element.valueArguments.none { it.text == "ModalityState.any()" }) {
            report(
                CodeSmell(
                    issue,
                    Entity.from(element),
                    message = "Call to runInEdt without ModalityState.any() within Dialog will not run until Dialog exits."
                )
            )
        }
    }

    companion object {
        private val KNOWN_DIALOG_SUPER_TYPES = setOf("DialogWrapper")
    }
}


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

package software.aws.toolkits.gradle.detekt.rules

import io.gitlab.arturbosch.detekt.api.CodeSmell
import io.gitlab.arturbosch.detekt.api.Debt
import io.gitlab.arturbosch.detekt.api.Entity
import io.gitlab.arturbosch.detekt.api.Issue
import io.gitlab.arturbosch.detekt.api.Rule
import io.gitlab.arturbosch.detekt.api.Severity
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
import io.gitlab.arturbosch.detekt.rules.fqNameOrNull
import org.jetbrains.kotlin.psi.KtCallExpression
import org.jetbrains.kotlin.psi.psiUtil.getCallNameExpression
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.util.getReceiverExpression
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall

@RequiresTypeResolution
class LazyLogRule : Rule() {
    override val issue = Issue("LazyLog", Severity.Style, "Use lazy logging synatax (e.g. warning {\"abc\"} ) instead of warning(\"abc\")", Debt.FIVE_MINS)

    // UI tests have issues with this TODO see if we want multiple detekt.yml files or disable for certain modules in this rule
    private val optOut = setOf("software.aws.toolkits.jetbrains.uitests")

    override fun visitCallExpression(element: KtCallExpression) {
        super.visitCallExpression(element)
        element.getCallNameExpression()?.let {
            if (!logMethods.contains(it.text)) {
                return
            }

            if (optOut.any { name -> element.containingKtFile.packageFqName.asString().startsWith(name) }) {
                return
            }

            if (bindingContext == BindingContext.EMPTY) return
            val resolvedCall = it.getResolvedCall(bindingContext)
            val type = resolvedCall?.extensionReceiver?.type?.fqNameOrNull()?.asString()
                ?: resolvedCall?.dispatchReceiver?.type?.fqNameOrNull()?.asString()

            if (type !in loggers) {
                return
            }

            if (element.lambdaArguments.size != 1) {
                val receiverName = resolvedCall?.getReceiverExpression()?.text ?: type
                report(
                    CodeSmell(
                        issue,
                        Entity.from(element),
                        message = "Use the lambda version of $receiverName.${it.text} instead"
                    )
                )
            }
        }
    }

    companion object {
        private val logMethods = setOf("error", "warn", "info", "debug", "trace")
        val loggers = setOf("org.slf4j.Logger")
    }
}


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

package software.aws.toolkits.gradle.detekt.rules

import io.gitlab.arturbosch.detekt.test.lint
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class BannedImportsRuleTest {
    private val rule = BannedImportsRule()

    @Test
    fun `Importing Assert fails`() {
        assertThat(rule.lint("import org.assertj.core.api.Assertions"))
            .singleElement()
            .matches { it.id == "BannedImports" && it.message == "Import the assertion you want to use directly instead of importing the top level Assertions" }
    }

    @Test
    fun `Importing Hamcrest fails`() {
        assertThat(rule.lint("import org.hamcrest.AnyClass"))
            .singleElement()
            .matches { it.id == "BannedImports" && it.message == "Use AssertJ instead of Hamcrest assertions" }
    }

    @Test
    fun `Importing Kotlin test assert fails`() {
        assertThat(rule.lint("import kotlin.test.assertTrue"))
            .singleElement()
            .matches { it.id == "BannedImports" && it.message == "Use AssertJ instead of Kotlin test assertions" }
        assertThat(rule.lint("import kotlin.test.assertFalse"))
            .singleElement()
            .matches { it.id == "BannedImports" && it.message == "Use AssertJ instead of Kotlin test assertions" }
    }

    @Test
    fun `Importing kotlin test notNull succeeds`() {
        assertThat(rule.lint("import kotlin.test.assertNotNull")).isEmpty()
    }

    @Test
    fun `Importing Assert assertThat succeeds`() {
        assertThat(rule.lint("import org.assertj.core.api.Assertions.assertThat")).isEmpty()
    }

    @Test
    fun `Importing Dispatchers fails`() {
        assertThat(rule.lint("import kotlinx.coroutines.Dispatchers"))
            .singleElement()
            .matches { it.id == "BannedImports" && it.message == "Use contexts from CoroutineUtils.kt instead of Dispatchers" }
    }

    @Test
    fun `Importing Dispatchers statically fails`() {
        assertThat(rule.lint("import kotlinx.coroutines.Dispatchers.IO"))
            .singleElement()
            .matches { it.id == "BannedImports" && it.message == "Use contexts from CoroutineUtils.kt instead of Dispatchers" }
    }
}


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

@file:Suppress("BannedPattern")
package software.aws.toolkits.gradle.detekt.rules

import io.gitlab.arturbosch.detekt.test.lint
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class BannedPatternRuleTest {
    @Test
    fun classContainingRegexCreatesError() {
        val rule = BannedPatternRule(listOf(BannedPattern("""blah\(\)""".toRegex(), "Use of method blah() is banned.")))
        assertThat(
            rule.lint(
                """
            fun hello() {
                blah()
            }
                """.trimIndent()
            )
        )
            .singleElement()
            .matches {
                it.id == "BannedPattern" &&
                    it.message == "Use of method blah() is banned." &&
                    it.location.source.line == 2 &&
                    it.location.source.column == 5
            }
    }

    @Test
    fun forbidPsiUtil() {
        val rule = BannedPatternRule(BannedPatternRule.DEFAULT_PATTERNS)
        assertThat(
            rule.lint(
                """
            import com.intellij.psi.util.PsiUtil
            class DockerfileParser(private val project: Project) {
                fun parse(virtualFile: VirtualFile): DockerfileDetails? {
                    val psiFile = PsiUtil.getPsiFile(project, virtualFile)
                }
            }
                """.trimIndent()
            )
        )
            .hasSize(2)
            .anyMatch {
                it.id == "BannedPattern" &&
                    it.message == "PsiUtil (java-api.jar) is not available in all IDEs, use PsiUtilCore or PsiManager instead (platform-api.jar)" &&
                    it.location.source.line == 1 &&
                    it.location.source.column == 8
            }
            .anyMatch {
                it.id == "BannedPattern" &&
                    it.message == "PsiUtil (java-api.jar) is not available in all IDEs, use PsiManager.getInstance(project).findFile() instead" &&
                    it.location.source.line == 4 &&
                    it.location.source.column == 23
            }
    }

    @Test
    fun allowPsiUtilCore() {
        val rule = BannedPatternRule(BannedPatternRule.DEFAULT_PATTERNS)
        assertThat(
            rule.lint(
                """
            import com.intellij.psi.util.PsiUtilCore
            class DockerfileParser(private val project: Project) {
                fun parse(virtualFile: VirtualFile): DockerfileDetails? {
                    val psiFile = PsiUtilCore.getPsiFile(project, virtualFile)
                }
            }
                """.trimIndent()
            )
        )
            .hasSize(0)
    }
}


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

package software.aws.toolkits.gradle.detekt.rules

import io.gitlab.arturbosch.detekt.test.lint
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test

class DialogModalityRuleTest {
    private val rule = DialogModalityRule()

    @Test
    fun runInEdtCallsShouldSpecifyModalityWhenCalledWithinDialog() {
        val code = """
            class Blah : DialogWrapper {
              fun blah() {
                runInEdt { }
              }
            }
        """
        assertThat(rule.lint(code)).singleElement()
            .matches {
                it.id == "RunInEdtWithoutModalityInDialog" &&
                    it.message == "Call to runInEdt without ModalityState.any() within Dialog will not run until Dialog exits."
            }
    }

    @Test
    fun callsThatSpecifyModalityAnyAreFine() {
        val code = """
            class Blah : DialogWrapper {
              fun blah() {
                runInEdt(ModalityState.any()) { }
              }
            }
        """.trimIndent()
        assertThat(rule.lint(code)).isEmpty()
    }

    @Test
    fun callsThatSpecifyWrongModalityAreNotFine() {
        val code = """
            class Blah : DialogWrapper() {
              fun blah() {
                runInEdt(ModalityState.current()) { }
              }
            }
        """

        assertThat(rule.lint(code)).singleElement()
            .matches {
                it.id == "RunInEdtWithoutModalityInDialog" &&
                    it.message == "Call to runInEdt without ModalityState.any() within Dialog will not run until Dialog exits."
            }
    }
}