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 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 {
                layer('mainBase')
                layer('mainAmd')

                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') {
                    slot('mainJs') {
                        fromRole('main') {
                            output('js')
                        }
                    }

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

                whenOutgoingVariant { publication ->
                    publication.configureAssembly {
                        sources.from(layout.projectDirectory.file("inputs/${publication.slotName()}.txt"))
                    }

                    publication.configureConfiguration {
                        attributes.attribute(Attribute.of('test.slot', String), publication.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 mainElements = configurations.getByName('browserMainJsElements')
                    def attr = mainElements.attributes.getAttribute(Attribute.of('test.slot', String))

                    println('mainAttr=' + attr)
                    println('configurations=' + [mainElements.name, configurations.getByName('browserAmdJsElements').name].sort().join(','))
                }
            }
            """);

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

        assertTrue(result.getOutput().contains("mainAttr=mainJs"));
        assertTrue(result.getOutput().contains("configurations=browserAmdJsElements,browserMainJsElements"));
        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 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");
    }

    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(VariantsArtifactsPlugin.class
                    .getProtectionDomain()
                    .getCodeSource()
                    .getLocation()
                    .toURI());

            var markerResource = VariantsArtifactsPlugin.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);
    }
}
