/*
 * Copyright 2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.gradle.kotlin.dsl.plugins.precompiled

import org.gradle.api.Action
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.initialization.Settings
import org.gradle.api.invocation.Gradle
import org.gradle.api.plugins.ObjectConfigurationAction
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.bundling.Jar
import org.gradle.kotlin.dsl.fixtures.FoldersDslExpression
import org.gradle.kotlin.dsl.fixtures.assertFailsWith
import org.gradle.kotlin.dsl.fixtures.assertInstanceOf
import org.gradle.kotlin.dsl.fixtures.assertStandardOutputOf
import org.gradle.kotlin.dsl.fixtures.withFolders
import org.gradle.kotlin.dsl.precompile.v1.PrecompiledInitScript
import org.gradle.kotlin.dsl.precompile.v1.PrecompiledProjectScript
import org.gradle.kotlin.dsl.precompile.v1.PrecompiledSettingsScript
import org.gradle.test.fixtures.file.LeaksFileHandles
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.equalTo
import org.hamcrest.MatcherAssert.assertThat
import org.jetbrains.kotlin.name.NameUtils
import org.junit.Test
import org.mockito.invocation.InvocationOnMock
import org.mockito.kotlin.KStubbing
import org.mockito.kotlin.any
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.inOrder
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import java.io.File


@LeaksFileHandles("Kotlin Compiler Daemon working directory")
class PrecompiledScriptPluginTemplatesTest : AbstractPrecompiledScriptPluginTest() {

    @Test
    fun `Project scripts from regular source-sets are compiled via the PrecompiledProjectScript template`() {

        givenPrecompiledKotlinScript(
            "my-project-script.gradle.kts",
            """

            tasks.register("my-task")

            """
        )

        val task = mock<TaskProvider<Task>>()
        val tasks = mock<TaskContainer> {
            on { register(any<String>()) } doReturn task
        }
        val project = mock<Project> {
            on { getTasks() } doReturn tasks
        }

        assertInstanceOf<PrecompiledProjectScript>(
            instantiatePrecompiledScriptOf(
                project,
                "My_project_script_gradle"
            )
        )

        inOrder(project, tasks, task) {
            verify(project).tasks
            verify(tasks).register("my-task")
            verifyNoMoreInteractions()
        }
    }

    @Test
    fun `Settings scripts from regular source-sets are compiled via the PrecompiledSettingsScript template`() {

        givenPrecompiledKotlinScript(
            "my-settings-script.settings.gradle.kts",
            """

            include("my-project")

            """
        )

        val settings = mock<Settings>()

        assertInstanceOf<PrecompiledSettingsScript>(
            instantiatePrecompiledScriptOf(
                settings,
                "My_settings_script_settings_gradle"
            )
        )

        verify(settings).include("my-project")
    }

    @Test
    fun `Gradle scripts from regular source-sets are compiled via the PrecompiledInitScript template`() {

        givenPrecompiledKotlinScript(
            "my-gradle-script.init.gradle.kts",
            """

            addListener("my-listener")

            """
        )

        val gradle = mock<Gradle>()

        assertInstanceOf<PrecompiledInitScript>(
            instantiatePrecompiledScriptOf(
                gradle,
                "My_gradle_script_init_gradle"
            )
        )

        verify(gradle).addListener("my-listener")
    }

    @Test
    fun `plugin adapter doesn't mask exceptions thrown by precompiled script`() {

        // given:
        val expectedMessage = "Not on my watch!"

        withKotlinDslPlugin()

        withFile(
            "src/main/kotlin/my-project-script.gradle.kts",
            """
            throw IllegalStateException("$expectedMessage")
            """
        )

        // when:
        compileKotlin()

        // then:
        @Suppress("unchecked_cast")
        val pluginAdapter =
            loadCompiledKotlinClass("MyProjectScriptPlugin")
                .getConstructor()
                .newInstance() as Plugin<Project>

        val exception =
            assertFailsWith(IllegalStateException::class) {
                pluginAdapter.apply(mock())
            }

        assertThat(
            exception.message,
            equalTo(expectedMessage)
        )
    }

    @Suppress("DEPRECATION")
    @Test
    fun `implicit imports are available to precompiled scripts`() {

        givenPrecompiledKotlinScript(
            "my-project-script.gradle.kts",
            """

            tasks.register<Jar>("jar")

            """
        )

        val task = mock<TaskProvider<Jar>>()
        val tasks = mock<TaskContainer> {
            on { register(any<String>(), any<Class<Jar>>()) } doReturn task
        }
        val project = mock<Project> {
            on { getTasks() } doReturn tasks
        }

        instantiatePrecompiledScriptOf(
            project,
            "My_project_script_gradle"
        )

        verify(tasks).register("jar", Jar::class.java)
    }

    @Test
    fun `precompiled script plugin ids are honored by java-gradle-plugin plugin`() {

        projectRoot.withFolders {

            "plugin" {

                "src/main/kotlin" {

                    // Plugin id for script with no package declaration is simply
                    // the file name minus the script file extension.

                    // Project plugins must be named `*.gradle.kts`
                    withFile(
                        "my-plugin.gradle.kts",
                        """
                        println("my-plugin applied!")
                        """
                    )

                    // Settings plugins must be named `*.settings.gradle.kts`
                    withFile(
                        "my-settings-plugin.settings.gradle.kts",
                        """
                        println("my-settings-plugin applied!")
                        """
                    )

                    // Gradle object plugins, a.k.a., precompiled init script plugins,
                    // must be named `*.init.gradle.kts`
                    withFile(
                        "my-init-plugin.init.gradle.kts",
                        """
                        println("my-init-plugin applied!")
                        """
                    )

                    // plugin id for script with package declaration is the
                    // package name dot the file name minus the `.gradle.kts` suffix
                    withFile(
                        "org/acme/my-other-plugin.gradle.kts",
                        """
                        package org.acme

                        println("my-other-plugin applied!")
                        """
                    )
                }

                withFile("settings.gradle.kts", defaultSettingsScript)

                withFile(
                    "build.gradle.kts",
                    scriptWithKotlinDslPlugin()
                )
            }
        }

        executer.inDirectory(file("plugin")).withTasks("jar").run()

        val pluginJar = file("plugin/build/libs/plugin.jar")
        assertThat("pluginJar was built", pluginJar.exists())
        val movedPluginJar = file("plugin.jar")
        pluginJar.renameTo(movedPluginJar)

        withDefaultSettings().appendText(
            """
            buildscript {
                dependencies {
                    classpath(files("${movedPluginJar.name}"))
                }
            }

            gradle.apply<MyInitPluginPlugin>()
            apply(plugin = "my-settings-plugin")
            """
        )

        withFile(
            "buildSrc/build.gradle",
            """
            dependencies {
                api files("../${movedPluginJar.name}")
            }
            """
        )

        withBuildScript(
            """
            plugins {
                id("my-plugin")
                id("org.acme.my-other-plugin")
            }
            """
        )


        assertThat(
            build("help").output,
            allOf(
                containsString("my-init-plugin applied!"),
                containsString("my-settings-plugin applied!"),
                containsString("my-plugin applied!"),
                containsString("my-other-plugin applied!")
            )
        )
    }

    @Test
    fun `precompiled script plugins can be published by maven-publish plugin`() {

        val repository = newDir("repository")

        publishPluginsTo(repository) {

            withFile(
                "my-plugin.gradle.kts",
                """
                println("my-plugin applied!")
                """
            )

            withFile(
                "org/acme/my-other-plugin.gradle.kts",
                """
                package org.acme

                println("org.acme.my-other-plugin applied!")
                """
            )

            withFile(
                "org/acme/plugins/my-init.init.gradle.kts",
                """
                package org.acme.plugins

                println("org.acme.plugins.my-init applied!")
                """
            )
        }

        val repositoriesBlock = repositoriesBlockFor(repository)

        withSettings(
            """
            pluginManagement {
                $repositoriesBlock
            }
            """
        )

        withBuildScript(
            """
            plugins {
                id("my-plugin") version "1.0"
                id("org.acme.my-other-plugin") version "1.0"
            }
            """
        )

        val initScript =
            withFile(
                "my-init-script.init.gradle.kts",
                """

                initscript {
                    $repositoriesBlock
                    dependencies {
                        classpath("org.acme:plugins:1.0")
                    }
                }

                apply<org.acme.plugins.MyInitPlugin>()

                // TODO: can't apply plugin by id
                // apply(plugin = "org.acme.plugins.my-init")
                """
            )

        assertThat(
            build("help", "-I", initScript.canonicalPath).output,
            allOf(
                containsString("org.acme.plugins.my-init applied!"),
                containsString("my-plugin applied!"),
                containsString("org.acme.my-other-plugin applied!")
            )
        )
    }

    @Test
    fun `precompiled script plugins can use Kotlin 1 dot 3 language features`() {

        givenPrecompiledKotlinScript(
            "my-plugin.gradle.kts",
            """

            // Coroutines are no longer experimental
            val coroutine = sequence {
                // Unsigned integer types
                yield(42UL)
            }

            when (val value = coroutine.first()) {
                42UL -> print("42!")
                else -> throw IllegalStateException()
            }
            """
        )

        assertStandardOutputOf("42!") {
            instantiatePrecompiledScriptOf(
                mock<Project>(),
                "My_plugin_gradle"
            )
        }
    }

    @Test
    fun `precompiled project script template honors HasImplicitReceiver`() {

        assertHasImplicitReceiverIsHonoredByScriptOf<Project>("my-project-plugin.gradle.kts")
    }

    @Test
    fun `precompiled settings script template honors HasImplicitReceiver`() {

        assertHasImplicitReceiverIsHonoredByScriptOf<Settings>("my-settings-plugin.settings.gradle.kts")
    }

    @Test
    fun `precompiled init script template honors HasImplicitReceiver`() {

        assertHasImplicitReceiverIsHonoredByScriptOf<Gradle>("my-init-plugin.init.gradle.kts")
    }

    @Test
    fun `nested plugins block fails to compile with reasonable message`() {

        withKotlinDslPlugin()
        withPrecompiledKotlinScript(
            "my-project-plugin.gradle.kts",
            """
            project(":nested") {
                plugins {
                    java
                }
            }
            """
        )

        // Error message relies on Kotlin's Deprecated.HIDDEN
        executer.noDeprecationChecks()
        buildAndFail("classes").run {
            assertHasDescription(
                "Execution failed for task ':compileKotlin'."
            )
            assertHasErrorOutput(
                """my-project-plugin.gradle.kts:3:17 'fun Project.plugins(block: PluginDependenciesSpec.() -> Unit): Nothing' is deprecated. The plugins {} block must not be used here. If you need to apply a plugin imperatively, please use apply<PluginType>() or apply(plugin = "id") instead."""
            )
        }
    }

    @Test
    fun `can apply plugin using ObjectConfigurationAction syntax`() {

        val pluginsRepository = newDir("repository")

        publishPluginsTo(pluginsRepository) {

            withFile(
                "MyInit.init.gradle.kts",
                """

                open class GradlePlugin : Plugin<Gradle> {
                    override fun apply(target: Gradle) = println("Gradle!")
                }

                apply { plugin<GradlePlugin>() }

                """
            )

            withFile(
                "MySettings.settings.gradle.kts",
                """

                open class SettingsPlugin : Plugin<Settings> {
                    override fun apply(target: Settings) = println("Settings!")
                }

                gradle.apply { plugin<MyInitPlugin>() }

                apply { plugin<SettingsPlugin>() }

                """
            )

            withFile(
                "MyProject.gradle.kts",
                """

                open class ProjectPlugin : Plugin<Project> {
                    override fun apply(target: Project) {
                        val projectName = target.name
                        target.task("run") {
                            doLast { println("Project " + projectName + "!") }
                        }
                    }
                }

                apply { plugin<ProjectPlugin>() }

                subprojects {
                    apply { plugin<ProjectPlugin>() }
                }
                """
            )
        }

        val pluginRepositoriesBlock = repositoriesBlockFor(pluginsRepository)

        file("bar").mkdirs()
        withSettings(
            """
            pluginManagement {
                $pluginRepositoriesBlock
            }

            plugins {
                id("MySettings") version "1.0"
            }

            rootProject.name = "foo"

            include("bar")
            """
        )

        withBuildScript(
            """
            plugins { id("MyProject") }
            """
        )

        assertThat(
            build("run", "-q").output,
            allOf(
                containsString("Gradle!"),
                containsString("Settings!"),
                containsString("Project foo!"),
                containsString("Project bar!")
            )
        )
    }

    @Test
    fun `can use PluginAware extensions against nested receiver`() {

        val scriptFileName = "my-project-plugin.gradle.kts"

        givenPrecompiledKotlinScript(
            scriptFileName,
            """
            project(":nested") {
                apply(from = "./gradle/conventions.gradle.kts")
            }
            """
        )

        val configurationAction = mock<ObjectConfigurationAction>()
        val nestedReceiver = mock<Project> {
            on { apply(any<Action<ObjectConfigurationAction>>()) } doAnswer {
                it.executeActionArgument(0, configurationAction)
                Unit
            }
        }
        val project = mock<Project> {
            onProject(":nested", nestedReceiver)
        }

        instantiatePrecompiledScriptOf(
            project,
            scriptClassNameForFile(scriptFileName)
        )

        inOrder(configurationAction) {
            verify(configurationAction).from("./gradle/conventions.gradle.kts")
            verifyNoMoreInteractions()
        }
    }

    private
    fun KStubbing<Project>.onProject(path: String, project: Project) {
        on { project(eq(path), any<Action<Project>>()) } doAnswer {
            it.executeActionArgument(1, project)
            project
        }
    }

    private
    fun <T : Any> InvocationOnMock.executeActionArgument(index: Int, configurationAction: T) {
        getArgument<Action<T>>(index).execute(configurationAction)
    }

    private
    inline fun <reified T : Any> assertHasImplicitReceiverIsHonoredByScriptOf(fileName: String) {

        // Action<T> <=> T.() -> Unit because HasImplicitReceiver
        givenPrecompiledKotlinScript(
            fileName,
            """
            fun <T : Any> applyActionTo(a: T, action: ${Action::class.qualifiedName}<T>) = action(a)
            object receiver
            applyActionTo(receiver) {
                require(this === receiver)
                print("42!")
            }
            """
        )

        assertStandardOutputOf("42!") {
            instantiatePrecompiledScriptOf(
                mock<T>(),
                scriptClassNameForFile(fileName)
            )
        }
    }

    private
    fun repositoriesBlockFor(repository: File): String = """
        repositories {
            maven { url = uri("${repository.toURI()}") }
        }
    """

    private
    fun publishPluginsTo(
        repository: File,
        group: String = "org.acme",
        version: String = "1.0",
        sourceFiles: FoldersDslExpression
    ) {
        withFolders {

            "plugins" {

                "src/main/kotlin" {
                    sourceFiles()
                }

                withFile("settings.gradle.kts", defaultSettingsScript)

                withFile(
                    "build.gradle.kts",
                    """

                    plugins {
                        `kotlin-dsl`
                        `maven-publish`
                    }

                    group = "$group"

                    version = "$version"

                    $repositoriesBlock

                    publishing {
                        ${repositoriesBlockFor(repository)}
                    }
                    """
                )
            }
        }

        build(
            existing("plugins"),
            "publish"
        )
    }

    private
    fun scriptClassNameForFile(fileName: String) =
        NameUtils.getScriptNameForFile(fileName).asString()
}
