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

    @TempDir
    Path testProjectDir;

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

            variants {
                layer('mainBase') {
                }

                layer('mainAmd') {
                }

                variant('browser') {
                    attributes {
                        string('jsRuntime', 'browser')
                        string('jsModule', 'amd')
                    }
                    role('main') {
                        layers('mainBase', 'mainAmd')
                    }
                    artifactSlot('mainCompiled')
                }
            }

                tasks.register('probe') {
                    doLast {
                    def browser = variants.require('browser')
                    println('attributes=' + browser.attributes.size())
                    println('roles=' + browser.roles.size())
                    println('slots=' + browser.artifactSlots.size())
                }
            }
            """);

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

        assertTrue(result.getOutput().contains("attributes=2"));
        assertTrue(result.getOutput().contains("roles=1"));
        assertTrue(result.getOutput().contains("slots=1"));
        assertTrue(result.task(":probe").getOutcome() == TaskOutcome.SUCCESS);
    }

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

            variants {
                layer('mainBase') {
                }

                variant('browser') {
                    role('main') {
                        layers('mainBase', 'missingLayer')
                    }
                }
            }
            """, "references unknown layer 'missingLayer'");
    }

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

            variants {
                layer('mainBase')

                variant('browser') {
                    role('test') {
                        layers('mainBase')
                    }
                }
            }
            """);

        BuildResult result = runner("help").build();
        assertTrue(result.getOutput().contains("BUILD SUCCESSFUL"));
    }

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

            variants {
                layer('a')

                variant('browser') {
                    role('main') {
                        layers('a', 'a')
                    }
                }
            }
            """, "contains duplicated layer reference 'a'");
    }

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

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

            afterEvaluate {
                variants.layer('late')
            }
            """, "Variants model is finalized and cannot configure layers");
    }

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

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

            afterEvaluate {
                variants.require('browser').role('late') { layers('a') }
            }
            """, "Variant 'browser' is finalized and cannot configure roles");
    }

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

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