package org.implab.gradle.common.sources;

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

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;

import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.GradleRunner;
import org.gradle.testkit.runner.TaskOutcome;
import org.gradle.testkit.runner.UnexpectedBuildFailure;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

class VariantsArtifactsPluginFunctionalTest {
    private static final String SETTINGS_FILE = "settings.gradle";
    private static final String BUILD_FILE = "build.gradle";
    private static final String ROOT_NAME = "rootProject.name = 'variants-artifacts-fixture'\n";

    @TempDir
    Path testProjectDir;

    @Test
    void materializesVariantArtifactsAndInvokesOutgoingHooks() throws Exception {
        writeFile(SETTINGS_FILE, ROOT_NAME);
        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");
        writeFile(BUILD_FILE, """
            import org.gradle.api.attributes.Attribute

            plugins {
                id 'org.implab.gradle-variants-artifacts'
            }

            variants {
                layers('mainBase', 'mainAmd')
                roles('main', 'test')

                variant('browser') {
                    role('main') {
                        layers('mainBase', 'mainAmd')
                    }
                }
            }

            variantSources {
                bind('mainBase') {
                    configureSourceSet {
                        declareOutputs('js')
                    }
                }

                bind('mainAmd') {
                    configureSourceSet {
                        declareOutputs('js')
                    }
                }

                whenBound { ctx ->
                    if (ctx.sourceSetName() == 'browserMainBase') {
                        ctx.configureSourceSet {
                            registerOutput('js', layout.projectDirectory.file('inputs/base.js'))
                        }
                    }

                    if (ctx.sourceSetName() == 'browserMainAmd') {
                        ctx.configureSourceSet {
                            registerOutput('js', layout.projectDirectory.file('inputs/amd.js'))
                        }
                    }
                }
            }

            variantArtifacts {
                variant('browser') {
                    primarySlot('mainJs') {
                        fromRole('main') {
                            output('js')
                        }
                    }

                    slot('amdJs') {
                        fromLayer('mainAmd') {
                            output('js')
                        }
                    }
                }

                whenOutgoingVariant { publication ->
                    publication.slots().each { slotPublication ->
                        slotPublication.configureTask {
                            from(layout.projectDirectory.file("inputs/${slotPublication.slotName()}.txt"))
                        }

                        slotPublication.configureArtifactAttributes {
                            attribute(Attribute.of('test.slot', String), slotPublication.slotName())
                        }
                    }
                }
            }

            tasks.register('probe') {
                dependsOn 'processBrowserMainJs', 'processBrowserAmdJs'

                doLast {
                    def mainDir = layout.buildDirectory.dir('variant-artifacts/browser/mainJs').get().asFile
                    def amdDir = layout.buildDirectory.dir('variant-artifacts/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 primaryAttr = elements.attributes.getAttribute(Attribute.of('test.slot', String))
                    def amdVariant = elements.outgoing.variants.getByName('amdJs')
                    def amdAttr = amdVariant.attributes.getAttribute(Attribute.of('test.slot', String))

                    println('primarySlot=' + variantArtifacts.requireVariant('browser').requirePrimarySlotName())
                    println('primaryAttr=' + primaryAttr)
                    println('amdAttr=' + amdAttr)
                    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("primarySlot=mainJs"));
        assertTrue(result.getOutput().contains("primaryAttr=mainJs"));
        assertTrue(result.getOutput().contains("amdAttr=amdJs"));
        assertTrue(result.getOutput().contains("configurations=browserElements"));
        assertTrue(result.getOutput().contains("secondaryVariants=amdJs"));
        assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
    }

    @Test
    void allowsSingleSlotVariantWithoutExplicitPrimarySlot() throws Exception {
        writeFile(SETTINGS_FILE, ROOT_NAME);
        writeFile(BUILD_FILE, """
            plugins {
                id 'org.implab.gradle-variants-artifacts'
            }

            variants {
                layer('main')

                variant('browser') {
                    role('main') {
                        layers('main')
                    }
                }
            }

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

            tasks.register('probe') {
                doLast {
                    println('primary=' + variantArtifacts.requireVariant('browser').requirePrimarySlotName())
                }
            }
            """);

        BuildResult result = runner("probe").build();
        assertTrue(result.getOutput().contains("primary=typesPackage"));
    }

    @Test
    void materializesDirectSlotInputsWithoutVariantSourceBindings() throws Exception {
        writeFile(SETTINGS_FILE, ROOT_NAME);
        writeFile("inputs/bundle.js", "console.log('bundle')\n");
        writeFile(BUILD_FILE, """
            plugins {
                id 'org.implab.gradle-variants-artifacts'
            }

            variants {
                layer('main')

                variant('browser') {
                    role('main') {
                        layers('main')
                    }
                }
            }

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

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

                doLast {
                    def bundleDir = layout.buildDirectory.dir('variant-artifacts/browser/bundle').get().asFile
                    assert new File(bundleDir, 'bundle.js').exists()
                    println('primary=' + variantArtifacts.requireVariant('browser').requirePrimarySlotName())
                }
            }
            """);

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

        assertTrue(result.getOutput().contains("primary=bundle"));
        assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
    }

    @Test
    void combinesDirectAndTopologyAwareSlotInputs() throws Exception {
        writeFile(SETTINGS_FILE, ROOT_NAME);
        writeFile("inputs/base.js", "console.log('base')\n");
        writeFile("inputs/marker.txt", "marker\n");
        writeFile(BUILD_FILE, """
            plugins {
                id 'org.implab.gradle-variants-artifacts'
            }

            variants {
                layer('main')

                variant('browser') {
                    role('main') {
                        layers('main')
                    }
                }
            }

            variantSources {
                bind('main') {
                    configureSourceSet {
                        declareOutputs('js')
                    }
                }

                whenBound { ctx ->
                    ctx.configureSourceSet {
                        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 'processBrowserBundle'

                doLast {
                    def bundleDir = layout.buildDirectory.dir('variant-artifacts/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 {
        assertBuildFails("""
            plugins {
                id 'org.implab.gradle-variants-artifacts'
            }

            variants {
                layer('main')
            }

            variantArtifacts {
                variant('browser') {
                    slot('mainJs') {
                        fromVariant {
                            output('js')
                        }
                    }
                }
            }
            """, "Variant artifact 'browser' references unknown variant 'browser'");
    }

    @Test
    void failsOnUnknownRoleReference() throws Exception {
        assertBuildFails("""
            plugins {
                id 'org.implab.gradle-variants-artifacts'
            }

            variants {
                layer('main')

                variant('browser') {
                    role('main') {
                        layers('main')
                    }
                }
            }

            variantArtifacts {
                variant('browser') {
                    slot('mainJs') {
                        fromRole('test') {
                            output('js')
                        }
                    }
                }
            }
            """, "Variant artifact 'browser', slot 'mainJs' references unknown role 'test'");
    }

    @Test
    void failsWhenPrimarySlotIsMissingForMultipleSlots() throws Exception {
        assertBuildFails("""
            plugins {
                id 'org.implab.gradle-variants-artifacts'
            }

            variants {
                layer('main')

                variant('browser') {
                    role('main') {
                        layers('main')
                    }
                }
            }

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

                    slot('js') {
                        fromVariant {
                            output('js')
                        }
                    }
                }
            }
            """, "Variant artifact 'browser' must declare primary slot because it has multiple slots");
    }

    @Test
    void failsOnLayerReferenceOutsideVariantTopology() throws Exception {
        assertBuildFails("""
            plugins {
                id 'org.implab.gradle-variants-artifacts'
            }

            variants {
                layer('mainBase')
                layer('extra')

                variant('browser') {
                    role('main') {
                        layers('mainBase')
                    }
                }
            }

            variantArtifacts {
                variant('browser') {
                    slot('extraJs') {
                        fromLayer('extra') {
                            output('js')
                        }
                    }
                }
            }
            """, "Variant artifact 'browser', slot 'extraJs' references unknown layer 'extra'");
    }

    @Test
    void failsOnLateMutationAfterFinalize() throws Exception {
        assertBuildFails("""
            plugins {
                id 'org.implab.gradle-variants-artifacts'
            }

            variants {
                layer('main')

                variant('browser') {
                    role('main') {
                        layers('main')
                    }
                }
            }

            afterEvaluate {
                variantArtifacts.variant('late') {
                    slot('js') {
                        fromVariant {
                            output('js')
                        }
                    }
                }
            }
            """, "variantArtifacts model is finalized and cannot configure variants");
    }

    @Test
    void preservesPrimaryResolutionAndAllowsSecondaryArtifactSelection() throws Exception {
        writeFile(SETTINGS_FILE, """
            rootProject.name = 'variants-artifacts-fixture'
            include 'producer', 'consumer'
            """);
        writeFile("producer/inputs/types.d.ts", "export type Foo = string\n");
        writeFile("producer/inputs/index.js", "export const foo = 'bar'\n");
        var buildscriptClasspath = pluginClasspath().stream()
                .map(File::getAbsolutePath)
                .map(path -> "'" + path.replace("\\", "\\\\") + "'")
                .collect(Collectors.joining(", "));
        writeFile(BUILD_FILE, """
            buildscript {
                dependencies {
                    classpath files(%s)
                }
            }

            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-artifacts'
            }

            project(':producer') {
                variants {
                    layer('main')

                    variant('browser') {
                        role('main') {
                            layers('main')
                        }
                    }
                }

                variantSources {
                    bind('main') {
                        configureSourceSet {
                            declareOutputs('types', 'js')
                        }
                    }

                    whenBound { ctx ->
                        ctx.configureSourceSet {
                            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')
                            }
                        }
                    }

                    whenOutgoingVariant { publication ->
                        publication.configureConfiguration {
                            attributes.attribute(variantAttr, publication.variantName())
                        }

                        publication.primarySlot().configureArtifactAttributes {
                            attribute(slotAttr, publication.primarySlot().slotName())
                        }

                        publication.requireSlot('js').configureArtifactAttributes {
                            attribute(slotAttr, '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)
                    }
                }
            }
            """.formatted(buildscriptClasspath));

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

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

    private GradleRunner runner(String... arguments) {
        return GradleRunner.create()
                .withProjectDir(testProjectDir.toFile())
                .withPluginClasspath(pluginClasspath())
                .withArguments(arguments)
                .forwardOutput();
    }

    private void assertBuildFails(String buildScript, String expectedError) throws Exception {
        writeFile(SETTINGS_FILE, ROOT_NAME);
        writeFile(BUILD_FILE, buildScript);

        var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build());
        var output = ex.getBuildResult().getOutput();

        assertTrue(output.contains(expectedError), () -> "Expected [" + expectedError + "] in output:\n" + output);
    }

    private static List<File> pluginClasspath() {
        try {
            var classesDir = Path.of(VariantArtifactsPlugin.class
                    .getProtectionDomain()
                    .getCodeSource()
                    .getLocation()
                    .toURI());

            var markerResource = VariantArtifactsPlugin.class.getClassLoader()
                    .getResource("META-INF/gradle-plugins/org.implab.gradle-variants-artifacts.properties");

            assertNotNull(markerResource, "Plugin marker resource is missing from test classpath");

            var markerPath = Path.of(markerResource.toURI());
            var resourcesDir = markerPath.getParent().getParent().getParent();

            return List.of(classesDir.toFile(), resourcesDir.toFile());
        } catch (Exception e) {
            throw new RuntimeException("Unable to build plugin classpath for test", e);
        }
    }

    private void writeFile(String relativePath, String content) throws IOException {
        Path path = testProjectDir.resolve(relativePath);
        Files.createDirectories(path.getParent());
        Files.writeString(path, content);
    }
}
