package org.implab.gradle.variants;

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

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

class VariantSourcesPluginFunctionalTest extends AbstractFunctionalTest {

    @Test
    void exposesDerivedViewsAndStableSourceSetProvider() throws Exception {
        writeSettings("variant-sources-derived-views");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantSourcesPlugin

                def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
                variantsExt.layers.create('main')
                variantsExt.layers.create('test')
                variantsExt.roles.create('production')
                variantsExt.roles.create('test')

                variantsExt.variant('browser') {
                    role('production') { layers('main') }
                    role('test') { layers('main', 'test') }
                }

                def lines = []

                variantSources.whenFinalized { ctx ->
                    lines << "units=" + ctx.compileUnits.units
                        .collect { "${it.variant().name}:${it.layer().name}" }
                        .sort()
                        .join(',')

                    def browser = ctx.variants.variants.find { it.name == 'browser' }
                    def production = ctx.variants.roles.find { it.name == 'production' }
                    def mainLayer = ctx.variants.layers.find { it.name == 'main' }
                    def projection = ctx.roleProjections.getProjection(browser, production)
                    def unit = ctx.compileUnits.requireUnit(browser, mainLayer)

                    def left = ctx.sourceSets.getSourceSet(unit)
                    def right = ctx.sourceSets.getSourceSet(unit)

                    lines << "projectionUnits=" + ctx.roleProjections.getUnits(projection)
                        .collect { it.layer().name }
                        .sort()
                        .join(',')
                    lines << "mainSourceSet=" + left.name
                    lines << "sameProvider=" + left.is(right)
                }

                afterEvaluate {
                    variantSources.whenFinalized { ctx ->
                        lines << "late:variants=" + ctx.variants.variants.collect { it.name }.sort().join(',')
                    }
                }

                tasks.register('probe') {
                    doLast {
                        lines.each { println(it) }
                    }
                }
                """);

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

        assertTrue(result.getOutput().contains("units=browser:main,browser:test"));
        assertTrue(result.getOutput().contains("projectionUnits=main"));
        assertTrue(result.getOutput().contains("mainSourceSet=browserMain"));
        assertTrue(result.getOutput().contains("sameProvider=true"));
        assertTrue(result.getOutput().contains("late:variants=browser"));
    }

    @Test
    void appliesSelectorPrecedenceForFutureMaterialization() throws Exception {
        writeSettings("variant-sources-precedence");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantSourcesPlugin

                def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
                variantsExt.layers.create('main')
                variantsExt.layers.create('test')
                variantsExt.roles.create('production')
                variantsExt.roles.create('test')

                variantsExt.variant('browser') {
                    role('production') { layers('main') }
                    role('test') { layers('main', 'test') }
                }

                variantsExt.variant('node') {
                    role('production') { layers('main') }
                }

                def events = []

                variantSources {
                    variant('browser') {
                        events << "variant:" + name
                    }
                    layer('main') {
                        events << "layer:" + name
                    }
                    unit('browser', 'main') {
                        events << "unit:" + name
                    }
                }

                afterEvaluate {
                    variantSources.whenFinalized { ctx ->
                        def browser = ctx.variants.variants.find { it.name == 'browser' }
                        def node = ctx.variants.variants.find { it.name == 'node' }
                        def mainLayer = ctx.variants.layers.find { it.name == 'main' }
                        def testLayer = ctx.variants.layers.find { it.name == 'test' }

                        def browserMain = ctx.sourceSets.getSourceSet(ctx.compileUnits.requireUnit(browser, mainLayer)).get()
                        def browserTest = ctx.sourceSets.getSourceSet(ctx.compileUnits.requireUnit(browser, testLayer)).get()
                        def nodeMain = ctx.sourceSets.getSourceSet(ctx.compileUnits.requireUnit(node, mainLayer)).get()
                        def bySourceSet = events.groupBy { it.split(':', 2)[1] }

                        println("browserMain=" + bySourceSet[browserMain.name].collect { it.split(':', 2)[0] }.join(','))
                        println("browserTest=" + bySourceSet[browserTest.name].collect { it.split(':', 2)[0] }.join(','))
                        println("nodeMain=" + bySourceSet[nodeMain.name].collect { it.split(':', 2)[0] }.join(','))
                    }
                }
                """);

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

        assertTrue(result.getOutput().contains("browserMain=variant,layer,unit"));
        assertTrue(result.getOutput().contains("browserTest=variant"));
        assertTrue(result.getOutput().contains("nodeMain=layer"));
    }

    @Test
    void failsLateConfigurationByDefaultAfterMaterialization() throws Exception {
        writeSettings("variant-sources-late-fail");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantSourcesPlugin

                def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
                variantsExt.layers.create('main')
                variantsExt.roles.create('production')
                variantsExt.variant('browser') {
                    role('production') { layers('main') }
                }

                afterEvaluate {
                    variantSources.whenFinalized { ctx ->
                        def browser = ctx.variants.variants.find { it.name == 'browser' }
                        def mainLayer = ctx.variants.layers.find { it.name == 'main' }
                        def unit = ctx.compileUnits.requireUnit(browser, mainLayer)

                        ctx.sourceSets.getSourceSet(unit).get()
                        variantSources.layer('main') {
                            declareOutputs('late')
                        }
                    }
                }
                """);

        assertBuildFails("Source sets for [layer=main] layer already materialized", "help");
    }

    @Test
    void allowsLateConfigurationWhenSelectedBeforeFirstSelector() throws Exception {
        writeSettings("variant-sources-late-allow");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantSourcesPlugin

                def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
                variantsExt.layers.create('main')
                variantsExt.roles.create('production')
                variantsExt.variant('browser') {
                    role('production') { layers('main') }
                }

                variantSources {
                    lateConfigurationPolicy {
                        allowLateConfiguration()
                    }
                }

                afterEvaluate {
                    variantSources.whenFinalized { ctx ->
                        def browser = ctx.variants.variants.find { it.name == 'browser' }
                        def mainLayer = ctx.variants.layers.find { it.name == 'main' }
                        def unit = ctx.compileUnits.requireUnit(browser, mainLayer)

                        def sourceSet = ctx.sourceSets.getSourceSet(unit).get()
                        variantSources.layer('main') {
                            declareOutputs('late')
                        }
                        sourceSet.output('late')
                        println('lateAllowed=ok')
                    }
                }
                """);

        BuildResult result = runner("help").build();
        assertTrue(result.getOutput().contains("lateAllowed=ok"));
    }

    @Test
    void warnsAndAppliesLateConfigurationWhenWarnModeSelected() throws Exception {
        writeSettings("variant-sources-late-warn");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantSourcesPlugin

                def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
                variantsExt.layers.create('main')
                variantsExt.roles.create('production')
                variantsExt.variant('browser') {
                    role('production') { layers('main') }
                }

                variantSources {
                    lateConfigurationPolicy {
                        warnOnLateConfiguration()
                    }
                }

                afterEvaluate {
                    variantSources.whenFinalized { ctx ->
                        def browser = ctx.variants.variants.find { it.name == 'browser' }
                        def mainLayer = ctx.variants.layers.find { it.name == 'main' }
                        def unit = ctx.compileUnits.requireUnit(browser, mainLayer)

                        def sourceSet = ctx.sourceSets.getSourceSet(unit).get()
                        variantSources.layer('main') {
                            declareOutputs('late')
                        }
                        sourceSet.output('late')
                        println('lateWarn=ok')
                    }
                }
                """);

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

        assertTrue(result.getOutput().contains("Source sets for [layer=main] layer already materialized"));
        assertTrue(result.getOutput().contains("lateWarn=ok"));
    }

    @Test
    void rejectsChangingLateConfigurationPolicyAfterFirstSelector() throws Exception {
        writeSettings("variant-sources-late-policy-fixed");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantSourcesPlugin

                def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
                variantsExt.layers.create('main')
                variantsExt.roles.create('production')
                variantsExt.variant('browser') {
                    role('production') { layers('main') }
                }

                variantSources {
                    variant('browser') {
                        declareOutputs('js')
                    }
                    lateConfigurationPolicy {
                        allowLateConfiguration()
                    }
                }
                """);

        assertBuildFails("Lazy configuration policy already applied", "help");
    }

    @Test
    void failsOnProjectedNameCollisionByDefault() throws Exception {
        writeSettings("variant-sources-name-collision-fail");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantSourcesPlugin

                def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
                variantsExt.layers.create('variantBar')
                variantsExt.layers.create('bar')
                variantsExt.roles.create('production')

                variantsExt.variant('foo') {
                    role('production') { layers('variantBar') }
                }
                variantsExt.variant('fooVariant') {
                    role('production') { layers('bar') }
                }
                """);

        assertBuildFails("The same source set names are produced by different compile units", "help");
    }

    @Test
    void resolvesProjectedNameCollisionDeterministicallyWhenConfigured() throws Exception {
        writeSettings("variant-sources-name-collision-resolve");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantSourcesPlugin

                def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
                variantsExt.layers.create('variantBar')
                variantsExt.layers.create('bar')
                variantsExt.roles.create('production')

                variantsExt.variant('foo') {
                    role('production') { layers('variantBar') }
                }
                variantsExt.variant('fooVariant') {
                    role('production') { layers('bar') }
                }

                variantSources {
                    namingPolicy {
                        resolveNameCollision()
                    }
                }

                afterEvaluate {
                    variantSources.whenFinalized { ctx ->
                        def foo = ctx.variants.variants.find { it.name == 'foo' }
                        def fooVariant = ctx.variants.variants.find { it.name == 'fooVariant' }
                        def variantBar = ctx.variants.layers.find { it.name == 'variantBar' }
                        def bar = ctx.variants.layers.find { it.name == 'bar' }

                        def later = ctx.compileUnits.requireUnit(fooVariant, bar)
                        def earlier = ctx.compileUnits.requireUnit(foo, variantBar)

                        println("map1=" + later.variant().name + ":" + later.layer().name + "->" + ctx.sourceSets.getSourceSet(later).name)
                        println("map2=" + earlier.variant().name + ":" + earlier.layer().name + "->" + ctx.sourceSets.getSourceSet(earlier).name)
                    }
                }
                """);

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

        assertTrue(result.getOutput().contains("map1=fooVariant:bar->fooVariantBar2"));
        assertTrue(result.getOutput().contains("map2=foo:variantBar->fooVariantBar"));
    }

    @Test
    void failsOnUnknownVariantSelectorTarget() throws Exception {
        writeSettings("variant-sources-missing-variant");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantSourcesPlugin

                def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
                variantsExt.layers.create('main')
                variantsExt.roles.create('production')
                variantsExt.variant('browser') {
                    role('production') { layers('main') }
                }

                variantSources {
                    variant('missing') {
                        declareOutputs('js')
                    }
                }
                """);

        assertBuildFails("Variant 'missing' is't declared", "help");
    }

    @Test
    void failsOnUnknownLayerSelectorTarget() throws Exception {
        writeSettings("variant-sources-missing-layer");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantSourcesPlugin

                def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
                variantsExt.layers.create('main')
                variantsExt.roles.create('production')
                variantsExt.variant('browser') {
                    role('production') { layers('main') }
                }

                variantSources {
                    layer('missing') {
                        declareOutputs('js')
                    }
                }
                """);

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

    @Test
    void failsOnUndeclaredCompileUnitSelectorTarget() throws Exception {
        writeSettings("variant-sources-missing-unit");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantSourcesPlugin

                def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
                variantsExt.layers.create('main')
                variantsExt.layers.create('test')
                variantsExt.roles.create('production')
                variantsExt.variant('browser') {
                    role('production') { layers('main') }
                }

                variantSources {
                    unit('browser', 'test') {
                        declareOutputs('js')
                    }
                }
                """);

        assertBuildFails("The CompileUnit isn't declared for variant 'browser', layer 'test'", "help");
    }

    @Test
    void rejectsChangingNamingPolicyAfterContextBecomesObservable() throws Exception {
        writeSettings("variant-sources-name-policy-fixed");
        writeBuildFile("""
                apply plugin: org.implab.gradle.variants.VariantSourcesPlugin

                def variantsExt = extensions.getByType(org.implab.gradle.variants.core.VariantsExtension)
                variantsExt.layers.create('main')
                variantsExt.roles.create('production')
                variantsExt.variant('browser') {
                    role('production') { layers('main') }
                }

                variantSources.whenFinalized {
                    variantSources.namingPolicy {
                        resolveNameCollision()
                    }
                }
                """);

        assertBuildFails("Naming policy already applied", "help");
    }
}
