package org.jboss.windup.tests.application;

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;
import javax.inject.Inject;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.jboss.windup.exec.WindupProcessor;
import org.jboss.windup.exec.WindupProgressMonitor;
import org.jboss.windup.exec.configuration.WindupConfiguration;
import org.jboss.windup.exec.configuration.options.UserRulesDirectoryOption;
import org.jboss.windup.graph.GraphContext;
import org.jboss.windup.graph.GraphContextFactory;
import org.jboss.windup.graph.model.ProjectModel;
import org.jboss.windup.graph.service.GraphService;
import org.jboss.windup.reporting.model.ApplicationReportModel;
import org.jboss.windup.reporting.model.MigrationIssuesReportModel;
import org.jboss.windup.reporting.model.ReportModel;
import org.jboss.windup.reporting.service.ReportService;
import org.jboss.windup.rules.apps.java.config.ExcludePackagesOption;
import org.jboss.windup.rules.apps.java.config.ScanPackagesOption;
import org.jboss.windup.rules.apps.java.config.SourceModeOption;
import org.jboss.windup.rules.apps.java.dependencyreport.CreateDependencyReportRuleProvider;
import org.jboss.windup.rules.apps.java.model.JavaApplicationOverviewReportModel;
import org.jboss.windup.rules.apps.java.model.JavaClassFileModel;
import org.jboss.windup.rules.apps.java.reporting.rules.CreateJavaApplicationOverviewReportRuleProvider;
import org.jboss.windup.rules.apps.java.reporting.rules.EnableCompatibleFilesReportOption;
import org.jboss.windup.rules.apps.tattletale.EnableTattletaleReportOption;
import org.junit.Assert;

import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;

/**
 * Base class for Windup end-to-end tests.
 *
 * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
 */
public abstract class WindupArchitectureTest
{
    public static final String REPORTS_TEMPLATES_MIGRATION_ISSUES_FTL = "/reports/templates/migration-issues.ftl";

    @Inject
    private WindupProcessor processor;

    @Inject
    private GraphContextFactory factory;

    /**
     * Get an instance of the {@link GraphContextFactory}.
     */
    protected GraphContextFactory getFactory()
    {
        return factory;
    }

    Path getDefaultPath()
    {
        return FileUtils.getTempDirectory().toPath().resolve("Windup")
                    .resolve("windupgraph_" + RandomStringUtils.randomAlphanumeric(6));
    }

    protected GraphContext createGraphContext()
    {
        return createGraphContext(getDefaultPath());
    }

    GraphContext createGraphContext(Path path)
    {
        return factory.create(path);
    }

    protected void runTest(String inputPath, boolean sourceMode) throws Exception
    {
        runTest(Collections.singletonList(inputPath), sourceMode);
    }

    protected void runTest(Iterable<String> inputPaths, boolean sourceMode) throws Exception
    {
        List<String> includeList = Collections.emptyList();
        List<String> excludeList = Collections.emptyList();
        runTest(createGraphContext(), inputPaths, null, sourceMode, includeList, excludeList);
    }

    protected void runTest(GraphContext graphContext, String inputPath, boolean sourceMode)
                throws Exception
    {
        runTest(graphContext, Collections.singletonList(inputPath), sourceMode);
    }

    void runTest(GraphContext graphContext, Iterable<String> inputPaths, boolean sourceMode)
                throws Exception
    {
        List<String> includeList = Collections.emptyList();
        List<String> excludeList = Collections.emptyList();
        runTest(graphContext, inputPaths, null, sourceMode, includeList, excludeList);
    }

    void runTest(GraphContext graphContext, String inputPath, boolean sourceMode,
                List<String> includePackages) throws Exception
    {
        runTest(graphContext, Collections.singletonList(inputPath), sourceMode, includePackages);
    }

    void runTest(GraphContext graphContext, Iterable<String> inputPaths, boolean sourceMode,
                List<String> includePackages) throws Exception
    {
        List<String> excludeList = Collections.emptyList();
        runTest(graphContext, inputPaths, null, sourceMode, includePackages, excludeList);
    }

    void runTest(final GraphContext graphContext,
                final String inputPath,
                final File userRulesDir,
                final boolean sourceMode,
                final List<String> includePackages,
                final List<String> excludePackages) throws Exception
    {
        runTest(graphContext, Collections.singletonList(inputPath), userRulesDir, sourceMode, includePackages, excludePackages);
    }

    void runTest(final GraphContext graphContext,
                final Iterable<String> inputPaths,
                final File userRulesDir,
                final boolean sourceMode,
                final List<String> includePackages,
                final List<String> excludePackages) throws Exception
    {
        Map<String, Object> otherOptions = Collections.emptyMap();
        runTest(graphContext, inputPaths, userRulesDir, sourceMode, includePackages, excludePackages, otherOptions);
    }

    void runTest(final GraphContext graphContext,
                final Iterable<String> inputPaths,
                final File userRulesDir,
                final boolean sourceMode,
                final List<String> includePackages,
                final List<String> excludePackages,
                final Map<String, Object> otherOptions) throws Exception
    {

        WindupConfiguration windupConfiguration = new WindupConfiguration().setGraphContext(graphContext);
        windupConfiguration.setAlwaysHaltOnException(true);
        for (String inputPath : inputPaths)
        {
            windupConfiguration.addInputPath(Paths.get(inputPath));
        }
        windupConfiguration.setOutputDirectory(graphContext.getGraphDirectory());
        if (userRulesDir != null)
        {
            windupConfiguration.setOptionValue(UserRulesDirectoryOption.NAME, userRulesDir);
        }
        windupConfiguration.setOptionValue(SourceModeOption.NAME, sourceMode);
        windupConfiguration.setOptionValue(ScanPackagesOption.NAME, includePackages);
        windupConfiguration.setOptionValue(ExcludePackagesOption.NAME, excludePackages);
        windupConfiguration.setOptionValue(EnableTattletaleReportOption.NAME, false);
        windupConfiguration.setOptionValue(EnableCompatibleFilesReportOption.NAME, true);

        for (Map.Entry<String, Object> otherOption : otherOptions.entrySet())
        {
            windupConfiguration.setOptionValue(otherOption.getKey(), otherOption.getValue());
        }

        RecordingWindupProgressMonitor recordingMonitor = new RecordingWindupProgressMonitor();
        WindupProgressMonitor testMonitor = overrideWindupProgressMonitor(recordingMonitor);
        windupConfiguration.setProgressMonitor(testMonitor);

        processor.execute(windupConfiguration);

        assertRecordedData(recordingMonitor);
    }

    /**
     * Override to customize what's expected.
     */
    protected void assertRecordedData(RecordingWindupProgressMonitor recordingMonitor)
    {
        Assert.assertFalse(recordingMonitor.isCancelled());
        Assert.assertTrue(recordingMonitor.isDone());
        Assert.assertFalse(recordingMonitor.getSubTaskNames().isEmpty());
        Assert.assertTrue(recordingMonitor.getTotalWork() > 0);
        Assert.assertTrue(recordingMonitor.getCompletedWork() > 0);
        Assert.assertEquals(recordingMonitor.getTotalWork(), recordingMonitor.getCompletedWork());
    }

    /**
     * Override this if you need to intercept the calls to WindupProgressMonitor.
     * The overriding method must keep the functionality of passed monitor.
     * See {@link CombinedWindupProgressMonitor}.
     */
    public WindupProgressMonitor overrideWindupProgressMonitor(final WindupProgressMonitor testMonitor)
    {
        return testMonitor;
    }

    JavaApplicationOverviewReportModel getMainApplicationReport(GraphContext context)
    {
        return (JavaApplicationOverviewReportModel) getReport(context, CreateJavaApplicationOverviewReportRuleProvider.TEMPLATE_APPLICATION_REPORT,
                    CreateJavaApplicationOverviewReportRuleProvider.DETAILS_REPORT);
    }

    Iterable<ReportModel> getApplicationDetailsReports(GraphContext context)
    {
        return getReports(context, CreateJavaApplicationOverviewReportRuleProvider.TEMPLATE_APPLICATION_REPORT);
    }

    MigrationIssuesReportModel getMigrationIssuesReport(GraphContext context, final ProjectModel projectModel)
    {
        Iterable<ReportModel> reportModels = Iterables.filter(getReports(context, REPORTS_TEMPLATES_MIGRATION_ISSUES_FTL),
                    new Predicate<ReportModel>()
                    {
                        @Override
                        public boolean apply(@Nullable ReportModel input)
                        {
                            if (!(input instanceof MigrationIssuesReportModel))
                                return false;

                            MigrationIssuesReportModel migrationIssuesReportModel = (MigrationIssuesReportModel) input;
                            return projectModel == null || projectModel.equals(migrationIssuesReportModel.getProjectModel());
                        }
                    });

        if (!reportModels.iterator().hasNext())
            return null;

        if (Iterables.size(reportModels) > 1)
            Assert.fail("Only one migration issues report expected for the application!");

        return (MigrationIssuesReportModel)reportModels.iterator().next();
    }

    MigrationIssuesReportModel getMigrationIssuesReport(GraphContext context)
    {
        return (MigrationIssuesReportModel) getReport(context, REPORTS_TEMPLATES_MIGRATION_ISSUES_FTL, "Migration Issues");
    }

    ApplicationReportModel getJarDependencyReport(GraphContext context)
    {
        return (ApplicationReportModel) getReport(context, CreateDependencyReportRuleProvider.TEMPLATE, CreateDependencyReportRuleProvider.REPORT_NAME);
    }

    Iterable<ReportModel> getReports(GraphContext context, String template)
    {
        ReportService reportService = new ReportService(context);
        return reportService.findAllByProperty(ReportModel.TEMPLATE_PATH, template);
    }

    ReportModel getReport(GraphContext context, String template, String name)
    {
        ReportService reportService = new ReportService(context);
        Iterable<ReportModel> reportModels = reportService.findAllByProperty(
                    ReportModel.TEMPLATE_PATH,
                    template);
        ReportModel reportModel = null;
        for (ReportModel candidateModel : reportModels)
        {
            if (StringUtils.equals(candidateModel.getReportName(), name))
            {
                reportModel = candidateModel;
                break;
            }
        }
        Assert.assertNotNull(reportModel);

        return reportModel;
    }

    protected void allDecompiledFilesAreLinked(GraphContext context)
    {
        GraphService<JavaClassFileModel> classModels = new GraphService<>(context, JavaClassFileModel.class);
        for (JavaClassFileModel javaClassFileModel : classModels.findAllWithoutProperty(JavaClassFileModel.SKIP_DECOMPILATION, true))
        {
            Assert.assertNotNull(javaClassFileModel.getJavaClass().getDecompiledSource());
        }

    }


    /**
     * Stores the info about incoming calls which the tests can review.
     */
    protected static class RecordingWindupProgressMonitor implements WindupProgressMonitor
    {
        private int totalWork = -1;
        private boolean done;
        private boolean cancelled;
        private List<String> taskNames = new ArrayList<>();
        private List<String> subTaskNames = new ArrayList<>();
        private int workDone;

        @Override
        public void beginTask(String name, int totalWork)
        {
            if (this.totalWork == -1)
                this.totalWork = totalWork;
            else
                throw new IllegalStateException("Total work already set.");
        }

        @Override
        public void done()
        {
            this.done = true;
        }

        @Override
        public boolean isCancelled()
        {
            return cancelled;
        }

        @Override
        public void setCancelled(boolean cancelled)
        {
            if (cancelled)
                this.cancelled = true;
        }

        @Override
        public void setTaskName(String name)
        {
            this.taskNames.add(name);
        }

        @Override
        public void subTask(String name)
        {
            this.subTaskNames.add(name);
        }

        @Override
        public void worked(int work)
        {
            this.workDone += work;
        }

        public int getTotalWork()
        {
            return totalWork;
        }

        public boolean isDone()
        {
            return done;
        }

        public List<String> getSubTaskNames()
        {
            return subTaskNames;
        }

        public int getCompletedWork()
        {
            return workDone;
        }

    }
}
