/*******************************************************************************
 * Copyright (c) 2017, 2018 École Polytechnique de Montréal
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License 2.0 which
 * accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *******************************************************************************/

package org.eclipse.tracecompass.internal.provisional.analysis.lami.ui.views;

import static org.eclipse.swtbot.swt.finder.SWTBotAssert.assertVisible;
import static org.junit.Assert.assertNotNull;

import java.util.Arrays;
import java.util.Collections;

import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.Logger;
import org.apache.log4j.SimpleLayout;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot;
import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotView;
import org.eclipse.swtbot.swt.finder.exceptions.WidgetNotFoundException;
import org.eclipse.swtbot.swt.finder.finders.UIThreadRunnable;
import org.eclipse.swtbot.swt.finder.junit.SWTBotJunit4ClassRunner;
import org.eclipse.swtbot.swt.finder.matchers.WidgetOfType;
import org.eclipse.swtbot.swt.finder.results.Result;
import org.eclipse.swtbot.swt.finder.utils.SWTBotPreferences;
import org.eclipse.swtbot.swt.finder.waits.ICondition;
import org.eclipse.swtbot.swt.finder.widgets.AbstractSWTBotControl;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotMenu;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotRootMenu;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotTreeItem;
import org.eclipse.tracecompass.analysis.lami.core.tests.shared.analysis.LamiAnalyses;
import org.eclipse.tracecompass.common.core.NonNullUtils;
import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiResultTable;
import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.module.LamiTableEntry;
import org.eclipse.tracecompass.internal.provisional.analysis.lami.core.types.LamiLongNumber;
import org.eclipse.tracecompass.internal.provisional.tmf.chart.core.chart.ChartType;
import org.eclipse.tracecompass.tmf.chart.ui.swtbot.tests.shared.SWTBotCustomChartUtils;
import org.eclipse.tracecompass.tmf.chart.ui.swtbot.tests.shared.SWTBotCustomChartUtils.AxisType;
import org.eclipse.tracecompass.tmf.core.analysis.ondemand.OnDemandAnalysisManager;
import org.eclipse.tracecompass.tmf.core.tests.shared.TmfTestTrace;
import org.eclipse.tracecompass.tmf.ui.swtbot.tests.shared.ConditionHelpers;
import org.eclipse.tracecompass.tmf.ui.swtbot.tests.shared.ConditionHelpers.SWTBotTestCondition;
import org.eclipse.tracecompass.tmf.ui.swtbot.tests.shared.SWTBotUtils;
import org.eclipse.tracecompass.tmf.ui.tests.shared.WaitUtils;
import org.eclipse.ui.IViewPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.eclipse.swtchart.Chart;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;

/**
 * Test the LAMI custom charts.
 *
 * Note: This tests only the LAMI chart specific behavior with a few stub
 * datasets. The LAMI custom chart model should be tested in the
 * o.e.t.a.lami.core.tests package and the chart maker specificities should be
 * in the custom chart's SWTbot package.
 *
 * @author Geneviève Bastien
 */
@RunWith(SWTBotJunit4ClassRunner.class)
public class LamiChartViewerTest {

    private static final String TRACE_TYPE = "org.eclipse.linuxtools.tmf.core.tests.secondtt";
    private static final String PROJECT_NAME = "test";
    private static final String VIEW_ID = LamiReportView.VIEW_ID;
    private static final TmfTestTrace TRACE = TmfTestTrace.A_TEST_10K;

    /** The Log4j logger instance. */
    private static final Logger fLogger = NonNullUtils.checkNotNull(Logger.getRootLogger());

    private final SWTWorkbenchBot fBot = new SWTWorkbenchBot();
    private @Nullable SWTBotView fViewBot = null;
    private @Nullable LamiReportViewTabPage fCurrentTab;

    /**
     * Start the SWT bot test, register the LAMI analyses and open a trace (any
     * trace) on a new project
     */
    @BeforeClass
    public static void beforeClass() {

        SWTBotUtils.initialize();
        Thread.currentThread().setName("SWTBotTest");
        /* set up for swtbot */
        SWTBotPreferences.TIMEOUT = 20000; /* 20 second timeout */
        SWTBotPreferences.KEYBOARD_LAYOUT = "EN_US";
        fLogger.removeAllAppenders();
        fLogger.addAppender(new ConsoleAppender(new SimpleLayout(), ConsoleAppender.SYSTEM_OUT));
        /* Finish waiting for eclipse to load */
        WaitUtils.waitForJobs();

        OnDemandAnalysisManager.getInstance().registerAnalysis(LamiAnalyses.MULTIPLE_ROW.getAnalysis());
        OnDemandAnalysisManager.getInstance().registerAnalysis(LamiAnalyses.MULTIPLE_SIMILAR_ROW.getAnalysis());

        // Create the project and open the trace
        String tracePath = TRACE.getFullPath();

        SWTBotUtils.createProject(PROJECT_NAME);
        SWTBotUtils.openTrace(PROJECT_NAME, tracePath, TRACE_TYPE);
        WaitUtils.waitForJobs();
    }

    /**
     * Delete the project and de-register the LAMI analyses
     */
    @AfterClass
    public static void afterClass() {
        SWTWorkbenchBot bot = new SWTWorkbenchBot();
        bot.closeAllEditors();
        SWTBotUtils.deleteProject(PROJECT_NAME, bot);
        fLogger.removeAllAppenders();
        OnDemandAnalysisManager.getInstance().unregisterAnalysis(LamiAnalyses.MULTIPLE_ROW.getAnalysis());
        OnDemandAnalysisManager.getInstance().unregisterAnalysis(LamiAnalyses.MULTIPLE_SIMILAR_ROW.getAnalysis());
    }

    /**
     * Reset the current perspective
     */
    @After
    public void resetPerspective() {
        SWTBotView viewBot = fViewBot;
        if (viewBot != null) {
            viewBot.close();
        }
        /*
         * UI Thread executes the reset perspective action, which opens a shell
         * to confirm the action. The current thread will click on the 'Yes'
         * button
         */
        Runnable runnable = () -> SWTBotUtils.anyButtonOf(fBot, "Yes", "Reset Perspective").click();
        UIThreadRunnable.asyncExec(() -> {
            IWorkbenchWindow activeWorkbenchWindow = PlatformUI.getWorkbench().getWorkbenchWindows()[0];
            ActionFactory.RESET_PERSPECTIVE.create(activeWorkbenchWindow).run();
        });
        runnable.run();
    }

    /**
     * Execute the LAMI analyses and return the viewBot corresponding to the
     * newly opened view. To avoid tests interracting with each other, it is
     * preferable to use the returned view bot in a try finally statement and
     * make sure the view is closed at the end of the test.
     *
     * @param analysis
     *            The analysis to execute
     * @return The view bot
     */
    private SWTBotView executeAnalysis(LamiAnalyses analysis) {
        // Open the LAMI analysis
        SWTBotTreeItem tracesFolder = SWTBotUtils.selectTracesFolder(fBot, PROJECT_NAME);
        SWTBotTreeItem externalAnalyses = SWTBotUtils.getTraceProjectItem(fBot, tracesFolder, TRACE.getPath(), "External Analyses", analysis.getAnalysis().getName());
        assertNotNull(externalAnalyses);
        fBot.waitUntil(isLamiAnalysisEnabled(externalAnalyses));
        externalAnalyses.doubleClick();

        fBot.shell("External Analysis Parameters").activate();
        fBot.button("OK").click();
        WaitUtils.waitForJobs();

        SWTBotView viewBot = fBot.viewById(VIEW_ID);
        viewBot.setFocus();
        final @Nullable LamiReportView lamiView = UIThreadRunnable.syncExec((Result<@Nullable LamiReportView>) () -> {
            IViewPart viewRef = viewBot.getViewReference().getView(true);
            return (viewRef instanceof LamiReportView) ? (LamiReportView) viewRef : null;
        });
        assertNotNull(lamiView);
        SWTBotUtils.maximize(lamiView);
        fViewBot = viewBot;

        fCurrentTab = lamiView.getCurrentSelectedPage();

        return viewBot;
    }

    /**
     * Test a few scatter charts with the multiple row dataset.
     *
     * @throws SecurityException
     *             If a security manager is present and any the wrong class is
     *             loaded or the class loader is not the same as its ancestor's
     *             loader.
     *
     * @throws IllegalArgumentException
     *             the object is not the correct class type
     */
    @Test
    public void testScatterMultipleRow() throws SecurityException, IllegalArgumentException {
        SWTBotView viewBot = executeAnalysis(LamiAnalyses.MULTIPLE_ROW);

        // Get the expected maximum and minimum values of each axis
        LamiResultTable resultTable = LamiAnalyses.MULTIPLE_ROW.getAnalysis().getResultTable(0);
        Long minX = Long.MAX_VALUE;
        Long maxX = Long.MIN_VALUE;
        Long minY = Long.MAX_VALUE;
        Long maxY = Long.MIN_VALUE;
        for (LamiTableEntry entry : resultTable.getEntries()) {
            Long wakeupTs = ((LamiLongNumber) entry.getValue(0)).getValue();
            Long switchTs = ((LamiLongNumber) entry.getValue(1)).getValue();
            Long latency = ((LamiLongNumber) entry.getValue(2)).getValue();
            if (wakeupTs != null) {
                minX = Math.min(minX, wakeupTs);
                maxX = Math.max(maxX, wakeupTs);
            }
            if (switchTs != null) {
                minX = Math.min(minX, switchTs);
                maxX = Math.max(maxX, switchTs);
            }
            if (latency != null) {
                minY = Math.min(minY, latency);
                maxY = Math.max(maxY, latency);
            }
        }

        // Create a new chart
        SWTBotRootMenu viewMenu = viewBot.viewMenu();
        SWTBotMenu menu = viewMenu.menu("New custom chart");
        menu.click();

        // Create a scatter chart of Wakeup timestamp vs scheduling latency
        // and Switch timestamp vs scheduling latency
        SWTBotCustomChartUtils.selectChartType(fBot, ChartType.SCATTER_CHART);
        SWTBotCustomChartUtils.addSeries(fBot, "Wakeup timestamp", Collections.singleton("Scheduling latency (ns)"));
        SWTBotCustomChartUtils.addSeries(fBot, "Switch timestamp", Collections.singleton("Scheduling latency (ns)"));
        SWTBotCustomChartUtils.confirmDialog(fBot);

        WaitUtils.waitForJobs();

        // Wait for the viewer and verify its parameters
        @Nullable Chart customChart = viewBot.bot().widget(WidgetOfType.widgetOfType(Chart.class), 0);
        Event mouseMove = new Event();
        mouseMove.type = SWT.MouseEnter;
        customChart.getDisplay().post(mouseMove);
        assertNotNull(customChart);

        fBot.waitUntil(ConditionHelpers.numberOfSeries(customChart, 2));
        SWTBotChart chartBot = new SWTBotChart(customChart);
        assertVisible(chartBot);

        // Verify the titles
        SWTBotCustomChartUtils.assertTitles(customChart, "Scheduling log", "Value (ss.SSS)", "Scheduling latency (ns)");

        // Make sure the axis formatters have the right range
        SWTBotCustomChartUtils.assertAxisRange(customChart, AxisType.X, minX, maxX);
        SWTBotCustomChartUtils.assertAxisRange(customChart, AxisType.Y, minY, maxY);
        SWTBotCustomChartUtils.assertAxisLogscale(customChart, AxisType.Y, false);

        // Verify the series titles
        SWTBotCustomChartUtils.assertSeriesTitle(customChart, ImmutableList.of("Scheduling latency by Wakeup timestamp", "Scheduling latency by Switch timestamp"));
        closeCharts();

        // Create the same chart, but with log scale enabled in Y. Make sure
        // the results are the same
        menu.click();

        SWTBotCustomChartUtils.selectChartType(fBot, ChartType.SCATTER_CHART);
        SWTBotCustomChartUtils.addSeries(fBot, "Wakeup timestamp", Collections.singleton("Scheduling latency (ns)"));
        SWTBotCustomChartUtils.addSeries(fBot, "Switch timestamp", Collections.singleton("Scheduling latency (ns)"));
        SWTBotCustomChartUtils.setLogScale(fBot, AxisType.Y);
        SWTBotCustomChartUtils.confirmDialog(fBot);

        WaitUtils.waitForJobs();

        // Wait for the viewer and verify its parameters
        customChart = viewBot.bot().widget(WidgetOfType.widgetOfType(Chart.class), 0);
        assertNotNull(customChart);
        customChart.getDisplay().post(mouseMove);

        fBot.waitUntil(ConditionHelpers.numberOfSeries(customChart, 2));
        chartBot = new SWTBotChart(customChart);
        assertVisible(chartBot);

        // Verify the titles
        SWTBotCustomChartUtils.assertTitles(customChart, "Scheduling log", "Value (ss.SSS)", "Scheduling latency (ns)");

        // Make sure the axis formatter have the right range
        SWTBotCustomChartUtils.assertAxisRange(customChart, AxisType.X, minX, maxX);
        // Logscale charts are clamped to 0
        SWTBotCustomChartUtils.assertAxisRange(customChart, AxisType.Y, 0, maxY);
        SWTBotCustomChartUtils.assertAxisLogscale(customChart, AxisType.Y, true);

        // Verify the series titles
        SWTBotCustomChartUtils.assertSeriesTitle(customChart, ImmutableList.of("Scheduling latency by Wakeup timestamp", "Scheduling latency by Switch timestamp"));
        closeCharts();

    }

    private void closeCharts() {
        LamiReportViewTabPage currentTab = fCurrentTab;
        assertNotNull(currentTab);
        UIThreadRunnable.syncExec(() -> currentTab.clearAllCustomViewers());
    }

    /**
     * Test a few charts with the multiple similar row dataset.
     *
     * @throws SecurityException
     *             If a security manager is present and any the wrong class is
     *             loaded or the class loader is not the same as its ancestor's
     *             loader.
     *
     * @throws IllegalArgumentException
     *             the object is not the correct class type
     */
    @Test
    public void testSimilarRows() throws SecurityException, IllegalArgumentException {
        SWTBotView viewBot = executeAnalysis(LamiAnalyses.MULTIPLE_SIMILAR_ROW);

        // Create a new chart
        SWTBotRootMenu viewMenu = viewBot.viewMenu();
        SWTBotMenu menu = viewMenu.menu("New custom chart");
        menu.click();

        // Create a bar chart of Wakee process (name) vs scheduling latency,
        // Priority and Target CPU
        SWTBotCustomChartUtils.selectChartType(fBot, ChartType.BAR_CHART);
        SWTBotCustomChartUtils.addSeries(fBot, "Wakee process (name)", ImmutableSet.of("Scheduling latency (ns)", "Priority", "Target CPU"));
        SWTBotCustomChartUtils.confirmDialog(fBot);

        WaitUtils.waitForJobs();

        // Wait for the viewer and verify its parameters
        @Nullable
        Chart customChart = viewBot.bot().widget(WidgetOfType.widgetOfType(Chart.class), 0);
        assertNotNull(customChart);
        Event mouseMove = new Event();
        mouseMove.type = SWT.MouseEnter;
        customChart.getDisplay().post(mouseMove);

        fBot.waitUntil(ConditionHelpers.numberOfSeries(customChart, 3));
        SWTBotChart chartBot = new SWTBotChart(customChart);
        assertVisible(chartBot);

        // Verify the titles
        SWTBotCustomChartUtils.assertTitles(customChart, "Scheduling log", "Wakee process (name)", "Value");

        // Make sure the axis formatter have the right categories and range
        String[] xValues = new String[6];
        Arrays.fill(xValues, "swapper/5");
        SWTBotCustomChartUtils.assertCategoriesAxis(customChart, AxisType.X, xValues);
        SWTBotCustomChartUtils.assertAxisRange(customChart, AxisType.Y, 0, 2);
        SWTBotCustomChartUtils.assertAxisLogscale(customChart, AxisType.Y, false);

        // Verify the series titles
        SWTBotCustomChartUtils.assertSeriesTitle(customChart, ImmutableList.of("Scheduling latency (ns)", "Priority", "Target CPU"));
        closeCharts();

        // Create a bar chart of Waker process (name) vs scheduling latency,
        // Priority and Target CPU
        menu.click();
        fBot.shell("Custom chart creation").activate();

        SWTBotCustomChartUtils.selectChartType(fBot, ChartType.BAR_CHART);
        SWTBotCustomChartUtils.addSeries(fBot, "Waker process (name)", ImmutableSet.of("Scheduling latency (ns)", "Priority", "Target CPU"));
        SWTBotCustomChartUtils.confirmDialog(fBot);

        WaitUtils.waitForJobs();

        // Wait for the viewer and verify its parameters
        customChart = viewBot.bot().widget(WidgetOfType.widgetOfType(Chart.class), 0);
        assertNotNull(customChart);
        customChart.getDisplay().post(mouseMove);

        fBot.waitUntil(ConditionHelpers.numberOfSeries(customChart, 3));
        chartBot = new SWTBotChart(customChart);
        assertVisible(chartBot);

        // Verify the titles
        SWTBotCustomChartUtils.assertTitles(customChart, "Scheduling log", "Waker process (name)", "Value");

        // Make sure the axis formatter have the right categories and range
        Arrays.fill(xValues, "?");
        SWTBotCustomChartUtils.assertCategoriesAxis(customChart, AxisType.X, xValues);
        SWTBotCustomChartUtils.assertAxisRange(customChart, AxisType.Y, 0, 2);
        SWTBotCustomChartUtils.assertAxisLogscale(customChart, AxisType.Y, false);

        // Verify the series titles
        SWTBotCustomChartUtils.assertSeriesTitle(customChart, ImmutableList.of("Scheduling latency (ns)", "Priority", "Target CPU"));
        closeCharts();
    }

    /**
     * Check if a LAMI analysis is enabled, ie the item is enabled, and not
     * striked-out
     *
     * @param item
     *            the item representing the LAMI analysis element
     * @return true or false, it should swallow all exceptions
     */
    private static ICondition isLamiAnalysisEnabled(final SWTBotTreeItem item) {
        return new SWTBotTestCondition() {

            private boolean fIsOk = false;

            @Override
            public boolean test() throws Exception {
                try {
                    if (!item.isEnabled()) {
                        return false;
                    }
                    Display.getDefault().syncExec(() -> {
                        // Make sure the object is not striked-out, this
                        // property will return a non empty StyleRange[] if if
                        // is
                        Object data = item.widget.getData("org.eclipse.jfacestyled_label_key_0");
                        if (data == null) {
                            fIsOk = false;
                            return;
                        }
                        if (data instanceof StyleRange[]) {
                            StyleRange[] sr = (StyleRange[]) data;
                            fIsOk = sr.length == 0;
                        } else {
                            fIsOk = false;
                        }
                    });

                } catch (Exception e) {
                }
                return fIsOk;
            }

            @Override
            public String getFailureMessage() {
                return "The lami analysis is not enabled";
            }
        };
    }

    private static class SWTBotChart extends AbstractSWTBotControl<Chart> {
        public SWTBotChart(Chart w) throws WidgetNotFoundException {
            super(w);
        }
    }

}
