package org.implab.gradle.variants;

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.LinkedHashSet;
import java.util.List;
import java.util.stream.Collectors;

import org.gradle.testkit.runner.GradleRunner;
import org.gradle.testkit.runner.UnexpectedBuildFailure;
import org.implab.gradle.common.core.lang.Deferred;
import org.implab.gradle.variants.sources.GenericSourceSet;
import org.junit.jupiter.api.io.TempDir;

abstract class AbstractFunctionalTest {
    private static final String SETTINGS_FILE = "settings.gradle";
    private static final String BUILD_FILE = "build.gradle";

    @TempDir
    Path testProjectDir;

    protected void writeSettings(String rootProjectName) throws IOException {
        writeFile(SETTINGS_FILE, "rootProject.name = '" + rootProjectName + "'\n");
    }

    protected void writeBuildFile(String body) throws IOException {
        writeFile(BUILD_FILE, buildscriptClasspathBlock() + "\n" + body);
    }

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

    protected void assertBuildFails(String expectedError, String... arguments) {
        var ex = org.junit.jupiter.api.Assertions.assertThrows(
                UnexpectedBuildFailure.class,
                () -> runner(arguments).build());

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

    private static String buildscriptClasspathBlock() {
        var classpath = pluginClasspath().stream()
                .map(file -> "'" + file.getAbsolutePath().replace("\\", "\\\\") + "'")
                .collect(Collectors.joining(", "));

        return """
                buildscript {
                    dependencies {
                        classpath files(%s)
                    }
                }
                """.formatted(classpath);
    }

    private static List<File> pluginClasspath() {
        try {
            var files = new LinkedHashSet<File>();
            files.add(codeSource(VariantSourcesPlugin.class));
            files.add(resourceRoot("META-INF/gradle-plugins/org.implab.gradle-variants.properties"));
            files.add(codeSource(GenericSourceSet.class));
            files.add(codeSource(Deferred.class));
            return List.copyOf(files);
        } catch (Exception e) {
            throw new RuntimeException("Unable to build plugin classpath for test", e);
        }
    }

    private static File codeSource(Class<?> type) throws Exception {
        return Path.of(type.getProtectionDomain().getCodeSource().getLocation().toURI()).toFile();
    }

    private static File resourceRoot(String resourceName) throws Exception {
        var url = VariantSourcesPlugin.class.getClassLoader().getResource(resourceName);
        if (url == null)
            throw new IllegalStateException("Classpath resource isn't found: " + resourceName);

        var path = Path.of(url.toURI());
        var segments = resourceName.split("/").length;
        for (var i = 0; i < segments; i++)
            path = path.getParent();
        return path.toFile();
    }

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