package org.implab.gradle.variants;

import static org.junit.jupiter.api.Assertions.assertTrue;

import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.TaskOutcome;
import org.junit.jupiter.api.Test;

class VariantArtifactsPluginFunctionalTest extends AbstractFunctionalTest {

    @Test
    void gradleReferenceLazyOutgoingConfigurationAllowsSecondaryArtifactSelection() throws Exception {
        writeFile("settings.gradle", """
                rootProject.name = 'gradle-reference-outgoing-resolution'
                include 'producer', 'consumer'
                """);
        writeFile("producer/inputs/typesPackage", "types\n");
        writeFile("producer/inputs/js", "js\n");
        writeBuildFile("""
                import org.gradle.api.attributes.Attribute

                def variantAttr = Attribute.of('test.variant', String)
                def slotAttr = Attribute.of('test.slot', String)

                project(':producer') {
                    def browserElements = configurations.consumable('browserElements')

                    println('reference: registered browserElements provider')

                    browserElements.configure { configuration ->
                        println('reference: configuring browserElements')

                        configuration.attributes.attribute(variantAttr, 'browser')
                        configuration.outgoing.attributes.attribute(slotAttr, 'typesPackage')
                        configuration.outgoing.artifact(layout.projectDirectory.file('inputs/typesPackage'))

                        configuration.outgoing.variants.create('js') { secondary ->
                            println('reference: creating js outgoing variant')

                            secondary.attributes.attribute(slotAttr, 'js')
                            secondary.artifact(layout.projectDirectory.file('inputs/js'))
                        }
                    }
                }

                project(':consumer') {
                    configurations {
                        compileView {
                            canBeResolved = true
                            canBeConsumed = false
                            canBeDeclared = true
                            attributes {
                                attribute(variantAttr, 'browser')
                                attribute(slotAttr, 'typesPackage')
                            }
                        }
                    }

                    dependencies {
                        compileView project(':producer')
                    }

                    tasks.register('probe') {
                        doLast {
                            println('reference: resolving primary files')

                            def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')

                            println('reference: resolving secondary files')

                            def jsFiles = configurations.compileView.incoming.artifactView {
                                attributes {
                                    attribute(slotAttr, 'js')
                                }
                            }.files.files.collect { it.name }.sort().join(',')

                            println('compileFiles=' + compileFiles)
                            println('jsFiles=' + jsFiles)
                        }
                    }
                }
                """);

        BuildResult result = runner(":consumer:probe").build();
        var output = result.getOutput();
        var registered = output.indexOf("reference: registered browserElements provider");
        var resolvingPrimary = output.indexOf("reference: resolving primary files");
        var configuring = output.indexOf("reference: configuring browserElements");
        var creatingSecondary = output.indexOf("reference: creating js outgoing variant");
        var resolvingSecondary = output.indexOf("reference: resolving secondary files");

        assertTrue(registered >= 0);
        assertTrue(resolvingPrimary >= 0);
        assertTrue(configuring >= 0);
        assertTrue(creatingSecondary >= 0);
        assertTrue(resolvingSecondary >= 0);
        assertTrue(registered < resolvingPrimary);
        assertTrue(resolvingPrimary < configuring);
        assertTrue(configuring < creatingSecondary);
        assertTrue(creatingSecondary < resolvingSecondary);
        assertTrue(output.contains("compileFiles=typesPackage"));
        assertTrue(output.contains("jsFiles=js"));
    }

    @Test
    void gradleReferenceRegisteredSecondaryArtifactVariantIsNotRealizedBeforeResolution() throws Exception {
        // Gradle issue: https://github.com/gradle/gradle/issues/27441
        // Registered outgoing artifact variants are not realized before dependency resolution.
        writeFile("settings.gradle", """
                rootProject.name = 'gradle-reference-registered-secondary-variant'
                include 'producer', 'consumer'
                """);
        writeFile("producer/inputs/typesPackage", "types\n");
        writeFile("producer/inputs/js", "js\n");
        writeFile("build.gradle", """
                import org.gradle.api.attributes.Attribute

                def variantAttr = Attribute.of('test.variant', String)
                def slotAttr = Attribute.of('test.slot', String)

                project(':producer') {
                    def browserElements = configurations.consumable('browserElements')

                    browserElements.configure { configuration ->
                        configuration.attributes.attribute(variantAttr, 'browser')
                        configuration.outgoing.attributes.attribute(slotAttr, 'typesPackage')
                        configuration.outgoing.artifact(layout.projectDirectory.file('inputs/typesPackage'))

                        configuration.outgoing.variants.register('js') { secondary ->
                            secondary.attributes.attribute(slotAttr, 'js')
                            secondary.artifact(layout.projectDirectory.file('inputs/js'))
                        }
                    }
                }

                project(':consumer') {
                    configurations {
                        compileView {
                            canBeResolved = true
                            canBeConsumed = false
                            canBeDeclared = true
                            attributes {
                                attribute(variantAttr, 'browser')
                                attribute(slotAttr, 'typesPackage')
                            }
                        }
                    }

                    dependencies {
                        compileView project(':producer')
                    }

                    tasks.register('probe') {
                        doLast {
                            def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
                            def jsFiles = configurations.compileView.incoming.artifactView {
                                attributes {
                                    attribute(slotAttr, 'js')
                                }
                            }.files.files.collect { it.name }.sort().join(',')

                            println('compileFiles=' + compileFiles)
                            println('jsFiles=' + jsFiles)
                        }
                    }
                }
                """);

        assertBuildFails("Cannot create variant 'js' after dependency configuration ':producer:browserElements' has been resolved",
                ":consumer:probe");
    }

    @Test
    void materializesPrimaryAndSecondarySlotsAndInvokesOutgoingHooks() throws Exception {
        writeSettings("variant-artifacts-slots");
        writeFile("inputs/base.js", "console.log('base')\n");
        writeFile("inputs/amd.js", "console.log('amd')\n");
        writeFile("inputs/mainJs.txt", "mainJs marker\n");
        writeFile("inputs/amdJs.txt", "amdJs marker\n");
        writeBuildFile("""
                import org.gradle.api.attributes.Attribute

                apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin

                def variantAttr = Attribute.of('test.variant', String)
                def slotAttr = Attribute.of('test.slot', String)

                variants.layers.create('mainBase')
                variants.layers.create('mainAmd')
                variants.roles.create('main')
                variants.roles.create('test')
                variants.variant('browser') {
                    role('main') {
                        layers('mainBase', 'mainAmd')
                    }
                }

                variantSources {
                    layer('mainBase') {
                        sourceSet {
                            declareOutputs('js')
                            registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
                        }
                    }
                    layer('mainAmd') {
                        sourceSet {
                            declareOutputs('js')
                            registerOutput('js', layout.projectDirectory.file('inputs/amd.js'))
                        }
                    }
                }

                variantArtifacts {
                    variant('browser') {
                        primarySlot('mainJs') {
                            fromRole('main') {
                                output('js')
                            }
                            from(layout.projectDirectory.file('inputs/mainJs.txt'))
                        }
                        slot('amdJs') {
                            fromLayer('mainAmd') {
                                output('js')
                            }
                            from(layout.projectDirectory.file('inputs/amdJs.txt'))
                        }
                    }

                    whenOutgoingConfiguration { publication ->
                        publication.configuration {
                            attributes.attribute(variantAttr, publication.variant.name)
                        }
                    }

                    whenOutgoingSlot { publication ->
                        def slotName = publication.artifactSlot.slot.name
                        publication.artifactAttributes {
                            attribute(slotAttr, slotName)
                        }
                    }
                }

                tasks.register('probe') {
                    dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_mainJs'
                    dependsOn 'assembleVariantArtifactSlot_v7_browser_s5_amdJs'

                    doLast {
                        def mainDir = layout.buildDirectory.dir('variant-assemblies/browser/mainJs').get().asFile
                        def amdDir = layout.buildDirectory.dir('variant-assemblies/browser/amdJs').get().asFile

                        assert new File(mainDir, 'base.js').exists()
                        assert new File(mainDir, 'amd.js').exists()
                        assert new File(mainDir, 'mainJs.txt').exists()

                        assert !new File(amdDir, 'base.js').exists()
                        assert new File(amdDir, 'amd.js').exists()
                        assert new File(amdDir, 'amdJs.txt').exists()

                        def elements = configurations.getByName('browserElements')
                        def amdVariant = elements.outgoing.variants.getByName('amdJs')

                        println('variantAttr=' + elements.attributes.getAttribute(variantAttr))
                        println('primarySlotAttr=' + elements.outgoing.attributes.getAttribute(slotAttr))
                        println('amdSlotAttr=' + amdVariant.attributes.getAttribute(slotAttr))
                        println('configurations=' + configurations.matching { it.name == 'browserElements' }.collect { it.name }.join(','))
                        println('secondaryVariants=' + elements.outgoing.variants.collect { it.name }.sort().join(','))
                    }
                }
                """);

        BuildResult result = runner("probe").build();

        assertTrue(result.getOutput().contains("variantAttr=browser"));
        assertTrue(result.getOutput().contains("primarySlotAttr=mainJs"));
        assertTrue(result.getOutput().contains("amdSlotAttr=amdJs"));
        assertTrue(result.getOutput().contains("configurations=browserElements"));
        assertTrue(result.getOutput().contains("secondaryVariants=amdJs"));
        assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
    }

    @Test
    void outgoingSlotHookFollowsMaterializedGradleArtifactVariants() throws Exception {
        writeSettings("variant-artifacts-materialized-gradle-variant");
        writeFile("inputs/typesPackage", "types\n");
        writeFile("inputs/js", "js\n");
        writeBuildFile("""
                import org.gradle.api.attributes.Attribute

                apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin

                def slotAttr = Attribute.of('test.slot', String)

                variants.layers.create('main')
                variants.roles.create('main')
                variants.variant('browser') {
                    role('main') {
                        layers('main')
                    }
                }

                variantArtifacts {
                    variant('browser') {
                        primarySlot('typesPackage') {
                            from(layout.projectDirectory.file('inputs/typesPackage'))
                        }
                    }

                    whenOutgoingConfiguration { publication ->
                        publication.configuration {
                            outgoing.variants.create('js') { secondary ->
                                secondary.artifact(layout.projectDirectory.file('inputs/js'))
                            }
                        }
                    }

                    whenOutgoingSlot { publication ->
                        publication.artifactAttributes {
                            attribute(slotAttr, publication.artifactSlot.slot.name)
                        }
                    }
                }

                tasks.register('probe') {
                    doLast {
                        def elements = configurations.getByName('browserElements')
                        def jsVariant = elements.outgoing.variants.getByName('js')

                        println('primarySlotAttr=' + elements.outgoing.attributes.getAttribute(slotAttr))
                        println('jsSlotAttr=' + jsVariant.attributes.getAttribute(slotAttr))
                    }
                }
                """);

        BuildResult result = runner("probe").build();

        assertTrue(result.getOutput().contains("primarySlotAttr=typesPackage"));
        assertTrue(result.getOutput().contains("jsSlotAttr=js"));
        assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
    }

    @Test
    void allowsSingleSlotVariantWithoutExplicitPrimarySlot() throws Exception {
        writeSettings("variant-artifacts-single-slot");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin

                variants.layers.create('main')
                variants.roles.create('main')
                variants.variant('browser') {
                    role('main') {
                        layers('main')
                    }
                }

                variantSources.layer('main') {
                    sourceSet {
                        declareOutputs('types')
                    }
                }

                variantArtifacts {
                    variant('browser') {
                        slot('typesPackage') {
                            fromVariant {
                                output('types')
                            }
                        }
                    }
                }

                tasks.register('probe') {
                    doLast {
                        variantArtifacts.whenAvailable { ctx ->
                            def browser = objects.named(org.implab.gradle.variants.core.Variant, 'browser')
                            println('primary=' + ctx.findOutgoing(browser).get().primarySlot.get().name)
                        }
                    }
                }
                """);

        BuildResult result = runner("probe").build();

        assertTrue(result.getOutput().contains("primary=typesPackage"));
    }

    @Test
    void materializesDirectSlotInputsWithoutVariantSourceBindings() throws Exception {
        writeSettings("variant-artifacts-direct-input");
        writeFile("inputs/bundle.js", "console.log('bundle')\n");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin

                variants.layers.create('main')
                variants.roles.create('main')
                variants.variant('browser') {
                    role('main') {
                        layers('main')
                    }
                }

                variantArtifacts {
                    variant('browser') {
                        primarySlot('bundle') {
                            from(layout.projectDirectory.file('inputs/bundle.js'))
                        }
                    }
                }

                tasks.register('probe') {
                    dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_bundle'

                    doLast {
                        def bundleDir = layout.buildDirectory.dir('variant-assemblies/browser/bundle').get().asFile
                        assert new File(bundleDir, 'bundle.js').exists()
                    }
                }
                """);

        BuildResult result = runner("probe").build();

        assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
    }

    @Test
    void combinesDirectAndTopologyAwareSlotInputs() throws Exception {
        writeSettings("variant-artifacts-combined-inputs");
        writeFile("inputs/base.js", "console.log('base')\n");
        writeFile("inputs/marker.txt", "marker\n");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin

                variants.layers.create('main')
                variants.roles.create('main')
                variants.variant('browser') {
                    role('main') {
                        layers('main')
                    }
                }

                variantSources.layer('main') {
                    sourceSet {
                        declareOutputs('js')
                        registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
                    }
                }

                variantArtifacts {
                    variant('browser') {
                        primarySlot('bundle') {
                            fromVariant {
                                output('js')
                            }
                            from(layout.projectDirectory.file('inputs/marker.txt'))
                        }
                    }
                }

                tasks.register('probe') {
                    dependsOn 'assembleVariantArtifactSlot_v7_browser_s6_bundle'

                    doLast {
                        def bundleDir = layout.buildDirectory.dir('variant-assemblies/browser/bundle').get().asFile
                        assert new File(bundleDir, 'base.js').exists()
                        assert new File(bundleDir, 'marker.txt').exists()
                    }
                }
                """);

        BuildResult result = runner("probe").build();

        assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
    }

    @Test
    void failsOnUnknownVariantReference() throws Exception {
        writeSettings("variant-artifacts-missing-variant");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin

                variants.layers.create('main')

                variantArtifacts {
                    variant('browser') {
                        slot('mainJs') {
                            fromVariant {
                                output('js')
                            }
                        }
                    }
                }
                """);

        assertBuildFails("isn't declared", "help");
    }

    @Test
    void failsOnUnknownRoleReference() throws Exception {
        writeSettings("variant-artifacts-missing-role");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin

                variants.layers.create('main')
                variants.roles.create('main')
                variants.variant('browser') {
                    role('main') {
                        layers('main')
                    }
                }

                variantArtifacts {
                    variant('browser') {
                        slot('mainJs') {
                            fromRole('test') {
                                output('js')
                            }
                        }
                    }
                }
                """);

        assertBuildFails("Role projection for variant 'browser' and role 'test' not found", "help");
    }

    @Test
    void failsWhenPrimarySlotIsMissingForMultipleSlots() throws Exception {
        writeSettings("variant-artifacts-missing-primary");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin

                variants.layers.create('main')
                variants.roles.create('main')
                variants.variant('browser') {
                    role('main') {
                        layers('main')
                    }
                }

                variantSources.layer('main') {
                    sourceSet {
                        declareOutputs('types', 'js')
                    }
                }

                variantArtifacts {
                    variant('browser') {
                        slot('typesPackage') {
                            fromVariant {
                                output('types')
                            }
                        }
                        slot('js') {
                            fromVariant {
                                output('js')
                            }
                        }
                    }
                }

                tasks.register('probe') {
                    doLast {
                        variantArtifacts.whenAvailable { ctx ->
                            def browser = objects.named(org.implab.gradle.variants.core.Variant, 'browser')
                            ctx.findOutgoing(browser).get().primarySlot.get()
                        }
                    }
                }
                """);

        assertBuildFails("Multiple slots declared for browser, please specify primary slot explicitly", "probe");
    }

    @Test
    void failsOnLayerReferenceOutsideVariantTopology() throws Exception {
        writeSettings("variant-artifacts-layer-outside-topology");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin

                variants.layers.create('mainBase')
                variants.layers.create('extra')
                variants.roles.create('main')
                variants.variant('browser') {
                    role('main') {
                        layers('mainBase')
                    }
                }

                variantArtifacts {
                    variant('browser') {
                        slot('extraJs') {
                            fromLayer('extra') {
                                output('js')
                            }
                        }
                    }
                }
                """);

        assertBuildFails("Compile unit for variant 'browser' and layer 'extra' not found", "help");
    }

    @Test
    void preservesPrimaryResolutionAndAllowsSecondaryArtifactSelection() throws Exception {
        writeFile("settings.gradle", """
                rootProject.name = 'variant-artifacts-resolution'
                include 'producer', 'consumer'
                """);
        writeFile("producer/inputs/types.d.ts", "export type Foo = string\n");
        writeFile("producer/inputs/index.js", "export const foo = 'bar'\n");
        writeBuildFile("""
                import org.gradle.api.attributes.Attribute

                def variantAttr = Attribute.of('test.variant', String)
                def slotAttr = Attribute.of('test.slot', String)

                subprojects {
                    apply plugin: org.implab.gradle.variants.VariantArtifactsPlugin
                }

                project(':producer') {
                    variants.layers.create('main')
                    variants.roles.create('main')
                    variants.variant('browser') {
                        role('main') {
                            layers('main')
                        }
                    }

                    variantSources.layer('main') {
                        sourceSet {
                            declareOutputs('types', 'js')
                            registerOutput('types', layout.projectDirectory.file('inputs/types.d.ts'))
                            registerOutput('js', layout.projectDirectory.file('inputs/index.js'))
                        }
                    }

                    variantArtifacts {
                        variant('browser') {
                            primarySlot('typesPackage') {
                                fromVariant {
                                    output('types')
                                }
                            }
                            slot('js') {
                                fromVariant {
                                    output('js')
                                }
                            }
                        }

                        whenOutgoingConfiguration { publication ->
                            publication.configuration {
                                attributes.attribute(variantAttr, publication.variant.name)
                            }
                        }

                        whenOutgoingSlot { publication ->
                            publication.artifactAttributes {
                                attribute(slotAttr, publication.artifactSlot.slot.name)
                            }
                        }
                    }

                }

                project(':consumer') {
                    configurations {
                        compileView {
                            canBeResolved = true
                            canBeConsumed = false
                            canBeDeclared = true
                            attributes {
                                attribute(variantAttr, 'browser')
                                attribute(slotAttr, 'typesPackage')
                            }
                        }
                    }

                    dependencies {
                        compileView project(':producer')
                    }

                    tasks.register('probe') {
                        doLast {
                            def compileFiles = configurations.compileView.files.collect { it.name }.sort().join(',')
                            def jsFiles = configurations.compileView.incoming.artifactView {
                                attributes {
                                    attribute(slotAttr, 'js')
                                }
                            }.files.files.collect { it.name }.sort().join(',')

                            println('compileFiles=' + compileFiles)
                            println('jsFiles=' + jsFiles)
                        }
                    }
                }
                """);

        BuildResult result = runner(":consumer:probe").build();

        assertTrue(result.getOutput().contains("compileFiles=typesPackage"));
        assertTrue(result.getOutput().contains("jsFiles=js"));
    }
}
