// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.aws.toolkits.jetbrains.services.s3

import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileTypes.FileTypes
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.ProjectRule
import com.intellij.testFramework.runInEdtAndWait
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.Bucket
import software.amazon.awssdk.services.s3.model.DeleteBucketRequest
import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest
import software.amazon.awssdk.services.s3.model.ListObjectVersionsRequest
import software.amazon.awssdk.services.s3.model.ListObjectVersionsResponse
import software.amazon.awssdk.services.s3.model.ObjectVersion
import software.amazon.awssdk.services.s3.paginators.ListObjectVersionsIterable
import software.aws.toolkits.core.s3.deleteBucketAndContents
import software.aws.toolkits.jetbrains.core.MockClientManagerRule
import software.aws.toolkits.jetbrains.services.s3.bucketActions.DeleteBucketAction
import software.aws.toolkits.jetbrains.services.s3.editor.S3VirtualBucket
import software.aws.toolkits.jetbrains.utils.associateFilePattern
import java.util.function.Consumer

class DeleteBucketTest {

    @Rule
    @JvmField
    val projectRule = ProjectRule()

    @JvmField
    @Rule
    val disposableRule = DisposableRule()

    @JvmField
    @Rule
    val mockClientManager = MockClientManagerRule()

    private val bucket = Bucket.builder().name("foo").build()

    @Test
    fun deleteEmptyBucket() {
        val s3Mock = mockClientManager.create<S3Client>()
        val mockBucket = S3BucketNode(projectRule.project, bucket)
        val emptyVersionList = mutableListOf<ObjectVersion>()

        s3Mock.stub {
            on { listObjectVersionsPaginator(any<ListObjectVersionsRequest>()) } doReturn
                ListObjectVersionsIterable(s3Mock, ListObjectVersionsRequest.builder().build())
        }
        s3Mock.stub {
            on { listObjectVersions(any<ListObjectVersionsRequest>()) } doReturn
                ListObjectVersionsResponse.builder().versions(emptyVersionList).isTruncated(false).build()
        }

        val deleteBucketAction = DeleteBucketAction()
        deleteBucketAction.performDelete(mockBucket)
        verify(s3Mock).deleteBucket(any<Consumer<DeleteBucketRequest.Builder>>())
    }

    @Test
    fun deleteBucketWithVersionedObjects() {
        val s3Mock = mockClientManager.create<S3Client>()
        val mockBucket = S3BucketNode(projectRule.project, bucket)

        val objectVersionList = mutableListOf(
            ObjectVersion.builder().eTag("123").key("1111").build(),
            ObjectVersion.builder().eTag("123").key("1111").build()
        )

        s3Mock.stub {
            on { listObjectVersionsPaginator(any<ListObjectVersionsRequest>()) } doReturn
                ListObjectVersionsIterable(s3Mock, ListObjectVersionsRequest.builder().build())
        }
        s3Mock.stub {
            on { listObjectVersions(any<ListObjectVersionsRequest>()) } doReturn
                ListObjectVersionsResponse.builder().versions(objectVersionList).isTruncated(false).build()
        }

        val deleteBucketAction = DeleteBucketAction()
        deleteBucketAction.performDelete(mockBucket)
        verify(s3Mock).deleteObjects(any<Consumer<DeleteObjectsRequest.Builder>>())
        verify(s3Mock).deleteBucket(any<Consumer<DeleteBucketRequest.Builder>>())
    }

    @Test(expected = NullPointerException::class)
    fun deleteBucketWhichDoesNotExist() {
        val s3Mock = mockClientManager.create<S3Client>()
        s3Mock.stub {
            on { listObjectVersionsPaginator(any<ListObjectVersionsRequest>()) } doReturn
                ListObjectVersionsIterable(s3Mock, ListObjectVersionsRequest.builder().build())
        }

        s3Mock.deleteBucketAndContents("")
        verifyNoMoreInteractions(s3Mock.deleteBucket(any<Consumer<DeleteBucketRequest.Builder>>()))
    }

    @Test
    fun deleteBucketClosesItsEditor() {
        val s3Mock = mockClientManager.create<S3Client>()
        val mockBucket = S3BucketNode(projectRule.project, bucket)
        val emptyVersionList = mutableListOf<ObjectVersion>()

        s3Mock.stub {
            on { listObjectVersionsPaginator(any<ListObjectVersionsRequest>()) } doReturn
                ListObjectVersionsIterable(s3Mock, ListObjectVersionsRequest.builder().build())
        }
        s3Mock.stub {
            on { listObjectVersions(any<ListObjectVersionsRequest>()) } doReturn
                ListObjectVersionsResponse.builder().versions(emptyVersionList).isTruncated(false).build()
        }

        runInEdtAndWait {
            // Silly hack because test file editor impl has a bunch of asserts about the document/psi that don't exist in the real impl
            associateFilePattern(FileTypes.PLAIN_TEXT, bucket.name(), disposableRule.disposable)

            assertThat(openEditor(projectRule.project, bucket.name())).isNotNull
        }

        val s3VirtualBucket = S3VirtualBucket(bucket.name(), "", s3Mock, projectRule.project)
        val fileEditorManager = FileEditorManager.getInstance(projectRule.project)

        assertThat(fileEditorManager.openFiles).contains(s3VirtualBucket)

        val deleteBucketAction = DeleteBucketAction()
        deleteBucketAction.performDelete(mockBucket)
        verify(s3Mock).deleteBucket(any<Consumer<DeleteBucketRequest.Builder>>())

        assertThat(fileEditorManager.openFiles).doesNotContain(s3VirtualBucket)
    }
}


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

package software.aws.toolkits.jetbrains.services.s3

import com.intellij.openapi.vfs.VirtualFile
import icons.AwsIcons.Resources.S3_BUCKET
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import org.mockito.Mockito.mock
import software.aws.toolkits.jetbrains.services.s3.editor.S3FileIconProvider
import software.aws.toolkits.jetbrains.services.s3.editor.S3VirtualBucket

class S3FileIconProviderTest {
    @Test
    fun s3IconProvider() {
        val provider = S3FileIconProvider()
        assertThat(provider.getIcon(mock(S3VirtualBucket::class.java), 0, null)).isEqualTo(S3_BUCKET)
        assertThat(provider.getIcon(mock(VirtualFile::class.java), 0, null)).isNull()
    }
}


// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.aws.toolkits.jetbrains.services.s3

import com.intellij.testFramework.ProjectRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import software.amazon.awssdk.services.s3.model.Bucket
import software.aws.toolkits.jetbrains.core.MockResourceCacheRule
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerEmptyNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.AwsExplorerErrorNode
import software.aws.toolkits.jetbrains.core.explorer.nodes.S3ExplorerRootNode
import software.aws.toolkits.jetbrains.core.region.MockRegionProviderRule
import software.aws.toolkits.jetbrains.services.s3.resources.S3Resources
import java.time.Instant
import java.util.concurrent.CompletableFuture

class S3ServiceNodeTest {

    @JvmField
    @Rule
    val projectRule = ProjectRule()

    @JvmField
    @Rule
    val resourceCache = MockResourceCacheRule()

    @JvmField
    @Rule
    val regionProvider = MockRegionProviderRule()

    @Test
    fun s3BucketsAreListed() {
        val bucketList = listOf("bcd", "abc", "AEF", "ZZZ")
        s3buckets(bucketList)
        val children = S3ServiceNode(projectRule.project, S3_EXPLORER_NODE).children

        assertThat(children).allMatch { it is S3BucketNode }
        assertThat(children.filterIsInstance<S3BucketNode>().map { it.displayName() }).containsExactlyInAnyOrder(
            "abc",
            "AEF",
            "bcd",
            "ZZZ"
        )
    }

    @Test
    fun noBucketsInTheRegion() {
        s3buckets(emptyList())
        val children = S3ServiceNode(projectRule.project, S3_EXPLORER_NODE).children
        assertThat(children).allMatch { it is AwsExplorerEmptyNode }
    }

    @Test
    fun errorLoadingBuckets() {
        resourceCache.addEntry(
            projectRule.project,
            S3Resources.LIST_REGIONALIZED_BUCKETS,
            CompletableFuture<List<S3Resources.RegionalizedBucket>>().also {
                it.completeExceptionally(RuntimeException("Simulated error"))
            }
        )
        val children = S3ServiceNode(projectRule.project, S3_EXPLORER_NODE).children
        assertThat(children).allMatch { it is AwsExplorerErrorNode }
    }

    private fun bucketData(bucketName: String) = Bucket.builder()
        .creationDate(Instant.parse("1995-10-23T10:12:35Z"))
        .name(bucketName)
        .build()

    private fun s3buckets(names: List<String>) {
        resourceCache.addEntry(
            projectRule.project,
            S3Resources.LIST_REGIONALIZED_BUCKETS,
            CompletableFuture.completedFuture(names.map { S3Resources.RegionalizedBucket(bucketData(it), regionProvider.defaultRegion()) })
        )
    }

    private companion object {
        val S3_EXPLORER_NODE = S3ExplorerRootNode()
    }
}


// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package software.aws.toolkits.jetbrains.services.s3

import com.intellij.openapi.application.runWriteAction
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileTypes.FileTypes
import com.intellij.openapi.fileTypes.ex.FileTypeManagerEx
import com.intellij.openapi.util.io.FileUtil
import com.intellij.testFramework.ProjectRule
import com.intellij.testFramework.RuleChain
import com.intellij.testFramework.runInEdtAndWait
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.stub
import org.mockito.kotlin.verify
import software.amazon.awssdk.core.sync.RequestBody
import software.amazon.awssdk.core.sync.ResponseTransformer
import software.amazon.awssdk.http.AbortableInputStream
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.Bucket
import software.amazon.awssdk.services.s3.model.CopyObjectRequest
import software.amazon.awssdk.services.s3.model.CopyObjectResponse
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest
import software.amazon.awssdk.services.s3.model.DeleteObjectResponse
import software.amazon.awssdk.services.s3.model.DeleteObjectsRequest
import software.amazon.awssdk.services.s3.model.DeleteObjectsResponse
import software.amazon.awssdk.services.s3.model.DeletedObject
import software.amazon.awssdk.services.s3.model.GetObjectRequest
import software.amazon.awssdk.services.s3.model.GetObjectResponse
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request
import software.amazon.awssdk.services.s3.model.ListObjectsV2Response
import software.amazon.awssdk.services.s3.model.ObjectIdentifier
import software.amazon.awssdk.services.s3.model.PutObjectRequest
import software.amazon.awssdk.services.s3.model.PutObjectResponse
import software.aws.toolkits.core.region.AwsRegion
import software.aws.toolkits.jetbrains.core.AwsClientManager
import software.aws.toolkits.jetbrains.core.MockClientManagerRule
import software.aws.toolkits.jetbrains.core.credentials.MockAwsConnectionManager.ProjectAccountSettingsManagerRule
import software.aws.toolkits.jetbrains.services.s3.editor.S3VirtualBucket
import java.net.URL
import java.time.Instant

class S3VirtualBucketTest {
    private val projectRule = ProjectRule()
    private val mockClientManager = MockClientManagerRule()
    private val settingsManagerRule = ProjectAccountSettingsManagerRule(projectRule)

    @Rule
    @JvmField
    val chain = RuleChain(projectRule, mockClientManager, settingsManagerRule)

    @Rule
    @JvmField
    val tempFolder = TemporaryFolder()

    @Test
    fun deleteObjects() {
        val s3Client = mockClientManager.create<S3Client>()
        val deleteCaptor = argumentCaptor<DeleteObjectsRequest>()

        val objectsToDelete = mutableListOf<ObjectIdentifier>()
        objectsToDelete.add(ObjectIdentifier.builder().key("testKey").build())

        s3Client.stub {
            on { deleteObjects(deleteCaptor.capture()) } doReturn
                (
                    DeleteObjectsResponse.builder()
                        .requestCharged("yes")
                        .deleted(listOf(DeletedObject.builder().deleteMarker(true).key("testKey").build()))
                    ).build()
        }

        val sut = S3VirtualBucket("TestBucket", "", s3Client, projectRule.project)
        runBlocking {
            sut.deleteObjects(listOf("testKey"))
        }

        val deleteRequest = deleteCaptor.firstValue
        assertThat(deleteRequest.bucket()).isEqualTo("TestBucket")
        assertThat(deleteRequest.delete().objects()[0].key()).isEqualTo("testKey")
    }

    @Test
    fun renameObject() {
        val s3Client = mockClientManager.create<S3Client>()
        val deleteCaptor = argumentCaptor<DeleteObjectRequest>()
        val copyCaptor = argumentCaptor<CopyObjectRequest>()

        s3Client.stub {
            on {
                copyObject(copyCaptor.capture())
            } doReturn CopyObjectResponse.builder()
                .versionId("1223")
                .build()
        }
        s3Client.stub {
            on {
                deleteObject(deleteCaptor.capture())
            } doReturn DeleteObjectResponse.builder()
                .versionId("1223")
                .deleteMarker(true)
                .requestCharged("yes")
                .build()
        }

        val sut = S3VirtualBucket("TestBucket", "", s3Client, projectRule.project)
        runBlocking {
            sut.renameObject("key", "renamedKey")
        }

        val copyRequestCapture = copyCaptor.firstValue
        assertThat(copyRequestCapture.destinationBucket()).isEqualTo("TestBucket")
        assertThat(copyRequestCapture.sourceBucket()).isEqualTo("TestBucket")
        assertThat(copyRequestCapture.sourceKey()).isEqualTo("key")

        val deleteRequestCapture = deleteCaptor.firstValue
        assertThat(deleteRequestCapture.bucket()).isEqualTo("TestBucket")
        assertThat(deleteRequestCapture.key()).isEqualTo("key")
    }

    @Test
    fun uploadObject() {
        val s3Client = mockClientManager.create<S3Client>()

        s3Client.stub {
            on {
                putObject(any<PutObjectRequest>(), any<RequestBody>())
            } doReturn PutObjectResponse.builder()
                .versionId("VersionFoo")
                .build()
        }

        val testFile = tempFolder.newFile("someFile.html").toPath()

        val sut = S3VirtualBucket("TestBucket", "", s3Client, projectRule.project)
        runBlocking {
            sut.upload(projectRule.project, testFile, "TestFile")
        }

        argumentCaptor<PutObjectRequest, RequestBody>().let { (request, body) ->
            verify(s3Client).putObject(request.capture(), body.capture())

            assertThat(request.firstValue.bucket()).isEqualTo("TestBucket")
            assertThat(request.firstValue.key()).isEqualTo("TestFile")

            assertThat(body.firstValue.contentType()).isEqualTo("text/html")
        }
    }

    @Test
    fun downloadObject() {
        val s3Client = mockClientManager.create<S3Client>()
        val downloadCaptor = argumentCaptor<GetObjectRequest>()
        val testBucket = Bucket.builder().name("TestBucket").build()

        s3Client.stub {
            on {
                getObject(downloadCaptor.capture(), any<ResponseTransformer<GetObjectResponse, GetObjectResponse>>())
            } doAnswer {
                @Suppress("UNCHECKED_CAST")
                val transformer = it.arguments[1] as ResponseTransformer<GetObjectResponse, GetObjectResponse>
                val data = "hello".toByteArray()
                transformer.transform(
                    GetObjectResponse.builder()
                        .eTag("1111")
                        .lastModified(Instant.parse("1995-10-23T10:12:35Z"))
                        .contentLength(data.size.toLong())
                        .build(),
                    AbortableInputStream.create(data.inputStream())
                )
            }
        }

        val testFile = FileUtil.createTempFile("myfile", ".txt")
        val sut = S3VirtualBucket(testBucket.name(), "", s3Client, projectRule.project)
        runBlocking {
            sut.download(projectRule.project, "key", null, testFile.outputStream())
        }

        val downloadRequestCapture = downloadCaptor.firstValue
        assertThat(downloadRequestCapture.bucket()).isEqualTo("TestBucket")
        assertThat(downloadRequestCapture.key()).contains("key")
    }

    @Test
    fun listObjects() {
        val s3Client = mockClientManager.create<S3Client>()
        val requestCaptor = argumentCaptor<ListObjectsV2Request>()

        s3Client.stub {
            on { listObjectsV2(requestCaptor.capture()) } doReturn ListObjectsV2Response.builder().build()
        }

        val sut = S3VirtualBucket("TestBucket", "", s3Client, projectRule.project)
        runBlocking {
            sut.listObjects("prefix/", "continuation")
        }

        val request = requestCaptor.firstValue
        assertThat(request.bucket()).isEqualTo("TestBucket")
        assertThat(request.prefix()).isEqualTo("prefix/")
        assertThat(request.delimiter()).isEqualTo("/")
    }

    @Test
    fun getUrl() {
        val awsConnectionManager = settingsManagerRule.settingsManager
        awsConnectionManager.changeRegionAndWait(AwsRegion("us-west-2", "US West (Oregon)", "aws"))

        // Use real manager for this since it can affect the S3Configuration that goes into S3Utilities
        AwsClientManager().getClient<S3Client>(awsConnectionManager.activeCredentialProvider, awsConnectionManager.activeRegion).use {
            val sut = S3VirtualBucket("test-bucket", "", it, projectRule.project)

            assertThat(sut.generateUrl("prefix/key", null)).isEqualTo(URL("https://test-bucket.s3.us-west-2.amazonaws.com/prefix/key"))
        }
    }

    @Test
    fun getUrlWithVersion() {
        val awsConnectionManager = settingsManagerRule.settingsManager
        awsConnectionManager.changeRegionAndWait(AwsRegion("us-west-2", "US West (Oregon)", "aws"))

        // Use real manager for this since it can affect the S3Configuration that goes into S3Utilities
        AwsClientManager().getClient<S3Client>(awsConnectionManager.activeCredentialProvider, awsConnectionManager.activeRegion).use {
            val sut = S3VirtualBucket("test-bucket", "", it, projectRule.project)

            assertThat(sut.generateUrl("prefix/key", "123")).isEqualTo(URL("https://test-bucket.s3.us-west-2.amazonaws.com/prefix/key?versionId=123"))
        }
    }

    @Test
    fun getUrlError() {
        val awsConnectionManager = settingsManagerRule.settingsManager
        awsConnectionManager.changeRegionAndWait(AwsRegion("us-west-2", "US West (Oregon)", "aws"))

        // Use real manager for this since it can affect the S3Configuration that goes into S3Utilities
        AwsClientManager().getClient<S3Client>(awsConnectionManager.activeCredentialProvider, awsConnectionManager.activeRegion).use {
            val sut = S3VirtualBucket("test-bucket", "", it, projectRule.project)

            assertThatThrownBy {
                sut.generateUrl("", null)
            }
        }
    }

    @Test
    fun handleDeletedBucket() {
        val s3Mock = mockClientManager.create<S3Client>()
        val testBucket = Bucket.builder().name("TestBucket").build()
        val s3VirtualBucket = S3VirtualBucket(testBucket.name(), "", s3Mock, projectRule.project)
        runInEdtAndWait {
            runWriteAction {
                FileTypeManagerEx.getInstanceEx().associatePattern(
                    FileTypes.PLAIN_TEXT,
                    testBucket.name()
                )
            }
            assertThat(openEditor(projectRule.project, testBucket.name())).isNotNull
        }
        val fileEditorManager = FileEditorManager.getInstance(projectRule.project)
        assertThat(fileEditorManager.openFiles).contains(s3VirtualBucket)
        s3VirtualBucket.handleDeletedBucket()
        assertThat(fileEditorManager.openFiles).doesNotContain(s3VirtualBucket)
    }
}


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

package software.aws.toolkits.jetbrains.services.s3.editor

import com.intellij.icons.AllIcons
import com.intellij.testFramework.ProjectRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.Bucket
import software.amazon.awssdk.services.s3.model.CommonPrefix
import software.amazon.awssdk.services.s3.model.ListObjectsV2Request
import software.amazon.awssdk.services.s3.model.ListObjectsV2Response
import software.amazon.awssdk.services.s3.model.S3Object
import software.aws.toolkits.core.utils.delegateMock
import java.time.Instant
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread

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

    private val lastModifiedTime = Instant.now()
    private val objectSize = 1L
    private val s3Bucket = Bucket.builder().name("foo").build()

    @Test
    fun `directory path`() {
        val s3Client = delegateMock<S3Client>()
        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val sut = S3TreeDirectoryNode(bucket, null, "my/folder/")

        assertThat(sut.directoryPath()).isEqualTo("my/folder/")
    }

    @Test
    fun `display name`() {
        val s3Client = delegateMock<S3Client>()
        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val sut = S3TreeDirectoryNode(bucket, null, "my/folder/")

        assertThat(sut.displayName()).isEqualTo("folder")
    }

    @Test
    fun `get icon`() {
        val s3Client = delegateMock<S3Client>()
        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val sut = S3TreeDirectoryNode(bucket, null, "my/folder/")

        assertThat(sut.icon).isEqualTo(AllIcons.Nodes.Folder)
    }

    @Test
    fun `get children with no pagination`() {
        val requestCaptor = argumentCaptor<ListObjectsV2Request>()
        val s3Client = delegateMock<S3Client> {
            on { listObjectsV2(requestCaptor.capture()) } doReturn ListObjectsV2Response.builder()
                .commonPrefixes(CommonPrefix.builder().prefix("my/folder/aFolder/").build(), CommonPrefix.builder().prefix("my/folder/zFolder/").build())
                .contents(createS3Object("my/folder/picture.png"), createS3Object("my/folder/file.txt"))
                .build()
        }
        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val sut = S3TreeDirectoryNode(bucket, null, "my/folder/")

        assertThat(sut.children).containsExactly(
            S3TreeDirectoryNode(bucket, sut, "my/folder/aFolder/"),
            S3TreeObjectNode(sut, "my/folder/file.txt", objectSize, lastModifiedTime),
            S3TreeObjectNode(sut, "my/folder/picture.png", objectSize, lastModifiedTime),
            S3TreeDirectoryNode(bucket, sut, "my/folder/zFolder/")
        )

        val request = requestCaptor.firstValue
        assertThat(request.bucket()).isEqualTo("foo")
        assertThat(request.prefix()).isEqualTo("my/folder/")
        assertThat(request.delimiter()).isEqualTo("/")
    }

    @Test
    fun `get children with pagination`() {
        val requestCaptor = argumentCaptor<ListObjectsV2Request>()
        val s3Client = delegateMock<S3Client> {
            on { listObjectsV2(requestCaptor.capture()) } doReturn ListObjectsV2Response.builder()
                .contents(createS3Object("my/folder/picture.png"))
                .nextContinuationToken("Token")
                .build()
        }

        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val sut = S3TreeDirectoryNode(bucket, null, "my/folder")

        assertThat(sut.children).containsExactly(
            S3TreeObjectNode(sut, "my/folder/picture.png", objectSize, lastModifiedTime),
            S3TreeContinuationNode(bucket, sut, sut.key, "Token")
        )
    }

    @Test
    fun `get children with more loaded`() {
        val requestCaptor = argumentCaptor<ListObjectsV2Request>()
        val s3Client = delegateMock<S3Client> {
            on { listObjectsV2(requestCaptor.capture()) }.thenReturn(
                ListObjectsV2Response.builder()
                    .contents(createS3Object("my/folder/file.txt"))
                    .nextContinuationToken("Token")
                    .build(),
                ListObjectsV2Response.builder()
                    .contents(createS3Object("my/folder/picture.png"))
                    .build()
            )
        }

        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val sut = S3TreeDirectoryNode(bucket, null, "my/folder/")

        assertThat(sut.children).containsExactly(
            S3TreeObjectNode(sut, "my/folder/file.txt", objectSize, lastModifiedTime),
            S3TreeContinuationNode(bucket, sut, sut.key, "Token")
        )

        (sut.children.last() as S3TreeContinuationNode<*>).loadMore()

        assertThat(sut.children).containsExactly(
            S3TreeObjectNode(sut, "my/folder/file.txt", objectSize, lastModifiedTime),
            S3TreeObjectNode(sut, "my/folder/picture.png", objectSize, lastModifiedTime)
        )

        assertThat(requestCaptor.allValues).hasSize(2)
        val firstRequest = requestCaptor.firstValue
        assertThat(firstRequest.bucket()).isEqualTo("foo")
        assertThat(firstRequest.prefix()).isEqualTo("my/folder/")
        assertThat(firstRequest.delimiter()).isEqualTo("/")
        assertThat(firstRequest.continuationToken()).isNull()

        val secondRequest = requestCaptor.secondValue
        assertThat(secondRequest.bucket()).isEqualTo("foo")
        assertThat(secondRequest.prefix()).isEqualTo("my/folder/")
        assertThat(secondRequest.delimiter()).isEqualTo("/")
        assertThat(secondRequest.continuationToken()).isEqualTo("Token")
    }

    @Test
    fun `get children fails shows error node`() {
        val s3Client = delegateMock<S3Client> {
            on { listObjectsV2(any<ListObjectsV2Request>()) } doThrow IllegalStateException("Bad!")
        }

        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val sut = S3TreeDirectoryNode(bucket, null, "my/folder")

        assertThat(sut.children).containsExactly(
            S3TreeErrorNode(bucket, sut)
        )
    }

    @Test
    fun `load more fails once then succeeds`() {
        val s3Client = delegateMock<S3Client> {
            on { listObjectsV2(any<ListObjectsV2Request>()) }
                .thenReturn(
                    ListObjectsV2Response.builder()
                        .contents(createS3Object("my/folder/file.txt"))
                        .nextContinuationToken("Token")
                        .build()
                )
                .thenThrow(IllegalStateException("Bad!"))
                .thenReturn(
                    ListObjectsV2Response.builder()
                        .contents(createS3Object("my/folder/picture.png"))
                        .build()
                )
        }

        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val sut = S3TreeDirectoryNode(bucket, null, "my/folder/")

        assertThat(sut.children).containsExactly(
            S3TreeObjectNode(sut, "my/folder/file.txt", objectSize, lastModifiedTime),
            S3TreeContinuationNode(bucket, sut, sut.key, "Token")
        )

        (sut.children.last() as S3TreeContinuationNode<*>).loadMore()

        assertThat(sut.children).containsExactly(
            S3TreeObjectNode(sut, "my/folder/file.txt", objectSize, lastModifiedTime),
            S3TreeErrorContinuationNode(bucket, sut, sut.key, "Token")
        )

        (sut.children.last() as S3TreeContinuationNode<*>).loadMore()

        assertThat(sut.children).containsExactly(
            S3TreeObjectNode(sut, "my/folder/file.txt", objectSize, lastModifiedTime),
            S3TreeObjectNode(sut, "my/folder/picture.png", objectSize, lastModifiedTime)
        )
    }

    @Test
    fun `load more is idempotent`() {
        val requestCaptor = argumentCaptor<ListObjectsV2Request>()
        val s3Client = delegateMock<S3Client> {
            on { listObjectsV2(requestCaptor.capture()) }.thenReturn(
                ListObjectsV2Response.builder()
                    .contents(createS3Object("my/folder/file.txt"))
                    .nextContinuationToken("Token")
                    .build(),
                ListObjectsV2Response.builder()
                    .contents(createS3Object("my/folder/picture.png"))
                    .build()
            )
        }

        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val sut = S3TreeDirectoryNode(bucket, null, "my/folder/")

        assertThat(sut.children).containsExactly(
            S3TreeObjectNode(sut, "my/folder/file.txt", objectSize, lastModifiedTime),
            S3TreeContinuationNode(bucket, sut, sut.key, "Token")
        )

        val continuationNode = sut.children.last() as S3TreeContinuationNode<*>
        continuationNode.loadMore()

        assertThat(sut.children).containsExactly(
            S3TreeObjectNode(sut, "my/folder/file.txt", objectSize, lastModifiedTime),
            S3TreeObjectNode(sut, "my/folder/picture.png", objectSize, lastModifiedTime)
        )

        continuationNode.loadMore()

        assertThat(sut.children).hasSize(2)
        assertThat(requestCaptor.allValues).hasSize(2)
    }

    @Test
    fun `load more only executes a single request`() {
        val latch = CountDownLatch(1)
        val executed = CountDownLatch(3)

        val s3Client = delegateMock<S3Client> {
            on { listObjectsV2(any<ListObjectsV2Request>()) }.thenReturn(
                ListObjectsV2Response.builder()
                    .contents(createS3Object("my/folder/file.txt"))
                    .nextContinuationToken("Token")
                    .build()
            ).thenAnswer {
                latch.await()

                ListObjectsV2Response.builder()
                    .contents(createS3Object("my/folder/picture.png"), createS3Object("my/folder/picture.png"))
                    .build()
            }
        }

        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val sut = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val continuationNode = sut.children.last() as S3TreeContinuationNode<*>

        repeat(3) {
            thread(start = true) {
                continuationNode.loadMore()
                executed.countDown()
            }
        }

        latch.countDown()

        assertThat(executed.await(5, TimeUnit.SECONDS)).isTrue()

        assertThat(sut.children).hasSize(3)
        verify(s3Client, times(2)).listObjectsV2(any<ListObjectsV2Request>())
    }

    private fun createS3Object(name: String) = S3Object.builder().key(name).size(objectSize).lastModified(lastModifiedTime).build()
}


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

package software.aws.toolkits.jetbrains.services.s3.editor

import com.intellij.openapi.fileTypes.PlainTextFileType
import com.intellij.openapi.fileTypes.UnknownFileType
import com.intellij.testFramework.ProjectRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.doThrow
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.Bucket
import software.amazon.awssdk.services.s3.model.ListObjectVersionsRequest
import software.amazon.awssdk.services.s3.model.ListObjectVersionsResponse
import software.amazon.awssdk.services.s3.model.ObjectVersion
import software.aws.toolkits.core.utils.delegateMock
import java.time.Instant
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread

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

    private val lastModifiedTime = Instant.now()
    private val objectSize = 1L
    private val s3Bucket = Bucket.builder().name("foo").build()

    @Test
    fun `directory path`() {
        val s3Client = delegateMock<S3Client>()
        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val parent = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val sut = S3TreeObjectNode(parent, "my/folder/file.txt", objectSize, lastModifiedTime)

        assertThat(sut.directoryPath()).isEqualTo("my/folder/")
    }

    @Test
    fun `display name`() {
        val s3Client = delegateMock<S3Client>()
        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val parent = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val sut = S3TreeObjectNode(parent, "my/folder/file.txt", objectSize, lastModifiedTime)

        assertThat(sut.displayName()).isEqualTo("file.txt")
    }

    @Test
    fun `file name with extension`() {
        val s3Client = delegateMock<S3Client>()
        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val parent = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val sut = S3TreeObjectNode(parent, "my/folder/file.txt", objectSize, lastModifiedTime)

        assertThat(sut.fileName()).isEqualTo("file.txt")
    }

    @Test
    fun `file name without extension`() {
        val s3Client = delegateMock<S3Client>()
        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val parent = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val sut = S3TreeObjectNode(parent, "my/folder/file", objectSize, lastModifiedTime)

        assertThat(sut.displayName()).isEqualTo("file")
    }

    @Test
    fun `known file type icon`() {
        val s3Client = delegateMock<S3Client>()
        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val parent = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val sut = S3TreeObjectNode(parent, "my/folder/file.txt", objectSize, lastModifiedTime)

        assertThat(sut.icon).isEqualTo(PlainTextFileType.INSTANCE.icon)
    }

    @Test
    fun `unknown file type icon`() {
        val s3Client = delegateMock<S3Client>()
        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val parent = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val sut = S3TreeObjectNode(parent, "my/folder/file.unknownFile", objectSize, lastModifiedTime)

        assertThat(sut.icon).isEqualTo(UnknownFileType.INSTANCE.icon)
    }

    @Test
    fun `showHistory false has no children`() {
        val s3Client = delegateMock<S3Client>()
        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val parent = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val sut = S3TreeObjectNode(parent, "my/folder/picture.png", objectSize, lastModifiedTime)
        sut.showHistory = false

        assertThat(sut.children).isEmpty()
    }

    @Test
    fun `get children with no pagination`() {
        val requestCaptor = argumentCaptor<ListObjectVersionsRequest>()
        val s3Client = delegateMock<S3Client> {
            on { listObjectVersions(requestCaptor.capture()) } doReturn
                ListObjectVersionsResponse.builder()
                    .versions(createS3ObjectVersion("my/folder/picture.png", "1"), createS3ObjectVersion("my/folder/picture.png", "2"))
                    .build()
        }
        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val parent = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val sut = S3TreeObjectNode(parent, "my/folder/picture.png", objectSize, lastModifiedTime)
        sut.showHistory = true

        assertThat(sut.children).containsExactly(
            S3TreeObjectVersionNode(sut, "1", objectSize, lastModifiedTime),
            S3TreeObjectVersionNode(sut, "2", objectSize, lastModifiedTime)
        )

        val request = requestCaptor.firstValue
        assertThat(request.bucket()).isEqualTo("foo")
        assertThat(request.prefix()).isEqualTo("my/folder/picture.png")
    }

    @Test
    fun `get children with pagination`() {
        val requestCaptor = argumentCaptor<ListObjectVersionsRequest>()
        val s3Client = delegateMock<S3Client> {
            on { listObjectVersions(requestCaptor.capture()) } doReturn ListObjectVersionsResponse.builder()
                .versions(createS3ObjectVersion("my/folder/picture.png", "1"), createS3ObjectVersion("my/folder/picture.png", "2"))
                .nextKeyMarker("KeyToken")
                .nextVersionIdMarker("VersionToken")
                .isTruncated(true)
                .build()
        }

        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val parent = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val sut = S3TreeObjectNode(parent, "my/folder/picture.png", objectSize, lastModifiedTime)
        sut.showHistory = true

        assertThat(sut.children).containsExactly(
            S3TreeObjectVersionNode(sut, "1", objectSize, lastModifiedTime),
            S3TreeObjectVersionNode(sut, "2", objectSize, lastModifiedTime),
            S3TreeContinuationNode(bucket, sut, sut.key, VersionContinuationToken("KeyToken", "VersionToken"))
        )
    }

    @Test
    fun `get children must match key`() {
        val requestCaptor = argumentCaptor<ListObjectVersionsRequest>()
        val s3Client = delegateMock<S3Client> {
            on { listObjectVersions(requestCaptor.capture()) } doReturn ListObjectVersionsResponse.builder()
                .versions(createS3ObjectVersion("my/folder/picture.png", "1"), createS3ObjectVersion("my/folder/file.txt", "1"))
                .nextKeyMarker("KeyToken")
                .nextVersionIdMarker("VersionToken")
                .isTruncated(true)
                .build()
        }

        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val parent = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val sut = S3TreeObjectNode(parent, "my/folder/picture.png", objectSize, lastModifiedTime)
        sut.showHistory = true

        assertThat(sut.children).containsExactly(
            S3TreeObjectVersionNode(sut, "1", objectSize, lastModifiedTime),
            S3TreeContinuationNode(bucket, sut, sut.key, VersionContinuationToken("KeyToken", "VersionToken"))
        )
    }

    @Test
    fun `get children fails`() {
        val s3Client = delegateMock<S3Client> {
            on { listObjectVersions(any<ListObjectVersionsRequest>()) } doThrow IllegalStateException("Network broke")
        }
        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val parent = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val sut = S3TreeObjectNode(parent, "my/folder/picture.png", objectSize, lastModifiedTime).apply { showHistory = true }

        assertThat(sut.children).containsExactly(
            S3TreeErrorNode(bucket, sut)
        )
    }

    @Test
    fun `get children fails with pagination`() {
        val s3Client = delegateMock<S3Client> {
            on { listObjectVersions(any<ListObjectVersionsRequest>()) }
                .thenReturn(
                    ListObjectVersionsResponse.builder()
                        .versions(createS3ObjectVersion("my/folder/picture.png", "1"))
                        .nextKeyMarker("KeyToken")
                        .nextVersionIdMarker("VersionToken")
                        .isTruncated(true)
                        .build()
                )
                .thenThrow(IllegalStateException("Network broke"))
                .thenReturn(
                    ListObjectVersionsResponse.builder()
                        .versions(createS3ObjectVersion("my/folder/picture.png", "2"))
                        .isTruncated(false)
                        .build()
                )
        }

        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val parent = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val sut = S3TreeObjectNode(parent, "my/folder/picture.png", objectSize, lastModifiedTime)
        sut.showHistory = true

        assertThat(sut.children).containsExactly(
            S3TreeObjectVersionNode(sut, "1", objectSize, lastModifiedTime),
            S3TreeContinuationNode(bucket, sut, sut.key, VersionContinuationToken("KeyToken", "VersionToken"))
        )

        (sut.children.last() as S3TreeContinuationNode<*>).loadMore()

        assertThat(sut.children).containsExactly(
            S3TreeObjectVersionNode(sut, "1", objectSize, lastModifiedTime),
            S3TreeErrorContinuationNode(bucket, sut, sut.key, VersionContinuationToken("KeyToken", "VersionToken"))
        )

        (sut.children.last() as S3TreeContinuationNode<*>).loadMore()

        assertThat(sut.children).containsExactly(
            S3TreeObjectVersionNode(sut, "1", objectSize, lastModifiedTime),
            S3TreeObjectVersionNode(sut, "2", objectSize, lastModifiedTime)
        )
    }

    @Test
    fun `get children ignores versionId 'null'`() {
        val requestCaptor = argumentCaptor<ListObjectVersionsRequest>()
        val s3Client = delegateMock<S3Client> {
            on { listObjectVersions(requestCaptor.capture()) } doReturn
                ListObjectVersionsResponse.builder()
                    .versions(createS3ObjectVersion("my/folder/picture.png", "null"))
                    .build()
        }
        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val parent = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val sut = S3TreeObjectNode(parent, "my/folder/picture.png", objectSize, lastModifiedTime)
        sut.showHistory = true

        assertThat(sut.children).isEmpty()
    }

    @Test
    fun `get children with more loaded`() {
        val requestCaptor = argumentCaptor<ListObjectVersionsRequest>()
        val s3Client = delegateMock<S3Client> {
            on { listObjectVersions(requestCaptor.capture()) }.thenReturn(
                ListObjectVersionsResponse.builder()
                    .versions(createS3ObjectVersion("my/folder/picture.png", "1"), createS3ObjectVersion("my/folder/picture.png", "2"))
                    .nextKeyMarker("KeyToken")
                    .nextVersionIdMarker("VersionToken")
                    .isTruncated(true)
                    .build(),
                ListObjectVersionsResponse.builder()
                    .versions(createS3ObjectVersion("my/folder/picture.png", "3"))
                    .build()
            )
        }

        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val parent = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val sut = S3TreeObjectNode(parent, "my/folder/picture.png", objectSize, lastModifiedTime)
        sut.showHistory = true

        assertThat(sut.children).containsExactly(
            S3TreeObjectVersionNode(sut, "1", objectSize, lastModifiedTime),
            S3TreeObjectVersionNode(sut, "2", objectSize, lastModifiedTime),
            S3TreeContinuationNode(bucket, sut, sut.key, VersionContinuationToken("KeyToken", "VersionToken"))
        )

        (sut.children.last() as S3TreeContinuationNode<*>).loadMore()

        assertThat(sut.children).containsExactly(
            S3TreeObjectVersionNode(sut, "1", objectSize, lastModifiedTime),
            S3TreeObjectVersionNode(sut, "2", objectSize, lastModifiedTime),
            S3TreeObjectVersionNode(sut, "3", objectSize, lastModifiedTime)
        )

        assertThat(requestCaptor.allValues).hasSize(2)
        val firstRequest = requestCaptor.firstValue
        assertThat(firstRequest.bucket()).isEqualTo("foo")
        assertThat(firstRequest.prefix()).isEqualTo("my/folder/picture.png")
        assertThat(firstRequest.keyMarker()).isNull()
        assertThat(firstRequest.versionIdMarker()).isNull()

        val secondRequest = requestCaptor.secondValue
        assertThat(secondRequest.bucket()).isEqualTo("foo")
        assertThat(secondRequest.prefix()).isEqualTo("my/folder/picture.png")
        assertThat(secondRequest.keyMarker()).isEqualTo("KeyToken")
        assertThat(secondRequest.versionIdMarker()).isEqualTo("VersionToken")
    }

    @Test
    fun `load more is idempotent`() {
        val requestCaptor = argumentCaptor<ListObjectVersionsRequest>()
        val s3Client = delegateMock<S3Client> {
            on { listObjectVersions(requestCaptor.capture()) }.thenReturn(
                ListObjectVersionsResponse.builder()
                    .versions(createS3ObjectVersion("my/folder/picture.png", "1"), createS3ObjectVersion("my/folder/picture.png", "2"))
                    .nextKeyMarker("KeyToken")
                    .nextVersionIdMarker("VersionToken")
                    .isTruncated(true)
                    .build(),
                ListObjectVersionsResponse.builder()
                    .versions(createS3ObjectVersion("my/folder/picture.png", "3"))
                    .build()
            )
        }

        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val parent = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val sut = S3TreeObjectNode(parent, "my/folder/picture.png", objectSize, lastModifiedTime)
        sut.showHistory = true

        assertThat(sut.children).containsExactly(
            S3TreeObjectVersionNode(sut, "1", objectSize, lastModifiedTime),
            S3TreeObjectVersionNode(sut, "2", objectSize, lastModifiedTime),
            S3TreeContinuationNode(bucket, sut, sut.key, VersionContinuationToken("KeyToken", "VersionToken"))
        )

        val continuationNode = sut.children.last() as S3TreeContinuationNode<*>
        continuationNode.loadMore()

        assertThat(sut.children).containsExactly(
            S3TreeObjectVersionNode(sut, "1", objectSize, lastModifiedTime),
            S3TreeObjectVersionNode(sut, "2", objectSize, lastModifiedTime),
            S3TreeObjectVersionNode(sut, "3", objectSize, lastModifiedTime)
        )

        continuationNode.loadMore()

        assertThat(sut.children).hasSize(3)
        assertThat(requestCaptor.allValues).hasSize(2)
    }

    @Test
    fun `load more only executes a single request`() {
        val latch = CountDownLatch(1)
        val executed = CountDownLatch(3)

        val s3Client = delegateMock<S3Client> {
            on { listObjectVersions(any<ListObjectVersionsRequest>()) }.thenReturn(
                ListObjectVersionsResponse.builder()
                    .versions(createS3ObjectVersion("my/folder/picture.png", "1"))
                    .nextKeyMarker("KeyToken")
                    .nextVersionIdMarker("VersionToken")
                    .isTruncated(true)
                    .build()
            ).thenAnswer {
                latch.await()

                ListObjectVersionsResponse.builder()
                    .versions(createS3ObjectVersion("my/folder/picture.png", "2"), createS3ObjectVersion("my/folder/picture.png", "3"))
                    .build()
            }
        }

        val bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        val parent = S3TreeDirectoryNode(bucket, null, "my/folder/")
        val sut = S3TreeObjectNode(parent, "my/folder/picture.png", objectSize, lastModifiedTime)
        sut.showHistory = true

        val continuationNode = sut.children.last() as S3TreeContinuationNode<*>

        repeat(3) {
            thread(start = true) {
                continuationNode.loadMore()
                executed.countDown()
            }
        }

        latch.countDown()

        assertThat(executed.await(5, TimeUnit.SECONDS)).isTrue()

        assertThat(sut.children).hasSize(3)
        verify(s3Client, times(2)).listObjectVersions(any<ListObjectVersionsRequest>())
    }

    private fun createS3ObjectVersion(name: String, versionId: String) =
        ObjectVersion.builder().key(name).versionId(versionId).size(objectSize).lastModified(lastModifiedTime).build()
}


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

package software.aws.toolkits.jetbrains.services.s3.editor

import com.intellij.openapi.fileTypes.PlainTextFileType
import com.intellij.openapi.fileTypes.UnknownFileType
import com.intellij.testFramework.ProjectRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import software.amazon.awssdk.services.s3.S3Client
import software.amazon.awssdk.services.s3.model.Bucket
import software.aws.toolkits.core.utils.delegateMock
import java.time.Instant

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

    private val s3Client = delegateMock<S3Client>()
    private val lastModifiedTime = Instant.now()
    private val objectSize = 1L
    private val s3Bucket = Bucket.builder().name("foo").build()

    private lateinit var bucket: S3VirtualBucket
    private lateinit var dirNode: S3TreeDirectoryNode

    @Before
    fun setUp() {
        bucket = S3VirtualBucket(s3Bucket.name(), "", s3Client, projectRule.project)
        dirNode = S3TreeDirectoryNode(bucket, null, "my/folder/")
    }

    @Test
    fun `directory path`() {
        val parent = S3TreeObjectNode(dirNode, "my/folder/file.txt", objectSize, lastModifiedTime)
        val sut = S3TreeObjectVersionNode(parent, "version1", objectSize, lastModifiedTime)

        assertThat(sut.directoryPath()).isEqualTo("my/folder/")
    }

    @Test
    fun `display name`() {
        val parent = S3TreeObjectNode(dirNode, "my/folder/file.txt", objectSize, lastModifiedTime)
        val sut = S3TreeObjectVersionNode(parent, "version1", objectSize, lastModifiedTime)

        assertThat(sut.displayName()).isEqualTo("version1")
    }

    @Test
    fun `file name with extension`() {
        val parent = S3TreeObjectNode(dirNode, "my/folder/file.txt", objectSize, lastModifiedTime)
        val sut = S3TreeObjectVersionNode(parent, "version1", objectSize, lastModifiedTime)

        assertThat(sut.fileName()).isEqualTo("file@version1.txt")
    }

    @Test
    fun `file name without extension`() {
        val parent = S3TreeObjectNode(dirNode, "my/folder/file", objectSize, lastModifiedTime)
        val sut = S3TreeObjectVersionNode(parent, "version1", objectSize, lastModifiedTime)

        assertThat(sut.fileName()).isEqualTo("file@version1")
    }

    @Test
    fun `known file type icon`() {
        val parent = S3TreeObjectNode(dirNode, "my/folder/file.txt", objectSize, lastModifiedTime)
        val sut = S3TreeObjectVersionNode(parent, "version1", objectSize, lastModifiedTime)

        assertThat(sut.icon).isEqualTo(PlainTextFileType.INSTANCE.icon)
    }

    @Test
    fun `unknown file type icon`() {
        val parent = S3TreeObjectNode(dirNode, "my/folder/file.unknownFile", objectSize, lastModifiedTime)
        val sut = S3TreeObjectVersionNode(parent, "version1", objectSize, lastModifiedTime)

        assertThat(sut.icon).isEqualTo(UnknownFileType.INSTANCE.icon)
    }
}


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

package software.aws.toolkits.jetbrains.services.s3.editor

import com.intellij.ide.DataManager
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.ProjectRule
import com.intellij.testFramework.TestApplicationManager
import com.intellij.testFramework.runInEdtAndWait
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.mock
import software.amazon.awssdk.services.s3.model.ListObjectsV2Response
import software.aws.toolkits.core.utils.test.aString

class S3ViewerPanelTest {
    @Rule
    @JvmField
    val disposableRule = DisposableRule()

    @Rule
    @JvmField
    val projectRule = ProjectRule()

    private lateinit var s3Bucket: S3VirtualBucket
    private lateinit var sut: S3ViewerPanel

    @Before
    fun setUp() {
        s3Bucket = mock {
            on { prefix }.thenReturn("")
            on { name }.thenReturn(aString())

            onBlocking { listObjects(any(), anyOrNull()) }.thenReturn(
                ListObjectsV2Response.builder()
                    .commonPrefixes({ it.prefix("folder/") })
                    .build()
            )
        }

        sut = S3ViewerPanel(disposableRule.disposable, projectRule.project, s3Bucket)

        TestApplicationManager.getInstance().setDataProvider(DataManager.getDataProvider(sut.component), disposableRule.disposable)
    }

    @Test
    fun `data provider selected nodes key returns table selected values`() {
        runInEdtAndWait {
            val dataProvider = DataManager.getInstance().getDataContext(sut.component)
            sut.treeTable.clearSelection()
            assertThat(dataProvider.getData(S3EditorDataKeys.SELECTED_NODES)).isEmpty()

            sut.treeTable.addRowSelectionInterval(0, 0)
            assertThat(dataProvider.getData(S3EditorDataKeys.SELECTED_NODES)).containsExactly(S3TreeDirectoryNode(s3Bucket, null, "folder/"))
        }
    }

    @Test
    fun `data provider bucket table key returns the table`() {
        val dataProvider = DataManager.getInstance().getDataContext(sut.component)
        assertThat(dataProvider.getData(S3EditorDataKeys.BUCKET_TABLE)).isEqualTo(sut.treeTable)
    }
}


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

package software.aws.toolkits.jetbrains.services.s3.objectActions

import com.intellij.openapi.ide.CopyPasteManager
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeContinuationNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeDirectoryNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeErrorNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeObjectNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeObjectVersionNode
import java.awt.datatransfer.DataFlavor
import java.time.Instant

class CopyPathActionTest : ObjectActionTestBase() {
    override val sut = CopyPathAction()

    @Test
    fun `copy path disabled with no nodes`() {
        assertThat(sut.updateAction(emptyList()).isEnabled).isFalse
    }

    @Test
    fun `copy path disabled with on multiple nodes`() {
        val nodes = listOf(
            S3TreeDirectoryNode(s3Bucket, null, "path1/"),
            S3TreeDirectoryNode(s3Bucket, null, "path2/")
        )
        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `copy path enabled with on single node`() {
        val nodes = listOf(
            S3TreeDirectoryNode(s3Bucket, null, "path1/"),
        )
        assertThat(sut.updateAction(nodes).isEnabled).isTrue
    }

    @Test
    fun `copy path disabled with on version nodes`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val obj = S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        val nodes = listOf(
            S3TreeObjectVersionNode(obj, "version", 1, Instant.now())
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `copy path action is disabled on error node`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeErrorNode(s3Bucket, dir)
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `copy path action is disabled on continuation node`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeContinuationNode(s3Bucket, dir, "path1/", "marker")
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `copy path for directory is correct`() {
        val nodes = listOf(
            S3TreeDirectoryNode(s3Bucket, null, "path1/"),
        )
        sut.executeAction(nodes)

        val data = CopyPasteManager.getInstance().getContents<String>(DataFlavor.stringFlavor)
        assertThat(data).isEqualTo("path1/")
    }

    @Test
    fun `copy path for object is correct`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        )
        sut.executeAction(nodes)

        val data = CopyPasteManager.getInstance().getContents<String>(DataFlavor.stringFlavor)
        assertThat(data).isEqualTo("path1/obj1")
    }
}


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

package software.aws.toolkits.jetbrains.services.s3.objectActions

import com.intellij.openapi.ide.CopyPasteManager
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeContinuationNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeDirectoryNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeErrorNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeObjectNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeObjectVersionNode
import java.awt.datatransfer.DataFlavor
import java.time.Instant

class CopyUriActionTest : ObjectActionTestBase() {
    override val sut = CopyUriAction()

    @Test
    fun `copy uri disabled with no nodes`() {
        assertThat(sut.updateAction(emptyList()).isEnabled).isFalse
    }

    @Test
    fun `copy uri disabled with on multiple nodes`() {
        val nodes = listOf(
            S3TreeDirectoryNode(s3Bucket, null, "path1/"),
            S3TreeDirectoryNode(s3Bucket, null, "path2/")
        )
        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `copy uri enabled with on single node`() {
        val nodes = listOf(
            S3TreeDirectoryNode(s3Bucket, null, "path1/"),
        )
        assertThat(sut.updateAction(nodes).isEnabled).isTrue
    }

    @Test
    fun `copy uri disabled with on version nodes`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val obj = S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        val nodes = listOf(
            S3TreeObjectVersionNode(obj, "version", 1, Instant.now())
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `copy uri action is disabled on error node`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeErrorNode(s3Bucket, dir)
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `copy uri action is disabled on continuation node`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeContinuationNode(s3Bucket, dir, "path1/", "marker")
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `copy uri for directory is correct`() {
        val nodes = listOf(
            S3TreeDirectoryNode(s3Bucket, null, "path1/"),
        )
        sut.executeAction(nodes)

        val data = CopyPasteManager.getInstance().getContents<String>(DataFlavor.stringFlavor)
        assertThat(data).isEqualTo("s3://$bucketName/path1/")
    }

    @Test
    fun `copy uri for object is correct`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        )
        sut.executeAction(nodes)

        val data = CopyPasteManager.getInstance().getContents<String>(DataFlavor.stringFlavor)
        assertThat(data).isEqualTo("s3://$bucketName/path1/obj1")
    }
}


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

package software.aws.toolkits.jetbrains.services.s3.objectActions

import com.intellij.openapi.ide.CopyPasteManager
import com.intellij.testFramework.DisposableRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.stub
import software.aws.toolkits.jetbrains.core.credentials.MockAwsConnectionManager
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeContinuationNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeDirectoryNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeErrorNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeObjectNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeObjectVersionNode
import java.awt.datatransfer.DataFlavor
import java.net.URL
import java.time.Instant

class CopyUrlActionTest : ObjectActionTestBase() {
    @Rule
    @JvmField
    val settingsManagerRule = MockAwsConnectionManager.ProjectAccountSettingsManagerRule(projectRule)

    @Rule
    @JvmField
    val disposableRule = DisposableRule()

    override val sut = CopyUrlAction()

    @Before
    fun setUpMock() {
        s3Bucket.stub {
            on { generateUrl(any(), anyOrNull()) }.thenAnswer {
                // Actual format is implementation detail below S3VirtualBucket
                URL("https://s3/${it.getArgument<String>(0)}?version=${it.getArgument<String>(1)}")
            }
        }
    }

    @Test
    fun `copy url disabled with no nodes`() {
        assertThat(sut.updateAction(emptyList()).isEnabled).isFalse
    }

    @Test
    fun `copy url disabled with on multiple nodes`() {
        val nodes = listOf(
            S3TreeDirectoryNode(s3Bucket, null, "path1/"),
            S3TreeDirectoryNode(s3Bucket, null, "path2/")
        )
        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `copy url enabled with on single node`() {
        val nodes = listOf(
            S3TreeDirectoryNode(s3Bucket, null, "path1/"),
        )
        assertThat(sut.updateAction(nodes).isEnabled).isTrue
    }

    @Test
    fun `copy url enabled with on version nodes`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val obj = S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        val nodes = listOf(
            S3TreeObjectVersionNode(obj, "version", 1, Instant.now())
        )

        assertThat(sut.updateAction(nodes).isEnabled).isTrue
    }

    @Test
    fun `copy url action is disabled on error node`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeErrorNode(s3Bucket, dir)
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `copy url action is disabled on continuation node`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeContinuationNode(s3Bucket, dir, "path1/", "marker")
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `copy url for directory is correct`() {
        val nodes = listOf(
            S3TreeDirectoryNode(s3Bucket, null, "path1/"),
        )
        sut.executeAction(nodes)

        val data = CopyPasteManager.getInstance().getContents<String>(DataFlavor.stringFlavor)
        assertThat(data).isEqualTo("https://s3/path1/?version=null")
    }

    @Test
    fun `copy url for object is correct`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        )
        sut.executeAction(nodes)

        val data = CopyPasteManager.getInstance().getContents<String>(DataFlavor.stringFlavor)
        assertThat(data).isEqualTo("https://s3/path1/obj1?version=null")
    }

    @Test
    fun `copy url for obj version value is correct`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val obj = S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        val nodes = listOf(
            S3TreeObjectVersionNode(obj, "version", 1, Instant.now())
        )
        sut.executeAction(nodes)

        val data = CopyPasteManager.getInstance().getContents<String>(DataFlavor.stringFlavor)
        assertThat(data).isEqualTo("https://s3/path1/obj1?version=version")
    }
}


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

package software.aws.toolkits.jetbrains.services.s3.objectActions

import com.intellij.openapi.ui.TestDialog
import com.intellij.openapi.ui.TestDialogManager
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.never
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyBlocking
import software.aws.toolkits.core.utils.test.retryableAssert
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeContinuationNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeDirectoryNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeErrorNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeObjectNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeObjectVersionNode
import java.time.Instant

class DeleteObjectActionTest : ObjectActionTestBase() {
    override val sut = DeleteObjectAction()

    @After
    fun tearDown() {
        TestDialogManager.setTestDialog(TestDialog.DEFAULT)
    }

    @Test
    fun `delete action is disabled on empty selection`() {
        assertThat(sut.updateAction(emptyList()).isEnabled).isFalse
    }

    @Test
    fun `delete action is disabled on directory selection`() {
        val nodes = listOf(
            S3TreeDirectoryNode(s3Bucket, null, "path1/")
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `delete action is enabled on object selection`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now()),
            S3TreeObjectNode(dir, "path1/obj2", 1, Instant.now())
        )

        assertThat(sut.updateAction(nodes).isEnabled).isTrue
    }

    @Test
    fun `delete action is disabled on object version selection`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val obj = S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        val nodes = listOf(
            S3TreeObjectVersionNode(obj, "version", 1, Instant.now())
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `delete action is disabled on mix of object and directory selection`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            dir,
            S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `delete action is disabled on error node`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeErrorNode(s3Bucket, dir)
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `delete action is disabled on continuation node`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeContinuationNode(s3Bucket, dir, "path1/", "marker")
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `delete denied confirmation is no-op`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        )

        TestDialogManager.setTestDialog(TestDialog.NO)

        sut.executeAction(nodes)

        verifyBlocking(s3Bucket, never()) {
            deleteObjects(any())
        }
    }

    @Test
    fun `delete confirmation cancelled is no-op`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        )

        TestDialogManager.setTestDialog {
            -1 // means cancel
        }

        sut.executeAction(nodes)

        verifyBlocking(s3Bucket, never()) {
            deleteObjects(any())
        }
    }

    @Test
    fun `delete confirmed confirmation deletes file`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now()),
            S3TreeObjectNode(dir, "path1/obj2", 1, Instant.now()),
            S3TreeObjectNode(dir, "path1/obj3", 1, Instant.now()),
        )

        TestDialogManager.setTestDialog(TestDialog.OK)

        sut.executeAction(nodes)

        retryableAssert {
            argumentCaptor<List<String>>().apply {
                verifyBlocking(s3Bucket) { deleteObjects(capture()) }

                assertThat(allValues).hasSize(1)
                assertThat(firstValue).containsAll(nodes.map { it.key })
            }

            // Happens async on a different thread
            retryableAssert {
                verify(treeTable, times(3)).invalidateLevel(any<S3TreeObjectNode>())
                verify(treeTable).refresh()
            }
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.s3.objectActions

import com.intellij.openapi.ui.TestDialog
import com.intellij.openapi.ui.TestDialogManager
import com.intellij.testFramework.DisposableRule
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
import org.mockito.kotlin.stub
import software.amazon.awssdk.services.s3.model.S3Exception
import software.aws.toolkits.core.utils.test.retryableAssert
import software.aws.toolkits.core.utils.touch
import software.aws.toolkits.jetbrains.core.MockClientManagerRule
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeContinuationNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeDirectoryNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeErrorNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeObjectNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeObjectVersionNode
import software.aws.toolkits.jetbrains.services.s3.objectActions.DownloadObjectAction.ConflictResolution
import software.aws.toolkits.jetbrains.utils.createMockFileChooser
import java.io.OutputStream
import java.time.Instant
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit

class DownloadObjectActionTest : ObjectActionTestBase() {
    @Rule
    @JvmField
    val tempFolder = TemporaryFolder()

    @Rule
    @JvmField
    val mockClientManager = MockClientManagerRule()

    @Rule
    @JvmField
    val testDisposable = DisposableRule()

    override val sut = DownloadObjectAction()

    @After
    fun tearDown() {
        TestDialogManager.setTestDialog(TestDialog.DEFAULT)
    }

    @Test
    fun `download action is disabled on empty selection`() {
        assertThat(sut.updateAction(emptyList()).isEnabled).isFalse
    }

    @Test
    fun `download action is disabled on directory selection`() {
        val nodes = listOf(
            S3TreeDirectoryNode(s3Bucket, null, "path1/")
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `download action is enabled on object selection`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        )

        assertThat(sut.updateAction(nodes).isEnabled).isTrue
    }

    @Test
    fun `download action is enabled on object version selection`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val obj = S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        val nodes = listOf(
            S3TreeObjectVersionNode(obj, "version", 1, Instant.now())
        )

        assertThat(sut.updateAction(nodes).isEnabled).isTrue
    }

    @Test
    fun `download action is disabled on mix of object and directory selection`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            dir,
            S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `download action is disabled on error node`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeErrorNode(s3Bucket, dir)
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `download action is disabled on continuation node`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeContinuationNode(s3Bucket, dir, "path1/", "marker")
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun downloadSingleFileToFile() {
        val destinationFile = tempFolder.newFile().toPath()

        createMockFileChooser(testDisposable.disposable, destinationFile)

        val testData = listOf(
            TestData("testFile-1")
        )

        val countDownLatch = setUpS3Mock(testData)
        val nodes = testData.convertToNodes()

        sut.executeAction(nodes)

        assertThat(countDownLatch.await(5, TimeUnit.SECONDS)).isTrue
        assertThat(destinationFile).hasContent("testFile-1-content")
    }

    @Test
    fun downloadSingleFileToFolder() {
        val destinationFolder = tempFolder.newFolder().toPath()

        createMockFileChooser(testDisposable.disposable, destinationFolder)

        val testData = listOf(
            TestData("testFile-1")
        )

        val countDownLatch = setUpS3Mock(testData)
        val nodes = testData.convertToNodes()

        sut.executeAction(nodes)

        assertThat(countDownLatch.await(5, TimeUnit.SECONDS)).isTrue
        assertThat(destinationFolder.resolve("testFile-1")).hasContent("testFile-1-content")
    }

    @Test
    fun downloadSingleFileToFolderWithConflictSkip() {
        val destinationFolder = tempFolder.newFolder().toPath()
        destinationFolder.resolve("testFile-1").touch()

        createMockFileChooser(testDisposable.disposable, destinationFolder)

        setUpConflictResolutionResponses(
            ConflictResolution.SINGLE_FILE_RESOLUTIONS,
            ConflictResolution.SKIP
        )

        val testData = listOf(
            TestData("testFile-1")
        )

        // 0 due to ee skipped our only file
        val countDownLatch = setUpS3Mock(testData, numberOfDownloads = 0)
        val nodes = testData.convertToNodes()

        sut.executeAction(nodes)

        assertThat(countDownLatch.await(5, TimeUnit.SECONDS)).isTrue
        assertThat(destinationFolder.resolve("testFile-1")).hasContent("")
    }

    @Test
    fun downloadSingleFileToFolderWithConflictOverwrite() {
        val destinationFolder = tempFolder.newFolder().toPath()

        createMockFileChooser(testDisposable.disposable, destinationFolder)

        setUpConflictResolutionResponses(
            ConflictResolution.SINGLE_FILE_RESOLUTIONS,
            ConflictResolution.OVERWRITE
        )

        val testData = listOf(
            TestData("testFile-1")
        )

        val countDownLatch = setUpS3Mock(testData)
        val nodes = testData.convertToNodes()

        sut.executeAction(nodes)

        assertThat(countDownLatch.await(5, TimeUnit.SECONDS)).isTrue
        assertThat(destinationFolder.resolve("testFile-1")).hasContent("testFile-1-content")
    }

    @Test
    fun downloadMultipleFiles() {
        val destinationFolder = tempFolder.newFolder().toPath()

        createMockFileChooser(testDisposable.disposable, destinationFolder)

        val testData = listOf(
            TestData("testFile-1"),
            TestData("testFile-2"),
            TestData("testFile-3")
        )

        val countDownLatch = setUpS3Mock(testData)
        val nodes = testData.convertToNodes()

        sut.executeAction(nodes)

        assertThat(countDownLatch.await(5, TimeUnit.SECONDS)).isTrue
        assertThat(destinationFolder.resolve("testFile-1")).hasContent("testFile-1-content")
        assertThat(destinationFolder.resolve("testFile-2")).hasContent("testFile-2-content")
        assertThat(destinationFolder.resolve("testFile-3")).hasContent("testFile-3-content")
    }

    @Test
    fun downloadMultipleFilesConflictSkipSome() {
        val destinationFolder = tempFolder.newFolder().toPath()
        destinationFolder.resolve("testFile-2").touch()
        destinationFolder.resolve("testFile-4").touch()

        createMockFileChooser(testDisposable.disposable, destinationFolder)

        val testData = listOf(
            TestData("testFile-1"),
            TestData("testFile-2"),
            TestData("testFile-3"),
            TestData("testFile-4"),
            TestData("testFile-5")
        )

        setUpConflictResolutionResponses(
            ConflictResolution.MULTIPLE_FILE_RESOLUTIONS,
            ConflictResolution.SKIP,
            ConflictResolution.SKIP
        )

        val countDownLatch = setUpS3Mock(testData, numberOfDownloads = 3)
        val nodes = testData.convertToNodes()

        sut.executeAction(nodes)

        assertThat(countDownLatch.await(5, TimeUnit.SECONDS)).isTrue
        assertThat(destinationFolder.resolve("testFile-1")).hasContent("testFile-1-content")
        assertThat(destinationFolder.resolve("testFile-2")).hasContent("")
        assertThat(destinationFolder.resolve("testFile-3")).hasContent("testFile-3-content")
        assertThat(destinationFolder.resolve("testFile-4")).hasContent("")
        assertThat(destinationFolder.resolve("testFile-5")).hasContent("testFile-5-content")
    }

    @Test
    fun downloadMultipleFilesConflictSkipThenOverwriteRest() {
        val destinationFolder = tempFolder.newFolder().toPath()
        destinationFolder.resolve("testFile-2").touch()
        destinationFolder.resolve("testFile-4").touch()
        destinationFolder.resolve("testFile-5").touch()

        createMockFileChooser(testDisposable.disposable, destinationFolder)

        val testData = listOf(
            TestData("testFile-1"),
            TestData("testFile-2"),
            TestData("testFile-3"),
            TestData("testFile-4"),
            TestData("testFile-5")
        )

        setUpConflictResolutionResponses(
            ConflictResolution.MULTIPLE_FILE_RESOLUTIONS,
            ConflictResolution.SKIP,
            ConflictResolution.OVERWRITE_ALL
        )

        val countDownLatch = setUpS3Mock(testData, numberOfDownloads = 4)
        val nodes = testData.convertToNodes()

        sut.executeAction(nodes)

        assertThat(countDownLatch.await(5, TimeUnit.SECONDS)).isTrue
        assertThat(destinationFolder.resolve("testFile-1")).hasContent("testFile-1-content")
        assertThat(destinationFolder.resolve("testFile-2")).hasContent("")
        assertThat(destinationFolder.resolve("testFile-3")).hasContent("testFile-3-content")
        assertThat(destinationFolder.resolve("testFile-4")).hasContent("testFile-4-content")
        assertThat(destinationFolder.resolve("testFile-5")).hasContent("testFile-5-content")
    }

    @Test
    fun downloadMultipleFilesConflictSkipAll() {
        val destinationFolder = tempFolder.newFolder().toPath()
        destinationFolder.resolve("testFile-2").touch()
        destinationFolder.resolve("testFile-4").touch()

        createMockFileChooser(testDisposable.disposable, destinationFolder)

        val testData = listOf(
            TestData("testFile-1"),
            TestData("testFile-2"),
            TestData("testFile-3"),
            TestData("testFile-4"),
            TestData("testFile-5")
        )

        setUpConflictResolutionResponses(
            ConflictResolution.MULTIPLE_FILE_RESOLUTIONS,
            ConflictResolution.SKIP_ALL
        )

        val countDownLatch = setUpS3Mock(testData, numberOfDownloads = 3)
        val nodes = testData.convertToNodes()

        sut.executeAction(nodes)

        assertThat(countDownLatch.await(5, TimeUnit.SECONDS)).isTrue
        assertThat(destinationFolder.resolve("testFile-1")).hasContent("testFile-1-content")
        assertThat(destinationFolder.resolve("testFile-2")).hasContent("")
        assertThat(destinationFolder.resolve("testFile-3")).hasContent("testFile-3-content")
        assertThat(destinationFolder.resolve("testFile-4")).hasContent("")
        assertThat(destinationFolder.resolve("testFile-5")).hasContent("testFile-5-content")
    }

    @Test
    fun singleExceptionLeadsToPartialDownload() {
        val destinationFolder = tempFolder.newFolder().toPath()

        createMockFileChooser(testDisposable.disposable, destinationFolder)

        val testData = listOf(
            TestData("testFile-1"),
            TestData("testFile-2", downloadError = true),
            TestData("testFile-3")
        )

        val countDownLatch = setUpS3Mock(testData, numberOfDownloads = 2)
        val nodes = testData.convertToNodes()

        sut.executeAction(nodes)

        assertThat(countDownLatch.await(5, TimeUnit.SECONDS)).isTrue
        assertThat(destinationFolder.resolve("testFile-1")).hasContent("testFile-1-content")
        retryableAssert { // We delete the file after we get the failure
            assertThat(destinationFolder.resolve("testFile-2")).doesNotExist()
        }
        assertThat(destinationFolder.resolve("testFile-3")).doesNotExist()
    }

    @Test
    fun cancelOnPromptIsSkip() {
        val destinationFolder = tempFolder.newFolder().toPath()
        destinationFolder.resolve("testFile-1").touch()

        createMockFileChooser(testDisposable.disposable, destinationFolder)

        val testData = listOf(
            TestData("testFile-1")
        )

        TestDialogManager.setTestDialog {
            -1 // Means cancel (esc)
        }

        val countDownLatch = setUpS3Mock(testData, numberOfDownloads = 0)
        val nodes = testData.convertToNodes()

        sut.executeAction(nodes)

        assertThat(countDownLatch.await(5, TimeUnit.SECONDS)).isTrue
        assertThat(destinationFolder.resolve("testFile-1")).hasContent("")
    }

    @Test
    fun downloadSingleVersionFileToFile() {
        val destinationFile = tempFolder.newFile().toPath()

        createMockFileChooser(testDisposable.disposable, destinationFile)

        val testData = listOf(
            TestData("testFile-1", isVersion = true)
        )

        val countDownLatch = setUpS3Mock(testData)
        val nodes = testData.convertToNodes()

        sut.executeAction(nodes)

        assertThat(countDownLatch.await(5, TimeUnit.SECONDS)).isTrue
        assertThat(destinationFile).hasContent("testFile-1-content-old-version")
    }

    @Test
    fun downloadSingleVersionFileToFolder() {
        val destinationFolder = tempFolder.newFolder().toPath()

        createMockFileChooser(testDisposable.disposable, destinationFolder)

        val testData = listOf(
            TestData("testFile-1", isVersion = true)
        )

        val countDownLatch = setUpS3Mock(testData)
        val nodes = testData.convertToNodes()

        sut.executeAction(nodes)

        assertThat(countDownLatch.await(5, TimeUnit.SECONDS)).isTrue
        assertThat(destinationFolder.resolve("testFile-1@testVersionId")).hasContent("testFile-1-content-old-version")
    }

    @Test
    fun downloadMixOfVersionedFilesAndNormalFilesToFolder() {
        val destinationFolder = tempFolder.newFolder().toPath()

        createMockFileChooser(testDisposable.disposable, destinationFolder)

        val testData = listOf(
            TestData("testFile-1"),
            TestData("testFile-1", isVersion = true),
            TestData("testFile-2"),
            TestData("testFile-3", isVersion = true)
        )

        val countDownLatch = setUpS3Mock(testData)
        val nodes = testData.convertToNodes()

        sut.executeAction(nodes)

        assertThat(countDownLatch.await(5, TimeUnit.SECONDS)).isTrue
        assertThat(destinationFolder.resolve("testFile-1")).hasContent("testFile-1-content")
        assertThat(destinationFolder.resolve("testFile-1@testVersionId")).hasContent("testFile-1-content-old-version")
        assertThat(destinationFolder.resolve("testFile-2")).hasContent("testFile-2-content")
        assertThat(destinationFolder.resolve("testFile-3@testVersionId")).hasContent("testFile-3-content-old-version")
    }

    private fun setUpConflictResolutionResponses(choices: List<ConflictResolution>, vararg responses: ConflictResolution) {
        var responseNum = 0
        TestDialogManager.setTestDialog {
            choices.indexOf(responses[responseNum++])
        }
    }

    private fun setUpS3Mock(testData: List<TestData>, numberOfDownloads: Int = testData.size): CountDownLatch {
        val countDownLatch = CountDownLatch(numberOfDownloads)

        s3Bucket.stub {
            onBlocking {
                download(any(), any(), anyOrNull(), any())
            }.thenAnswer { invoke ->
                val key = invoke.getArgument<String>(1)

                if (testData.first { it.key == key }.downloadError) {
                    countDownLatch.countDown()
                    throw S3Exception.builder().message("Test Error for $key").build()
                }

                val versionId = invoke.getArgument<String?>(2)

                val contentPostfix = if (versionId != null) "-old-version" else ""
                val content = "$key-content$contentPostfix".toByteArray()

                invoke.getArgument<OutputStream>(3).use {
                    it.write(content)
                }

                countDownLatch.countDown()
            }
        }

        return countDownLatch
    }

    private data class TestData(val key: String, val isVersion: Boolean = false, val downloadError: Boolean = false)

    private fun List<TestData>.convertToNodes(): List<S3TreeNode> {
        val parent = S3TreeDirectoryNode(s3Bucket, null, "")

        return this.map {
            if (it.isVersion) {
                val obj = S3TreeObjectNode(parent, it.key, 1, java.time.Instant.now())
                S3TreeObjectVersionNode(obj, "testVersionId", 1, Instant.now())
            } else {
                S3TreeObjectNode(parent, it.key, 1, Instant.now())
            }
        }
    }
}


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

package software.aws.toolkits.jetbrains.services.s3.objectActions

import com.intellij.openapi.ui.TestDialog
import com.intellij.openapi.ui.TestDialogManager
import org.assertj.core.api.Assertions.assertThat
import org.junit.After
import org.junit.Test
import org.mockito.kotlin.any
import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyBlocking
import software.aws.toolkits.core.utils.test.aString
import software.aws.toolkits.core.utils.test.retryableAssert
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeContinuationNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeDirectoryNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeErrorNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeObjectNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeObjectVersionNode
import java.time.Instant

class NewFolderActionTest : ObjectActionTestBase() {
    override val sut = NewFolderAction()

    @After
    fun tearDown() {
        TestDialogManager.setTestDialog(TestDialog.DEFAULT)
    }

    @Test
    fun `new folder action is enabled on empty selection`() {
        assertThat(sut.updateAction(emptyList()).isEnabled).isTrue
    }

    @Test
    fun `new folder action is enabled on directory selection`() {
        val nodes = listOf(
            S3TreeDirectoryNode(s3Bucket, null, "path1/")
        )

        assertThat(sut.updateAction(nodes).isEnabled).isTrue
    }

    @Test
    fun `new folder action is enabled on object selection`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        )

        assertThat(sut.updateAction(nodes).isEnabled).isTrue
    }

    @Test
    fun `new folder action is disabled on object version selection`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val obj = S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        val nodes = listOf(
            S3TreeObjectVersionNode(obj, "version", 1, Instant.now())
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `new folder action is disabled on multiple selection`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            dir,
            S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `new folder action is disabled on error node`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeErrorNode(s3Bucket, dir)
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `new folder action is disabled on continuation node`() {
        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeContinuationNode(s3Bucket, dir, "path1/", "marker")
        )

        assertThat(sut.updateAction(nodes).isEnabled).isFalse
    }

    @Test
    fun `new folder on an object uses its parent directory as key prefix`() {
        val input = aString()

        TestDialogManager.setTestInputDialog { input }

        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val obj = S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        val nodes = listOf(obj)

        sut.executeAction(nodes)

        retryableAssert {
            argumentCaptor<String>().apply {
                verifyBlocking(s3Bucket) { newFolder(capture()) }

                assertThat(allValues).hasSize(1)
                assertThat(firstValue).isEqualTo("path1/$input")
            }

            verify(treeTable).invalidateLevel(obj)
            verify(treeTable).refresh()
        }
    }

    @Test
    fun `new folder on directory uses its key as prefix`() {
        val input = aString()

        TestDialogManager.setTestInputDialog { input }

        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(dir)

        sut.executeAction(nodes)

        retryableAssert {
            argumentCaptor<String>().apply {
                verifyBlocking(s3Bucket) { newFolder(capture()) }

                assertThat(allValues).hasSize(1)
                assertThat(firstValue).isEqualTo("path1/$input")
            }

            verify(treeTable).invalidateLevel(dir)
            verify(treeTable).refresh()
        }
    }

    @Test
    fun `new folder with no select uses no prefix`() {
        val input = aString()

        TestDialogManager.setTestInputDialog { input }

        sut.executeAction(emptyList())

        retryableAssert {
            argumentCaptor<String>().apply {
                verifyBlocking(s3Bucket) { newFolder(capture()) }

                assertThat(allValues).hasSize(1)
                assertThat(firstValue).isEqualTo(input)
            }

            verify(treeTable).invalidateLevel(treeTable.rootNode)
            verify(treeTable).refresh()
        }
    }

    @Test
    fun `new folder action can be cancelled`() {
        TestDialogManager.setTestInputDialog { null }

        val dir = S3TreeDirectoryNode(s3Bucket, null, "path1/")
        val nodes = listOf(
            S3TreeObjectNode(dir, "path1/obj1", 1, Instant.now())
        )

        sut.executeAction(nodes)

        verifyBlocking(s3Bucket, never()) { newFolder(any()) }
        verify(treeTable, never()).invalidateLevel(any())
        verify(treeTable, never()).refresh()
    }
}


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

package software.aws.toolkits.jetbrains.services.s3.objectActions

import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.Presentation
import com.intellij.openapi.actionSystem.impl.SimpleDataContext
import com.intellij.testFramework.ProjectRule
import com.intellij.testFramework.runInEdtAndWait
import com.intellij.ui.treeStructure.treetable.TreeTableTree
import org.assertj.core.api.Assertions.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.mock
import software.aws.toolkits.core.utils.test.aString
import software.aws.toolkits.jetbrains.services.s3.editor.S3EditorDataKeys
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeDirectoryNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeNode
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeTable
import software.aws.toolkits.jetbrains.services.s3.editor.S3TreeTableModel
import software.aws.toolkits.jetbrains.services.s3.editor.S3VirtualBucket

abstract class ObjectActionTestBase {
    @Rule
    @JvmField
    val projectRule = ProjectRule()

    protected abstract val sut: S3ObjectAction

    protected val bucketName = aString()
    protected lateinit var treeTable: S3TreeTable
    protected lateinit var s3Bucket: S3VirtualBucket

    @Before
    fun setUp() {
        s3Bucket = mock {
            on { name }.thenReturn(bucketName)
        }
        val mockModel = mock<TreeTableTree> {
            on { model }.thenReturn(S3TreeTableModel(mock(), emptyArray(), mock()))
        }
        treeTable = mock {
            on { bucket }.thenReturn(s3Bucket)
            on { rootNode }.thenReturn(S3TreeDirectoryNode(s3Bucket, null, ""))
            on { tree }.thenReturn(mockModel)
        }
    }

    @Test
    fun `action is disabled when UI element is not present`() {
        val projectContext = SimpleDataContext.getProjectContext(projectRule.project)
        val dc = SimpleDataContext.builder()
            .setParent(projectContext)
            .add(S3EditorDataKeys.BUCKET_TABLE, null)
            .build()
        val actionEvent = AnActionEvent.createFromAnAction(sut, null, ActionPlaces.UNKNOWN, dc)

        sut.update(actionEvent)

        assertThat(actionEvent.presentation.isEnabled).isFalse
    }

    protected fun AnAction.executeAction(nodes: List<S3TreeNode>) {
        val event = createEventFor(this, nodes)
        runInEdtAndWait {
            actionPerformed(event)
        }
    }

    protected fun AnAction.updateAction(nodes: List<S3TreeNode>): Presentation {
        val event = createEventFor(this, nodes)
        update(event)
        return event.presentation
    }

    private fun createEventFor(action: AnAction, nodes: List<S3TreeNode>): AnActionEvent {
        val projectContext = SimpleDataContext.getProjectContext(projectRule.project)
        val dc = SimpleDataContext.builder()
            .setParent(projectContext)
            .add(S3EditorDataKeys.SELECTED_NODES, nodes)
            .add(S3EditorDataKeys.BUCKET_TABLE, treeTable)
            .build()

        return AnActionEvent.createFromAnAction(action, null, ActionPlaces.UNKNOWN, dc)
    }
}