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

package software.aws.toolkits.jetbrains.services.lambda.go

import com.goide.sdk.GoSdk
import com.goide.sdk.GoSdkService
import com.goide.sdk.GoSdkUtil
import com.goide.vgo.VgoTestUtil
import com.intellij.execution.executors.DefaultDebugExecutor
import com.intellij.openapi.module.WebModuleTypeBase
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.PsiTestUtil
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
import com.intellij.testFramework.runInEdtAndWait
import org.assertj.core.api.Assertions.assertThat
import org.junit.Assume.assumeFalse
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.core.utils.RuleUtils
import software.aws.toolkits.jetbrains.core.credentials.MockCredentialManagerRule
import software.aws.toolkits.jetbrains.core.credentials.MockCredentialsManager
import software.aws.toolkits.jetbrains.core.region.getDefaultRegion
import software.aws.toolkits.jetbrains.services.lambda.execution.local.createHandlerBasedRunConfiguration
import software.aws.toolkits.jetbrains.utils.FrameworkTestUtils
import software.aws.toolkits.jetbrains.utils.checkBreakPointHit
import software.aws.toolkits.jetbrains.utils.executeRunConfigurationAndWait
import software.aws.toolkits.jetbrains.utils.jsonToMap
import software.aws.toolkits.jetbrains.utils.rules.HeavyGoCodeInsightTestFixtureRule
import software.aws.toolkits.jetbrains.utils.rules.addGoModFile
import software.aws.toolkits.jetbrains.utils.rules.compatibleGoForIde
import software.aws.toolkits.jetbrains.utils.rules.ensureCorrectGoVersion
import software.aws.toolkits.jetbrains.utils.rules.runGoModTidy
import software.aws.toolkits.jetbrains.utils.samImageRunDebugTest
import software.aws.toolkits.jetbrains.utils.setSamExecutableFromEnvironment

@RunWith(Parameterized::class)
class GoLocalRunConfigurationIntegrationTest(private val runtime: LambdaRuntime) {
    companion object {
        @JvmStatic
        @Parameterized.Parameters(name = "{0}")
        fun parameters(): Collection<Array<LambdaRuntime>> = listOf(
            arrayOf(LambdaRuntime.GO1_X)
        )
    }

    @Rule
    @JvmField
    val projectRule = HeavyGoCodeInsightTestFixtureRule()

    @Rule
    @JvmField
    val credentialManager = MockCredentialManagerRule()

    @Rule
    @JvmField
    val disposableRule = DisposableRule()

    private val input = RuleUtils.randomName()
    private val mockId = "MockCredsId"
    private val mockCreds = AwsBasicCredentials.create("Access", "ItsASecret")
    private lateinit var goModFile: VirtualFile

    // language=go
    private val fileContents = """
        package main
        
        import (
        	"github.com/aws/aws-lambda-go/lambda"
        	"strings"
        )
        
        func handler(request string) (string, error) {
        	return strings.ToUpper(request), nil
        }
        
        func main() {
        	lambda.Start(handler)
        }
    """.trimIndent()

    // language=go
    private val envVarsFileContents = """
        package main

        import (
        	"github.com/aws/aws-lambda-go/lambda"
        	"os"
        	"strings"
        )
        
        func handler() (interface{}, error) {
        	entries := map[string]string{}
        	for _, item := range os.Environ() {
        		entry := strings.Split(item, "=")
        		entries[entry[0]] = entry[1]
        	}
        	return entries, nil
        }
        
        func main() {
        	lambda.Start(handler)
        }
    """.trimIndent()

    @Before
    fun setUp() {
        setSamExecutableFromEnvironment()
        FrameworkTestUtils.ensureBuiltInServerStarted()

        val fixture = projectRule.fixture
        fixture.ensureCorrectGoVersion(disposableRule.disposable)

        PsiTestUtil.addModule(projectRule.project, WebModuleTypeBase.getInstance(), "main", fixture.tempDirFixture.findOrCreateDir("."))
        goModFile = projectRule.fixture.addGoModFile("hello-world").virtualFile

        // This block does 2 things:
        // 1. sets up vgo support which is required for sam cli
        // 2. Makes VgoDlvPositionConverter#toRemotePath work so we can set breakpoints
        runInEdtAndWait {
            VgoTestUtil.setupVgoIntegration(fixture)
        }

        credentialManager.addCredentials(mockId, mockCreds)
    }

    @Test
    fun sessionCredentialsArePassed() {
        projectRule.fixture.addLambdaFile(envVarsFileContents)
        runGoModTidy(goModFile)

        val mockSessionId = "mockSessionId"
        val mockSessionCreds = AwsSessionCredentials.create("access", "secret", "session")

        MockCredentialsManager.getInstance().addCredentials(mockSessionId, mockSessionCreds)

        val runConfiguration = createHandlerBasedRunConfiguration(
            project = projectRule.project,
            runtime = runtime.toSdkRuntime(),
            handler = "handler",
            credentialsProviderId = mockSessionId
        )

        assertThat(runConfiguration).isNotNull

        val executeLambda = executeRunConfigurationAndWait(runConfiguration)

        assertThat(executeLambda.exitCode).isEqualTo(0)
        assertThat(jsonToMap(executeLambda.stdout))
            .containsEntry("AWS_ACCESS_KEY_ID", mockSessionCreds.accessKeyId())
            .containsEntry("AWS_SECRET_ACCESS_KEY", mockSessionCreds.secretAccessKey())
            .containsEntry("AWS_SESSION_TOKEN", mockSessionCreds.sessionToken())
    }

    @Test
    fun samIsExecuted() {
        projectRule.fixture.addLambdaFile(envVarsFileContents)
        runGoModTidy(goModFile)

        val envVars = mutableMapOf("Foo" to "Bar", "Bat" to "Baz")

        val runConfiguration = createHandlerBasedRunConfiguration(
            project = projectRule.project,
            runtime = runtime.toSdkRuntime(),
            handler = "handler",
            input = "\"${input}\"",
            credentialsProviderId = mockId,
            environmentVariables = envVars
        )

        assertThat(runConfiguration).isNotNull

        val executeLambda = executeRunConfigurationAndWait(runConfiguration)

        assertThat(executeLambda.exitCode).isEqualTo(0)
        assertThat(jsonToMap(executeLambda.stdout))
            .describedAs("Region is passed")
            .containsEntry("AWS_REGION", getDefaultRegion().id)
        assertThat(jsonToMap(executeLambda.stdout))
            .describedAs("Envvars are passed")
            .containsEntry("Foo", "Bar")
            .containsEntry("Bat", "Baz")
        assertThat(jsonToMap(executeLambda.stdout))
            .describedAs("Credentials are passed")
            .containsEntry("AWS_ACCESS_KEY_ID", mockCreds.accessKeyId())
            .containsEntry("AWS_SECRET_ACCESS_KEY", mockCreds.secretAccessKey())
            // An empty AWS_SESSION_TOKEN is inserted by Samcli/the Lambda runtime as of 1.13.1
            .containsEntry("AWS_SESSION_TOKEN", "")
    }

    @Test
    fun samIsExecutedWithFileInput() {
        projectRule.fixture.addLambdaFile(envVarsFileContents)
        runGoModTidy(goModFile)

        val runConfiguration = createHandlerBasedRunConfiguration(
            project = projectRule.project,
            runtime = runtime.toSdkRuntime(),
            handler = "handler",
            input = projectRule.fixture.tempDirFixture.createFile("tmp", "\"${input}\"").canonicalPath!!,
            inputIsFile = true,
            credentialsProviderId = mockId,
        )

        assertThat(runConfiguration).isNotNull

        val executeLambda = executeRunConfigurationAndWait(runConfiguration)

        assertThat(executeLambda.exitCode).isEqualTo(0)
    }

    @Test
    fun samIsExecutedWithDebugger() {
        projectRule.fixture.addLambdaFile(fileContents)
        runGoModTidy(goModFile)

        val runConfiguration = createHandlerBasedRunConfiguration(
            project = projectRule.project,
            runtime = runtime.toSdkRuntime(),
            handler = "handler",
            input = "\"${input}\"",
            credentialsProviderId = mockId
        )

        assertThat(runConfiguration).isNotNull

        projectRule.addBreakpoint()
        val debuggerIsHit = checkBreakPointHit(projectRule.project)

        val executeLambda = executeRunConfigurationAndWait(runConfiguration, DefaultDebugExecutor.EXECUTOR_ID)

        assertThat(executeLambda.exitCode).isEqualTo(0)
        assertThat(executeLambda.stdout).contains(input.toUpperCase())

        assertThat(debuggerIsHit.get()).isTrue
    }

    @Test
    fun `works when handler is 'main'`() {
        assumeFalse(true) // TODO: fix when new build images are ready
        // fails if [Lambda.findPsiElementsForHandler] finds the handler in the Go standard library
        val sdkDir = GoSdkUtil.suggestSdkDirectory()!!.children.sortedByDescending { it.name }.first().canonicalPath!!
        VfsRootAccess.allowRootAccess(projectRule.project, sdkDir)
        runInEdtAndWait {
            GoSdkService.getInstance(projectRule.project).setSdk(GoSdk.fromHomePath(sdkDir))
        }
        projectRule.fixture.addLambdaFile(fileContents)
        runGoModTidy(goModFile)

        val runConfiguration = createHandlerBasedRunConfiguration(
            project = projectRule.project,
            runtime = runtime.toSdkRuntime(),
            handler = "main",
            input = "\"${input}\"",
            credentialsProviderId = mockId
        )

        assertThat(runConfiguration).isNotNull

        val executeLambda = executeRunConfigurationAndWait(runConfiguration, DefaultDebugExecutor.EXECUTOR_ID)

        assertThat(executeLambda.exitCode).isEqualTo(0)
    }

    @Test
    fun samIsExecutedImage(): Unit = samImageRunDebugTest(
        projectRule = projectRule,
        relativePath = "samProjects/image/$runtime",
        templatePatches = mapOf("[GoVersion]" to (compatibleGoForIde())),
        sourceFileName = "main.go",
        runtime = runtime,
        mockCredentialsId = mockId,
        input = input,
        expectedOutput = input.toUpperCase()
    )

    @Test
    fun samIsExecutedWithDebuggerImage() {
        samImageRunDebugTest(
            projectRule = projectRule,
            relativePath = "samProjects/image/$runtime",
            templatePatches = mapOf("[GoVersion]" to (compatibleGoForIde())),
            sourceFileName = "main.go",
            runtime = runtime,
            mockCredentialsId = mockId,
            input = input,
            addBreakpoint = { projectRule.addBreakpoint() }
        )
    }

    private fun CodeInsightTestFixture.addLambdaFile(contents: String) {
        val psiFile = addFileToProject("hello-world/main.go", contents)

        runInEdtAndWait {
            openFileInEditor(psiFile.virtualFile)
        }
    }
}