/*
 * Copyright 2025 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.internal.cc.impl

import org.gradle.api.internal.ConfigurationCacheDegradationController

import javax.inject.Inject

class ConfigurationCacheGracefulDegradationIntegrationTest extends AbstractConfigurationCacheIntegrationTest {
    public static final String CONFIGURATION_CACHE_INCOMPATIBLE_TASKS_OR_FEATURES_FOOTER = "Some tasks or features in this build are not compatible with the configuration cache."
    public static final String CONFIGURATION_CACHE_DISABLED_REASON = "Configuration cache disabled because incompatible"
    public static final String CONFIGURATION_CACHE_DISABLED_READ_ONLY_REASON = "Configuration cache disabled as cache is in read-only mode."
    public static final String INVOCATION_OF_TASK_PROJECT_AT_EXECUTION_TIME = "invocation of 'Task.project' at execution time is unsupported with the configuration cache."
    public static final String INVOCATION_OF_TASK_DEPENDENCIES_AT_EXECUTION_TIME = "invocation of 'Task.taskDependencies' at execution time is unsupported with the configuration cache."

    def "a compatible build does not print degradation reasons"() {
        buildFile """
            System.getenv("HOME")  // Add a configuration input to force report to be generated
            tasks.register("compatible") {
                doLast {
                    println("Hello")
                }
            }
        """

        when:
        configurationCacheRun("compatible", "-D${LOG_REPORT_LINK_AS_WARNING}=true")

        then:
        assertNoConfigurationCacheDegradation()
    }

    def configurationCache = newConfigurationCacheFixture()

    def "should not fail evaluating lazy properties in Groovy in graceful degradation mode"() {
        def configurationCache = newConfigurationCacheFixture()

        given:
        // any way to trigger graceful degradation will do
        enableSourceDependencies()

        buildFile """
        class TaskWithLazyProperty extends DefaultTask {
            private String value
            @Input
            String getLazyValue() {
                if (value == null) {
                    value = project.name
                }
                return value
            }
        }
        tasks.register("lazy", TaskWithLazyProperty) { task ->
           doLast {
               println("Value is " + task.lazyValue)
           }
        }
        """

        when:
        configurationCacheRun("lazy")

        then:
        configurationCache.assertNoConfigurationCache()
        result.assertTaskExecuted(":lazy")
    }

    def "should not fail evaluating lazy properties in Kotlin in graceful degradation mode"() {
        def configurationCache = newConfigurationCacheFixture()

        given:
        // any way to trigger graceful degradation will do
        enableSourceDependencies()

        buildKotlinFile """
        abstract class TaskWithLazyProperty: DefaultTask() {
            @get:Input
            val lazyValue: String by lazy {
                this.project.name
            }
        }
        tasks.register("lazy", TaskWithLazyProperty::class) {
            doLast {
                println("Value is " + lazyValue)
            }
        }
        """

        when:
        configurationCacheRun("lazy")

        then:
        configurationCache.assertNoConfigurationCache()
        result.assertTaskExecuted(":lazy")
    }

    def "a task can require CC degradation#mode"() {
        buildFile """
            ${taskWithInjectedDegradationController()}
            tasks.register("a", DegradingTask) { task ->
               getDegradationController().requireConfigurationCacheDegradation(task, provider { "Project access" })
               doLast {
                   println("Project path is \${project.path}")
               }
            }
        """

        when:
        configurationCacheRun("a", *args)

        then:
        configurationCache.assertNoConfigurationCache()

        and:
        problems.assertResultConsoleSummaryHasNoProblems(result)
        problems.htmlReport(result.output).assertContents {
            totalProblemsCount = 1
            withProblem(INVOCATION_OF_TASK_PROJECT_AT_EXECUTION_TIME)
            withIncompatibleTask(":a", "Project access.")
        }

        and:
        outputContains("Project path is :")
        assertConfigurationCacheDegradation()

        where:
        mode               | args
        ""                 | []
        " with IP enabled" | ["-Dorg.gradle.unsafe.isolated-projects=true"]
    }

    def "a task can require CC degradation for multiple reasons"() {
        buildFile """
            ${taskWithInjectedDegradationController()}
            tasks.register("a", DegradingTask) { task ->
                def shouldAccessTaskProjectProvider = providers.systemProperty("accessTaskProject").map { Boolean.parseBoolean(it) }.orElse(false)
                def shouldAccessTaskDependenciesProvider = providers.systemProperty("accessTaskDependencies").map { Boolean.parseBoolean(it) }.orElse(false)

                getDegradationController().requireConfigurationCacheDegradation(task, shouldAccessTaskProjectProvider.map { it ? "Project access" : null })
                getDegradationController().requireConfigurationCacheDegradation(task, shouldAccessTaskDependenciesProvider.map { it ? "TaskDependencies access" : null })

                doLast {
                    if (shouldAccessTaskProjectProvider.get()) {
                        it.project
                        println "Task's project accessed!"
                    }
                    if (shouldAccessTaskDependenciesProvider.get()) {
                        it.taskDependencies
                        println "Task's dependencies accessed!"
                    }
                }
            }
        """

        when:
        configurationCacheRun("a", *args)

        then:
        configurationCache.assertNoConfigurationCache()

        and:
        problems.assertResultConsoleSummaryHasNoProblems(result)
        problems.htmlReport(result.output).assertContents {
            totalProblemsCount = expectedProblems.size()
            expectedProblems.forEach { withProblem(it) }
            withIncompatibleTask(":a", degradationReason)
        }

        where:
        expectedProblems                                                                                    | _
        [INVOCATION_OF_TASK_PROJECT_AT_EXECUTION_TIME]                                                      | _
        [INVOCATION_OF_TASK_PROJECT_AT_EXECUTION_TIME, INVOCATION_OF_TASK_DEPENDENCIES_AT_EXECUTION_TIME]   | _
        __
        args                                                          | expectedOutputs                                               | degradationReason
        ["-DaccessTaskProject=true"]                                  | ["Task's project accessed!"]                                  | "Project access."
        ["-DaccessTaskProject=true", "-DaccessTaskDependencies=true"] | ["Task's project accessed!", "Task's dependencies accessed!"] | "Project access, TaskDependencies access."
    }

    def "features may cause CC degradation"() {
        enableSourceDependencies()

        when:
        configurationCacheRun("help")

        then:
        configurationCache.assertNoConfigurationCache()

        and:
        // feature degradation is reported as a report problem, but it is silently suppressed from the console
        problems.assertResultConsoleSummaryHasNoProblems(result)
        problems.htmlReport(result.output).assertContents {
            totalProblemsCount = 1
            withProblem("Feature 'source dependencies' is incompatible with the configuration cache.")
        }

        and:
        assertConfigurationCacheDegradation()
    }

    def "CC problems in warning mode are not hidden by CC degradation"() {
        buildFile """
            ${taskWithInjectedDegradationController()}
            tasks.register("foo", DegradingTask) { task ->
                getDegradationController().requireConfigurationCacheDegradation(task, provider { "Project access" })
                doLast {
                    println "Hello from foo \${project.path}"
                }
            }

            tasks.register("bar") {
                doLast {
                    println "Hello from bar \${project.path}"
                }
            }
        """

        when:
        configurationCacheRunLenient "foo", "bar"

        then:
        configurationCache.assertNoConfigurationCache()

        and:
        problems.assertResultHasConsoleSummary(result) {
            totalProblemsCount = 1
            withProblem("Build file 'build.gradle': line 17: invocation of 'Task.project' at execution time is unsupported with the configuration cache.")
        }
        problems.htmlReport(result.output).assertContents {
            // gracefully degraded task appears in the report
            withProblem(INVOCATION_OF_TASK_PROJECT_AT_EXECUTION_TIME)
            withProblem(INVOCATION_OF_TASK_PROJECT_AT_EXECUTION_TIME)
            withIncompatibleTask(":foo", "Project access.")
        }
    }

    def "a task in included build can require CC degradation"() {
        buildFile("included/build.gradle", """
            ${taskWithInjectedDegradationController()}
            tasks.register("foo", DegradingTask) { task ->
                getDegradationController().requireConfigurationCacheDegradation(task, provider { "Project access" })
                doLast {
                    println "Hello from included build \${project.path}"
                }
            }
        """)
        settingsFile """
            includeBuild("included")
        """

        when:
        configurationCacheRun ":included:foo"

        then:
        configurationCache.assertNoConfigurationCache()

        and:
        problems.assertResultConsoleSummaryHasNoProblems(result)
        problems.htmlReport(result.output).assertContents {
            totalProblemsCount = 1
            withProblem(INVOCATION_OF_TASK_PROJECT_AT_EXECUTION_TIME)
            withIncompatibleTask(":included:foo", "Project access.")
        }

        and:
        outputContains("Hello from included build :")
        assertConfigurationCacheDegradation()
    }

    def "a buildSrc internal task that requires CC degradation does not introduce root build CC degradation"() {
        file("buildSrc/src/main/java/MyClass.java") << "class MyClass {}"
        buildFile("buildSrc/build.gradle", """
            ${taskWithInjectedDegradationController()}
            def fooTask = tasks.register("foo", DegradingTask) { task ->
                getDegradationController().requireConfigurationCacheDegradation(task, provider { "Project access" })
                doLast {
                    println "Hello from foo \${project.path}"
                }
            }
            tasks.withType(JavaCompile).configureEach {
                dependsOn(fooTask)
            }
        """)

        when:
        configurationCacheRun "help"

        then:
        configurationCache.assertStateStored()

        and:
        executed(":buildSrc:compileJava", ":buildSrc:foo", ":help")
    }

    def "depending on a CC degrading task from included build introduces CC degradation"() {
        buildFile("included/build.gradle", """
            ${taskWithInjectedDegradationController()}
            tasks.register("foo", DegradingTask) { task ->
                getDegradationController().requireConfigurationCacheDegradation(task, provider { "Project access" })
                doLast {
                    println "Hello from included build \${project.path}"
                }
            }
        """)
        settingsFile """
            includeBuild("included")
        """
        buildFile """
            tasks.register("bar") {
                dependsOn gradle.includedBuild("included").task(":foo")
                doLast {
                    println "Hello from root build"
                }
            }
        """

        when:
        configurationCacheRun "bar"

        then:
        configurationCache.assertNoConfigurationCache()

        and:
        problems.assertResultConsoleSummaryHasNoProblems(result)
        problems.htmlReport(result.output).assertContents {
            totalProblemsCount = 1
            withProblem(INVOCATION_OF_TASK_PROJECT_AT_EXECUTION_TIME)
            withIncompatibleTask(":included:foo", "Project access.")
        }

        and:
        outputContains("Hello from included build :")
        outputContains("Hello from root build")
        assertConfigurationCacheDegradation()
    }

    def "a dependency task in #build build can require CC degradation for the non-root build"() {
        buildFile("$build/build.gradle", """
            ${taskWithInjectedDegradationController()}

            def fooTask = tasks.register("foo", DegradingTask) { task ->
                getDegradationController().requireConfigurationCacheDegradation(task, provider { "Because reasons" })
            }

            tasks.register("bar") {
                dependsOn(fooTask)
            }
        """)
        settingsFile """
            $settingsConfiguration
        """

        when:
        configurationCacheRun ":$build:bar"

        then:
        configurationCache.assertNoConfigurationCache()

        and:
        problems.assertResultConsoleSummaryHasNoProblems(result)
        problems.htmlReport(result.output).assertContents {
            totalProblemsCount = 0
            withIncompatibleTask(":$build:foo", "Because reasons.")
        }

        and:
        assertConfigurationCacheDegradation()

        where:
        build      | settingsConfiguration
        "buildSrc" | ""
        "included" | "include('included')"
    }

    def "no CC degradation if incompatible task is not presented in the task graph"() {
        buildFile """
            ${taskWithInjectedDegradationController()}
            tasks.register("a", DegradingTask) { task ->
                getDegradationController().requireConfigurationCacheDegradation(task, provider { "Project access" })
                doLast {
                    println("Project path is \${project.path}")
                }
            }

            tasks.register("b") {
                doLast {
                    println "Hello from B"
                }
            }

            tasks.all {
                println "\$it configured"
            }
        """

        when:
        configurationCacheRun "b"

        then:
        configurationCache.assertStateStored()
        outputContains("task ':a' configured")
        outputContains("task ':b' configured")
        outputContains("Hello from B")
    }

    def "ignore CC degradation requests at execution time"() {
        buildFile """
            ${taskWithInjectedDegradationController()}
            tasks.register("foo", DegradingTask) { task ->
                def reason = provider { "Misconfiguration!" }
                doLast {
                    getDegradationController().requireConfigurationCacheDegradation(task, reason)
                }
            }
        """

        when:
        configurationCacheRun ":foo", "-d"

        then:
        configurationCache.assertStateStored()

        and:
        outputContains("Configuration cache degradation request of task :foo is ignored at execution time")
    }

    def "tasks instantiated during execution have degradation requests ignored"() {
        buildFile """
            ${taskWithInjectedDegradationController()}
            tasks.register("a", DegradingTask) { task ->
                println("Should be configured")
                getDegradationController().requireConfigurationCacheDegradation(task, provider { "Project access" })
            }
        """

        when:
        // :tasks instantiates tasks instances at execution time
        configurationCacheRun ":tasks"

        then:
        configurationCache.assertStateStored()

        and:
        outputContains("Should be configured")
        notExecuted ":a"
    }

    def "user code exceptions in degradation reasons evaluation are surfaced"() {
        buildFile """
            ${taskWithInjectedDegradationController()}
            tasks.register("foo", DegradingTask) { task ->
                getDegradationController().requireConfigurationCacheDegradation(
                    task,
                    provider { throw new IllegalStateException("Reason evaluation failed!") }
                )
            }
        """

        when:
        configurationCacheFails ":foo"

        then:
        failureDescriptionContains("Reason evaluation failed!")
    }

    def "user code exceptions in task graph traversing are surfaced"() {
        given:
        buildFile """
            ${taskWithInjectedDegradationController()}
            tasks.register("foo", DegradingTask) { task ->
                // add a request to ensure we need to verify whether tasks are scheduled
                getDegradationController().requireConfigurationCacheDegradation(task, provider { null })
            }

            gradle.addListener(new TaskExecutionGraphListener() {
                void graphPopulated(TaskExecutionGraph graph) {
                    throw new RuntimeException("Graph traversing failed!")
                }
            })
        """

        when:
        configurationCacheFails ":foo"

        then:
        failureDescriptionContains("Graph traversing failed!")
    }

    def "degradation controller is available in vintage"() {
        given:
        buildFile """
            ${taskWithInjectedDegradationController()}

            tasks.register("foo", DegradingTask) { task ->
                getDegradationController().requireConfigurationCacheDegradation(task, provider { "Because reasons" })
                doLast {
                    println("Hello from " + project.path)
                }
            }
        """
        executer.expectDocumentedDeprecationWarning("Invocation of Task.project at execution time has been deprecated. This will fail with an error in Gradle 10. This API is incompatible with the configuration cache, which will become the only mode supported by Gradle in a future release. Consult the upgrading guide for further information: https://docs.gradle.org/current/userguide/upgrading_version_7.html#task_project")

        when:
        run ":foo"

        then:
        executed ":foo"

        and:
        assertNoConfigurationCacheDegradation()
    }

    def "degradation works in read-only mode"() {
        given:
        buildFile """
            ${taskWithInjectedDegradationController()}

            tasks.register("foo", DegradingTask) { task ->
                getDegradationController().requireConfigurationCacheDegradation(task, provider { "Because reasons" })
                doLast {
                    println("Hello from " + project.path)
                }
            }
        """

        when:
        configurationCacheRun(":foo", ENABLE_READ_ONLY_CACHE)

        then:
        executed ":foo"

        and:
        // no problems on the console
        problems.assertResultConsoleSummaryHasNoProblems(result)
        // but problems should be in CC report
        problems.htmlReport(result.output).assertContents {
            totalProblemsCount = 1
            withProblem(INVOCATION_OF_TASK_PROJECT_AT_EXECUTION_TIME)
            withIncompatibleTask(":foo", "Because reasons.")
        }

        and:
        // expect link to CC report
        outputContains(CONFIGURATION_CACHE_INCOMPATIBLE_TASKS_OR_FEATURES_FOOTER)
        // but disablement reason is not about incompatible tasks, but read-only mode
        postBuildOutputDoesNotContain(CONFIGURATION_CACHE_DISABLED_REASON)
        postBuildOutputContains(CONFIGURATION_CACHE_DISABLED_READ_ONLY_REASON)
    }

    def "CC report link is present even when no problems were reported"() {
        given:
        buildFile """
            ${taskWithInjectedDegradationController()}
            tasks.register("foo", DegradingTask) { task ->
                getDegradationController().requireConfigurationCacheDegradation(task, provider { "Because reasons" })
                doLast {
                    println("Hello")
                }
            }
        """

        when:
        run ":foo", ENABLE_CLI_OPT // disable printing a report link by default

        then:
        configurationCache.assertNoConfigurationCache()

        and:
        problems.assertResultConsoleSummaryHasNoProblems(result)
        problems.htmlReport(result.output).assertContents {
            totalProblemsCount = 0
            withIncompatibleTask(":foo", "Because reasons.")
        }

        and:
        executed ":foo"
        assertConfigurationCacheDegradation()
    }

    def "provides no extra header for link if other problems are present"() {
        buildFile """
            ${taskWithInjectedDegradationController()}
            tasks.register("foo", DegradingTask) { task ->
                getDegradationController().requireConfigurationCacheDegradation(task, provider { "Because reasons" })
                doLast {
                    println("Hello")
                }
            }
            tasks.register("bar") { task ->
                doLast {
                    println(project.name)
                }
            }
        """

        when:
        configurationCacheRunLenient("foo", "bar")

        then:
        configurationCache.assertNoConfigurationCache()

        and:
        assertConfigurationCacheDegradation(true)
    }

    def "CC incompatible tasks and requested CC degradation are correctly reported"() {
        buildFile """
            ${taskWithInjectedDegradationController()}

            def fooTask = tasks.register("foo", DegradingTask) { task ->
                getDegradationController().requireConfigurationCacheDegradation(task, provider { "Because reasons" })
            }

            tasks.register("bar") {
                dependsOn(fooTask)
                notCompatibleWithConfigurationCache("Project access")
                doLast {
                    project.path
                }
            }
        """

        when:
        configurationCacheRun "bar"

        then:
        configurationCache.assertNoConfigurationCache()
        assertConfigurationCacheDegradation(true)

        and:
        executed(":foo", ":bar")

        and:
        problems.assertResultHasConsoleSummary(result) {
            totalProblemsCount = 1
            withProblem("Build file 'build.gradle': line 17: invocation of 'Task.project' at execution time is unsupported with the configuration cache")
        }
        problems.htmlReport(result.output).assertContents {
            totalProblemsCount = 1
            withProblem(INVOCATION_OF_TASK_PROJECT_AT_EXECUTION_TIME)
            withIncompatibleTask(":bar", "Project access.")
            withIncompatibleTask(":foo", "Because reasons.")
        }
    }

    private static String taskWithInjectedDegradationController() {
        """
            abstract class DegradingTask extends DefaultTask {
                @${Inject.name}
                abstract ${ConfigurationCacheDegradationController.name} getDegradationController()
            }
        """
    }

    private void assertConfigurationCacheDegradation(boolean hasOtherProblems = false) {
        if (hasOtherProblems) {
            outputDoesNotContain(CONFIGURATION_CACHE_INCOMPATIBLE_TASKS_OR_FEATURES_FOOTER)
        } else {
            outputContains(CONFIGURATION_CACHE_INCOMPATIBLE_TASKS_OR_FEATURES_FOOTER)
        }
        postBuildOutputContains(CONFIGURATION_CACHE_DISABLED_REASON)
    }

    private void assertNoConfigurationCacheDegradation() {
        outputDoesNotContain(CONFIGURATION_CACHE_INCOMPATIBLE_TASKS_OR_FEATURES_FOOTER)
        postBuildOutputDoesNotContain(CONFIGURATION_CACHE_DISABLED_REASON)
    }

    private void enableSourceDependencies() {
        settingsFile("""
            sourceControl {
                vcsMappings {
                    withModule("org.test:sourceModule") {
                        from(GitVersionControlSpec) {
                            url = "some-repo"
                        }
                    }
                }
            }
        """)
    }
}
