/*
 * 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.api

import org.gradle.integtests.fixtures.AbstractHttpDependencyResolutionTest
import org.gradle.integtests.fixtures.resolve.ResolveTestFixture
import spock.lang.Unroll

class ResolvedFilesApiIntegrationTest extends AbstractHttpDependencyResolutionTest {
    def setup() {
        settingsFile << """
rootProject.name = 'test'
"""
        buildFile << """
def usage = Attribute.of('usage', String)
allprojects {
    dependencies {
        attributesSchema {
           attribute(usage)
        }
    }
    configurations {
        compile {
            attributes.attribute(usage, 'compile')
        }
    }
}
"""
        new ResolveTestFixture(buildFile).addDefaultVariantDerivationStrategy()
    }

    def "result includes files from local and external components and file dependencies in a fixed order"() {
        mavenRepo.module("org", "test", "1.0").publish()
        mavenRepo.module("org", "test2", "1.0").publish()

        settingsFile << """
include 'a', 'b'
"""
        buildFile << """
allprojects {
    repositories { maven { url '$mavenRepo.uri' } }
}
dependencies {
    compile files('test-lib.jar')
    compile project(':a')
    compile 'org:test:1.0'
    artifacts {
        compile file('test.jar')
    }
}
project(':a') {
    dependencies {
        compile files('a-lib.jar')
        compile project(':b')
        compile 'org:test:1.0'
    }
    artifacts {
        compile file('a.jar')
    }
}
project(':b') {
    dependencies {
        compile files('b-lib.jar')
        compile 'org:test2:1.0'
    }
    artifacts {
        compile file('b.jar')
    }
}

task show {
    doLast {
        println "files 1: " + configurations.compile.collect { it.name }
        println "files 2: " + configurations.compile.incoming.files.collect { it.name }
        println "files 3: " + configurations.compile.files.collect { it.name }
        println "files 4: " + configurations.compile.resolve().collect { it.name }
        println "files 5: " + configurations.compile.incoming.artifactView({}).files.collect { it.name }
        println "files 6: " + configurations.compile.incoming.artifactView({componentFilter { true }}).files.collect { it.name }
        println "files 7: " + configurations.compile.incoming.artifactView({componentFilter { true }}).artifacts.artifactFiles.collect { it.name }
        println "files 8: " + configurations.compile.files { true }.collect { it.name }
        println "files 9: " + configurations.compile.fileCollection { true }.collect { it.name }
        println "files 10: " + configurations.compile.fileCollection { true }.files.collect { it.name }
        println "files 11: " + configurations.compile.resolvedConfiguration.getFiles { true }.collect { it.name }
        println "files 12: " + configurations.compile.resolvedConfiguration.lenientConfiguration.getFiles { true }.collect { it.name }
    }
}
"""

        when:
        run 'show'

        then:
        outputContains("files 1: [test-lib.jar, a.jar, a-lib.jar, test-1.0.jar, b.jar, b-lib.jar, test2-1.0.jar")
        outputContains("files 2: [test-lib.jar, a.jar, a-lib.jar, test-1.0.jar, b.jar, b-lib.jar, test2-1.0.jar")
        outputContains("files 3: [test-lib.jar, a.jar, a-lib.jar, test-1.0.jar, b.jar, b-lib.jar, test2-1.0.jar")
        outputContains("files 4: [test-lib.jar, a.jar, a-lib.jar, test-1.0.jar, b.jar, b-lib.jar, test2-1.0.jar")
        outputContains("files 5: [test-lib.jar, a.jar, a-lib.jar, test-1.0.jar, b.jar, b-lib.jar, test2-1.0.jar")
        outputContains("files 6: [test-lib.jar, a.jar, a-lib.jar, test-1.0.jar, b.jar, b-lib.jar, test2-1.0.jar")
        outputContains("files 7: [test-lib.jar, a.jar, a-lib.jar, test-1.0.jar, b.jar, b-lib.jar, test2-1.0.jar")
        // Note: the filtered views order files differently. This is documenting existing behaviour rather than necessarily desired behaviour
        outputContains("files 8: [test-lib.jar, a.jar, a-lib.jar, b.jar, b-lib.jar, test2-1.0.jar, test-1.0.jar")
        outputContains("files 9: [test-lib.jar, a.jar, a-lib.jar, b.jar, b-lib.jar, test2-1.0.jar, test-1.0.jar")
        outputContains("files 10: [test-lib.jar, a.jar, a-lib.jar, b.jar, b-lib.jar, test2-1.0.jar, test-1.0.jar")
        outputContains("files 11: [test-lib.jar, a.jar, a-lib.jar, b.jar, b-lib.jar, test2-1.0.jar, test-1.0.jar")
        outputContains("files 12: [test-lib.jar, a.jar, a-lib.jar, b.jar, b-lib.jar, test2-1.0.jar, test-1.0.jar")
    }

    @Unroll
    def "applies compatibility rules to select variant"() {
        settingsFile << """
include 'a', 'b'
"""
        buildFile << """
class FreeRule implements AttributeCompatibilityRule<String> {
    void execute(CompatibilityCheckDetails<String> details) {
        if (details.consumerValue == 'preview' && details.producerValue == 'free') {
            details.compatible()
        }
    }
}

class PaidRule implements AttributeCompatibilityRule<String> {
    void execute(CompatibilityCheckDetails<String> details) {
        if (details.consumerValue == 'preview' && details.producerValue == 'paid') {
            details.compatible()
        }
    }
}

def flavor = Attribute.of('flavor', String)

allprojects {
    dependencies {
        attributesSchema.attribute(flavor)
    }
}

configurations {
    compile.attributes.attribute(flavor, 'preview')
}

dependencies {
    compile project(':a')
}

project(':a') {
    dependencies {
        attributesSchema.attribute(flavor) {
            compatibilityRules.add(FreeRule)
        }
        compile project(':b')
    }
    ${freeAndPaidFlavoredJars('a')}
}
project(':b') {
    dependencies {
        attributesSchema.attribute(flavor) {
            compatibilityRules.add(PaidRule)
        }
    }    
    ${freeAndPaidFlavoredJars('b')}
}

task show {
    inputs.files ${expression}
    doLast {
        println "files: " + ${expression}.collect { it.name }
    }
}
"""
        expect:
        succeeds("show")
        output.contains("files: [a-free.jar, b-paid.jar]")
        result.assertTasksExecuted(':a:freeJar', ':b:paidJar', ':show')

        where:
        expression                                                                                         | _
        "configurations.compile"                                                                           | _
        "configurations.compile.incoming.files"                                                            | _
        "configurations.compile.fileCollection { true }"                                                   | _
        "configurations.compile.incoming.artifactView({}).files"                                           | _
        "configurations.compile.incoming.artifactView({componentFilter { true }}).files"                   | _
        "configurations.compile.incoming.artifactView({componentFilter { true }}).artifacts.artifactFiles" | _
    }

    @Unroll
    def "applies disambiguation rules to select variant"() {
        settingsFile << """
include 'a', 'b'
"""
        buildFile << """
class SelectFreeRule implements AttributeDisambiguationRule<String> {
    void execute(MultipleCandidatesDetails<String> details) {
        details.closestMatch('free')
    }
}
class SelectPaidRule implements AttributeDisambiguationRule<String> {
    void execute(MultipleCandidatesDetails<String> details) {
        details.closestMatch('paid')
    }
}

def flavor = Attribute.of('flavor', String)

dependencies {
    compile project(':a')
}

project(':a') {
    dependencies {
        attributesSchema.attribute(flavor) {
            disambiguationRules.add(SelectFreeRule)
        }
        compile project(':b')
    }
    ${freeAndPaidFlavoredJars('a')}
}
project(':b') {
    dependencies {
            attributesSchema.attribute(flavor) {
            disambiguationRules.add(SelectPaidRule)
        }
    }    
    ${freeAndPaidFlavoredJars('b')}
}

task show {
    inputs.files ${expression}
    doLast {
        println "files: " + ${expression}.collect { it.name }
    }
}
"""
        expect:
        succeeds("show")
        output.contains("files: [a-free.jar, b-paid.jar]")
        result.assertTasksExecuted(':a:freeJar', ':b:paidJar', ':show')

        where:
        expression                                                                                         | _
        "configurations.compile"                                                                           | _
        "configurations.compile.incoming.files"                                                            | _
        "configurations.compile.fileCollection { true }"                                                   | _
        "configurations.compile.incoming.artifactView({}).files"                                           | _
        "configurations.compile.incoming.artifactView({componentFilter { true }}).files"                   | _
        "configurations.compile.incoming.artifactView({componentFilter { true }}).artifacts.artifactFiles" | _
    }

    @Unroll
    def "reports failure when there is more than one compatible variant"() {
        settingsFile << """
include 'a', 'b'
"""
        buildFile << """
def flavor = Attribute.of('flavor', String)

dependencies {
    compile project(':a')
}

project(':a') {
    dependencies {
        attributesSchema.attribute(flavor)
        compile project(':b')
    }
    ${freeAndPaidFlavoredJars('a')}
}
project(':b') {
    dependencies {
        attributesSchema.attribute(flavor)
    }    
    ${freeAndPaidFlavoredJars('b')}
}

task show {
    doLast {
        println "files: " + ${expression}.collect { it.name }
    }
}
"""
        expect:
        fails("show")
        failure.assertHasCause("""More than one variant of project :a matches the consumer attributes:
  - Configuration ':a:compile' variant free:
      - Unmatched attributes:
          - Found artifactType 'jar' but wasn't required.
          - Found flavor 'free' but wasn't required.
      - Compatible attribute:
          - Required usage 'compile' and found compatible value 'compile'.
  - Configuration ':a:compile' variant paid:
      - Unmatched attributes:
          - Found artifactType 'jar' but wasn't required.
          - Found flavor 'paid' but wasn't required.
      - Compatible attribute:
          - Required usage 'compile' and found compatible value 'compile'.""")

        where:
        expression                                                                                         | _
        "configurations.compile"                                                                           | _
        "configurations.compile.incoming.files"                                                            | _
        "configurations.compile.files"                                                                     | _
        "configurations.compile.resolve()"                                                                 | _
        "configurations.compile.files { true }"                                                            | _
        "configurations.compile.fileCollection { true }"                                                   | _
        "configurations.compile.resolvedConfiguration.getFiles { true }"                                   | _
        "configurations.compile.incoming.artifactView({}).files"                                           | _
        "configurations.compile.incoming.artifactView({componentFilter { true }}).files"                   | _
        "configurations.compile.incoming.artifactView({componentFilter { true }}).artifacts.artifactFiles" | _
    }

    @Unroll
    def "reports failure when there is no compatible variant"() {
        mavenRepo.module("test", "test", "1.2").publish()

        settingsFile << """
include 'a', 'b'
"""
        buildFile << """
def flavor = Attribute.of('flavor', String)

dependencies {
    compile project(':a')
}

allprojects {
    dependencies.attributesSchema.attribute(flavor)
    repositories {
        maven { url '${mavenRepo.uri}' }
    }
}

configurations.compile {
    attributes.attribute(flavor, 'preview')
    attributes.attribute(Attribute.of('artifactType', String), 'dll')
}

project(':a') {
    dependencies {
        compile project(':b')
        compile 'test:test:1.2'
        compile files('things.jar')
    }
    ${freeAndPaidFlavoredJars('a')}
}
project(':b') {
    ${freeAndPaidFlavoredJars('b')}
}

task show {
    doLast {
        println "files: " + ${expression}.collect { it.name }
    }
}
"""

        expect:
        fails("show")
        failure.assertHasCause("""No variants of project :a match the consumer attributes:
  - Configuration ':a:compile' variant free:
      - Incompatible attributes:
          - Required artifactType 'dll' and found incompatible value 'jar'.
          - Required flavor 'preview' and found incompatible value 'free'.
      - Other attribute:
          - Required usage 'compile' and found compatible value 'compile'.
  - Configuration ':a:compile' variant paid:
      - Incompatible attributes:
          - Required artifactType 'dll' and found incompatible value 'jar'.
          - Required flavor 'preview' and found incompatible value 'paid'.
      - Other attribute:
          - Required usage 'compile' and found compatible value 'compile'.""")

        failure.assertHasCause("""No variants of test:test:1.2 match the consumer attributes:
  - test:test:1.2 configuration runtime:
      - Incompatible attribute:
          - Required artifactType 'dll' and found incompatible value 'jar'.
      - Other attributes:
          - Required flavor 'preview' but no value provided.
          - Required usage 'compile' but no value provided.""")

        failure.assertHasCause("""No variants of things.jar match the consumer attributes:
  - things.jar:
      - Incompatible attribute:
          - Required artifactType 'dll' and found incompatible value 'jar'.
      - Other attributes:
          - Required flavor 'preview' but no value provided.
          - Required usage 'compile' but no value provided.""")

        where:
        expression                                                                                         | _
        "configurations.compile"                                                                           | _
        "configurations.compile.incoming.files"                                                            | _
        "configurations.compile.files"                                                                     | _
        "configurations.compile.resolve()"                                                                 | _
        "configurations.compile.files { true }"                                                            | _
        "configurations.compile.fileCollection { true }"                                                   | _
        "configurations.compile.resolvedConfiguration.getFiles { true }"                                   | _
        "configurations.compile.incoming.artifactView({}).files"                                           | _
        "configurations.compile.incoming.artifactView({componentFilter { true }}).files"                   | _
        "configurations.compile.incoming.artifactView({componentFilter { true }}).artifacts.artifactFiles" | _
    }

    @Unroll
    def "reports failure to resolve component when files are queried using #expression"() {
        buildFile << """
allprojects {
    repositories { maven { url '$mavenHttpRepo.uri' } }
}
dependencies {
    compile 'org:test:1.0+'
    compile 'org:test2:2.0'
}

task show {
    doLast {
        ${expression}.collect { it.name }
    }
}
"""

        given:
        mavenHttpRepo.getModuleMetaData('org', 'test').expectGetMissing()
        mavenHttpRepo.directory('org', 'test').expectGetMissing()
        def m = mavenHttpRepo.module('org', 'test2', '2.0').publish()
        m.pom.expectGetBroken()

        when:
        fails 'show'

        then:
        failure.assertHasCause("Could not resolve all files for configuration ':compile'.")
        failure.assertHasCause("Could not find any matches for org:test:1.0+ as no versions of org:test are available.")
        failure.assertHasCause("Could not resolve org:test2:2.0.")

        where:
        expression                                                                                         | _
        "configurations.compile"                                                                           | _
        "configurations.compile.incoming.files"                                                            | _
        "configurations.compile.files"                                                                     | _
        "configurations.compile.resolve()"                                                                 | _
        "configurations.compile.files { true }"                                                            | _
        "configurations.compile.fileCollection { true }"                                                   | _
        "configurations.compile.resolvedConfiguration.getFiles { true }"                                   | _
        "configurations.compile.incoming.artifactView({}).files"                                           | _
        "configurations.compile.incoming.artifactView({componentFilter { true }}).files"                   | _
        "configurations.compile.incoming.artifactView({componentFilter { true }}).artifacts.artifactFiles" | _
    }

    @Unroll
    def "reports failure to download artifact when files are queried using #expression"() {
        buildFile << """
allprojects {
    repositories { maven { url '$mavenHttpRepo.uri' } }
}
dependencies {
    compile 'org:test:1.0'
    compile 'org:test2:2.0'
}

task show {
    doLast {
        ${expression}.collect { it.name }
    }
}
"""

        given:
        def m1 = mavenHttpRepo.module('org', 'test', '1.0').publish()
        m1.pom.expectGet()
        m1.artifact.expectGetMissing()
        def m2 = mavenHttpRepo.module('org', 'test2', '2.0').publish()
        m2.pom.expectGet()
        m2.artifact.expectGet()

        when:
        fails 'show'

        then:
        failure.assertHasCause("Could not resolve all files for configuration ':compile'.")
        failure.assertHasCause("Could not find test.jar (org:test:1.0).")

        where:
        expression                                                                                         | _
        "configurations.compile"                                                                           | _
        "configurations.compile.incoming.files"                                                            | _
        "configurations.compile.files"                                                                     | _
        "configurations.compile.resolve()"                                                                 | _
        "configurations.compile.files { true }"                                                            | _
        "configurations.compile.fileCollection { true }"                                                   | _
        "configurations.compile.resolvedConfiguration.getFiles { true }"                                   | _
        "configurations.compile.incoming.artifactView({}).files"                                           | _
        "configurations.compile.incoming.artifactView({componentFilter { true }}).files"                   | _
        "configurations.compile.incoming.artifactView({componentFilter { true }}).artifacts.artifactFiles" | _
    }

    @Unroll
    def "reports failure to query file dependency when files are queried using #expression"() {
        buildFile << """
dependencies {
    compile files { throw new RuntimeException('broken') }
    compile files('lib.jar')
}

task show {
    doLast {
        ${expression}.collect { it.name }
    }
}
"""
        when:
        fails 'show'

        then:
        failure.assertHasCause("Could not resolve all files for configuration ':compile'.")
        failure.assertHasCause("broken")

        where:
        expression                                                                                         | _
        "configurations.compile"                                                                           | _
        "configurations.compile.incoming.files"                                                            | _
        "configurations.compile.files"                                                                     | _
        "configurations.compile.resolve()"                                                                 | _
        "configurations.compile.files { true }"                                                            | _
        "configurations.compile.fileCollection { true }"                                                   | _
        "configurations.compile.resolvedConfiguration.getFiles { true }"                                   | _
        "configurations.compile.incoming.artifactView({}).files"                                           | _
        "configurations.compile.incoming.artifactView({componentFilter { true }}).files"                   | _
        "configurations.compile.incoming.artifactView({componentFilter { true }}).artifacts.artifactFiles" | _
    }

    @Unroll
    def "reports multiple failures to resolve artifacts when files are queried using #expression"() {
        settingsFile << "include 'a'"
        buildFile << """
allprojects {
    repositories { maven { url '$mavenHttpRepo.uri' } }
}
dependencies {
    compile 'org:test:1.0'
    compile 'org:test2:2.0'
    compile files { throw new RuntimeException('broken 1') }
    compile files { throw new RuntimeException('broken 2') }
    compile project(':a')
}

project(':a') {
    configurations.compile.outgoing.variants {
        v1 { }
        v2 { }
    }
}

task show {
    doLast {
        ${expression}.collect { it.name }
    }
}
"""

        given:
        def m1 = mavenHttpRepo.module('org', 'test', '1.0').publish()
        m1.pom.expectGet()
        m1.artifact.expectGetMissing()
        def m2 = mavenHttpRepo.module('org', 'test2', '2.0').publish()
        m2.pom.expectGet()
        m2.artifact.expectGetBroken()

        when:
        fails 'show'

        then:
        failure.assertHasCause("Could not resolve all files for configuration ':compile'.")
        failure.assertHasCause("Could not find test.jar (org:test:1.0).")
        failure.assertHasCause("Could not download test2.jar (org:test2:2.0)")
        failure.assertHasCause("broken 1")
        failure.assertHasCause("broken 2")
        failure.assertHasCause("More than one variant of project :a matches the consumer attributes")

        where:
        expression                                                                                         | _
        "configurations.compile"                                                                           | _
        "configurations.compile.incoming.files"                                                            | _
        "configurations.compile.files"                                                                     | _
        "configurations.compile.resolve()"                                                                 | _
        "configurations.compile.files { true }"                                                            | _
        "configurations.compile.fileCollection { true }"                                                   | _
        "configurations.compile.resolvedConfiguration.getFiles { true }"                                   | _
        "configurations.compile.incoming.artifactView({}).files"                                           | _
        "configurations.compile.incoming.artifactView({componentFilter { true }}).files"                   | _
        "configurations.compile.incoming.artifactView({componentFilter { true }}).artifacts.artifactFiles" | _
    }

    private String freeAndPaidFlavoredJars(String prefix) {
        """
            task freeJar(type: Jar) { archiveName = '$prefix-free.jar' }
            task paidJar(type: Jar) { archiveName = '$prefix-paid.jar' }
            tasks.withType(Jar) { destinationDir = buildDir }
            configurations.compile.outgoing.variants {
                free {
                    attributes.attribute(flavor, 'free')
                    artifact freeJar
                }
                paid {
                    attributes.attribute(flavor, 'paid')
                    artifact paidJar
                }
            }
        """
    }
}
