/*
 * Copyright 2021 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.configurationcache.isolated

class IsolatedProjectToolingModelsWithDependencyResolutionIntegrationTest extends AbstractIsolatedProjectsToolingApiIntegrationTest {
    def "caches BuildAction that queries model that performs dependency resolution"() {
        given:
        withSomeToolingModelBuilderPluginThatPerformsDependencyResolutionInBuildSrc()
        settingsFile << """
            include("a")
            include("b")
            include("c")
            include("d")
        """
        file("a/build.gradle") << """
            plugins.apply(my.MyPlugin)
            dependencies {
                implementation(project(":b"))
            }
        """
        file("b/build.gradle") << """
            plugins.apply(my.MyPlugin)
            dependencies {
                implementation(project(":c"))
            }
        """
        file("c/build.gradle") << """
            plugins.apply(my.MyPlugin)
        """
        file("d/build.gradle") << """
            plugins.apply(my.MyPlugin)
        """

        when:
        executer.withArguments(ENABLE_CLI)
        def model = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model.size() == 4
        model[0].message == "project :a classpath = 2"
        model[1].message == "project :b classpath = 1"
        model[2].message == "project :c classpath = 0"
        model[3].message == "project :d classpath = 0"

        and:
        fixture.assertStateStored {
            projectConfigured(":buildSrc")
            projectConfigured(":")
            buildModelCreated()
            modelsCreated(":a", ":b", ":c", ":d")
        }

        when:
        executer.withArguments(ENABLE_CLI)
        def model2 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model2.size() == 4
        model2[0].message == "project :a classpath = 2"
        model2[1].message == "project :b classpath = 1"
        model2[2].message == "project :c classpath = 0"
        model2[3].message == "project :d classpath = 0"

        and:
        fixture.assertStateLoaded()

        when:
        changeFile("a/build.gradle")

        executer.withArguments(ENABLE_CLI)
        def model3 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model3.size() == 4
        model3[0].message == "project :a classpath = 2"
        model3[1].message == "project :b classpath = 1"
        model3[2].message == "project :c classpath = 0"
        model3[3].message == "project :d classpath = 0"

        and:
        fixture.assertStateUpdated {
            fileChanged("a/build.gradle")
            projectConfigured(":buildSrc")
            projectConfigured(":")
            modelsCreated(":a")
            modelsReused(":", ":b", ":c", ":d")
        }
    }

    def "updates cached state when project dependency is added to project"() {
        given:
        withSomeToolingModelBuilderPluginThatPerformsDependencyResolutionInBuildSrc()
        settingsFile << """
            include("a")
            include("b")
            include("c")
        """
        file("a/build.gradle") << """
            plugins.apply(my.MyPlugin)
        """
        file("b/build.gradle") << """
            plugins.apply(my.MyPlugin)
        """
        file("c/build.gradle") << """
            plugins.apply(my.MyPlugin)
        """

        when:
        executer.withArguments(ENABLE_CLI)
        def model = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model.size() == 3
        model[0].message == "project :a classpath = 0"
        model[1].message == "project :b classpath = 0"
        model[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateStored {
            projectConfigured(":buildSrc")
            projectConfigured(":")
            buildModelCreated()
            modelsCreated(":a", ":b", ":c")
        }

        when:
        executer.withArguments(ENABLE_CLI)
        def model2 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model2.size() == 3
        model2[0].message == "project :a classpath = 0"
        model2[1].message == "project :b classpath = 0"
        model2[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateLoaded()

        when:
        changeFile("a/build.gradle", """
            dependencies {
                implementation(project(":b"))
            }
        """)

        executer.withArguments(ENABLE_CLI)
        def model3 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model3.size() == 3
        model3[0].message == "project :a classpath = 1"
        model3[1].message == "project :b classpath = 0"
        model3[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateUpdated {
            fileChanged("a/build.gradle")
            projectConfigured(":buildSrc")
            projectConfigured(":")
            projectConfigured(":b") // has not been consumed by project dependency previously, but is now
            modelsCreated(":a")
            modelsReused(":", ":b", ":c")
        }

        when:
        executer.withArguments(ENABLE_CLI)
        def model4 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model4.size() == 3
        model4[0].message == "project :a classpath = 1"
        model4[1].message == "project :b classpath = 0"
        model4[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateLoaded()

        when:
        changeFile("a/build.gradle")

        executer.withArguments(ENABLE_CLI)
        def model5 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model5.size() == 3
        model5[0].message == "project :a classpath = 1"
        model5[1].message == "project :b classpath = 0"
        model5[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateUpdated {
            fileChanged("a/build.gradle")
            projectConfigured(":buildSrc")
            projectConfigured(":")
            modelsCreated(":a")
            modelsReused(":", ":b", ":c")
        }
    }

    def "updates cached state when project dependency graph is added to project"() {
        given:
        withSomeToolingModelBuilderPluginThatPerformsDependencyResolutionInBuildSrc()
        settingsFile << """
            include("a")
            include("b")
            include("c")
        """
        file("a/build.gradle") << """
            plugins.apply(my.MyPlugin)
        """
        file("b/build.gradle") << """
            plugins.apply(my.MyPlugin)
            dependencies {
                implementation(project(":c"))
            }
        """
        file("c/build.gradle") << """
            plugins.apply(my.MyPlugin)
        """

        when:
        executer.withArguments(ENABLE_CLI)
        def model = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model.size() == 3
        model[0].message == "project :a classpath = 0"
        model[1].message == "project :b classpath = 1"
        model[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateStored {
            projectConfigured(":buildSrc")
            projectConfigured(":")
            buildModelCreated()
            modelsCreated(":a", ":b", ":c")
        }

        when:
        executer.withArguments(ENABLE_CLI)
        def model2 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model2.size() == 3
        model2[0].message == "project :a classpath = 0"
        model2[1].message == "project :b classpath = 1"
        model2[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateLoaded()

        when:
        changeFile("a/build.gradle", """
            dependencies {
                implementation(project(":b"))
            }
        """)

        executer.withArguments(ENABLE_CLI)
        def model3 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model3.size() == 3
        model3[0].message == "project :a classpath = 2"
        model3[1].message == "project :b classpath = 1"
        model3[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateUpdated {
            fileChanged("a/build.gradle")
            projectConfigured(":buildSrc")
            projectConfigured(":")
            projectConfigured(":b") // has not been consumed by project dependency previously, but is now
            modelsCreated(":a")
            modelsReused(":", ":b", ":c")
        }

        when:
        executer.withArguments(ENABLE_CLI)
        def model4 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model4.size() == 3
        model4[0].message == "project :a classpath = 2"
        model4[1].message == "project :b classpath = 1"
        model4[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateLoaded()

        when:
        changeFile("a/build.gradle")

        executer.withArguments(ENABLE_CLI)
        def model5 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model5.size() == 3
        model5[0].message == "project :a classpath = 2"
        model5[1].message == "project :b classpath = 1"
        model5[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateUpdated {
            fileChanged("a/build.gradle")
            projectConfigured(":buildSrc")
            projectConfigured(":")
            modelsCreated(":a")
            modelsReused(":", ":b", ":c")
        }
    }

    def "updates cached state when project dependency graph is removed from project"() {
        given:
        withSomeToolingModelBuilderPluginThatPerformsDependencyResolutionInBuildSrc()
        settingsFile << """
            include("a")
            include("b")
            include("c")
        """
        file("a/build.gradle") << """
            plugins.apply(my.MyPlugin)
            dependencies {
                implementation(project(":b"))
            }
        """
        file("b/build.gradle") << """
            plugins.apply(my.MyPlugin)
            dependencies {
                implementation(project(":c"))
            }
        """
        file("c/build.gradle") << """
            plugins.apply(my.MyPlugin)
        """

        when:
        executer.withArguments(ENABLE_CLI)
        def model = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model.size() == 3
        model[0].message == "project :a classpath = 2"
        model[1].message == "project :b classpath = 1"
        model[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateStored {
            projectConfigured(":buildSrc")
            projectConfigured(":")
            buildModelCreated()
            modelsCreated(":a", ":b", ":c")
        }

        when:
        executer.withArguments(ENABLE_CLI)
        def model2 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model2.size() == 3
        model2[0].message == "project :a classpath = 2"
        model2[1].message == "project :b classpath = 1"
        model2[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateLoaded()

        when:
        file("a/build.gradle").replace('implementation(project(":b"))', "")
        waitForChangesToBePickedUp()

        executer.withArguments(ENABLE_CLI)
        def model3 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model3.size() == 3
        model3[0].message == "project :a classpath = 0"
        model3[1].message == "project :b classpath = 1"
        model3[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateUpdated {
            fileChanged("a/build.gradle")
            projectConfigured(":buildSrc")
            projectConfigured(":")
            modelsCreated(":a")
            modelsReused(":", ":b", ":c")
        }

        when:
        executer.withArguments(ENABLE_CLI)
        def model4 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model4.size() == 3
        model4[0].message == "project :a classpath = 0"
        model4[1].message == "project :b classpath = 1"
        model4[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateLoaded()

        when:
        changeFile("a/build.gradle")

        executer.withArguments(ENABLE_CLI)
        def model5 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model5.size() == 3
        model5[0].message == "project :a classpath = 0"
        model5[1].message == "project :b classpath = 1"
        model5[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateUpdated {
            fileChanged("a/build.gradle")
            projectConfigured(":buildSrc")
            projectConfigured(":")
            modelsCreated(":a")
            modelsReused(":", ":b", ":c")
        }
    }

    def "updates cached state when upstream project dependency changes"() {
        given:
        withSomeToolingModelBuilderPluginThatPerformsDependencyResolutionInBuildSrc()
        settingsFile << """
            include("a")
            include("b")
            include("c")
        """
        file("a/build.gradle") << """
            plugins.apply(my.MyPlugin)
            dependencies {
                implementation project(":b")
            }
        """
        file("b/build.gradle") << """
            plugins.apply(my.MyPlugin)
            dependencies {
                implementation project(":c")
            }
        """
        file("c/build.gradle") << """
            plugins.apply(my.MyPlugin)
        """

        when:
        executer.withArguments(ENABLE_CLI)
        def model = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model.size() == 3
        model[0].message == "project :a classpath = 2"
        model[1].message == "project :b classpath = 1"
        model[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateStored {
            projectConfigured(":buildSrc")
            projectConfigured(":")
            buildModelCreated()
            modelsCreated(":a", ":b", ":c")
        }

        when:
        executer.withArguments(ENABLE_CLI)
        def model2 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model2.size() == 3
        model2[0].message == "project :a classpath = 2"
        model2[1].message == "project :b classpath = 1"
        model2[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateLoaded()

        when:
        changeFile("c/build.gradle")

        executer.withArguments(ENABLE_CLI)
        def model3 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model3.size() == 3
        model3[0].message == "project :a classpath = 2"
        model3[1].message == "project :b classpath = 1"
        model3[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateUpdated {
            fileChanged("c/build.gradle")
            projectConfigured(":buildSrc")
            projectConfigured(":")
            modelsCreated(":a", ":b", ":c")
            modelsReused(":")
        }

        when:
        executer.withArguments(ENABLE_CLI)
        def model4 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model4.size() == 3
        model4[0].message == "project :a classpath = 2"
        model4[1].message == "project :b classpath = 1"
        model4[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateLoaded()

        when:
        changeFile("b/build.gradle")

        executer.withArguments(ENABLE_CLI)
        def model5 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model5.size() == 3
        model5[0].message == "project :a classpath = 2"
        model5[1].message == "project :b classpath = 1"
        model5[2].message == "project :c classpath = 0"

        and:
        fixture.assertStateUpdated {
            fileChanged("b/build.gradle")
            projectConfigured(":buildSrc")
            projectConfigured(":")
            modelsCreated(":a", ":b")
            modelsReused(":", ":c")
        }
    }

    def "caches BuildAction when there are cycles in the dependency graph"() {
        given:
        withSomeToolingModelBuilderPluginThatPerformsDependencyResolutionInBuildSrc()
        settingsFile << """
            include("a")
            include("b")
            include("c")
            include("d")
        """
        file("a/build.gradle") << """
            plugins.apply(my.MyPlugin)
            dependencies {
                implementation(project(":b"))
            }
        """
        file("b/build.gradle") << """
            plugins.apply(my.MyPlugin)
            dependencies {
                implementation(project(":c"))
            }
        """
        file("c/build.gradle") << """
            plugins.apply(my.MyPlugin)
            dependencies {
                implementation(project(":a"))
            }
        """
        file("d/build.gradle") << """
            plugins.apply(my.MyPlugin)
        """

        when:
        executer.withArguments(ENABLE_CLI)
        def model = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model.size() == 4
        model[0].message == "project :a classpath = 3"
        model[1].message == "project :b classpath = 3"
        model[2].message == "project :c classpath = 3"
        model[3].message == "project :d classpath = 0"

        and:
        fixture.assertStateStored {
            projectConfigured(":buildSrc")
            projectConfigured(":")
            buildModelCreated()
            modelsCreated(":a", ":b", ":c", ":d")
        }

        when:
        executer.withArguments(ENABLE_CLI)
        def model2 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model2.size() == 4
        model2[0].message == "project :a classpath = 3"
        model2[1].message == "project :b classpath = 3"
        model2[2].message == "project :c classpath = 3"
        model2[3].message == "project :d classpath = 0"

        and:
        fixture.assertStateLoaded()

        when:
        changeFile("a/build.gradle", """
            dependencies {
                implementation(project(":d"))
            }
        """)

        executer.withArguments(ENABLE_CLI)
        def model3 = runBuildAction(new FetchCustomModelForEachProject())

        then:
        model3.size() == 4
        model3[0].message == "project :a classpath = 4"
        model3[1].message == "project :b classpath = 4"
        model3[2].message == "project :c classpath = 4"
        model3[3].message == "project :d classpath = 0"

        and:
        fixture.assertStateUpdated {
            fileChanged("a/build.gradle")
            projectConfigured(":buildSrc")
            projectConfigured(":")
            projectConfigured(":d")
            modelsCreated(":a", ":b", ":c")
            modelsReused(":", ":d")
        }
    }
}
