/*
 * Copyright 2016 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.integtests.resolve

import org.gradle.integtests.fixtures.AbstractIntegrationSpec
import org.gradle.internal.build.BuildStateRegistry
import org.gradle.util.Path
import spock.lang.Unroll

/**
 * This tests the following scenario with different version settings:
 *
 * ProjectA ─(project dependency)─> myorg.ModuleC
 *  |
 *  └> myorg.ModuleB(1.0) ────────> myorg.ModuleC($versionExternal)
 *
 * Of myorg.ModuleC...
 * - ...all referenced version exist in a repository as 'module' (see test setup)
 * - ...one version exists as 'project' in the build (either through multi-project or composite)
 *
 * This abstract specification leaves out the concrete implementation of setting up the project structure.
 * Subclasses can do that via multi-project or composite build.
 */
abstract class AbstractProjectDependencyConflictResolutionIntegrationSpec extends AbstractIntegrationSpec {

    abstract String getIncludeMechanism();

    abstract String getBuildId();

    abstract String getProjectPath();

    abstract String dependsOnMechanism(String projectName, String taskName);

    abstract String declareDependency(String moduleName, String moduleVersion);

    abstract String declaredDependencyId(String moduleName, String moduleVersion);

    abstract void moduleDefinition(String name, String definition);

    /**
     * Indicates that external modules are automatically substituted by corresponding projects (independent of versions).
     * This is the behavior in composite builds.
     */
    abstract boolean isAutoDependencySubstitution();

    @Unroll
    def "project dependency (#versionInProject) vs external dependency (#versionExternal) resolves to winner, when preferProjectModules=#preferProjectModules and forceVersion=#forceVersion and depSubstitution=#depSubstitution"() {
        given:
        //required for composite builds
        buildTestFixture.withBuildInSubDir()

        //publish all the versions of ModuleC that we might need
        mavenRepo.module("myorg", "ModuleC", versionExternal).publish()
        mavenRepo.module("myorg", "ModuleC", versionInProject).publish()
        if (forceVersion != null) {
            mavenRepo.module("myorg", "ModuleC", forceVersion).publish()
        }

        //publish ModuleB:1.0 and declare its dependency to ModuleC:$versionExternal
        mavenRepo.module("myorg", "ModuleB", '1.0').dependsOn("myorg", "ModuleC", versionExternal).publish()

        //setup the project structure
        settingsFile << "$includeMechanism 'ModuleC'\n$includeMechanism 'ProjectA'\n"

        when:

        def preferProjectModulesOption = ''
        def forcedVersionOption = ''
        def depSubstitutionOption = '';

        if (preferProjectModules) {
            preferProjectModulesOption = 'preferProjectModules()'
        }
        if (forceVersion != null) {
            forcedVersionOption = "conf ('myorg:ModuleC:$forceVersion') { force = true }"
        }
        if (!autoDependencySubstitution) { //only interpret depSubstitution in scenario without auto substitution (multi-project builds)
            depSubstitutionOption = depSubstitution;
        }

        buildFile << """
            task check {
                dependsOn ${dependsOnMechanism('ProjectA', 'checkModuleC_conf')}
            }
            ${checkHelper(buildId, projectPath)}
"""
        moduleDefinition('ModuleC', """
            group "myorg"
            version = $versionInProject

            configurations { conf }
            configurations.create("default").extendsFrom(configurations.conf)
""")

        moduleDefinition('ProjectA', """
            repositories { maven { url "${mavenRepo.uri}" } }

            configurations { conf }

            configurations.conf.resolutionStrategy {
                $preferProjectModulesOption
                dependencySubstitution { $depSubstitutionOption }
            }

            dependencies {
                conf "myorg:ModuleB:1.0"
                conf ${declareDependency('ModuleC', versionInProject)}
                $forcedVersionOption
            }

            ${check('ModuleC', declaredDependencyId('ModuleC', versionInProject),'conf', autoDependencySubstitution ? winnerAutoSubstitution : winner)}
""")

        then:
        succeeds('check')

        where:
                                versionInProject | versionExternal | winner                                | winnerAutoSubstitution | preferProjectModules | forceVersion | depSubstitution
                                // === without preferProjectModules() ===
        /*project <  external*/ "1.9"            | "2.0"           | 'moduleId("myorg", "ModuleC", "2.0")' | 'projectId("ModuleC")' | false                | null          | ''
        /*project == external*/ "2.0"            | "2.0"           | 'projectId("ModuleC")'                | 'projectId("ModuleC")' | false                | null          | ''
        /*project >  external*/ "2.1"            | "2.0"           | 'projectId("ModuleC")'                | 'projectId("ModuleC")' | false                | null          | ''
                                // === with preferProjectModules() ===
        /*project <  external*/ "1.9"            | "2.0"           | 'projectId("ModuleC")'                | 'projectId("ModuleC")' | true                 | null          | ''
        /*project == external*/ "2.0"            | "2.0"           | 'projectId("ModuleC")'                | 'projectId("ModuleC")' | true                 | null          | ''
        /*project >  external*/ "2.1"            | "2.0"           | 'projectId("ModuleC")'                | 'projectId("ModuleC")' | true                 | null          | ''
                                // === without preferProjectModules() but dependency substitution ===
        /*project <  external*/ "1.9"            | "2.0"           | 'moduleId("myorg", "ModuleC", "2.0")' | 'projectId("ModuleC")' | false                | null          | "substitute project(':ModuleC') with module('myorg:ModuleC:1.9')"
        /*project == external*/ "2.0"            | "2.0"           | 'moduleId("myorg", "ModuleC", "2.0")' | 'projectId("ModuleC")' | false                | null          | "substitute project(':ModuleC') with module('myorg:ModuleC:2.0')"
        /*project >  external*/ "2.1"            | "2.0"           | 'moduleId("myorg", "ModuleC", "2.1")' | 'projectId("ModuleC")' | false                | null          | "substitute project(':ModuleC') with module('myorg:ModuleC:2.1')"
                                // === with preferProjectModules() but dependency substitution <- no change, substitution wins ===
        /*project <  external*/ "1.9"            | "2.0"           | 'moduleId("myorg", "ModuleC", "2.0")' | 'projectId("ModuleC")' | true                 | null          | "substitute project(':ModuleC') with module('myorg:ModuleC:1.9')"
        /*project == external*/ "2.0"            | "2.0"           | 'moduleId("myorg", "ModuleC", "2.0")' | 'projectId("ModuleC")' | true                 | null          | "substitute project(':ModuleC') with module('myorg:ModuleC:2.0')"
        /*project >  external*/ "2.1"            | "2.0"           | 'moduleId("myorg", "ModuleC", "2.1")' | 'projectId("ModuleC")' | true                 | null          | "substitute project(':ModuleC') with module('myorg:ModuleC:2.1')"
                                // === with dependency substitution (same effect as preferProjectModules() only for the explicitly substituted dependency) ===
        /*project <  external*/ "1.9"            | "2.0"           | 'projectId("ModuleC")'                | 'projectId("ModuleC")' | false                | null          | "substitute module('myorg:ModuleC') with project(':ModuleC')"
        /*project == external*/ "2.0"            | "2.0"           | 'projectId("ModuleC")'                | 'projectId("ModuleC")' | false                | null          | "substitute module('myorg:ModuleC') with project(':ModuleC')"
        /*project >  external*/ "2.1"            | "2.0"           | 'projectId("ModuleC")'                | 'projectId("ModuleC")' | false                | null          | "substitute module('myorg:ModuleC') with project(':ModuleC')"
                                // === Force: without preferProjectModules() ===
        /*project != forced  */ "1.9"            | "2.0"           | 'moduleId("myorg", "ModuleC", "1.5")' | 'projectId("ModuleC")' | false                | '1.5'         | ''
        /*project == forced  */ "1.9"            | "2.0"           | 'projectId("ModuleC")'                | 'projectId("ModuleC")' | false                | '1.9'         | ''
        /*project != forced  */ "1.9"            | "2.0"           | 'projectId("ModuleC")'                | 'projectId("ModuleC")' | false                | '1.5'         | "substitute module('myorg:ModuleC') with project(':ModuleC')"
        /*project == forced  */ "1.9"            | "2.0"           | 'projectId("ModuleC")'                | 'projectId("ModuleC")' | false                | '1.9'         | "substitute module('myorg:ModuleC') with project(':ModuleC')"
                                // === Force: with preferProjectModules() <- no change ===
        /*project != forced  */ "1.9"            | "2.0"           | 'moduleId("myorg", "ModuleC", "1.5")' | 'projectId("ModuleC")' | true                 | '1.5'         | ''
        /*project == forced  */ "1.9"            | "2.0"           | 'projectId("ModuleC")'                | 'projectId("ModuleC")' | true                 | '1.9'         | ''
    }


    static String check(String moduleName, String declaredDependencyId, String confName, String winner) { """
        task check${moduleName}_${confName} {
            doLast {
                def deps = configurations.${confName}.incoming.resolutionResult.allDependencies as List
                def projectDependency = deps.find {
                    it instanceof org.gradle.api.artifacts.result.ResolvedDependencyResult &&
                        it.requested.matchesStrictly($declaredDependencyId)
                }

                assert projectDependency && projectDependency.selected.componentId == $winner
            }
        }
"""
    }

    static String checkHelper(String buildId, String projectPath) { """
        def moduleId(String group, String name, String version) {
            def mid = org.gradle.api.internal.artifacts.DefaultModuleIdentifier.newId(group, name)
            return org.gradle.internal.component.external.model.DefaultModuleComponentIdentifier.newId(mid, version)
        }

        def projectId(String projectName) {
            def buildId = $buildId
            def projectPath = $projectPath
            return project.services.get(${BuildStateRegistry.name}).getBuild(buildId).getIdentifierForProject(${Path.name}.path(projectPath))
        }
"""
    }

}
