package org.gradle.kotlin.dsl.integration

import org.gradle.kotlin.dsl.fixtures.AbstractKotlinIntegrationTest

import org.gradle.util.GradleVersion
import org.hamcrest.CoreMatchers.containsString

import org.hamcrest.MatcherAssert.assertThat
import org.junit.Test


/**
 * See https://docs.gradle.org/current/userguide/build_environment.html
 */
class DelegatedGradlePropertiesIntegrationTest : AbstractKotlinIntegrationTest() {

    private
    fun expectDeprecation(feature: String, advice: String) {
        executer.expectDocumentedDeprecationWarning(
            "$feature has been deprecated. " +
                "This is scheduled to be removed in Gradle 10. " +
                "$advice " +
                "Consult the upgrading guide for further information: " +
                "https://docs.gradle.org/${GradleVersion.current().version}/userguide/upgrading_version_9.html#kotlin_dsl_delegated_properties"
        )
    }

    @Test
    fun `non-nullable delegated property access of non-existing gradle property throws`() {

        withSettings(
            """
            @file:Suppress("DEPRECATION")

            val nonExisting: String by settings
            println(nonExisting)
            """
        )

        expectDeprecation(
            "The 'val name: Type by settings' property delegate syntax",
            "Use 'val property = providers.gradleProperty(name).get()' for Gradle properties or 'val property = extra[name] as Type' for extra properties instead."
        )

        assertThat(
            buildAndFail("help").error,
            containsString("Cannot get non-null property 'nonExisting' on settings '${projectRoot.name}' as it does not exist")
        )

        withSettings("")
        withBuildScript(
            """
            @file:Suppress("DEPRECATION")

            val nonExisting: String by project
            println(nonExisting)
            """
        )

        expectDeprecation(
            "The 'val name: Type by project' property delegate syntax",
            "Use 'val property = project.property(name)' instead."
        )

        assertThat(
            buildAndFail("help").error,
            containsString("Cannot get non-null property 'nonExisting' on root project '${projectRoot.name}' as it does not exist")
        )
    }

    @Test
    fun `delegated properties follow Gradle mechanics and allow to model optional properties via nullable kotlin types`() {

        // given: build root gradle.properties file
        withFile(
            "gradle.properties",
            """
            setBuildProperty=build value
            emptyBuildProperty=

            userHomeOverriddenBuildProperty=build value
            cliOverriddenBuildProperty=build value

            projectMutatedBuildProperty=build value
            """.trimIndent()
        )

        // and: gradle user home gradle.properties file
        withFile(
            "gradle-user-home/gradle.properties",
            """
            setUserHomeProperty=user home value
            emptyUserHomeProperty=

            userHomeOverriddenBuildProperty=user home value
            cliOverriddenUserHomeProperty=user home value

            projectMutatedUserHomeProperty=user home value
            """.trimIndent()
        )

        // and: isolated gradle user home
        executer.withGradleUserHomeDir(existing("gradle-user-home"))
        executer.requireIsolatedDaemons()

        // and: gradle command line with properties
        val buildArguments = arrayOf(
            "-PsetCliProperty=cli value",
            "-PemptyCliProperty=",
            "-PcliOverriddenBuildProperty=cli value",
            "-PcliOverriddenUserHomeProperty=cli value",
            "-Dorg.gradle.project.setOrgGradleProjectSystemProperty=system property value",
            "-Dorg.gradle.project.emptyOrgGradleProjectSystemProperty=",
            "help"
        )

        // when: both settings and project scripts asserting on delegated properties
        withSettings("@file:Suppress(\"DEPRECATION\")\n" + requirePropertiesFromSettings())
        withBuildScript("@file:Suppress(\"DEPRECATION\")\n" + requirePropertiesFromProject())

        // then:
        executer.noDeprecationChecks()
        build(*buildArguments)

        // when: project script buildscript block asserting on delegated properties
        withSettings("")
        withBuildScript(
            """
            @file:Suppress("DEPRECATION")

            buildscript {
                ${requirePropertiesFromProject()}
            }
            """
        )

        // then:
        executer.noDeprecationChecks()
        build(*buildArguments)
    }

    private
    fun requirePropertiesFromSettings() =
        """
        ${requireNotOverriddenPropertiesFrom("settings")}
        ${requireOverriddenPropertiesFrom("settings")}
        ${requireEnvironmentPropertiesFrom("settings")}
        ${requireProjectMutatedPropertiesOriginalValuesFrom("settings")}
        """.trimIndent()

    private
    fun requirePropertiesFromProject() =
        """
        ${requireNotOverriddenPropertiesFrom("project")}
        ${requireOverriddenPropertiesFrom("project")}
        ${requireEnvironmentPropertiesFrom("project")}
        ${requireProjectExtraProperties()}
        ${requireProjectMutatedPropertiesOriginalValuesFrom("project")}
        ${requireProjectPropertiesMutation()}
        """.trimIndent()

    private
    fun requireNotOverriddenPropertiesFrom(source: String) =
        """
        ${requireProperty<String>(source, "setUserHomeProperty", """"user home value"""")}
        ${requireProperty<String>(source, "emptyUserHomeProperty", """""""")}
        ${requireProperty<String>(source, "setBuildProperty", """"build value"""")}
        ${requireProperty<String>(source, "emptyBuildProperty", """""""")}
        ${requireProperty<String>(source, "setCliProperty", """"cli value"""")}
        ${requireProperty<String>(source, "emptyCliProperty", """""""")}
        ${requireNullableProperty<String>(source, "unsetProperty", "null")}
        """.trimIndent()

    private
    fun requireOverriddenPropertiesFrom(source: String) =
        """
        ${requireProperty<String>(source, "userHomeOverriddenBuildProperty", """"user home value"""")}
        ${requireProperty<String>(source, "cliOverriddenBuildProperty", """"cli value"""")}
        ${requireProperty<String>(source, "cliOverriddenUserHomeProperty", """"cli value"""")}
        """.trimIndent()

    private
    fun requireEnvironmentPropertiesFrom(source: String) =
        """
        ${requireProperty<String>(source, "setOrgGradleProjectSystemProperty", """"system property value"""")}
        ${requireProperty<String>(source, "emptyOrgGradleProjectSystemProperty", """""""")}
        """.trimIndent()

    private
    fun requireProjectExtraProperties() =
        """
        run {
            extra["setExtraProperty"] = "extra value"
            extra["emptyExtraProperty"] = ""
            extra["unsetExtraProperty"] = null

            val setExtraProperty: String by project
            require(setExtraProperty == "extra value")

            val emptyExtraProperty: String by project
            require(emptyExtraProperty == "")

            val unsetExtraProperty: String? by project
            require(unsetExtraProperty == null)

            setProperty("setExtraProperty", "mutated")
            require(setExtraProperty == "mutated")
        }
        """.trimIndent()

    private
    fun requireProjectMutatedPropertiesOriginalValuesFrom(source: String) =
        """
        ${requireProperty<String>(source, "projectMutatedBuildProperty", """"build value"""")}
        ${requireProperty<String>(source, "projectMutatedUserHomeProperty", """"user home value"""")}
        """.trimIndent()

    private
    fun requireProjectPropertiesMutation() =
        """
        run {
            val projectMutatedBuildProperty: String by project
            require(projectMutatedBuildProperty == "build value")

            setProperty("projectMutatedBuildProperty", "mutated")
            require(projectMutatedBuildProperty == "mutated")

            val projectMutatedUserHomeProperty: String by project
            require(projectMutatedUserHomeProperty == "user home value")

            setProperty("projectMutatedUserHomeProperty", "mutated")
            require(projectMutatedUserHomeProperty == "mutated")
        }
        """.trimIndent()

    private
    inline fun <reified T : Any> requireProperty(source: String, name: String, valueRepresentation: String) =
        requireProperty(source, name, T::class.qualifiedName!!, valueRepresentation)

    private
    inline fun <reified T : Any> requireNullableProperty(source: String, name: String, valueRepresentation: String) =
        requireProperty(source, name, "${T::class.qualifiedName!!}?", valueRepresentation)

    private
    fun requireProperty(source: String, name: String, type: String, valueRepresentation: String) =
        """
        run {
            val $name: $type by $source
            require($name == $valueRepresentation) {
                ${"\"".repeat(3)}expected $name to be '$valueRepresentation' but was '${'$'}$name'${"\"".repeat(3)}
            }
        }
        """.trimIndent()
}
