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

package software.aws.toolkits.jetbrains.gateway.welcomescreen
// TODO: fixme
//
// import com.intellij.testFramework.ApplicationRule
// import com.intellij.testFramework.RuleChain
// import org.assertj.core.api.Assertions.assertThat
// import org.junit.Ignore
// import org.junit.Rule
// import org.junit.Test
// import org.mockito.kotlin.any
// import org.mockito.kotlin.argumentCaptor
// import org.mockito.kotlin.mock
// import org.mockito.kotlin.times
// import org.mockito.kotlin.verify
// import software.amazon.awssdk.services.mde.MdeClient
// import software.amazon.awssdk.services.mde.model.EnvironmentStatus
// import software.amazon.awssdk.services.mde.model.GetEnvironmentMetadataResponse
// import software.amazon.awssdk.services.mde.model.IdeConfiguration
// import software.amazon.awssdk.services.mde.model.InstanceType
// import software.amazon.awssdk.services.mde.model.StartEnvironmentRequest
// import software.amazon.awssdk.services.mde.model.TagResourceRequest
// import software.amazon.awssdk.services.mde.model.UntagResourceRequest
// import software.aws.toolkits.jetbrains.core.MockClientManagerRule
// import software.aws.toolkits.jetbrains.gateway.GatewayProduct
//
// class ConfigureActionTest {
//    val applicationRule = ApplicationRule()
//    val mockClientManagerRule = MockClientManagerRule()
//
//    @JvmField
//    @Rule
//    val ruleChain = RuleChain(applicationRule, mockClientManagerRule)
//
//    @Test
//    fun `noop if no changes`() {
//        val client = mockClientManagerRule.create<MdeClient>()
//        val initialState = GetEnvironmentMetadataResponse.builder()
//            .id("id")
//            .arn("arn")
//            .status(EnvironmentStatus.RUNNING)
//            .instanceType(InstanceType.DEV_STANDARD1_MICRO)
//            .tags(listOf("3", "2", "1").associateWith { "" })
//            .build()
//        val context = ConfigureAction.ReconfigureEnvironmentSettings(
//            labels = "1,2,3",
//            type = InstanceType.DEV_STANDARD1_MICRO
//        )
//
//        ConfigureAction.reconfigureIdeFromContext(client, initialState, context, mock())
//    }
//
//    @Test
//    fun `untag removed tags`() {
//        val client = mockClientManagerRule.create<MdeClient>()
//        val initialState = GetEnvironmentMetadataResponse.builder()
//            .id("id")
//            .arn("arn")
//            .status(EnvironmentStatus.RUNNING)
//            .instanceType(InstanceType.DEV_STANDARD1_MICRO)
//            .tags(listOf("3", "2", "1").associateWith { "" })
//            .build()
//        val context = ConfigureAction.ReconfigureEnvironmentSettings(
//            labels = "1",
//            type = InstanceType.DEV_STANDARD1_MICRO
//        )
//
//        ConfigureAction.reconfigureIdeFromContext(client, initialState, context, mock())
//
//        argumentCaptor<UntagResourceRequest>().apply {
//            verify(client).untagResource(capture())
//            verify(client, times(0)).tagResource(any<TagResourceRequest>())
//
//            assertThat(firstValue.tagKeys()).containsExactlyInAnyOrder("2", "3")
//        }
//    }
//
//    @Test
//    fun `tag new tags`() {
//        val client = mockClientManagerRule.create<MdeClient>()
//        val initialState = GetEnvironmentMetadataResponse.builder()
//            .id("id")
//            .arn("arn")
//            .status(EnvironmentStatus.RUNNING)
//            .instanceType(InstanceType.DEV_STANDARD1_MICRO)
//            .tags(listOf("3", "2", "1").associateWith { "" })
//            .build()
//        val context = ConfigureAction.ReconfigureEnvironmentSettings(
//            labels = "1,2,3,4,5,6",
//            type = InstanceType.DEV_STANDARD1_MICRO
//        )
//
//        ConfigureAction.reconfigureIdeFromContext(client, initialState, context, mock())
//
//        argumentCaptor<TagResourceRequest>().apply {
//            verify(client).tagResource(capture())
//            verify(client, times(0)).untagResource(any<UntagResourceRequest>())
//
//            assertThat(firstValue.tags().keys).containsExactlyInAnyOrder("4", "5", "6")
//        }
//    }
//
//    @Test
//    @Ignore
//    fun `changes instance type`() {
//        val client = mockClientManagerRule.create<MdeClient>()
//        val initialState = GetEnvironmentMetadataResponse.builder()
//            .id("id")
//            .arn("arn")
//            .status(EnvironmentStatus.RUNNING)
//            .instanceType(InstanceType.DEV_STANDARD1_MICRO)
//            .build()
//        val context = ConfigureAction.ReconfigureEnvironmentSettings(
//            labels = "1,2,3,4,5,6",
//            type = InstanceType.DEV_STANDARD1_LARGE
//        )
//
//        ConfigureAction.reconfigureIdeFromContext(client, initialState, context, mock())
//
//        argumentCaptor<StartEnvironmentRequest>().apply {
//            verify(client).startEnvironment(capture())
//
//            assertThat(firstValue.instanceType()).isEqualTo(InstanceType.DEV_STANDARD1_LARGE)
//        }
//    }
//
//    @Test
//    @Ignore
//    fun `changes IDE`() {
//        val client = mockClientManagerRule.create<MdeClient>()
//        val initialState = GetEnvironmentMetadataResponse.builder()
//            .id("id")
//            .arn("arn")
//            .status(EnvironmentStatus.RUNNING)
//            .instanceType(InstanceType.DEV_STANDARD1_MICRO)
//            .build()
//        val context = ConfigureAction.ReconfigureEnvironmentSettings(
//            labels = "1,2,3,4,5,6",
//            type = InstanceType.DEV_STANDARD1_LARGE,
//            ide = GatewayProduct("ecrImage", "build", "product", "name", emptyList())
//        )
//
//        ConfigureAction.reconfigureIdeFromContext(client, initialState, context, mock())
//
//        argumentCaptor<StartEnvironmentRequest>().apply {
//            verify(client).startEnvironment(capture())
//
//            assertThat(firstValue.ides()).satisfies {
//                assertThat(it.size).isEqualTo(1)
//                assertThat(it.first()).isEqualTo(IdeConfiguration.builder().runtime("ecrImage").build())
//            }
//        }
//    }
// }


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

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

import base.AwsReuseSolutionTestBase
import com.intellij.execution.executors.DefaultDebugExecutor
import com.intellij.ide.util.PropertiesComponent
import com.jetbrains.rider.projectView.solutionDirectory
import com.jetbrains.rider.test.scriptingApi.removeAllBreakpoints
import org.assertj.core.api.Assertions.assertThat
import org.testng.annotations.AfterMethod
import org.testng.annotations.BeforeMethod
import org.testng.annotations.Test
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.core.utils.writeText
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.services.lambda.execution.local.createTemplateRunConfiguration
import software.aws.toolkits.jetbrains.utils.executeRunConfigurationAndWaitRider
import software.aws.toolkits.jetbrains.utils.jsonToMap
import software.aws.toolkits.jetbrains.utils.setSamExecutableFromEnvironment
import java.nio.file.Files

class Dotnet50LocalLambdaImageRunConfigurationIntegrationTest :
    DotnetLocalLambdaImageRunConfigurationIntegrationTestBase("ImageLambda5X", LambdaRuntime.DOTNET5_0)

class Dotnet60LocalLambdaRunConfigurationIntegrationTest : DotnetLocalLambdaRunConfigurationIntegrationTestBase("EchoLambda6X", LambdaRuntime.DOTNET6_0)

class Dotnet60LocalLambdaImageRunConfigurationIntegrationTest :
    DotnetLocalLambdaImageRunConfigurationIntegrationTestBase("ImageLambda6X", LambdaRuntime.DOTNET6_0)

abstract class DotnetLocalLambdaRunConfigurationIntegrationTestBase(private val solutionName: String, private val runtime: LambdaRuntime) :
    AwsReuseSolutionTestBase() {

    override val waitForCaches = false

    private val mockId = "MockCredsId"
    private val mockCreds = AwsBasicCredentials.create("Access", "ItsASecret")
    private val handler = "EchoLambda::EchoLambda.Function::FunctionHandler"

    private var initialImmediateWindow: Boolean = false

    @BeforeMethod
    fun setUp() {
        // Disable the immediate window due to double release of editor in 203, this issue should be fixed in later Rider versions FIX_WHEN_MIN_IS_211
        initialImmediateWindow = PropertiesComponent.getInstance().getBoolean("debugger.immediate.window.in.watches")
        PropertiesComponent.getInstance().setValue("debugger.immediate.window.in.watches", false, true)

        setSamExecutableFromEnvironment()

        MockCredentialsManager.getInstance().addCredentials(mockId, mockCreds)
    }

    @AfterMethod
    fun tearDown() {
        PropertiesComponent.getInstance().setValue("debugger.immediate.window.in.watches", initialImmediateWindow)
    }

    override fun getSolutionDirectoryName(): String = solutionName

    @Test
    fun samIsExecutedDebugger() {
        setBreakpoint()

        val runConfiguration = createHandlerBasedRunConfiguration(
            project = project,
            runtime = runtime.toSdkRuntime(),
            credentialsProviderId = mockId,
            handler = handler
        )

        val executeLambda = executeRunConfigurationAndWaitRider(runConfiguration, DefaultDebugExecutor.EXECUTOR_ID)
        assertThat(executeLambda.exitCode).isEqualTo(0)
    }

    @Test
    fun samIsExecuted() {
        val envVars = mutableMapOf("Foo" to "Bar", "Bat" to "Baz")

        val runConfiguration = createHandlerBasedRunConfiguration(
            project = project,
            runtime = runtime.toSdkRuntime(),
            credentialsProviderId = mockId,
            handler = handler,
            environmentVariables = envVars
        )

        val executeLambda = executeRunConfigurationAndWaitRider(runConfiguration)

        assertThat(executeLambda.exitCode).isEqualTo(0)
        assertThat(jsonToMap(executeLambda.stdout))
            .describedAs("Environment variables are passed")
            .containsEntry("Foo", "Bar")
            .containsEntry("Bat", "Baz")
        assertThat(jsonToMap(executeLambda.stdout))
            .describedAs("Region is set")
            .containsEntry("AWS_REGION", getDefaultRegion().id)
        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() {
        val input = Files.createTempFile(tempTestDirectory.toPath(), "tmp", null).also {
            it.writeText("Hello World")
        }

        val runConfiguration = createHandlerBasedRunConfiguration(
            project = project,
            runtime = runtime.toSdkRuntime(),
            input = input.toString(),
            inputIsFile = true,
            credentialsProviderId = mockId,
            handler = handler,
        )

        val executeLambda = executeRunConfigurationAndWaitRider(runConfiguration)

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

abstract class DotnetLocalLambdaImageRunConfigurationIntegrationTestBase(private val solutionName: String, private val runtime: LambdaRuntime) :
    AwsReuseSolutionTestBase() {

    override val waitForCaches = false

    private val mockId = "MockCredsId"
    private val mockCreds = AwsBasicCredentials.create("Access", "ItsASecret")

    private var initialImmediateWindow: Boolean = false

    @BeforeMethod
    fun setUp() {
        // Disable the immediate window due to double release of editor in 203, this issue should be fixed in later Rider versions FIX_WHEN_MIN_IS_211
        initialImmediateWindow = PropertiesComponent.getInstance().getBoolean("debugger.immediate.window.in.watches")
        PropertiesComponent.getInstance().setValue("debugger.immediate.window.in.watches", false, true)

        setSamExecutableFromEnvironment()

        MockCredentialsManager.getInstance().addCredentials(mockId, mockCreds)
    }

    @AfterMethod(alwaysRun = true)
    fun tearDown() {
        PropertiesComponent.getInstance().setValue("debugger.immediate.window.in.watches", initialImmediateWindow)
        removeAllBreakpoints(project)
    }

    override fun getSolutionDirectoryName(): String = solutionName

    @Test
    fun samIsExecutedImage() {
        val template = "${project.solutionDirectory}/template.yaml"

        val runConfiguration = createTemplateRunConfiguration(
            project = project,
            runtime = runtime,
            templateFile = template,
            logicalId = "HelloWorldFunction",
            input = "\"Hello World\"",
            credentialsProviderId = mockId,
            isImage = true
        )

        val executeLambda = executeRunConfigurationAndWaitRider(runConfiguration)
        assertThat(executeLambda.exitCode).isEqualTo(0)

        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 samIsExecutedDebuggerImage() {
        setBreakpoint()

        val template = "${project.solutionDirectory}/template.yaml"

        val runConfiguration = createTemplateRunConfiguration(
            project = project,
            runtime = runtime,
            templateFile = template,
            logicalId = "HelloWorldFunction",
            input = "\"Hello World\"",
            credentialsProviderId = mockId,
            isImage = true
        )

        val executeLambda = executeRunConfigurationAndWaitRider(runConfiguration, DefaultDebugExecutor.EXECUTOR_ID)
        assertThat(executeLambda.exitCode).isEqualTo(0)
    }
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.execution.ExecutorRegistry
import com.intellij.execution.Output
import com.intellij.execution.OutputListener
import com.intellij.execution.configurations.RunConfiguration
import com.intellij.execution.executors.DefaultDebugExecutor
import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.runners.ExecutionEnvironmentBuilder
import com.intellij.execution.runners.ProgramRunner
import com.intellij.openapi.application.runInEdt
import com.intellij.xdebugger.XDebugProcess
import com.intellij.xdebugger.XDebugSession
import com.intellij.xdebugger.XDebuggerManager
import com.intellij.xdebugger.XDebuggerManagerListener
import com.jetbrains.rdclient.util.idea.waitAndPump
import com.jetbrains.rider.projectView.solutionDirectory
import com.jetbrains.rider.test.scriptingApi.DebugTestExecutionContext
import com.jetbrains.rider.test.scriptingApi.debugProgramAfterAttach
import com.jetbrains.rider.test.scriptingApi.dumpFullCurrentData
import com.jetbrains.rider.test.scriptingApi.resumeSession
import com.jetbrains.rider.test.scriptingApi.waitForDotNetDebuggerInitializedOrCanceled
import com.jetbrains.rider.test.scriptingApi.waitForPause
import java.time.Duration
import java.util.concurrent.CompletableFuture
import kotlin.test.assertNotNull

private fun executeAndWaitOnAttach(runConfiguration: RunConfiguration): CompletableFuture<XDebugSession> {
    val project = runConfiguration.project
    val executorId = DefaultDebugExecutor.EXECUTOR_ID

    val sessionFuture = CompletableFuture<XDebugSession>()
    project.messageBus.connect().subscribe(
        XDebuggerManager.TOPIC,
        object : XDebuggerManagerListener {

            override fun processStarted(debugProcess: XDebugProcess) {
                println("Debugger attached: $debugProcess")
                debugProcess.processHandler.addProcessListener(object : OutputListener() {
                    override fun processTerminated(event: ProcessEvent) {
                        println("Debug worker terminated with exit code: ${this.output.exitCode}")
                        println("Debug worker stdout: ${this.output.stdout}")
                        println("Debug worker stderr: ${this.output.stderr}")
                    }
                })
                sessionFuture.complete(debugProcess.session)
            }
        }
    )

    val executor = ExecutorRegistry.getInstance().getExecutorById(executorId)
    assertNotNull(executor)
    val executionFuture = CompletableFuture<Output>()
    // In the real world create and execute runs on EDT
    runInEdt {
        try {
            @Suppress("UnsafeCallOnNullableType")
            val runner = ProgramRunner.getRunner(executorId, runConfiguration)!!
            val executionEnvironmentBuilder = ExecutionEnvironmentBuilder.create(executor, runConfiguration)
                .runner(runner)
            val executionEnvironment = executionEnvironmentBuilder.build()

            // TODO: exception isn't propagated out and test is forced to wait to timeout instead of exiting immediately
            executionEnvironment.runner.execute(executionEnvironment)
        } catch (e: Throwable) {
            executionFuture.completeExceptionally(e)
        }
    }
    return sessionFuture
}

fun executeRunConfigurationAndWaitRider(runConfiguration: RunConfiguration, executorId: String = DefaultRunExecutor.EXECUTOR_ID): Output {
    if (executorId == DefaultRunExecutor.EXECUTOR_ID) {
        val executeLambda = executeRunConfiguration(runConfiguration, executorId)
        // waitAndPump lets us run on EDT in the test itself without deadlocking since Rider runs tests on EDT
        // 4 is arbitrary, but Image-based functions can take > 3 min on first build/run, so 4 is a safe number
        waitAndPump(Duration.ofMinutes(4), { executeLambda.isDone })
        if (!executeLambda.isDone) {
            throw IllegalStateException("Took too long to execute Rider run configuration!")
        }
        return executeLambda.get()
    }

    val session = executeAndWaitOnAttach(runConfiguration)
    waitAndPump(Duration.ofMinutes(4), { session.isDone })

    val executionFuture = CompletableFuture<Output>()
    val waitAndTest: DebugTestExecutionContext.() -> Unit = {
        waitForDotNetDebuggerInitializedOrCanceled()
        waitForPause()
        this.session.debugProcess.processHandler.addProcessListener(object : OutputListener() {
            override fun processTerminated(event: ProcessEvent) {
                super.processTerminated(event)
                executionFuture.complete(this.output)
            }
        })
        // prints current debugger state for comparison against .gold file
        dumpFullCurrentData()
        resumeSession()
    }

    debugProgramAfterAttach(session.get(), runConfiguration.project.solutionDirectory.resolve("expected.gold"), waitAndTest, true)

    return executionFuture.get()
}


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

package model.daemon

import com.jetbrains.rd.generator.nova.Ext
import com.jetbrains.rd.generator.nova.ExternalGenerator
import com.jetbrains.rd.generator.nova.FlowTransform
import com.jetbrains.rd.generator.nova.GeneratorBase
import com.jetbrains.rd.generator.nova.PredefinedType.string
import com.jetbrains.rd.generator.nova.csharp.CSharp50Generator
import com.jetbrains.rd.generator.nova.doc
import com.jetbrains.rd.generator.nova.field
import com.jetbrains.rd.generator.nova.kotlin.Kotlin11Generator
import com.jetbrains.rd.generator.nova.setting
import com.jetbrains.rd.generator.nova.sink
import com.jetbrains.rd.generator.nova.util.syspropertyOrInvalid
import com.jetbrains.rider.model.nova.ide.IdeRoot
import com.jetbrains.rider.model.nova.ide.SolutionModel
import java.io.File

object DaemonKotlinGenerator : ExternalGenerator(
    Kotlin11Generator(
        FlowTransform.AsIs,
        "com.jetbrains.rider.model",
        File(syspropertyOrInvalid("ktDaemonGeneratedOutput"))
    ),
    IdeRoot
)

object DaemonCSharpGenerator : ExternalGenerator(
    CSharp50Generator(
        FlowTransform.Reversed,
        "JetBrains.Rider.Model",
        File(syspropertyOrInvalid("csDaemonGeneratedOutput"))
    ),
    IdeRoot
)

@Suppress("unused")
object LambdaDaemonModel : Ext(SolutionModel.Solution) {

    private val LambdaRequest = structdef {
        field("methodName", string)
        field("handler", string)
    }

    init {
        setting(Kotlin11Generator.Namespace, "software.aws.toolkits.jetbrains.protocol")
        setting(CSharp50Generator.Namespace, "AWS.Toolkit.Rider.Model")

        setting(GeneratorBase.AcceptsGenerator) { generator ->
            generator == DaemonKotlinGenerator.generator ||
                generator == DaemonCSharpGenerator.generator
        }

        sink("runLambda", LambdaRequest)
            .doc("Signal from backend to run lambda on local environment")

        sink("debugLambda", LambdaRequest)
            .doc("Signal from backend to debug lambda on local environemnt")

        sink("createNewLambda", LambdaRequest)
            .doc("Signal from backend to create a new AWS Lambda")
    }
}


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

package model.project

import com.jetbrains.rd.generator.nova.Ext
import com.jetbrains.rd.generator.nova.ExternalGenerator
import com.jetbrains.rd.generator.nova.FlowTransform
import com.jetbrains.rd.generator.nova.GeneratorBase
import com.jetbrains.rd.generator.nova.PredefinedType.string
import com.jetbrains.rd.generator.nova.call
import com.jetbrains.rd.generator.nova.csharp.CSharp50Generator
import com.jetbrains.rd.generator.nova.doc
import com.jetbrains.rd.generator.nova.field
import com.jetbrains.rd.generator.nova.kotlin.Kotlin11Generator
import com.jetbrains.rd.generator.nova.setting
import com.jetbrains.rd.generator.nova.util.syspropertyOrInvalid
import com.jetbrains.rider.model.nova.ide.IdeRoot
import com.jetbrains.rider.model.nova.ide.SolutionModel
import java.io.File

object AwsProjectKotlinGenerator : ExternalGenerator(
    Kotlin11Generator(
        FlowTransform.AsIs,
        "com.jetbrains.rider.model",
        File(syspropertyOrInvalid("ktAwsProjectGeneratedOutput"))
    ),
    IdeRoot
)

object AwsProjectCSharpGenerator : ExternalGenerator(
    CSharp50Generator(
        FlowTransform.Reversed,
        "JetBrains.Rider.Model",
        File(syspropertyOrInvalid("csAwsProjectGeneratedOutput"))
    ),
    IdeRoot
)

@Suppress("unused")
object AwsProjectModel : Ext(SolutionModel.Solution) {

    private val AwsProjectOutputRequest = structdef {
        field("projectPath", string)
    }

    private val AwsProjectOutput = structdef {
        field("assemblyName", string)
        field("location", string)
    }

    init {
        setting(Kotlin11Generator.Namespace, "software.aws.toolkits.jetbrains.protocol")
        setting(CSharp50Generator.Namespace, "AWS.Toolkit.Rider.Model")

        setting(GeneratorBase.AcceptsGenerator) { generator ->
            generator == AwsProjectKotlinGenerator.generator ||
                generator == AwsProjectCSharpGenerator.generator
        }

        call("getProjectOutput", AwsProjectOutputRequest, AwsProjectOutput)
            .doc("Get AWS project output information")
    }
}


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

package model.psi

import com.jetbrains.rd.generator.nova.Ext
import com.jetbrains.rd.generator.nova.ExternalGenerator
import com.jetbrains.rd.generator.nova.FlowTransform
import com.jetbrains.rd.generator.nova.GeneratorBase
import com.jetbrains.rd.generator.nova.PredefinedType.bool
import com.jetbrains.rd.generator.nova.PredefinedType.int
import com.jetbrains.rd.generator.nova.PredefinedType.string
import com.jetbrains.rd.generator.nova.PredefinedType.void
import com.jetbrains.rd.generator.nova.async
import com.jetbrains.rd.generator.nova.call
import com.jetbrains.rd.generator.nova.csharp.CSharp50Generator
import com.jetbrains.rd.generator.nova.doc
import com.jetbrains.rd.generator.nova.field
import com.jetbrains.rd.generator.nova.immutableList
import com.jetbrains.rd.generator.nova.kotlin.Kotlin11Generator
import com.jetbrains.rd.generator.nova.nullable
import com.jetbrains.rd.generator.nova.setting
import com.jetbrains.rd.generator.nova.util.syspropertyOrInvalid
import com.jetbrains.rider.model.nova.ide.IdeRoot
import com.jetbrains.rider.model.nova.ide.ShellModel.IconModel
import com.jetbrains.rider.model.nova.ide.SolutionModel
import java.io.File

object PsiKotlinGenerator : ExternalGenerator(
    Kotlin11Generator(
        FlowTransform.AsIs,
        "com.jetbrains.rider.model",
        File(syspropertyOrInvalid("ktPsiGeneratedOutput"))
    ),
    IdeRoot
)

object PsiCSharpGenerator : ExternalGenerator(
    CSharp50Generator(
        FlowTransform.Reversed,
        "JetBrains.Rider.Model",
        File(syspropertyOrInvalid("csPsiGeneratedOutput"))
    ),
    IdeRoot
)

@Suppress("unused")
object LambdaPsiModel : Ext(SolutionModel.Solution) {

    private val HandlerCompletionItem = structdef {
        field("handler", string)
        field("iconId", IconModel.nullable)
    }

    private val HandlerExistRequest = structdef {
        field("className", string)
        field("methodName", string)
        field("projectId", int)
    }

    init {
        setting(Kotlin11Generator.Namespace, "software.aws.toolkits.jetbrains.protocol")
        setting(CSharp50Generator.Namespace, "AWS.Toolkit.Rider.Model")

        setting(GeneratorBase.AcceptsGenerator) { generator ->
            generator == PsiKotlinGenerator.generator ||
                generator == PsiCSharpGenerator.generator
        }

        call("determineHandlers", void, immutableList(HandlerCompletionItem)).async
            .doc("Get collection of HandlerCompletionItems with available handler functions for a solution")

        call("isHandlerExists", HandlerExistRequest, bool).async
            .doc("Check whether handler with specified name exists for a partucular project")
    }
}


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

package model.setting

import com.jetbrains.rd.generator.nova.Ext
import com.jetbrains.rd.generator.nova.ExternalGenerator
import com.jetbrains.rd.generator.nova.FlowTransform
import com.jetbrains.rd.generator.nova.GeneratorBase
import com.jetbrains.rd.generator.nova.PredefinedType.bool
import com.jetbrains.rd.generator.nova.csharp.CSharp50Generator
import com.jetbrains.rd.generator.nova.doc
import com.jetbrains.rd.generator.nova.kotlin.Kotlin11Generator
import com.jetbrains.rd.generator.nova.setting
import com.jetbrains.rd.generator.nova.source
import com.jetbrains.rd.generator.nova.util.syspropertyOrInvalid
import com.jetbrains.rider.model.nova.ide.IdeRoot
import com.jetbrains.rider.model.nova.ide.SolutionModel
import java.io.File

object AwsSettingsKotlinGenerator : ExternalGenerator(
    Kotlin11Generator(
        FlowTransform.AsIs,
        "com.jetbrains.rider.model",
        File(syspropertyOrInvalid("ktAwsSettingsGeneratedOutput"))
    ),
    IdeRoot
)

object AwsSettingsCSharpGenerator : ExternalGenerator(
    CSharp50Generator(
        FlowTransform.Reversed,
        "JetBrains.Rider.Model",
        File(syspropertyOrInvalid("csAwsSettingsGeneratedOutput"))
    ),
    IdeRoot
)

@Suppress("unused")
object AwsSettingModel : Ext(SolutionModel.Solution) {

    init {
        setting(Kotlin11Generator.Namespace, "software.aws.toolkits.jetbrains.protocol")
        setting(CSharp50Generator.Namespace, "AWS.Toolkit.Rider.Model")

        setting(GeneratorBase.AcceptsGenerator) { generator ->
            generator == AwsSettingsKotlinGenerator.generator ||
                generator == AwsSettingsCSharpGenerator.generator
        }

        source("showLambdaGutterMarks", bool)
            .doc("Flag indicating whether Lambda gutter marks should be shown in editor")
    }
}


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

package software.aws.toolkits.jetbrains.icons

import com.intellij.openapi.util.IconLoader
import com.intellij.openapi.util.IconPathPatcher

/**
 * Icons Patcher for icons set from Rider backend (R#).
 * Rider backend do not have access to fronted icons (e.g. LambdaFunction.svg, new.svg). To share an existing frontend icons
 * and reuse them instead of creating a duplicate set on a backend, we can replace a fake backend icon with any frontend icon by path.
 * This class is used to set frontend icons for gutter mark popup menu that is fully composed on Rider backend.
 */
internal class RiderAwsIconsPatcher : IconPathPatcher() {

    companion object {
        fun install() = myInstallPatcher

        private val myInstallPatcher: Unit by lazy {
            IconLoader.installPathPatcher(RiderAwsIconsPatcher())
        }
    }

    override fun patchPath(path: String, classLoader: ClassLoader?): String? = myIconsOverrideMap[path]

    override fun getContextClassLoader(path: String, originalClassLoader: ClassLoader?): ClassLoader? =
        if (myIconsOverrideMap.containsKey(path)) {
            javaClass.classLoader
        } else {
            originalClassLoader
        }

    private val myIconsOverrideMap = mapOf(
        "/resharper/LambdaRunMarkers/Lambda.svg" to "AwsIcons.Resources.LAMBDA_FUNCTION",
        "/resharper/LambdaRunMarkers/CreateNew.svg" to "AllIcons.Actions.New"
    )
}


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

package software.aws.toolkits.jetbrains.icons

import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.StartupActivity

internal class RiderAwsIconsPatcherRegistrar : StartupActivity {

    override fun runActivity(project: Project) {
        RiderAwsIconsPatcher.install()
    }
}


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

package software.aws.toolkits.jetbrains.project

import com.intellij.openapi.project.Project
import com.jetbrains.rdclient.util.idea.LifetimedProjectComponent
import com.jetbrains.rider.projectView.solution
import software.aws.toolkits.jetbrains.protocol.awsProjectModel

@Suppress("ComponentNotRegistered")
class AwsProjectHost(project: Project) : LifetimedProjectComponent(project) {

    val model = project.solution.awsProjectModel
}


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

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

import com.intellij.execution.Executor
import com.intellij.execution.ProgramRunnerUtil
import com.intellij.execution.RunManager
import com.intellij.execution.configurations.ConfigurationTypeUtil
import com.intellij.execution.executors.DefaultDebugExecutor
import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.lang.LanguageUtil
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.LangDataKeys
import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.actionSystem.impl.SimpleDataContext
import com.intellij.openapi.project.Project
import com.intellij.psi.SmartPointerManager
import com.jetbrains.rdclient.util.idea.LifetimedProjectComponent
import com.jetbrains.rider.projectView.solution
import software.aws.toolkits.jetbrains.protocol.lambdaDaemonModel
import software.aws.toolkits.jetbrains.services.lambda.dotnet.DotNetLambdaHandlerResolver
import software.aws.toolkits.jetbrains.services.lambda.dotnet.element.RiderLambdaHandlerFakePsiElement
import software.aws.toolkits.jetbrains.services.lambda.execution.LambdaRunConfigurationType
import software.aws.toolkits.jetbrains.services.lambda.execution.local.LocalLambdaRunConfiguration
import software.aws.toolkits.jetbrains.services.lambda.execution.local.LocalLambdaRunConfigurationProducer
import software.aws.toolkits.jetbrains.services.lambda.upload.CreateLambdaFunctionAction
import software.aws.toolkits.jetbrains.utils.DotNetRuntimeUtils

/**
 * Lambda Host class is used for communication with ReSharper backend through protocol
 * for all operation related to AWS Lambda.
 */
@Suppress("ComponentNotRegistered")
class LambdaDaemonHost(project: Project) : LifetimedProjectComponent(project) {

    val model = project.solution.lambdaDaemonModel

    init {
        initRunLambdaHandler()
        initDebugLambdaHandler()
        initCreateNewLambdaHandler()
    }

    private fun initRunLambdaHandler() =
        model.runLambda.advise(componentLifetime) { lambdaRequest ->
            runConfiguration(
                methodName = lambdaRequest.methodName,
                handler = lambdaRequest.handler,
                executor = DefaultRunExecutor.getRunExecutorInstance()
            )
        }

    private fun initDebugLambdaHandler() =
        model.debugLambda.advise(componentLifetime) { lambdaRequest ->
            runConfiguration(
                methodName = lambdaRequest.methodName,
                handler = lambdaRequest.handler,
                executor = DefaultDebugExecutor.getDebugExecutorInstance()
            )
        }

    private fun initCreateNewLambdaHandler() =
        model.createNewLambda.advise(componentLifetime) { createLambdaRequest ->
            val handler = createLambdaRequest.handler

            val handlerResolver = DotNetLambdaHandlerResolver()
            val fieldId = handlerResolver.getFieldIdByHandlerName(project, handler)
            val psiElement = RiderLambdaHandlerFakePsiElement(project, handler, fieldId).navigationElement
            val smartPsiElementPointer = SmartPointerManager.createPointer(psiElement)

            val action = CreateLambdaFunctionAction(
                handlerName = handler,
                elementPointer = smartPsiElementPointer,
                lambdaHandlerResolver = handlerResolver
            )

            val context = SimpleDataContext.builder()
                .add(LangDataKeys.LANGUAGE, LanguageUtil.getRootLanguage(psiElement))
                .setParent(SimpleDataContext.getProjectContext(project))
                .build()

            ActionUtil.invokeAction(
                action,
                context,
                ActionPlaces.EDITOR_GUTTER_POPUP,
                null,
                null
            )
        }

    private fun runConfiguration(methodName: String, handler: String, executor: Executor) {
        val runManager = RunManager.getInstance(project)

        // Find configuration if exists
        val configurationType = ConfigurationTypeUtil.findConfigurationType(LambdaRunConfigurationType::class.java)
        val runConfigurations = runManager.getConfigurationsList(configurationType)

        var settings = runConfigurations.filterIsInstance<LocalLambdaRunConfiguration>().firstOrNull { configuration ->
            configuration.handler() == handler
        }?.let { configuration ->
            runManager.findSettings(configuration)
        }

        // Or generate a new one if configuration is missing
        if (settings == null) {
            val factory = LocalLambdaRunConfigurationProducer.getFactory()
            val template = runManager.getConfigurationTemplate(factory)

            val configuration = template.configuration as LocalLambdaRunConfiguration
            val runtime = DotNetRuntimeUtils.getCurrentDotNetCoreRuntime()

            LocalLambdaRunConfigurationProducer.setAccountOptions(configuration)
            configuration.useHandler(runtime.toSdkRuntime(), handler)

            val configurationToAdd = factory.createConfiguration("[Local] $methodName", configuration)
            settings = runManager.createConfiguration(configurationToAdd, factory)

            runManager.setTemporaryConfiguration(settings)
            runManager.addConfiguration(settings)
        }

        runManager.selectedConfiguration = settings

        ProgramRunnerUtil.executeConfiguration(settings, executor)
    }
}


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

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

import com.intellij.openapi.project.Project
import com.jetbrains.rdclient.util.idea.LifetimedProjectComponent
import com.jetbrains.rider.projectView.solution
import software.aws.toolkits.jetbrains.protocol.lambdaPsiModel

@Suppress("ComponentNotRegistered")
class LambdaPsiHost(project: Project) : LifetimedProjectComponent(project) {

    val model = project.solution.lambdaPsiModel
}


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

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

import com.intellij.codeInsight.completion.PrefixMatcher
import com.intellij.codeInsight.completion.impl.CamelHumpMatcher
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.openapi.project.Project
import com.jetbrains.rd.framework.impl.RpcTimeouts
import com.jetbrains.rd.ui.icons.toIdeaIcon
import com.jetbrains.rider.projectView.solution
import software.aws.toolkits.jetbrains.protocol.HandlerCompletionItem
import software.aws.toolkits.jetbrains.protocol.lambdaPsiModel

class DotNetHandlerCompletion : HandlerCompletion {
    override fun getPrefixMatcher(prefix: String): PrefixMatcher = CamelHumpMatcher(prefix)

    override fun getLookupElements(project: Project): Collection<LookupElement> {
        val completionItems = getHandlersFromBackend(project)
        return completionItems.map { completionItem ->
            LookupElementBuilder.create(completionItem.handler).let { element ->
                if (completionItem.iconId != null) {
                    element.withIcon(completionItem.iconId.toIdeaIcon(project))
                } else {
                    element
                }
            }.withInsertHandler { context, item -> context.document.setText(item.lookupString) }
        }
    }

    fun getHandlersFromBackend(project: Project): List<HandlerCompletionItem> =
        project.solution.lambdaPsiModel.determineHandlers.sync(Unit, RpcTimeouts.default)
}


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

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

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessOutput
import com.intellij.execution.util.ExecUtil
import com.intellij.util.text.nullize
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.intellij.lang.annotations.Language
import software.aws.toolkits.core.utils.AttributeBagKey
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.SamDebugSupport
import software.aws.toolkits.jetbrains.services.lambda.steps.GetPorts.Companion.DEBUG_PORTS
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.jetbrains.utils.execution.steps.Step
import software.aws.toolkits.jetbrains.utils.execution.steps.StepEmitter
import software.aws.toolkits.resources.message
import java.time.Duration

class FindDockerContainer : Step() {
    override val stepName = message("sam.debug.find_container")
    override val hidden = false
    override fun execute(context: Context, stepEmitter: StepEmitter, ignoreCancellation: Boolean) {
        val frontendPort = context.getRequiredAttribute(DEBUG_PORTS).first()
        val container = runProcessUntil(
            duration = Duration.ofMillis(SamDebugSupport.debuggerConnectTimeoutMs()),
            cmd = GeneralCommandLine(
                "docker",
                "ps",
                "-q",
                "-f",
                "publish=$frontendPort"
            )
        ) {
            it.stdout.trim().nullize()
        }
        context.putAttribute(DOCKER_CONTAINER, container)
    }

    companion object {
        val DOCKER_CONTAINER = AttributeBagKey.create<String>("DOCKER_CONTAINER")
    }
}

class FindPid : Step() {
    override val stepName = message("sam.debug.dotnet_find_pid")
    override val hidden = false
    override fun execute(context: Context, stepEmitter: StepEmitter, ignoreCancellation: Boolean) {
        val dockerContainer = context.getRequiredAttribute(FindDockerContainer.DOCKER_CONTAINER)
        val pid = runProcessUntil(
            duration = Duration.ofMillis(SamDebugSupport.debuggerConnectTimeoutMs()),
            cmd = GeneralCommandLine(
                "docker",
                "exec",
                "-i",
                dockerContainer,
                "/bin/sh",
                "-c",
                FIND_PID_SCRIPT
            )
        ) {
            it.stdout.trim().nullize()?.toIntOrNull()
        }
        context.putAttribute(DOTNET_PID, pid)
    }

    companion object {
        val DOTNET_PID = AttributeBagKey.create<Int>("DOTNET_PID")

        @Language("sh")
        private const val FIND_PID_SCRIPT =
            """
            for i in /proc/*/cmdline ; do
                if tr '\0' ' ' < "${'$'}i" 2>/dev/null | grep -cq 'dotnet'; then
                    echo  ${'$'}i | sed -n 's/.*\/proc\/\(.*\)\/cmdline.*/\1/p'
                    break
                fi;
            done;
            """
    }
}

private fun <T> runProcessUntil(duration: Duration, cmd: GeneralCommandLine, resultChecker: (ProcessOutput) -> T?): T = runBlocking {
    val start = System.nanoTime()
    var lastAttemptOutput: ProcessOutput? = null
    while (System.nanoTime() - start <= duration.toNanos()) {
        lastAttemptOutput = ExecUtil.execAndGetOutput(cmd, timeoutInMilliseconds = duration.toMillis().toInt())

        resultChecker(lastAttemptOutput)?.let {
            return@runBlocking it
        }

        delay(100)
    }

    throw IllegalStateException(
        "Command did not return expected result within $duration, last attempt; cmd: '${cmd.commandLineString}', " +
            "exitCode: ${lastAttemptOutput?.exitCode}, stdOut: '${lastAttemptOutput?.stdout?.trim()}', stdErr: '${lastAttemptOutput?.stderr?.trim()}'"
    )
}


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

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

import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.xdebugger.XDebugProcessStarter
import com.jetbrains.rider.ideaInterop.fileTypes.csharp.CSharpLanguage
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.core.utils.buildList
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.ImageDebugSupport
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.RuntimeDebugSupport
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.SamRunningState
import software.aws.toolkits.jetbrains.utils.DotNetDebuggerUtils
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.jetbrains.utils.execution.steps.Step

/**
 * Rider uses it's own DebuggerWorker process that run under .NET with extra parameters to configure:
 *    - debugger mode (client/server)
 *    - frontend port
 *    - backend port
 *
 * Workflow:
 * 1. Find the correct container by filtering `docker ps` for one of the published port of the debugger
 * 2. Use `docker exec` and a shell script to locate the dotnet PID of the runtime.
 * 3. Launch the Debugger worker in server mode using `docker exec`
 * 4. Send the command to the worker to attach to the correct PID.
 */
class DotNetRuntimeDebugSupport : RuntimeDebugSupport {
    override fun numberOfDebugPorts(): Int = DotnetDebugUtils.NUMBER_OF_DEBUG_PORTS

    override fun samArguments(debugPorts: List<Int>): List<String> = listOf("--debugger-path", DotNetDebuggerUtils.debuggerBinDir.path)
    override fun additionalDebugProcessSteps(environment: ExecutionEnvironment, state: SamRunningState): List<Step> = listOf(FindDockerContainer(), FindPid())

    override suspend fun createDebugProcess(
        context: Context,
        environment: ExecutionEnvironment,
        state: SamRunningState,
        debugHost: String,
        debugPorts: List<Int>
    ): XDebugProcessStarter = DotnetDebugUtils.createDebugProcess(environment, debugHost, debugPorts, context)
}

abstract class DotnetImageDebugSupport : ImageDebugSupport {
    override fun numberOfDebugPorts(): Int = DotnetDebugUtils.NUMBER_OF_DEBUG_PORTS
    override fun supportsPathMappings(): Boolean = false
    override val languageId = CSharpLanguage.id

    override fun samArguments(debugPorts: List<Int>): List<String> = buildList {
        addAll(super.samArguments(debugPorts))
        add("--debugger-path")
        add(DotNetDebuggerUtils.debuggerBinDir.path)
    }

    override fun containerEnvVars(debugPorts: List<Int>): Map<String, String> = mapOf(
        "_AWS_LAMBDA_DOTNET_DEBUGGING" to "1"
    )

    override fun additionalDebugProcessSteps(environment: ExecutionEnvironment, state: SamRunningState): List<Step> = listOf(FindDockerContainer(), FindPid())

    override suspend fun createDebugProcess(
        context: Context,
        environment: ExecutionEnvironment,
        state: SamRunningState,
        debugHost: String,
        debugPorts: List<Int>
    ): XDebugProcessStarter = DotnetDebugUtils.createDebugProcess(environment, debugHost, debugPorts, context)
}

class Dotnet50ImageDebug : DotnetImageDebugSupport() {
    override val id: String = LambdaRuntime.DOTNET5_0.toString()
    override fun displayName() = LambdaRuntime.DOTNET5_0.toString().capitalize()
}

class Dotnet60ImageDebug : DotnetImageDebugSupport() {
    override val id: String = LambdaRuntime.DOTNET6_0.toString()
    override fun displayName() = LambdaRuntime.DOTNET6_0.toString().capitalize()
}


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

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

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.filters.TextConsoleBuilderFactory
import com.intellij.execution.process.CapturingProcessAdapter
import com.intellij.execution.process.OSProcessHandler
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessHandlerFactory
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.openapi.rd.defineNestedLifetime
import com.intellij.xdebugger.XDebugProcessStarter
import com.jetbrains.rd.framework.IdKind
import com.jetbrains.rd.framework.Identities
import com.jetbrains.rd.framework.Protocol
import com.jetbrains.rd.framework.Serializers
import com.jetbrains.rd.framework.SocketWire
import com.jetbrains.rd.util.lifetime.isAlive
import com.jetbrains.rd.util.lifetime.onTermination
import com.jetbrains.rd.util.put
import com.jetbrains.rd.util.reactive.adviseUntil
import com.jetbrains.rdclient.protocol.RdDispatcher
import com.jetbrains.rider.debugger.DebuggerWorkerProcessHandler
import com.jetbrains.rider.debugger.RiderDebuggerWorkerModelManager
import com.jetbrains.rider.model.debuggerWorker.DotNetCoreAttachStartInfo
import com.jetbrains.rider.model.debuggerWorker.DotNetDebuggerSessionModel
import com.jetbrains.rider.model.debuggerWorkerConnectionHelperModel
import com.jetbrains.rider.projectView.solution
import com.jetbrains.rider.run.IDebuggerOutputListener
import com.jetbrains.rider.run.bindToSettings
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.jetbrains.concurrency.AsyncPromise
import software.aws.toolkits.core.utils.debug
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.core.coroutines.disposableCoroutineScope
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineBgContext
import software.aws.toolkits.jetbrains.core.coroutines.getCoroutineUiContext
import software.aws.toolkits.jetbrains.services.lambda.dotnet.FindDockerContainer.Companion.DOCKER_CONTAINER
import software.aws.toolkits.jetbrains.services.lambda.dotnet.FindPid.Companion.DOTNET_PID
import software.aws.toolkits.jetbrains.services.lambda.execution.sam.SamDebugSupport
import software.aws.toolkits.jetbrains.utils.DotNetDebuggerUtils
import software.aws.toolkits.jetbrains.utils.execution.steps.Context
import software.aws.toolkits.resources.message
import java.net.InetAddress
import java.util.Timer
import kotlin.concurrent.schedule

object DotnetDebugUtils {
    private val LOG = getLogger<DotnetDebugUtils>()
    private const val DEBUGGER_MODE = "server"

    private const val REMOTE_DEBUGGER_DIR = "/tmp/lambci_debug_files"
    private const val REMOTE_NETCORE_CLI_PATH = "/var/lang/bin/dotnet"
    const val NUMBER_OF_DEBUG_PORTS = 2

    fun createDebugProcess(
        environment: ExecutionEnvironment,
        debugHost: String,
        debugPorts: List<Int>,
        context: Context
    ): XDebugProcessStarter {
        val frontendPort = debugPorts[0]
        val backendPort = debugPorts[1]
        val promise = AsyncPromise<XDebugProcessStarter>()
        val edtContext = getCoroutineUiContext()
        val bgContext = getCoroutineBgContext()

        // Define a debugger lifetime to be able to dispose the debugger process and all nested component on termination
        // Using the environment as the disposable root seems to make 2nd usage of debug to fail since the lifetime is already terminated
        val debuggerLifetimeDefinition = environment.project.defineNestedLifetime()
        val debuggerLifetime = debuggerLifetimeDefinition.lifetime

        val scheduler = RdDispatcher(debuggerLifetime)

        val debugHostAddress = InetAddress.getByName(debugHost)

        val protocol = Protocol(
            name = environment.runProfile.name,
            serializers = Serializers(),
            identity = Identities(IdKind.Client),
            scheduler = scheduler,
            wire = SocketWire.Client(debuggerLifetime, scheduler, hostAddress = debugHostAddress, port = frontendPort, optId = "FrontendToDebugWorker"),
            lifetime = debuggerLifetime
        )

        val workerModel = runBlocking(edtContext) {
            RiderDebuggerWorkerModelManager.createDebuggerModel(debuggerLifetime, protocol)
        }

        disposableCoroutineScope(environment, environment.runProfile.name).launch(bgContext) {
            try {
                val dockerContainer = context.getRequiredAttribute(DOCKER_CONTAINER)
                val pid = context.getRequiredAttribute(DOTNET_PID)
                val riderDebuggerProcessHandler = startDebugWorker(dockerContainer, backendPort, frontendPort)
                riderDebuggerProcessHandler.addProcessListener(object : CapturingProcessAdapter() {
                    override fun processTerminated(event: ProcessEvent) {
                        super.processTerminated(event)
                        // if we don't get this message, we can assume the debugger terminated prematurely
                        // TODO: can we detect this without waiting for the timeout task to kick in?
                        LOG.debug {
                            """
                                Rider debugger worker exited with code: ${output.exitCode}
                                stdout: ${output.stdout}
                                stderr: ${output.stderr}
                            """.trimIndent()
                        }
                    }
                })

                LOG.debug { "Waiting for debug process worker to be available" }

                withContext(edtContext) {
                    protocol.wire.connected.adviseUntil(debuggerLifetime) connected@{ isConnected ->
                        if (!isConnected) {
                            return@connected false
                        }

                        workerModel.initialized.adviseUntil(debuggerLifetime) initialized@{ isInitialized ->
                            if (!isInitialized) {
                                return@initialized false
                            }

                            // Fire backend to connect to debugger.
                            environment.project.solution.debuggerWorkerConnectionHelperModel.ports.put(
                                debuggerLifetime,
                                environment.executionId,
                                backendPort
                            )

                            val startInfo = DotNetCoreAttachStartInfo(
                                processId = pid
                            )

                            val sessionModel = DotNetDebuggerSessionModel(startInfo)
                            sessionModel.sessionProperties.bindToSettings(debuggerLifetime, environment.project).apply {
                                enableHeuristicPathResolve.set(true)
                            }

                            workerModel.activeSession.set(sessionModel)

                            val console = TextConsoleBuilderFactory.getInstance().createBuilder(environment.project).console

                            promise.setResult(
                                DotNetDebuggerUtils.createAndStartSession(
                                    executionConsole = console,
                                    env = environment,
                                    sessionLifetime = debuggerLifetime,
                                    processHandler = DebuggerWorkerProcessHandler(
                                        riderDebuggerProcessHandler,
                                        workerModel,
                                        true,
                                        riderDebuggerProcessHandler.commandLine,
                                        debuggerLifetime
                                    ),
                                    protocol = protocol,
                                    sessionModel = sessionModel,
                                    outputEventsListener = object : IDebuggerOutputListener {}
                                )
                            )

                            return@initialized true
                        }

                        return@connected true
                    }
                }
            } catch (t: Throwable) {
                LOG.warn(t) { "Failed to start debugger" }
                debuggerLifetimeDefinition.terminate(true)
                withContext(edtContext) {
                    promise.setError(t)
                }
            }
        }

        val checkDebuggerTask = Timer("Debugger Worker launch timer", true).schedule(SamDebugSupport.debuggerConnectTimeoutMs()) {
            if (debuggerLifetimeDefinition.isAlive && !protocol.wire.connected.value) {
                runBlocking(edtContext) {
                    debuggerLifetimeDefinition.terminate()
                    promise.setError(message("lambda.debug.process.start.timeout"))
                }
            }
        }

        debuggerLifetime.onTermination {
            checkDebuggerTask.cancel()
        }

        return promise.get()!!
    }

    private fun startDebugWorker(dockerContainer: String, backendPort: Int, frontendPort: Int): OSProcessHandler {
        val runDebuggerCommand = GeneralCommandLine(
            "docker",
            "exec",
            "--env",
            "RIDER_DEBUGGER_LOG_DIR=/tmp/log",
            "-i",
            dockerContainer,
            // ideally we'd use the dotnet binary bundled with Rider, but linux-x64/dotnet is no longer guaranteed to exist in 2022.1.1+
            REMOTE_NETCORE_CLI_PATH,
            "$REMOTE_DEBUGGER_DIR/${DotNetDebuggerUtils.debuggerAssemblyFile.name}",
            "--mode=$DEBUGGER_MODE",
            "--frontend-port=$frontendPort",
            "--backend-port=$backendPort"
        )

        return ProcessHandlerFactory.getInstance().createProcessHandler(runDebuggerCommand)
    }
}


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

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

import com.intellij.openapi.module.Module
import com.intellij.psi.PsiElement
import com.jetbrains.rider.projectView.solutionDirectory
import software.aws.toolkits.jetbrains.services.lambda.LambdaBuilder
import software.aws.toolkits.jetbrains.services.lambda.dotnet.element.RiderLambdaHandlerFakePsiElement
import software.aws.toolkits.resources.message
import java.nio.file.Path
import java.nio.file.Paths

class DotNetLambdaBuilder : LambdaBuilder() {
    override fun handlerBaseDirectory(module: Module, handlerElement: PsiElement): Path {
        val element = handlerElement as RiderLambdaHandlerFakePsiElement
        val projectFolder = element.getContainingProjectFile()?.parent?.path
            ?: throw IllegalStateException(message("lambda.run.configuration.handler_root_not_found"))
        return Paths.get(projectFolder)
    }

    override fun getBuildDirectory(module: Module): Path = Paths.get(module.project.solutionDirectory.path, ".aws-sam", "build")
}


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

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

import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.NavigatablePsiElement
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.workspaceModel.ide.WorkspaceModel
import com.jetbrains.rd.framework.impl.RpcTimeouts
import com.jetbrains.rider.model.MethodExistingRequest
import com.jetbrains.rider.model.backendPsiHelperModel
import com.jetbrains.rider.model.publishableProjectsModel
import com.jetbrains.rider.projectView.solution
import com.jetbrains.rider.projectView.workspace.getId
import com.jetbrains.rider.projectView.workspace.getProjectModelEntities
import com.jetbrains.rider.projectView.workspace.isProject
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.protocol.HandlerExistRequest
import software.aws.toolkits.jetbrains.protocol.lambdaPsiModel
import software.aws.toolkits.jetbrains.services.lambda.LambdaHandlerResolver
import software.aws.toolkits.jetbrains.services.lambda.dotnet.element.RiderLambdaHandlerFakePsiElement
import java.nio.file.Path

class DotNetLambdaHandlerResolver : LambdaHandlerResolver {
    override fun findPsiElements(
        project: Project,
        handler: String,
        searchScope: GlobalSearchScope
    ): Array<NavigatablePsiElement> {
        val fieldId = getFieldIdByHandlerName(project, handler)
        if (fieldId < 0) return emptyArray()

        return arrayOf(RiderLambdaHandlerFakePsiElement(project, handler, fieldId))
    }

    override fun determineHandler(element: PsiElement): String? = null

    override fun determineHandlers(element: PsiElement, file: VirtualFile): Set<String> {
        val handler = determineHandler(element) ?: return emptySet()
        return setOf(handler)
    }

    override fun isHandlerValid(project: Project, handler: String): Boolean {
        val (assemblyName, type, methodName) = getHandlerParts(handler) ?: return false
        val projectId = getProjectId(project, assemblyName) ?: return false
        val handlerExistRequest = HandlerExistRequest(
            className = type,
            methodName = methodName,
            projectId = projectId
        )

        return project.solution.lambdaPsiModel.isHandlerExists.sync(handlerExistRequest, RpcTimeouts.default)
    }

    fun getFieldIdByHandlerName(project: Project, handler: String): Int {
        val (assemblyName, type, methodName) = getHandlerParts(handler) ?: return -1
        val projectId = getProjectId(project, assemblyName) ?: return -1

        val model = project.solution.backendPsiHelperModel
        val fileIdResponse = model.findPublicMethod.sync(
            request = MethodExistingRequest(
                className = type,
                methodName = methodName,
                targetFramework = "",
                projectId = projectId
            ),
            timeouts = RpcTimeouts.default
        )

        return fileIdResponse?.fileId ?: -1
    }

    private fun getHandlerParts(handler: String): Triple<String, String, String>? {
        val handlerParts = handler.split("::")
        if (handlerParts.size != 3) return null

        return Triple(handlerParts[0], handlerParts[1], handlerParts[2])
    }

    private fun getProjectId(project: Project, assemblyName: String): Int? {
        val workspaceModel = WorkspaceModel.getInstance(project)
        val publishableProjects = project.solution.publishableProjectsModel.publishableProjects.values.toList()
        val projectToProcess = publishableProjects.find { it.projectName == assemblyName }
        if (projectToProcess == null) {
            LOG.warn { "No publishable project with name '$assemblyName` in solution projects:  $publishableProjects" }
            return null
        }

        val projectPath = Path.of(projectToProcess.projectFilePath)
        val projectId = workspaceModel.getProjectModelEntities(projectPath, project).singleOrNull { it.isProject() }?.getId(project)
        if (projectId == null) {
            LOG.warn { "Project Id is not found for project path: '$projectPath'" }
            return null
        }

        return projectId
    }

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


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

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

import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.Sdk
import com.jetbrains.rider.ideaInterop.fileTypes.csharp.CSharpLanguage
import com.jetbrains.rider.ideaInterop.fileTypes.vb.VbLanguage
import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.services.lambda.BuiltInRuntimeGroups
import software.aws.toolkits.jetbrains.services.lambda.SdkBasedRuntimeGroup
import software.aws.toolkits.jetbrains.utils.DotNetRuntimeUtils

class DotNetRuntimeGroup : SdkBasedRuntimeGroup() {
    override val id: String = BuiltInRuntimeGroups.Dotnet
    override val supportsPathMappings: Boolean = false

    override val languageIds: Set<String> = setOf(
        CSharpLanguage.id,
        VbLanguage.id
    )
    override val supportedRuntimes = listOf(
        LambdaRuntime.DOTNET5_0,
        LambdaRuntime.DOTNET6_0
    )

    override fun runtimeForSdk(sdk: Sdk): LambdaRuntime? = null

    override fun determineRuntime(project: Project): LambdaRuntime = DotNetRuntimeUtils.getCurrentDotNetCoreRuntime()
}


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

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

import com.intellij.ui.ColorUtil
import com.intellij.ui.components.JBScrollPane
import com.intellij.ui.components.JBTabbedPane
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import com.intellij.xml.util.XmlUtil
import com.jetbrains.rider.ideaInterop.fileTypes.sln.SolutionFileType
import com.jetbrains.rider.projectView.actions.projectTemplating.backend.ReSharperTemplateGeneratorBase
import com.jetbrains.rider.projectView.actions.projectTemplating.impl.ProjectTemplateDialogContext
import com.jetbrains.rider.projectView.actions.projectTemplating.impl.ProjectTemplateTransferableModel
import com.jetbrains.rider.ui.themes.RiderTheme
import software.aws.toolkits.jetbrains.services.lambda.BuiltInRuntimeGroups
import software.aws.toolkits.jetbrains.services.lambda.RuntimeGroup
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamInitSelectionPanel
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamProjectGenerator
import software.aws.toolkits.jetbrains.utils.DotNetRuntimeUtils
import software.aws.toolkits.resources.message
import java.awt.Dimension
import java.io.File
import javax.swing.JScrollPane
import javax.swing.JTabbedPane
import javax.swing.JTextPane

abstract class DotNetSamProjectGeneratorRoot(
    private val context: ProjectTemplateDialogContext,
    group: String,
    categoryName: String,
    model: ProjectTemplateTransferableModel
) : ReSharperTemplateGeneratorBase(
    model = model,
    createSolution = true,
    createProject = true,
    item = context.entity
) {
    companion object {
        private const val SAM_HELLO_WORLD_PROJECT_NAME = "HelloWorld"
    }

    // TODO: Decouple SamProjectGenerator from the framework wizards so we can re-use its panels
    private val generator = SamProjectGenerator()
    private val samPanel = SamInitSelectionPanel(
        generator.wizardFragments,
        // Only show templates for DotNet in Rider
        runtimeFilter = { RuntimeGroup.getById(BuiltInRuntimeGroups.Dotnet).supportedRuntimes.contains(it) },
        // needed to rerun the validation when the wizard is changed
        wizardUpdateCallback = { validateData() }
    )

    fun getSamPanel() = samPanel

    fun getSamGenerator() = generator

    private val projectStructurePanel: JTabbedPane

    private val structurePane = JTextPane().apply {
        contentType = "text/html"
        isEditable = false
        background = RiderTheme.activeFieldBackground
        border = null
    }

    init {
        title.labels = arrayOf(group, categoryName)
        initProjectTextField()
        initSamPanel()

        projectStructurePanel = JBTabbedPane()
        val structureScroll = JBScrollPane(structurePane).apply {
            horizontalScrollBarPolicy = JScrollPane.HORIZONTAL_SCROLLBAR_NEVER
            verticalScrollBarPolicy = JBScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED
            border = JBUI.Borders.empty()
            background = UIUtil.getTextFieldBackground()
            preferredSize = Dimension(1, JBUI.scale(60))
        }

        projectStructurePanel.add("Resulting project structure", structureScroll)

        updateInfo()
        super.initialize()
        super.layout()

        // Call this init method after super.initialize() to make sure solutionNameField override a base listener
        initSolutionTextField()

        addAdditionPane(samPanel.mainPanel)
        addAdditionPane(projectStructurePanel)
    }

    override fun validateData() {
        super.validateData()
        if (validationError.value != null) {
            return
        }
        samPanel.validate()?.let {
            validationError.set(it.message)
            return
        }
        validationError.set(null)
    }

    override fun updateInfo() {
        super.updateInfo()
        val sep = File.separator
        val builder = StringBuilder()
        val font = JBUI.Fonts.label()
        builder.appendLine("<span style=\"font-family:${font.family};font-size:${font.size}\"")

        val solutionDirectory = getSolutionDirectory()
        val projectDirectory = getProjectDirectory()

        val parentName = solutionDirectory?.parentFile?.name
        val parentStr = if (parentName.isNullOrEmpty()) sep else "$sep$parentName$sep"

        val vcsMarker = vcsPanel?.getVcsMarker()
        if (solutionDirectory != null && vcsMarker != null) {
            builder.appendLine(
                htmlText(
                    "$sep${solutionDirectory.parentFile.name}$sep",
                    "${solutionDirectory.name}$sep$vcsMarker"
                )
            )
        }

        if (solutionDirectory != null) {
            val solutionName = getSolutionName() + SolutionFileType.solutionExtensionWithDot
            builder.appendLine(htmlText(parentStr, "${solutionDirectory.name}$sep$solutionName"))
        }

        if (projectDirectory != null) {
            val projectsText = "project files"
            val projectFilesLabel = XmlUtil.escape("<$projectsText>")
            if (solutionDirectory != null && solutionDirectory != projectDirectory) {
                builder.appendLine(htmlText(parentStr, "${solutionDirectory.name}${sep}src$sep${projectDirectory.name}$sep$projectFilesLabel"))
                builder.appendLine(htmlText(parentStr, "${solutionDirectory.name}${sep}test$sep${projectDirectory.name}.Test$sep$projectFilesLabel"))
            } else {
                builder.appendLine(htmlText(parentStr, "src$sep${projectDirectory.name}$sep$projectFilesLabel"))
                builder.appendLine(htmlText(parentStr, "test$sep${projectDirectory.name}.Test$sep$projectFilesLabel"))
            }
        }

        builder.appendLine("")
        structurePane.text = builder.toString()
        validateData()
    }

    override fun refreshUI() {
        super.refreshUI()
        // This restore project name when user change a solution name and switch between templates
        projectNameField.text = SAM_HELLO_WORLD_PROJECT_NAME
        validationError.set(null)
        validateData()
    }

    private fun initSolutionTextField() {
        solutionNameField.text = getPossibleName(SAM_HELLO_WORLD_PROJECT_NAME)
    }

    /**
     * The project name is generated inside SAM CLI generator and cannot be re-defined via parameters.
     * Hardcode the project name to the generated one - "HelloWorld".
     */
    private fun initProjectTextField() {
        projectNameField.text = SAM_HELLO_WORLD_PROJECT_NAME
        projectNameField.isEnabled = false
        projectNameSetByUser = true

        sameDirectoryCheckBox.isEnabled = false
    }

    private fun initSamPanel() {
        samPanel.setRuntime(DotNetRuntimeUtils.getCurrentDotNetCoreRuntime())
    }

    private fun htmlText(baseDir: String, relativePath: String) =
        "...$baseDir$relativePath
"
}


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

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

import com.jetbrains.rd.util.lifetime.Lifetime
import com.jetbrains.rd.util.reactive.IOptProperty
import com.jetbrains.rd.util.reactive.OptProperty
import com.jetbrains.rd.util.reactive.Property
import com.jetbrains.rider.projectView.actions.projectTemplating.RiderProjectTemplateGenerator
import com.jetbrains.rider.projectView.actions.projectTemplating.RiderProjectTemplateProvider
import com.jetbrains.rider.projectView.actions.projectTemplating.RiderProjectTemplateState
import com.jetbrains.rider.projectView.actions.projectTemplating.impl.ProjectTemplateDialogContext
import com.jetbrains.rider.projectView.actions.projectTemplating.impl.ProjectTemplateTransferableModel
import icons.AwsIcons
import software.aws.toolkits.jetbrains.core.compatability.RiderProjectTemplate
import software.aws.toolkits.resources.message

class DotNetSamProjectProvider : RiderProjectTemplateProvider {

    override val isReady = Property(true)

    override fun load(lifetime: Lifetime, context: ProjectTemplateDialogContext): IOptProperty<RiderProjectTemplateState> {
        val state = RiderProjectTemplateState(arrayListOf(), arrayListOf())

        state.new.add(RiderSamProject())
        return OptProperty(state)
    }

    private class RiderSamProject : RiderProjectTemplate() {

        override val group = "AWS"
        override val icon = AwsIcons.Resources.SERVERLESS_APP
        override val name = message("sam.init.name")

        override fun createGenerator(
            context: ProjectTemplateDialogContext,
            transferableModel: ProjectTemplateTransferableModel
        ): RiderProjectTemplateGenerator =
            DotNetSamProjectGenerator(
                context = context,
                group = group,
                categoryName = name,
                model = transferableModel
            )

        override fun getKeywords(): Array<String> = arrayOf(name)
    }
}


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

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

import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamAppTemplateBased
import software.aws.toolkits.resources.message

class DotNetSamProjectTemplate : SamAppTemplateBased() {
    override fun displayName(): String = message("sam.init.template.hello_world.name")

    override val dependencyManager: String = "cli-package"

    override val appTemplateName: String = "hello-world"

    override fun description(): String = message("sam.init.template.hello_world.description")

    override fun supportedZipRuntimes(): Set<LambdaRuntime> = setOf(LambdaRuntime.DOTNET6_0)
    override fun supportedImageRuntimes(): Set<LambdaRuntime> = setOf(LambdaRuntime.DOTNET5_0, LambdaRuntime.DOTNET6_0)
}


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

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

import com.intellij.openapi.ui.TextFieldWithBrowseButton
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamProjectTemplate
import software.aws.toolkits.jetbrains.services.lambda.wizard.SamProjectWizard
import software.aws.toolkits.jetbrains.services.lambda.wizard.SdkSelector

class DotNetSamProjectWizard : SamProjectWizard {
    override fun listTemplates(): Collection<SamProjectTemplate> =
        listOf(DotNetSamProjectTemplate())

    override fun createSdkSelectionPanel(projectLocation: TextFieldWithBrowseButton?): SdkSelector? = null
}


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

package software.aws.toolkits.jetbrains.services.lambda.dotnet.element

import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.psi.impl.FakePsiElement
import com.intellij.workspaceModel.ide.WorkspaceModel
import com.jetbrains.rider.projectView.workspace.containingProjectEntity
import com.jetbrains.rider.projectView.workspace.getProjectModelEntity
import com.jetbrains.rider.projectView.workspace.getVirtualFileAsContentRoot
import com.jetbrains.rider.util.idea.getPsiFile
import software.aws.toolkits.jetbrains.core.compatability.toVirtualFile
import javax.swing.Icon

/**
 * Fake frontend PSI instance for Lambda Handler element
 *
 * Note: Rider get PSI from backend. We need to fake a frontend PSI element
 *       to proceed with frontend instances, e.g. when running or debugging lambdas
 */
class RiderLambdaHandlerFakePsiElement(
    private val project: Project,
    private val myName: String,
    private val fileId: Int,
    private val icon: Icon? = null
) : FakePsiElement() {
    override fun getParent() = null

    override fun getContainingFile(): PsiFile? = WorkspaceModel
        .getInstance(project)
        .getProjectModelEntity(fileId)
        ?.getVirtualFileAsContentRoot()
        ?.getPsiFile(FileDocumentManager.getInstance(), PsiDocumentManager.getInstance(project))

    override fun isValid() = true
    override fun getProject() = project
    override fun isWritable() = true
    override fun getIcon(open: Boolean) = icon
    override fun getName() = myName
    override fun toString() = name
    override fun getManager() = PsiManager.getInstance(project)

    fun getContainingProjectFile(): VirtualFile? = WorkspaceModel
        .getInstance(project)
        .getProjectModelEntity(fileId)
        ?.containingProjectEntity()
        ?.url
        ?.toVirtualFile()
}


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

package software.aws.toolkits.jetbrains.settings

import com.intellij.openapi.project.Project
import com.jetbrains.rider.projectView.solution
import software.aws.toolkits.jetbrains.protocol.awsSettingModel

class LambdaGutterMarkSettings(private val project: Project) : LambdaSettingsChangeListener {
    override fun samShowAllHandlerGutterIconsSettingsChange(isShow: Boolean) {
        project.solution.awsSettingModel.showLambdaGutterMarks.fire(isShow)
    }
}


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

package software.aws.toolkits.jetbrains.settings

import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.StartupActivity
import com.jetbrains.rider.projectView.solution
import software.aws.toolkits.jetbrains.protocol.awsSettingModel

/**
 * Syncs any required settings over to the Rider backend
 */
class RiderSyncSettings : StartupActivity {
    @Override
    override fun runActivity(project: Project) {
        project.solution.awsSettingModel.showLambdaGutterMarks.fire(LambdaSettings.getInstance(project).showAllHandlerGutterIcons)
    }
}


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

package software.aws.toolkits.jetbrains.utils

import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.runners.ExecutionEnvironment
import com.intellij.execution.ui.ExecutionConsole
import com.intellij.xdebugger.XDebugProcess
import com.intellij.xdebugger.XDebugProcessStarter
import com.intellij.xdebugger.XDebugSession
import com.jetbrains.rd.framework.IProtocol
import com.jetbrains.rd.util.lifetime.Lifetime
import com.jetbrains.rider.RiderEnvironment
import com.jetbrains.rider.debugger.DebuggerWorkerPlatform
import com.jetbrains.rider.debugger.DotNetDebugProcess
import com.jetbrains.rider.debugger.DotNetDebugRunner
import com.jetbrains.rider.debugger.actions.utils.OptionsUtil
import com.jetbrains.rider.model.debuggerWorker.DotNetDebuggerSessionModel
import com.jetbrains.rider.run.IDebuggerOutputListener
import java.io.File

object DotNetDebuggerUtils {
    val debuggerName = DebuggerWorkerPlatform.AnyCpu.assemblyName

    val debuggerAssemblyFile: File = RiderEnvironment.getBundledFile(debuggerName)

    val debuggerBinDir: File = debuggerAssemblyFile.parentFile

    fun createAndStartSession(
        executionConsole: ExecutionConsole,
        env: ExecutionEnvironment,
        sessionLifetime: Lifetime,
        processHandler: ProcessHandler,
        protocol: IProtocol,
        sessionModel: DotNetDebuggerSessionModel,
        outputEventsListener: IDebuggerOutputListener
    ): XDebugProcessStarter {
        val fireInitializedManually = env.getUserData(DotNetDebugRunner.FIRE_INITIALIZED_MANUALLY) ?: false

        return object : XDebugProcessStarter() {
            override fun start(session: XDebugSession): XDebugProcess = DotNetDebugProcess(
                sessionLifetime,
                session,
                processHandler,
                executionConsole,
                protocol,
                sessionModel,
                fireInitializedManually,
                outputEventsListener,
                OptionsUtil.toDebugKind(sessionModel.sessionProperties.debugKind.valueOrNull),
                env.project,
                env.executionId
            )
        }
    }
}


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

package software.aws.toolkits.jetbrains.utils

import software.aws.toolkits.core.lambda.LambdaRuntime
import software.aws.toolkits.core.utils.getLogger
import software.aws.toolkits.core.utils.warn
import java.io.IOException

object DotNetRuntimeUtils {

    private val logger = getLogger<DotNetRuntimeUtils>()

    private val defaultDotNetCoreRuntime = LambdaRuntime.DOTNET6_0

    /**
     * Get information about current .NET runtime
     *
     * @return the [software.aws.toolkits.core.lambda.LambdaRuntime] instance of current available runtime or
     *         defaultDotnetCoreRuntime value if not defined.
     */
    fun getCurrentDotNetCoreRuntime(): LambdaRuntime {
        val runtimeList = try {
            java.lang.Runtime.getRuntime().exec("dotnet --list-runtimes").inputStream.bufferedReader().readLines()
        } catch (e: IOException) {
            logger.warn { "Error getting current runtime version: $e" }
            return defaultDotNetCoreRuntime
        }

        val versionRegex = Regex("(\\d+.\\d+.\\d+)")
        val versions = runtimeList
            .filter { it.startsWith("Microsoft.NETCore.App") }
            .map { runtimeString ->
                val match = versionRegex.find(runtimeString) ?: return@map null
                match.groups[1]?.value ?: return@map null
            }
            .filterNotNull()

        val version = versions.maxByOrNull { it } ?: return defaultDotNetCoreRuntime
        // Take the first 2 numbers so `5.0.2` would be `5.0`
        val versionString = version.split('.').take(2).joinToString(".")

        return LambdaRuntime.fromValue("dotnetcore$versionString") ?: LambdaRuntime.fromValue("dotnet$versionString") ?: defaultDotNetCoreRuntime
    }

    const val RUNTIME_CONFIG_JSON_21 =
        """{
  "runtimeOptions": {
    "tfm": "netcoreapp2.1",
    "framework": {
      "name": "Microsoft.NETCore.App",
      "version": "2.1.0"
    },
    "rollForward": "Major"
  }
}"""
}


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

import com.intellij.workspaceModel.ide.impl.virtualFile
import com.intellij.workspaceModel.storage.url.VirtualFileUrl

fun VirtualFileUrl.toVirtualFile() = this.virtualFile


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

abstract class RiderProjectTemplate : com.jetbrains.rider.projectView.actions.projectTemplating.RiderProjectTemplate


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

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

import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.progress.DumbProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.rootManager
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.LocalFileSystem
import com.jetbrains.rider.ideaInterop.fileTypes.msbuild.CsprojFileType
import com.jetbrains.rider.projectView.SolutionManager
import com.jetbrains.rider.projectView.actions.projectTemplating.backend.ReSharperTemplatesInteraction
import com.jetbrains.rider.projectView.actions.projectTemplating.impl.ProjectTemplateDialogContext
import com.jetbrains.rider.projectView.actions.projectTemplating.impl.ProjectTemplateTransferableModel
import kotlinx.coroutines.runBlocking
import software.aws.toolkits.resources.message
import java.io.File

class DotNetSamProjectGenerator(
    private val context: ProjectTemplateDialogContext,
    group: String,
    categoryName: String,
    model: ProjectTemplateTransferableModel
) : DotNetSamProjectGeneratorRoot(context, group, categoryName, model) {

    override fun expand() = Runnable {
        val samPanel = getSamPanel()
        val generator = getSamGenerator()
        val samSettings = samPanel.getNewProjectSettings()

        val solutionDirectory = getSolutionDirectory()
            ?: throw Exception(message("sam.init.error.no.solution.basepath"))

        val fileSystem = LocalFileSystem.getInstance()
        if (!solutionDirectory.exists()) {
            FileUtil.createDirectory(solutionDirectory)
        }

        val outDirVf = fileSystem.refreshAndFindFileByIoFile(solutionDirectory)
            ?: throw Exception(message("sam.init.error.no.virtual.file"))

        val progressManager = ProgressManager.getInstance()
        val samProjectBuilder = generator.createModuleBuilder()
        progressManager.runProcessWithProgressSynchronously(
            {
                samProjectBuilder.runSamInit(
                    context.project,
                    projectNameField.text,
                    samSettings,
                    null,
                    outDirVf
                )
            },
            message("sam.init.generating.template"),
            false,
            null
        )

        // Create solution file
        val projectFiles =
            File(solutionDirectory, "src").walk().filter { it.extension == CsprojFileType.defaultExtension } +
                File(solutionDirectory, "test").walk().filter { it.extension == CsprojFileType.defaultExtension }

        // Get the rest of generated files and copy to "SolutionItems" default folder in project structure
        val solutionFiles = solutionDirectory.listFiles()?.filter { it.isFile }?.toList() ?: emptyList()

        val solutionFile = ReSharperTemplatesInteraction.createSolution(
            name = getSolutionName(),
            directory = solutionDirectory,
            projectFiles = projectFiles.toList(),
            protocolHost = context.protocolHost,
            solutionFiles = solutionFiles
        ) ?: throw Exception(message("sam.init.error.solution.create.fail"))

        val project = runBlocking {
            SolutionManager.openExistingSolution(
                projectToClose = null,
                forceOpenInNewFrame = false,
                solutionFile = solutionFile,
                forceConsiderTrusted = true
            )
        } ?: return@Runnable

        vcsPanel?.createInitializer()?.execute(project)

        val modifiableModel = ModuleManager.getInstance(project).modules.firstOrNull()?.rootManager?.modifiableModel ?: return@Runnable
        try {
            val progressIndicator = if (progressManager.hasProgressIndicator()) progressManager.progressIndicator else DumbProgressIndicator()

            samProjectBuilder.runPostSamInit(project, modifiableModel, progressIndicator, samSettings, outDirVf)
        } finally {
            modifiableModel.dispose()
        }
    }
}


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

import com.intellij.workspaceModel.ide.impl.toVirtualFile
import com.intellij.workspaceModel.storage.url.VirtualFileUrl

fun VirtualFileUrl.toVirtualFile() = this.toVirtualFile()


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

abstract class RiderProjectTemplate : com.jetbrains.rider.projectView.actions.projectTemplating.RiderProjectTemplate {
    override val localizedGroup: String
        get() = group
    override val localizedName: String
        get() = name
}


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

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

import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.progress.DumbProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.rootManager
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vfs.LocalFileSystem
import com.jetbrains.rider.ideaInterop.fileTypes.msbuild.CsprojFileType
import com.jetbrains.rider.projectView.SolutionManager
import com.jetbrains.rider.projectView.actions.projectTemplating.backend.ReSharperTemplatesInteraction
import com.jetbrains.rider.projectView.actions.projectTemplating.impl.ProjectTemplateDialogContext
import com.jetbrains.rider.projectView.actions.projectTemplating.impl.ProjectTemplateTransferableModel
import kotlinx.coroutines.launch
import software.aws.toolkits.jetbrains.core.coroutines.applicationCoroutineScope
import software.aws.toolkits.resources.message
import java.io.File

class DotNetSamProjectGenerator(
    private val context: ProjectTemplateDialogContext,
    group: String,
    categoryName: String,
    model: ProjectTemplateTransferableModel
) : DotNetSamProjectGeneratorRoot(context, group, categoryName, model) {
    override fun expand() = Runnable {
        val samPanel = getSamPanel()
        val generator = getSamGenerator()
        val samSettings = samPanel.getNewProjectSettings()

        val solutionDirectory = getSolutionDirectory()
            ?: throw Exception(message("sam.init.error.no.solution.basepath"))

        val fileSystem = LocalFileSystem.getInstance()
        if (!solutionDirectory.exists()) {
            FileUtil.createDirectory(solutionDirectory)
        }

        val outDirVf = fileSystem.refreshAndFindFileByIoFile(solutionDirectory)
            ?: throw Exception(message("sam.init.error.no.virtual.file"))

        val progressManager = ProgressManager.getInstance()
        val samProjectBuilder = generator.createModuleBuilder()
        progressManager.runProcessWithProgressSynchronously(
            {
                samProjectBuilder.runSamInit(
                    context.project,
                    projectNameField.text,
                    samSettings,
                    null,
                    outDirVf
                )
            },
            message("sam.init.generating.template"),
            false,
            null
        )

        // Create solution file
        val projectFiles =
            File(solutionDirectory, "src").walk().filter { it.extension == CsprojFileType.defaultExtension } +
                File(solutionDirectory, "test").walk().filter { it.extension == CsprojFileType.defaultExtension }

        // Get the rest of generated files and copy to "SolutionItems" default folder in project structure
        val solutionFiles = solutionDirectory.listFiles()?.filter { it.isFile }?.toList() ?: emptyList()

        val solutionFile = ReSharperTemplatesInteraction.createSolution(
            name = getSolutionName(),
            directory = solutionDirectory,
            projectFiles = projectFiles.toList(),
            protocolHost = context.protocolHost,
            solutionFiles = solutionFiles
        ) ?: throw Exception(message("sam.init.error.solution.create.fail"))

        applicationCoroutineScope().launch {
            val project =
                SolutionManager.openExistingSolution(
                    projectToClose = null,
                    forceOpenInNewFrame = false,
                    solutionFile = solutionFile,
                    forceConsiderTrusted = true
                ) ?: return@launch
            vcsPanel?.createInitializer()?.execute(project)

            val modifiableModel = ModuleManager.getInstance(project).modules.firstOrNull()?.rootManager?.modifiableModel ?: return@launch
            try {
                val progressIndicator = if (progressManager.hasProgressIndicator()) progressManager.progressIndicator else DumbProgressIndicator()

                samProjectBuilder.runPostSamInit(project, modifiableModel, progressIndicator, samSettings, outDirVf)
            } finally {
                modifiableModel.dispose()
            }
        }
    }
}


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

package base

import com.intellij.ide.GeneralSettings
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.jetbrains.rider.projectView.solutionDirectory
import com.jetbrains.rider.test.base.BaseTestWithSolutionBase
import com.jetbrains.rider.test.debugger.XDebuggerTestHelper
import com.jetbrains.rider.test.protocol.testProtocolHost
import com.jetbrains.rider.test.scriptingApi.getVirtualFileFromPath
import com.jetbrains.rider.test.scriptingApi.useCachedTemplates
import org.testng.annotations.AfterClass
import org.testng.annotations.BeforeClass
import java.io.File
import java.time.Duration

/**
 * Base test class that uses the same solution per test class.
 * Solution re-open logic takes time. We can avoid this by using the same solution instance per every test in a class
 */
abstract class AwsReuseSolutionTestBase : BaseTestWithSolutionBase() {

    private var myProject: Project? = null
    var project: Project
        get() = this.myProject!!
        set(value) {
            this.myProject = value
        }

    protected open val waitForCaches: Boolean get() = false
    protected open val persistCaches: Boolean get() = false
    protected open val restoreNuGetPackages: Boolean get() = false

    protected abstract fun getSolutionDirectoryName(): String

    protected open fun getCustomSolutionFileName(): String? = null
    protected open fun preprocessTempDirectory(tempDir: File) {}

    override val testCaseNameToTempDir: String
        get() = getSolutionDirectoryName()

    // TODO: Remove when https://youtrack.jetbrains.com/issue/RIDER-47995 is fixed FIX_WHEN_MIN_IS_213
    @BeforeClass
    fun allowDotnetRoots() {
        allowCustomDotnetRoots()
    }

    @BeforeClass(alwaysRun = true)
    fun setUpClassSolution() {
        val host = ApplicationManager.getApplication().testProtocolHost
        setUpCustomToolset(msBuild, host)
        openSolution(getSolutionDirectoryName())
    }

    @AfterClass(alwaysRun = true)
    fun closeSolution() {
        try {
            closeSolutionAndResetSettings(myProject)
        } finally {
            myProject = null
        }
    }

    // 15 is a magic number (it's the return statement since they are all the same), but the only
    // example of it used that I could find it is used that way:
    // https://github.com/JetBrains/fsharp-support/blob/93ab17493a34a0bc0fd4c70b11adde02f81455c4/rider-fsharp/src/test/kotlin/debugger/AsyncDebuggerTest.kt#L6
    // Unlike our other projects we do not have a document to work with, so there might not be a nice way to do it.
    fun setBreakpoint(line: Int = 15) {
        // Same as com.jetbrains.rider.test.scriptingApi.toggleBreakpoint, but with the correct base directory
        XDebuggerTestHelper.toggleBreakpoint(project, getVirtualFileFromPath("src/HelloWorld/Function.cs", project.solutionDirectory), line - 1)
    }

    private fun openSolution(solutionDirName: String) {
        GeneralSettings.getInstance().isConfirmExit = false

        val params = OpenSolutionParams()
        params.backendLoadedTimeout = backendStartTimeout
        params.customSolutionName = getCustomSolutionFileName()
        params.preprocessTempDirectory = { preprocessTempDirectory(it) }
        params.persistCaches = persistCaches
        params.waitForCaches = waitForCaches
        params.restoreNuGetPackages = restoreNuGetPackages

        useCachedTemplates = false

        myProject = openSolution(solutionDirName, params)
    }

    override val backendShellLoadTimeout: Duration = backendStartTimeout
}


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

package base

import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.util.ExecUtil
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess
import com.intellij.util.text.SemVer
import com.jetbrains.rider.test.base.PrepareTestEnvironment
import java.io.File
import java.nio.file.Paths
import java.time.Duration

// sometimes Windows Rider tests time out while starting the backend
val backendStartTimeout = Duration.ofMinutes(3)

val versions by lazy {
    // would be nice if this were json https://github.com/dotnet/runtime/issues/3049
    val output = ExecUtil.execAndGetOutput(GeneralCommandLine("dotnet", "--list-sdks"))
    if (output.exitCode != 0) {
        throw IllegalStateException("Failed to locate dotnet version: ${output.stderr}")
    }

    output.stdout.trim().lines().map {
        val (version, path) = it.split(' ', limit = 2)
        val sdkSemVer = SemVer.parseFromText(version) ?: throw RuntimeException("Could not parse .NET SDK version as SemVar: $version")
        sdkSemVer to path.trim('[', ']')
    }.sortedByDescending { it.first }
}

val dotNetSdk by lazy {
    val version = ApplicationInfo.getInstance().build.baselineVersion

    val sdk = when {
        // FIX_WHEN_MIN_IS_212: Rider is not aware of .NET 6.0 until 212
        version < 212 ->
            versions.firstOrNull { it.first.major < 6 }
                ?: throw RuntimeException("Current IDE profile '$version' requires .NET < 6, but only found: $versions")
        // otherwise use latest
        else -> versions.first()
    }

    val (sdkVersion, sdkRoot) = sdk
    val sdkVersionFolder = sdkVersion.rawVersion
    val sdkPath = Paths.get(sdkRoot, sdkVersionFolder).toAbsolutePath().toString()

    println("Using .NET SDK '$sdkVersionFolder' at path: '$sdkPath'")

    return@lazy sdkPath
}

val msBuild by lazy {
    Paths.get(dotNetSdk, "MSBuild.dll").toAbsolutePath().toString()
}

// TODO: Remove when https://youtrack.jetbrains.com/issue/RIDER-47995 is fixed FIX_WHEN_MIN_IS_213
fun allowCustomDotnetRoots() {
    // Rider Test Framework miss VFS root access for the case when running tests on local environment with custom SDK path
    // This should be fixed on Rider Test Framework level. Workaround it until related ticket RIDER-47995 is fixed.
    VfsRootAccess.allowRootAccess(
        ApplicationManager.getApplication(),
        dotNetSdk,
        File(PrepareTestEnvironment.dotnetCoreCliPath).parentFile.absolutePath
    )
}


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

package base

import com.jetbrains.rider.test.scriptingApi.setUpCustomToolset

// FIX_WHEN_MIN_IS_221: signature changed in 221
fun setUpCustomToolset(path: String, host: com.jetbrains.rdclient.protocol.IProtocolHost) =
    setUpCustomToolset(path, host)