package de.hftstuttgart.dta;

import de.hftstuttgart.dta.model.Result;
import de.hftstuttgart.dta.model.ResultSummary;
import de.hftstuttgart.dta.util.MySummaryGeneratingListener;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.junit.platform.engine.discovery.DiscoverySelectors;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
import org.junit.platform.launcher.core.LauncherFactory;
import org.junit.platform.launcher.listeners.TestExecutionSummary;

import javax.tools.*;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

import java.util.*;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class Testrunner
{
    static
    {
        InputStream stream = Testrunner.class.getClassLoader()
            .getResourceAsStream("logging.properties");
        try
        {
            LogManager.getLogManager().readConfiguration(stream);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }

    private static final Logger LOG = Logger.getLogger(Testrunner.class.getName());

    public static String[] sourceFolders;       // folders with Java Files to compile
    public static String[] libraryFolders;      // folders with libraries in jar-file format
    public static String classFolder;           // folder to put compiled class files into
    public static String[] classPathItems;      // items for the classpath
    public static String resultFolder;          // folder the result file gets serialized to

    public static void main(String[] args) throws Exception
    {
        LOG.info("OpenJDK11 JUnit5/Jupiter Testrunner started");

        LOG.info("initializing fields...");
        sourceFolders = args[0].split(":");
        libraryFolders = args[1].split(":");
        resultFolder = args[2];
        classFolder = Files.createTempDirectory("testrunner").toAbsolutePath().toString();

        // finding all source files
        Set<File> sourceFiles = new HashSet<>();
        for (String folder : sourceFolders)
        {
            sourceFiles.addAll(getAllJavaFilesInFolder(Paths.get(folder).toFile()));
        }

        // call compilation and generate Results for failed compiles
        Set<Result> compilationErrors = generateCompileResults(compile(sourceFiles, new File(classFolder)));

        // run unit tests found in the compiled class files
        ResultSummary resultSummary = runTests();

        // add compilation errors to summary
        resultSummary.results.addAll(compilationErrors);

        // serialize result
        writeResult(resultSummary);
    }

    public static String[] buildClassPathItems(boolean runtime)
    {
        Set<String> classPathItemsBuild = new HashSet<>(Arrays.asList(libraryFolders));
        classPathItemsBuild.add(classFolder);
        if (runtime)
        {
            classPathItemsBuild.addAll(Arrays.stream(sourceFolders).collect(Collectors.toSet()));
        }
        return classPathItemsBuild.toArray(new String[0]);
    }

    public static Set<File> buildClassPath(String... paths)
    {
        Set<File> files = new HashSet<>();

        for (String path : paths)
        {
            if (path.endsWith("*"))
            {
                path = path.substring(0, path.length() - 1);
                File pathFile = new File(path);
                if (!pathFile.exists() || !pathFile.isDirectory())
                {
                    continue;
                }

                for (File file : Objects.requireNonNull(pathFile.listFiles()))
                {
                    if (file.isFile() && file.getName().endsWith(".jar"))
                    {
                        files.add(file);
                    } else
                    {
                        files.addAll(buildClassPath(Paths.get(file.getPath(), "*").toString()));
                    }
                }
            } else
            {
                File file = new File(path);
                if (file.exists())
                {
                    files.add(file);
                }
            }
        }
        return files;
    }

    public static List<Diagnostic> compile(Set<File> files, File outputDir)
    {
        classPathItems = buildClassPathItems(false);
        LOG.info("compilation started");
        List<Diagnostic> compilationErrors = new LinkedList<>();

        // Create the compiler and add a diagnostic listener to get the compilation errors
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        DiagnosticListener listener = compilationErrors::add;
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(listener, null, StandardCharsets.UTF_8);
        Iterable<? extends JavaFileObject> fileObjects = fileManager.getJavaFileObjects(files.toArray(new File[0]));

        if (!outputDir.exists())
        {
            outputDir.mkdir();
        }

        // Set the compiler option for a specific output path
        List<String> options = new ArrayList<>();
        options.add("-d"); // output dir
        options.add(outputDir.getAbsolutePath());
        options.add("-cp"); // custom classpath
        String os=System.getProperty("os.name");
        final String osSpecificCpDelim=(os.indexOf("Windows")>-1?";":":");
        String cp = buildClassPath(classPathItems).stream()
            .map(f -> f.getPath())
            .reduce((s1, s2) -> s1 + osSpecificCpDelim + s2).orElse("");
        LOG.info("classpath for compilation: " + cp);
        options.add(cp);

        // compile it
        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, listener, options, null, fileObjects);
        boolean compileResult = task.call();

        // If the compilation failed, remove the failed file from the pathsToCompile list and try to compile again without this file
        if (!compileResult)
        {
            File currentFile = new File(((JavaFileObject) compilationErrors.get(compilationErrors.size() - 1).getSource()).toUri().getPath());
            LOG.log(Level.WARNING,"compilation of file '" + currentFile.getAbsolutePath() + "' failed");
            files.removeIf(file -> file.getAbsolutePath().equalsIgnoreCase(currentFile.getAbsolutePath()));
            if (files.size() > 0)
            {
                LOG.info(String.format("retry compilation without %s", currentFile.getName()));
                compile(files, outputDir);
            }
        } else {
            LOG.info("compilation finished");
        }

        return compilationErrors;
    }

    public static ClassLoader createCustomClassLoader() throws MalformedURLException
    {
        LOG.info("creating custom class loader for testing");
        URL[] urls = buildClassPath(classPathItems).stream().map(f -> {
            try {
                return f.toURI().toURL();
            } catch (MalformedURLException e) {
                LOG.log(Level.SEVERE, e.getMessage(), e);
                throw new RuntimeException(e.getMessage(), e);
            }
        }).toArray(URL[]::new);

        LOG.info(String.format("JUnit ClassLoader context classpath: %s", Arrays.deepToString(urls)));
        ClassLoader parentClassLoader = Thread.currentThread().getContextClassLoader();
        return URLClassLoader.newInstance(urls, parentClassLoader);
    }

    public static Set<Result> generateCompileResults(List<Diagnostic> compilationErrors)
    {
        return compilationErrors.stream().map(e ->
        {
            Result result = new Result();
//            Pattern pattern = Pattern.compile(String.format("^.*%s(.*\\.java).*$", File.separator));
//            Matcher matcher = pattern.matcher(String.valueOf(e.getSource()));
//
//            result.name = (matcher.matches() && matcher.group(1) != null) ? matcher.group(1) : String.valueOf(e.getSource());
            String sourcePath=String.valueOf(e.getSource());
            result.name=sourcePath.substring(sourcePath.lastIndexOf(File.separator)+1, sourcePath.length()-1);
            result.state = Result.State.COMPILATIONERROR.ordinal();
            result.failureReason = e.getMessage(Locale.ENGLISH);
            result.failureType = "Compilation Failed";
            result.stacktrace = e.toString();

            result.lineNumber = (int) e.getLineNumber();
            result.columnNumber = (int) e.getColumnNumber();
            result.position = (int) e.getPosition();

            return result;
        })
        .collect(Collectors.toCollection(HashSet::new));
    }

    public static ResultSummary generateResultSummary(TestExecutionSummary summary, Set<TestIdentifier> successes)
    {
        LOG.info("JUnit results:");
        LOG.info(String.format(
            "Number of Tests: %d, Number of fails: %d, Successful tests: %s, Failed tests: %s",
            summary.getTestsFoundCount(),
            summary.getTestsFailedCount(),
            successes.stream()
                .map(s -> s.getDisplayName())
                .reduce((s1, s2) -> s1 + ":" + s2).orElse("-"),
            summary.getFailures().stream()
                .map(f -> f.getTestIdentifier().getDisplayName())
                .reduce((s1, s2) -> s1 + ":" + s2).orElse("-")
        ));

        ResultSummary resultSummary = new ResultSummary();
        resultSummary.results.addAll(successes.stream().map(s ->
        {
            Result result = new Result();
            result.name = s.getDisplayName();
            result.state = Result.State.SUCCESS.ordinal();

            return result;
        })
        .collect(Collectors.toCollection(HashSet::new)));

        resultSummary.results.addAll(summary.getFailures().stream().map(f ->
        {
            Result result = new Result();
            result.name = f.getTestIdentifier().getDisplayName();
            result.state = Result.State.FAILURE.ordinal();

            result.failureReason = f.getException().getMessage();
            result.failureType = f.getException().getClass().getName();
            result.stacktrace = Arrays.stream(f.getException().getStackTrace())
                .map(s -> s.toString())
                .reduce((s1, s2) -> s1 + "\n" + s2)
                .orElse(null);

            return result;
        })
        .collect(Collectors.toCollection(HashSet::new)));

        return resultSummary;
    }

    public static List<File> getAllJavaFilesInFolder(File path)
    {
        // check if provided path is a directory, otherwise throw a IllegalArgumentException
        if (!path.isDirectory())
        {
            String error = path.getAbsolutePath() + " is not a path";
            LOG.severe(error);
            throw new IllegalArgumentException(error);
        }

        List<File> files = new LinkedList<>();

        // recursively check for java files
        Stream.of(Objects.requireNonNull(path.listFiles()))
            .forEach(file ->
            {
                // if directory, make recursion
                if (file.isDirectory())
                {
                    files.addAll(getAllJavaFilesInFolder(file));
                }
                // if java file add to list
                else if (file.getAbsolutePath().endsWith(".java"))
                {
                    files.add(file);
                }
            });

        return files;
    }

    public static ResultSummary runTests() throws MalformedURLException
    {
        classPathItems = buildClassPathItems(true);
        LOG.info("saving original class loader");
        ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader();
        // get custom one
        ClassLoader customClassLoader = createCustomClassLoader();

        TestExecutionSummary summary;
        Set<TestIdentifier> successes;

        try
        {
            LOG.info("changing classloader");
            Thread.currentThread().setContextClassLoader(customClassLoader);
            LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
                .selectors(DiscoverySelectors.selectClasspathRoots(Collections.singleton(Paths.get(classFolder).toAbsolutePath())))
                .build();

            MySummaryGeneratingListener listener = new MySummaryGeneratingListener();
            Launcher launcher = LauncherFactory.create();
            launcher.registerTestExecutionListeners(listener);

            LOG.info("discovering UnitTests...");
            TestPlan plan = launcher.discover(request);

            for (TestIdentifier root : plan.getRoots())
            {
                for (TestIdentifier test : plan.getChildren(root))
                {
                    LOG.info(String.format("Testclass identified: %s", test.getDisplayName()));
                }
            }

            LOG.info("launching tests");
            launcher.execute(plan);

            LOG.info("catching test results");
            summary = listener.getSummary();
            successes = listener.getSuccessfulTestidentifiers();
        } finally
        {
            LOG.info("restore original classloader");
            Thread.currentThread().setContextClassLoader(originalClassLoader);
        }

        LOG.info("generate result summary from junit");
        return generateResultSummary(summary, successes);
    }

    public static void writeResult(ResultSummary resultSummary) throws IOException
    {
        Path fileName = Paths.get(resultFolder, "result.json");
        LOG.info(String.format("serializing result as json into %s", fileName.toAbsolutePath().toString()));
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper
            .writerWithDefaultPrettyPrinter()
            .writeValue(fileName.toFile(), resultSummary);
    }
}