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 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 VariantsSourcesPluginFunctionalTest {
    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-sources-fixture'\n";

    @TempDir
    Path testProjectDir;

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

                variants {
                    layer('mainBase')
                    layer('mainAmd')

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

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

                def events = []
                def localEvents = []

                variantSources {
                    bind('mainBase') {
                        configureSourceSet {
                            declareOutputs('compiled')
                        }
                    }
                    bind('mainAmd') {
                        configureSourceSet {
                            declareOutputs('compiled')
                        }
                    }
                    bind('mainAmd').whenRegistered { ctx ->
                        localEvents << "${ctx.variantName()}:${ctx.roleName()}:${ctx.layerName()}:${ctx.sourceSetName()}"
                    }
                    whenRegistered { ctx ->
                        events << "${ctx.variantName()}:${ctx.roleName()}:${ctx.layerName()}:${ctx.sourceSetName()}"
                    }
                }

                tasks.register('probe') {
                    doLast {
                        println("sources=" + sources.collect { it.name }.sort().join(','))
                        println("events=" + events.sort().join('|'))
                        println("local=" + localEvents.sort().join('|'))

                        def base = sources.getByName('browserMainBase')
                        def amd = sources.getByName('browserMainAmd')
                        def nodeBase = sources.getByName('nodeMainBase')

                        base.output('compiled')
                        amd.output('compiled')
                        nodeBase.output('compiled')

                        println('outputs=ok')
                    }
                }
                """);

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

        assertTrue(result.getOutput().contains("sources=browserMainAmd,browserMainBase,nodeMainBase"));
        assertTrue(result.getOutput().contains(
                "events=browser:main:mainAmd:browserMainAmd|browser:main:mainBase:browserMainBase|node:main:mainBase:nodeMainBase"));
        assertTrue(result.getOutput().contains("local=browser:main:mainAmd:browserMainAmd"));
        assertTrue(result.getOutput().contains("outputs=ok"));
        assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
    }

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

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

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

                tasks.register('probe') {
                    doLast {
                        def ss = sources.getByName('browserMain')
                        ss.output('compiled')
                        println('bindClosure=ok')
                    }
                }
                """);

        BuildResult result = runner("probe").build();
        assertTrue(result.getOutput().contains("bindClosure=ok"));
    }

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

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

                variantSources {
                    bind('missing')
                }
                """);

        var ex = assertThrows(UnexpectedBuildFailure.class, () -> runner("help").build());
        assertTrue(ex.getBuildResult().getOutput().contains("Layer binding 'missing' references unknown layer"));
    }

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

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

                variantSources {
                    whenRegistered {
                        configureSourceSet {
                            declareOutputs('generated')
                        }
                    }
                }

                tasks.register('probe') {
                    doLast {
                        def ss = sources.getByName('browserMain')
                        ss.output('generated')
                        println('contextProvider=ok')
                    }
                }
                """);

        BuildResult result = runner("probe").build();
        assertTrue(result.getOutput().contains("contextProvider=ok"));
    }

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

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

                def events = []

                afterEvaluate {
                    variantSources {
                        bind('main') {
                            configureSourceSet {
                                declareOutputs('late')
                            }
                        }

                        bind('main').whenRegistered { ctx ->
                            events << "layer:${ctx.sourceSetName()}"
                        }

                        whenRegistered { ctx ->
                            events << "global:${ctx.sourceSetName()}"
                        }
                    }
                }

                tasks.register('probe') {
                    doLast {
                        def ss = sources.getByName('browserMain')
                        ss.output('late')
                        println("events=" + events.sort().join('|'))
                        println('lateReplay=ok')
                    }
                }
                """);

        BuildResult result = runner("probe").build();
        assertTrue(result.getOutput().contains("events=global:browserMain|layer:browserMain"));
        assertTrue(result.getOutput().contains("lateReplay=ok"));
    }

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

                variants {
                    layer('main')

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

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

                def registeredEvents = []
                def browserRegisteredEvents = []
                def boundEvents = []
                def browserBoundEvents = []
                def localBoundEvents = []

                variantSources {
                    bind('main').sourceSetNamePattern = '{layer}'

                    bind('main') {
                        configureSourceSet {
                            declareOutputs('compiled')
                        }
                    }

                    bind('main') {
                        whenBound {
                            localBoundEvents << "${variantName()}:${roleName()}:${layerName()}:${sourceSetName()}"
                        }
                    }

                    whenRegistered { ctx ->
                        registeredEvents << "${ctx.variantName()}:${ctx.roleName()}:${ctx.layerName()}:${ctx.sourceSetName()}"
                    }

                    whenRegistered('browser') { ctx ->
                        browserRegisteredEvents << "${ctx.variantName()}:${ctx.roleName()}:${ctx.layerName()}:${ctx.sourceSetName()}"
                    }

                    whenBound { ctx ->
                        boundEvents << "${ctx.variantName()}:${ctx.roleName()}:${ctx.layerName()}:${ctx.sourceSetName()}"
                    }

                    whenBound('browser') { ctx ->
                        browserBoundEvents << "${ctx.variantName()}:${ctx.roleName()}:${ctx.layerName()}:${ctx.sourceSetName()}"
                    }
                }

                tasks.register('probe') {
                    doLast {
                        println("sources=" + sources.collect { it.name }.sort().join(','))

                        def main = sources.getByName('main')
                        main.output('compiled')

                        println("registered=" + registeredEvents.sort().join('|'))
                        println("browserRegistered=" + browserRegisteredEvents.sort().join('|'))
                        println("bound=" + boundEvents.sort().join('|'))
                        println("browserBound=" + browserBoundEvents.sort().join('|'))
                        println("localBound=" + localBoundEvents.sort().join('|'))
                        println('sharedPattern=ok')
                    }
                }
                """);

        BuildResult result = runner("probe").build();
        assertTrue(result.getOutput().contains("sources=main"));
        assertTrue(result.getOutput().contains("registered=browser:main:main:main"));
        assertTrue(result.getOutput().contains("browserRegistered=browser:main:main:main"));
        assertTrue(result.getOutput().contains("bound=browser:main:main:main|node:main:main:main"));
        assertTrue(result.getOutput().contains("browserBound=browser:main:main:main"));
        assertTrue(result.getOutput().contains("localBound=browser:main:main:main|node:main:main:main"));
        assertTrue(result.getOutput().contains("sharedPattern=ok"));
    }

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

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

            var markerResource = VariantsSourcesPlugin.class.getClassLoader()
                    .getResource("META-INF/gradle-plugins/org.implab.gradle-variants-sources.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);
    }
}
