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

import org.gradle.integtests.fixtures.GradleMetadataResolveRunner
import org.gradle.integtests.fixtures.RequiredFeature
import org.gradle.integtests.fixtures.RequiredFeatures
import spock.lang.Issue
import spock.lang.Unroll

class DependenciesAttributesIntegrationTest extends AbstractModuleDependencyResolveTest {

    def setup() {
        buildFile << """
            def CUSTOM_ATTRIBUTE = Attribute.of('custom', String)
            dependencies.attributesSchema.attribute(CUSTOM_ATTRIBUTE)
        """
    }

    def "can declare attributes on dependencies"() {
        given:
        repository {
            'org:test:1.0'()
        }

        buildFile << """
            dependencies {
                conf('org:test:1.0') {
                    attributes {
                        attribute(CUSTOM_ATTRIBUTE, 'test value')
                    }
                }
            }
        """

        when:
        repositoryInteractions {
            'org:test:1.0' {
                expectResolve()
            }
        }
        succeeds 'checkDeps'

        then:
        resolve.expectGraph {
            root(":", ":test:") {
                module('org:test:1.0')
            }
        }

        and:
        outputDoesNotContain("Cannot set attributes for dependency \"org:test:1.0\": it was probably created by a plugin using internal APIs")
    }

    def "can declare attributes on constraints"() {
        given:
        repository {
            'org:test:1.0'()
        }

        buildFile << """
            dependencies {
                constraints {
                    conf('org:test:1.0') {
                        attributes {
                            attribute(CUSTOM_ATTRIBUTE, 'test value')
                        }
                    }
                }
                conf 'org:test'
            }
        """

        when:
        repositoryInteractions {
            'org:test:1.0' {
                expectResolve()
            }
        }
        succeeds 'checkDeps'

        then:
        resolve.expectGraph {
            root(":", ":test:") {
                edge('org:test', 'org:test:1.0')
                edge('org:test:1.0', 'org:test:1.0')
            }
        }

        and:
        outputDoesNotContain("Cannot set attributes for constraint \"org:test:1.0\": it was probably created by a plugin using internal APIs")
    }

    @RequiredFeatures(
        @RequiredFeature(feature = GradleMetadataResolveRunner.GRADLE_METADATA, value = "true")
    )
    @Unroll("Selects variant #expectedVariant using custom attribute value #attributeValue")
    def "attribute value is used during selection"() {
        given:
        repository {
            'org:test:1.0' {
                variant('api') {
                    attribute('custom', 'c1')
                }
                variant('runtime') {
                    attribute('custom', 'c2')
                }
            }
        }

        buildFile << """
            dependencies {
                conf('org:test:1.0') {
                    attributes {
                        attribute(CUSTOM_ATTRIBUTE, '$attributeValue')
                    }
                }
            }
        """

        when:
        repositoryInteractions {
            'org:test:1.0' {
                expectResolve()
            }
        }
        succeeds 'checkDeps'

        then:
        resolve.expectGraph {
            root(":", ":test:") {
                module('org:test:1.0') {
                    configuration = expectedVariant
                    variant(expectedVariant, expectedAttributes)
                }
            }
        }

        where:
        attributeValue | expectedVariant | expectedAttributes
        'c1'           | 'api'           | ['org.gradle.status': defaultStatus(), 'org.gradle.usage': 'java-api', custom: 'c1']
        'c2'           | 'runtime'       | ['org.gradle.status': defaultStatus(), 'org.gradle.usage': 'java-runtime', custom: 'c2']
    }

    @RequiredFeatures(
        @RequiredFeature(feature = GradleMetadataResolveRunner.GRADLE_METADATA, value = "true")
    )
    @Unroll("Selects variant #expectedVariant using typed attribute value #attributeValue")
    @Issue("gradle/gradle#5232")
    def "can declare typed attributes without failing serialization"() {
        given:
        repository {
            'org:test:1.0' {
                variant('api') {
                    attribute('lifecycle', 'c1')
                }
                variant('runtime') {
                    attribute('lifecycle', 'c2')
                }
            }
        }

        buildFile << """
            interface Lifecycle extends Named {}
            
            def LIFECYCLE_ATTRIBUTE = Attribute.of('lifecycle', Lifecycle)
            dependencies.attributesSchema.attribute(LIFECYCLE_ATTRIBUTE)
            
            dependencies {
                conf('org:test:1.0') {
                    attributes {
                        attribute(LIFECYCLE_ATTRIBUTE, objects.named(Lifecycle, '$attributeValue'))
                    }
                }
            }
        """

        when:
        repositoryInteractions {
            'org:test:1.0' {
                expectResolve()
            }
        }
        succeeds 'checkDeps'

        then:
        resolve.expectGraph {
            root(":", ":test:") {
                module('org:test:1.0') {
                    configuration = expectedVariant
                    variant(expectedVariant, expectedAttributes)
                }
            }
        }

        and:
        outputDoesNotContain("Cannot set attributes for dependency \"org:test:1.0\": it was probably created by a plugin using internal APIs")

        where:
        attributeValue | expectedVariant | expectedAttributes
        'c1'           | 'api'           | ['org.gradle.status': defaultStatus(), 'org.gradle.usage': 'java-api', lifecycle: 'c1']
        'c2'           | 'runtime'       | ['org.gradle.status': defaultStatus(), 'org.gradle.usage': 'java-runtime', lifecycle: 'c2']
    }

    @RequiredFeatures(
        @RequiredFeature(feature = GradleMetadataResolveRunner.GRADLE_METADATA, value = "true")
    )
    @Issue("gradle/gradle#5232")
    def "Serializes and reads back failed resolution when failure comes from an unmatched typed attribute"() {
        given:
        repository {
            'org:test:1.0' {
                attribute('lifecycle', 'some')
            }
        }

        buildFile << """
            interface Lifecycle extends Named {}
            
            def LIFECYCLE_ATTRIBUTE = Attribute.of('lifecycle', Lifecycle)
            dependencies.attributesSchema.attribute(LIFECYCLE_ATTRIBUTE)
            
            dependencies {
                conf('org:test:[1.0,)') {
                    attributes {
                        attribute(LIFECYCLE_ATTRIBUTE, objects.named(Lifecycle, 'other'))
                    }
                }
            }
            
            configurations.conf.incoming.afterResolve {
                // afterResolve will trigger the problem when reading
                it.resolutionResult.allComponents {
                    println "Success for \${it.id}"
                }
            }
        """

        when:
        repositoryInteractions {
            'org:test' {
                expectVersionListing()
            }
            'org:test:1.0' {
                expectGetMetadata()
            }
        }
        fails 'checkDeps'

        then:
        failure.assertHasCause("""Could not find any version that matches org:test:[1.0,).""")

        and:
        outputContains("Success for project :")
    }

    @RequiredFeatures(
        @RequiredFeature(feature = GradleMetadataResolveRunner.GRADLE_METADATA, value = "true")
    )
    def "Merges consumer configuration attributes with dependency attributes"() {
        given:
        repository {
            'org:test:1.0' {
                variant('api') {
                    attribute('custom', 'c1')
                }
                variant('runtime') {
                    attribute('custom', 'c2')
                }
            }
        }

        buildFile << """
            configurations.conf.attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, "java-api"))

            dependencies {
                conf('org:test:1.0') {
                    attributes {
                        attribute(CUSTOM_ATTRIBUTE, 'c1')
                    }
                }
            }
        """

        when:
        repositoryInteractions {
            'org:test:1.0' {
                expectResolve()
            }
        }
        succeeds 'checkDeps'

        then:
        resolve.expectGraph {
            root(":", ":test:") {
                module('org:test:1.0') {
                    configuration = 'api'
                    variant('api', ['org.gradle.status': DependenciesAttributesIntegrationTest.defaultStatus(), 'org.gradle.usage': 'java-api', custom: 'c1'])
                }
            }
        }
    }

    @RequiredFeatures(
        @RequiredFeature(feature = GradleMetadataResolveRunner.GRADLE_METADATA, value = "true")
    )
    def "Fails resolution because consumer configuration attributes and dependency attributes conflict"() {
        given:
        repository {
            'org:test:1.0' {
                variant('api') {
                    attribute('custom', 'c1')
                }
                variant('runtime') {
                    attribute('custom', 'c2')
                }
            }
        }

        buildFile << """
            configurations.conf.attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, "java-api"))

            dependencies {
                conf('org:test:1.0') {
                    attributes {
                        attribute(CUSTOM_ATTRIBUTE, 'c2')
                    }
                }
            }
        """

        when:
        repositoryInteractions {
            'org:test:1.0' {
                expectGetMetadata()
            }
        }
        fails 'checkDeps'

        then:
        failure.assertHasCause("""Unable to find a matching variant of org:test:1.0:
  - Variant 'api':
      - Required custom 'c2' and found incompatible value 'c1'.
      - Found org.gradle.status '${defaultStatus()}' but wasn't required.
      - Required org.gradle.usage 'java-api' and found compatible value 'java-api'.
  - Variant 'runtime':
      - Required custom 'c2' and found compatible value 'c2'.
      - Found org.gradle.status '${defaultStatus()}' but wasn't required.
      - Required org.gradle.usage 'java-api' and found incompatible value 'java-runtime'""")
    }

    @RequiredFeatures(
        @RequiredFeature(feature = GradleMetadataResolveRunner.GRADLE_METADATA, value = "true")
    )
    @Unroll("Selects variant #expectedVariant using custom attribute value #dependencyValue overriding configuration attribute #configurationValue")
    def "dependency attribute value overrides configuration attribute"() {
        given:
        repository {
            'org:test:1.0' {
                variant('api') {
                    attribute('custom', 'c1')
                }
                variant('runtime') {
                    attribute('custom', 'c2')
                }
            }
        }

        buildFile << """
            configurations.conf.attributes.attribute(CUSTOM_ATTRIBUTE, '$configurationValue')

            dependencies {
                conf('org:test:1.0') {
                    attributes {
                        attribute(CUSTOM_ATTRIBUTE, '$dependencyValue')
                    }
                }
            }
        """

        when:
        repositoryInteractions {
            'org:test:1.0' {
                expectResolve()
            }
        }
        succeeds 'checkDeps'

        then:
        resolve.expectGraph {
            root(":", ":test:") {
                module('org:test:1.0') {
                    configuration = expectedVariant
                    variant(expectedVariant, expectedAttributes)
                }
            }
        }

        where:
        configurationValue | dependencyValue | expectedVariant | expectedAttributes
        'c2'               | 'c1'            | 'api'           | ['org.gradle.status': defaultStatus(), 'org.gradle.usage': 'java-api', custom: 'c1']
        'c1'               | 'c2'            | 'runtime'       | ['org.gradle.status': defaultStatus(), 'org.gradle.usage': 'java-runtime', custom: 'c2']
    }

    @RequiredFeatures(
        @RequiredFeature(feature = GradleMetadataResolveRunner.GRADLE_METADATA, value = "true")
    )
    @Unroll("Selects variant #expectedVariant using custom attribute value #dependencyValue overriding configuration attribute #configurationValue using dependency constraint")
    def "dependency attribute value overrides configuration attribute using dependency constraint"() {
        given:
        repository {
            'org:test:1.0' {
                variant('api') {
                    attribute('custom', 'c1')
                }
                variant('runtime') {
                    attribute('custom', 'c2')
                }
            }
        }

        buildFile << """
            configurations.conf.attributes.attribute(CUSTOM_ATTRIBUTE, '$configurationValue')

            dependencies {
                constraints {
                    conf('org:test:1.0') {
                       attributes {
                          attribute(CUSTOM_ATTRIBUTE, '$dependencyValue')
                       }
                    }
                }
                conf 'org:test'
            }
        """

        when:
        repositoryInteractions {
            'org:test:1.0' {
                expectResolve()
            }
        }
        succeeds 'checkDeps'

        then:
        resolve.expectGraph {
            root(":", ":test:") {
                edge('org:test', 'org:test:1.0') {
                    configuration = expectedVariant
                    variant(expectedVariant, expectedAttributes)
                }
                module('org:test:1.0') {
                    configuration = expectedVariant
                    variant(expectedVariant, expectedAttributes)
                }
            }
        }

        where:
        configurationValue | dependencyValue | expectedVariant | expectedAttributes
        'c2'               | 'c1'            | 'api'           | ['org.gradle.status': defaultStatus(), 'org.gradle.usage': 'java-api', custom: 'c1']
        'c1'               | 'c2'            | 'runtime'       | ['org.gradle.status': defaultStatus(), 'org.gradle.usage': 'java-runtime', custom: 'c2']
    }

    @RequiredFeatures(
        @RequiredFeature(feature = GradleMetadataResolveRunner.GRADLE_METADATA, value = "true")
    )
    def "Fails resolution because consumer configuration attributes and constraint attributes conflict"() {
        given:
        repository {
            'org:test:1.0' {
                variant('api') {
                    attribute('custom', 'c1')
                }
                variant('runtime') {
                    attribute('custom', 'c2')
                }
            }
        }

        buildFile << """
            configurations.conf.attributes.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage, "java-api"))

            dependencies {
                constraints {
                    conf('org:test:1.0') {
                        attributes {
                            attribute(CUSTOM_ATTRIBUTE, 'c2')
                        }
                    }
                }
                conf 'org:test'
            }
        """

        when:
        repositoryInteractions {
            'org:test:1.0' {
                expectGetMetadata()
            }
        }
        fails 'checkDeps'

        then:
        failure.assertHasCause("""Unable to find a matching variant of org:test:1.0:
  - Variant 'api':
      - Required custom 'c2' and found incompatible value 'c1'.
      - Found org.gradle.status '${defaultStatus()}' but wasn't required.
      - Required org.gradle.usage 'java-api' and found compatible value 'java-api'.
  - Variant 'runtime':
      - Required custom 'c2' and found compatible value 'c2'.
      - Found org.gradle.status '${defaultStatus()}' but wasn't required.
      - Required org.gradle.usage 'java-api' and found incompatible value 'java-runtime'""")
    }

    @RequiredFeatures(
        @RequiredFeature(feature = GradleMetadataResolveRunner.GRADLE_METADATA, value = "true")
    )
    def "Fails resolution because dependency attributes and constraint attributes conflict"() {
        given:
        repository {
            'org:test:1.0' {
                variant('api') {
                    attribute('custom', 'c1')
                }
                variant('runtime') {
                    attribute('custom', 'c2')
                }
            }
        }

        buildFile << """
            dependencies {
                constraints {
                    conf('org:test:1.0') {
                        attributes {
                            attribute(CUSTOM_ATTRIBUTE, 'c2')
                        }
                    }
                }
                conf('org:test') {
                   attributes {
                      attribute(CUSTOM_ATTRIBUTE, 'c1')
                   }
                }
            }
        """

        when:
        repositoryInteractions {
            'org:test:1.0' {
                expectGetMetadata()
            }
        }
        fails 'checkDeps'

        then:
        failure.assertHasCause("""Cannot select a variant of 'org:test' because different values for attribute 'custom' are requested:
  - Dependency path ':test:unspecified' wants 'org:test' with attribute custom = c1
  - Constraint path ':test:unspecified' wants 'org:test:1.0' with attribute custom = c2""")
    }

    @RequiredFeatures(
        @RequiredFeature(feature = GradleMetadataResolveRunner.GRADLE_METADATA, value = "true")
    )
    @Unroll("Selects variant #expectedVariant using dependency attribute value #attributeValue set in a metadata rule")
    def "attribute value set by metadata rule is used during selection"() {
        given:
        repository {
            'org:testA:1.0' {
                variant('api') {
                    attribute('custom', 'c1')
                }
                variant('runtime') {
                    attribute('custom', 'c2')
                }
            }
            'org:testB:1.0' {
                variant('api') {
                    attribute('custom', 'c1')
                }
                variant('runtime') {
                    attribute('custom', 'c2')
                }
            }

            'org:directA:1.0' {
                dependsOn 'org:testA:1.0'
            }
            'org:directB:1.0'()
        }

        buildFile << """
            // this is actually the rules that we want to test
            class ModifyDependencyRule implements ComponentMetadataRule {
                Attribute attribute

                @javax.inject.Inject
                ModifyDependencyRule(Attribute attribute) {
                    this.attribute = attribute
                }

                void execute(ComponentMetadataContext context) {
                    context.details.allVariants {
                        withDependencies {
                            it.each {
                               it.attributes {
                                  it.attribute(attribute, '$attributeValue')
                               }
                            }
                        }
                    }
                }
            }

            class AddDependencyRule implements ComponentMetadataRule {
                Attribute attribute

                @javax.inject.Inject
                AddDependencyRule(Attribute attribute) {
                    this.attribute = attribute
                }

                void execute(ComponentMetadataContext context) {
                    context.details.allVariants {
                        withDependencies {
                            it.add('org:testB:1.0') {
                               it.attributes {
                                  it.attribute(attribute, '$attributeValue')
                               }
                            }
                        }
                    }
                }
            }

            dependencies {
                components {
                    // first notation: mutation of existing dependencies
                    withModule('org:directA', ModifyDependencyRule) {
                        params(CUSTOM_ATTRIBUTE)
                    }
                    // 2d notation: adding dependencies (this is a different code path)
                    withModule('org:directB', AddDependencyRule) {
                        params(CUSTOM_ATTRIBUTE)
                    }
                }
                conf('org:directA:1.0')
                conf('org:directB:1.0')
            }
        """

        when:
        repositoryInteractions {
            'org:directA:1.0' {
                expectResolve()
            }
            'org:testA:1.0' {
                expectResolve()
            }
            'org:directB:1.0' {
                expectResolve()
            }
            'org:testB:1.0' {
                expectResolve()
            }
        }
        succeeds 'checkDeps'

        then:
        resolve.expectGraph {
            root(":", ":test:") {
                module('org:directA:1.0') {
                    module('org:testA:1.0') {
                        configuration = expectedVariant
                        variant(expectedVariant, expectedAttributes)
                    }
                }
                module('org:directB:1.0') {
                    module('org:testB:1.0') {
                        configuration = expectedVariant
                        variant(expectedVariant, expectedAttributes)
                    }
                }
            }
        }

        where:
        attributeValue | expectedVariant | expectedAttributes
        'c1'           | 'api'           | ['org.gradle.status': defaultStatus(), 'org.gradle.usage': 'java-api', custom: 'c1']
        'c2'           | 'runtime'       | ['org.gradle.status': defaultStatus(), 'org.gradle.usage': 'java-runtime', custom: 'c2']
    }


    @RequiredFeatures(
        @RequiredFeature(feature = GradleMetadataResolveRunner.GRADLE_METADATA, value = "true")
    )
    @Unroll("Selects variant #expectedVariant using transitive dependency attribute value #attributeValue set in a metadata rule")
    def "attribute value set by metadata rule on transitive dependency is used during selection"() {
        given:
        repository {
            'org:testA:1.0' {
                variant('api') {
                    attribute('custom', 'c1')
                }
                variant('runtime') {
                    attribute('custom', 'c2')
                }
            }
            'org:testB:1.0' {
                variant('api') {
                    attribute('custom', 'c1')
                }
                variant('runtime') {
                    attribute('custom', 'c2')
                }
            }

            'org:directA:1.0' {
                dependsOn 'org:transitiveA:1.0'
            }
            'org:directB:1.0' {
                dependsOn 'org:transitiveB:1.0'
            }

            'org:transitiveA:1.0' {
                dependsOn 'org:testA:1.0'
            }

            'org:transitiveB:1.0'()
        }

        buildFile << """
            // this is actually the rules that we want to test
            class ModifyDependencyRule implements ComponentMetadataRule {
                Attribute attribute

                @javax.inject.Inject
                ModifyDependencyRule(Attribute attribute) {
                    this.attribute = attribute
                }

                void execute(ComponentMetadataContext context) {
                    context.details.allVariants {
                        withDependencies {
                            it.each {
                               it.attributes {
                                  it.attribute(attribute, '$attributeValue')
                               }
                            }
                        }
                    }
                }
            }

            class AddDependencyRule implements ComponentMetadataRule {
                Attribute attribute

                @javax.inject.Inject
                AddDependencyRule(Attribute attribute) {
                    this.attribute = attribute
                }

                void execute(ComponentMetadataContext context) {
                    context.details.allVariants {
                        withDependencies {
                            it.add('org:testB:1.0') {
                               it.attributes {
                                  it.attribute(attribute, '$attributeValue')
                               }
                            }
                        }
                    }
                }
            }

            dependencies {
                components {
                    // first notation: mutation of existing dependencies
                    withModule('org:transitiveA', ModifyDependencyRule) {
                        params(CUSTOM_ATTRIBUTE)
                    }
                    // 2d notation: adding dependencies (this is a different code path)
                    withModule('org:transitiveB', AddDependencyRule) {
                        params(CUSTOM_ATTRIBUTE)
                    }
                }
                conf('org:directA:1.0')
                conf('org:directB:1.0')
            }
        """

        when:
        repositoryInteractions {
            'org:directA:1.0' {
                expectResolve()
            }
            'org:transitiveA:1.0' {
                expectResolve()
            }
            'org:testA:1.0' {
                expectResolve()
            }
            'org:directB:1.0' {
                expectResolve()
            }
            'org:transitiveB:1.0' {
                expectResolve()
            }
            'org:testB:1.0' {
                expectResolve()
            }
        }
        succeeds 'checkDeps'

        then:
        resolve.expectGraph {
            root(":", ":test:") {
                module('org:directA:1.0') {
                    module('org:transitiveA:1.0') {
                        module('org:testA:1.0') {
                            configuration = expectedVariant
                            variant(expectedVariant, expectedAttributes)
                        }
                    }
                }
                module('org:directB:1.0') {
                    module('org:transitiveB:1.0') {
                        module('org:testB:1.0') {
                            configuration = expectedVariant
                            variant(expectedVariant, expectedAttributes)
                        }
                    }
                }
            }
        }

        where:
        attributeValue | expectedVariant | expectedAttributes
        'c1'           | 'api'           | ['org.gradle.status': defaultStatus(), 'org.gradle.usage': 'java-api', custom: 'c1']
        'c2'           | 'runtime'       | ['org.gradle.status': defaultStatus(), 'org.gradle.usage': 'java-runtime', custom: 'c2']
    }

    @RequiredFeatures(
        @RequiredFeature(feature = GradleMetadataResolveRunner.GRADLE_METADATA, value = "true")
    )
    @Unroll("Selects direct=#expectedDirectVariant, transitive=[#expectedTransitiveVariantA, #expectedTransitiveVariantB], leaf=#expectedLeafVariant making sure dependency attribute value doesn't leak to transitives")
    def "Attribute value on dependency only affects selection of this dependency (using component metadata rules)"() {
        given:
        repository {
            def modules = ['direct', 'transitive', 'leaf']
            modules.eachWithIndex { module, idx ->
                ['A', 'B'].each { appendix ->
                    "org:${module}${appendix}:1.0" {
                        if (idx < modules.size() - 1) {
                            dependsOn("org:${modules[idx + 1]}${appendix}:1.0")
                        }
                        variant('api') {
                            attribute('custom', 'c1')
                        }
                        variant('runtime') {
                            attribute('custom', 'c2')
                        }
                    }
                }
            }
        }

        buildFile << """
            configurations.conf.attributes.attribute(CUSTOM_ATTRIBUTE, '$configurationAttributeValue')

            class ModifyDependencyRule implements ComponentMetadataRule {
                Attribute attribute
                String value

                @javax.inject.Inject
                ModifyDependencyRule(Attribute attribute, String value) {
                    this.attribute = attribute
                    this.value = value
                }

                void execute(ComponentMetadataContext context) {
                    context.details.allVariants {
                        withDependencies {
                            it.each {
                               it.attributes {
                                  it.attribute(attribute, value)
                               }
                            }
                        }
                    }
                }
            }

            dependencies {
                components {
                    // transitive module will override the configuration attribute
                    // and it shouldn't affect the selection of 'direct' or 'leaf' dependencies
                    withModule('org:directA', ModifyDependencyRule) {
                        params(CUSTOM_ATTRIBUTE)
                        params('$transitiveAttributeValueA')
                    } 
                    withModule('org:directB', ModifyDependencyRule) {
                        params(CUSTOM_ATTRIBUTE)
                        params('$transitiveAttributeValueB')
                    }                    
                }
                conf('org:directA:1.0')
                conf('org:directB:1.0')
            }
        """

        when:
        repositoryInteractions {
            ['direct', 'transitive', 'leaf'].each { module ->
                ['A', 'B'].each { appendix ->
                    "org:${module}${appendix}:1.0" {
                        expectResolve()
                    }
                }
            }
        }
        succeeds 'checkDeps'

        then:
        resolve.expectGraph {
            root(":", ":test:") {
                module('org:directA:1.0') {
                    configuration = expectedDirectVariant
                    variant(expectedDirectVariant, ['org.gradle.status': DependenciesAttributesIntegrationTest.defaultStatus(), 'org.gradle.usage': "java-$expectedDirectVariant", custom: configurationAttributeValue])
                    module('org:transitiveA:1.0') {
                        configuration = expectedTransitiveVariantA
                        variant(expectedTransitiveVariantA, ['org.gradle.status': DependenciesAttributesIntegrationTest.defaultStatus(), 'org.gradle.usage': "java-$expectedTransitiveVariantA", custom: transitiveAttributeValueA])
                        module('org:leafA:1.0') {
                            configuration = expectedLeafVariant
                            variant(expectedLeafVariant, ['org.gradle.status': DependenciesAttributesIntegrationTest.defaultStatus(), 'org.gradle.usage': "java-$expectedLeafVariant", custom: configurationAttributeValue])
                        }
                    }
                }
                module('org:directB:1.0') {
                    configuration = expectedDirectVariant
                    variant(expectedDirectVariant, ['org.gradle.status': DependenciesAttributesIntegrationTest.defaultStatus(), 'org.gradle.usage': "java-$expectedDirectVariant", custom: configurationAttributeValue])
                    module('org:transitiveB:1.0') {
                        configuration = expectedTransitiveVariantB
                        variant(expectedTransitiveVariantB, ['org.gradle.status': DependenciesAttributesIntegrationTest.defaultStatus(), 'org.gradle.usage': "java-$expectedTransitiveVariantB", custom: transitiveAttributeValueB])
                        module('org:leafB:1.0') {
                            configuration = expectedLeafVariant
                            variant(expectedLeafVariant, ['org.gradle.status': DependenciesAttributesIntegrationTest.defaultStatus(), 'org.gradle.usage': "java-$expectedLeafVariant", custom: configurationAttributeValue])
                        }
                    }
                }
            }
        }

        where:
        configurationAttributeValue | transitiveAttributeValueA | transitiveAttributeValueB | expectedDirectVariant | expectedTransitiveVariantA | expectedTransitiveVariantB | expectedLeafVariant
        'c1'                        | 'c1'                      | 'c1'                      | 'api'                 | 'api'                      | 'api'                      | 'api'
        'c1'                        | 'c2'                      | 'c2'                      | 'api'                 | 'runtime'                  | 'runtime'                  | 'api'
        'c2'                        | 'c2'                      | 'c2'                      | 'runtime'             | 'runtime'                  | 'runtime'                  | 'runtime'
        'c2'                        | 'c1'                      | 'c1'                      | 'runtime'             | 'api'                      | 'api'                      | 'runtime'

        'c1'                        | 'c1'                      | 'c2'                      | 'api'                 | 'api'                      | 'runtime'                  | 'api'
        'c1'                        | 'c2'                      | 'c1'                      | 'api'                 | 'runtime'                  | 'api'                      | 'api'
        'c2'                        | 'c2'                      | 'c1'                      | 'runtime'             | 'runtime'                  | 'api'                      | 'runtime'
        'c2'                        | 'c1'                      | 'c2'                      | 'runtime'             | 'api'                      | 'runtime'                  | 'runtime'
    }

    @RequiredFeatures(
        @RequiredFeature(feature = GradleMetadataResolveRunner.GRADLE_METADATA, value = "true")
    )
    @Unroll("Selects direct=#expectedDirectVariant, transitive=[#expectedTransitiveVariantA, #expectedTransitiveVariantB], leaf=#expectedLeafVariant making sure dependency attribute value doesn't leak to transitives (using published metadata)")
    def "Attribute value on dependency only affects selection of this dependency (using published metadata)"() {
        given:
        repository {
            def modules = ['direct', 'transitive', 'leaf']
            modules.eachWithIndex { module, idx ->
                ['A', 'B'].each { appendix ->
                    "org:${module}${appendix}:1.0" {
                        if (idx < modules.size() - 1) {
                            ['api', 'runtime'].each { name ->
                                variant(name) {
                                    dependsOn("org:${modules[idx + 1]}${appendix}:1.0") {
                                        if (module == 'direct') {
                                            attributes.custom = "${appendix == 'A' ? transitiveAttributeValueA : transitiveAttributeValueB}"
                                        }
                                    }
                                }
                            }
                        }
                        variant('api') {
                            attribute('custom', 'c1')
                        }
                        variant('runtime') {
                            attribute('custom', 'c2')
                        }
                    }
                }
            }
        }

        buildFile << """
            configurations.conf.attributes.attribute(CUSTOM_ATTRIBUTE, '$configurationAttributeValue')

            dependencies {                
                conf('org:directA:1.0')
                conf('org:directB:1.0')
            }
        """

        when:
        repositoryInteractions {
            ['direct', 'transitive', 'leaf'].each { module ->
                ['A', 'B'].each { appendix ->
                    "org:${module}${appendix}:1.0" {
                        expectResolve()
                    }
                }
            }
        }
        succeeds 'checkDeps'

        then:
        resolve.expectGraph {
            root(":", ":test:") {
                module('org:directA:1.0') {
                    configuration = expectedDirectVariant
                    variant(expectedDirectVariant, ['org.gradle.status': DependenciesAttributesIntegrationTest.defaultStatus(), 'org.gradle.usage': "java-$expectedDirectVariant", custom: configurationAttributeValue])
                    module('org:transitiveA:1.0') {
                        configuration = expectedTransitiveVariantA
                        variant(expectedTransitiveVariantA, ['org.gradle.status': DependenciesAttributesIntegrationTest.defaultStatus(), 'org.gradle.usage': "java-$expectedTransitiveVariantA", custom: transitiveAttributeValueA])
                        module('org:leafA:1.0') {
                            configuration = expectedLeafVariant
                            variant(expectedLeafVariant, ['org.gradle.status': DependenciesAttributesIntegrationTest.defaultStatus(), 'org.gradle.usage': "java-$expectedLeafVariant", custom: configurationAttributeValue])
                        }
                    }
                }
                module('org:directB:1.0') {
                    configuration = expectedDirectVariant
                    variant(expectedDirectVariant, ['org.gradle.status': DependenciesAttributesIntegrationTest.defaultStatus(), 'org.gradle.usage': "java-$expectedDirectVariant", custom: configurationAttributeValue])
                    module('org:transitiveB:1.0') {
                        configuration = expectedTransitiveVariantB
                        variant(expectedTransitiveVariantB, ['org.gradle.status': DependenciesAttributesIntegrationTest.defaultStatus(), 'org.gradle.usage': "java-$expectedTransitiveVariantB", custom: transitiveAttributeValueB])
                        module('org:leafB:1.0') {
                            configuration = expectedLeafVariant
                            variant(expectedLeafVariant, ['org.gradle.status': DependenciesAttributesIntegrationTest.defaultStatus(), 'org.gradle.usage': "java-$expectedLeafVariant", custom: configurationAttributeValue])
                        }
                    }
                }
            }
        }

        where:
        configurationAttributeValue | transitiveAttributeValueA | transitiveAttributeValueB | expectedDirectVariant | expectedTransitiveVariantA | expectedTransitiveVariantB | expectedLeafVariant
        'c1'                        | 'c1'                      | 'c1'                      | 'api'                 | 'api'                      | 'api'                      | 'api'
        'c1'                        | 'c2'                      | 'c2'                      | 'api'                 | 'runtime'                  | 'runtime'                  | 'api'
        'c2'                        | 'c2'                      | 'c2'                      | 'runtime'             | 'runtime'                  | 'runtime'                  | 'runtime'
        'c2'                        | 'c1'                      | 'c1'                      | 'runtime'             | 'api'                      | 'api'                      | 'runtime'

        'c1'                        | 'c1'                      | 'c2'                      | 'api'                 | 'api'                      | 'runtime'                  | 'api'
        'c1'                        | 'c2'                      | 'c1'                      | 'api'                 | 'runtime'                  | 'api'                      | 'api'
        'c2'                        | 'c2'                      | 'c1'                      | 'runtime'             | 'runtime'                  | 'api'                      | 'runtime'
        'c2'                        | 'c1'                      | 'c2'                      | 'runtime'             | 'api'                      | 'runtime'                  | 'runtime'
    }

    @RequiredFeatures(
        @RequiredFeature(feature = GradleMetadataResolveRunner.GRADLE_METADATA, value = "true")
    )
    def "fails when 2 transitive dependencies requires a different attribute value"() {
        given:
        repository {
            'org:directA:1.0' {
                variant('api') {
                    dependsOn('org:transitive:1.0') {
                        attribute('custom', 'c1')
                    }
                }
                variant('runtime') {
                    dependsOn('org:transitive:1.0') {
                        attribute('custom', 'c1')
                    }
                }
            }
            'org:directB:1.0' {
                variant('api') {
                    dependsOn('org:transitive:1.0') {
                        attribute('custom', 'c2')
                    }
                }
                variant('runtime') {
                    dependsOn('org:transitive:1.0') {
                        attribute('custom', 'c2')
                    }
                }
            }
            'org:transitive:1.0' {
                variant('api') {
                    attribute('custom', 'c1')
                }
                variant('runtime') {
                    attribute('custom', 'c2')
                }
            }
        }

        buildFile << """
            dependencies {
               conf('org:directA:1.0')
               conf('org:directB:1.0')
            }
        """

        when:
        repositoryInteractions {
            'org:directA:1.0' {
                expectGetMetadata()
            }
            'org:directB:1.0' {
                expectGetMetadata()
            }
            'org:transitive:1.0' {
                expectGetMetadata()
            }
        }
        fails 'checkDeps'

        then:
        failure.assertHasCause("""Cannot select a variant of 'org:transitive' because different values for attribute 'custom' are requested:
  - Dependency path ':test:unspecified' --> 'org:directA:1.0' wants 'org:transitive:1.0' with attribute custom = c1
  - Dependency path ':test:unspecified' --> 'org:directB:1.0' wants 'org:transitive:1.0' with attribute custom = c2""")
    }

    static Closure<String> defaultStatus() {
        { -> GradleMetadataResolveRunner.useIvy() ? 'integration' : 'release' }
    }
}
