/*******************************************************************************
 * Copyright (c) 2015, 2017 Ericsson and others
 *
 * 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
 *
 * Contributors:
 *   Matthew Khouzam - Initial API and implementation
 *   Mikael Ferland - Add tests for BasicSymbolProvider dialog
 *******************************************************************************/

package org.eclipse.tracecompass.lttng2.ust.ui.swtbot.tests;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.Logger;
import org.apache.log4j.SimpleLayout;
import org.eclipse.core.runtime.FileLocator;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot;
import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotView;
import org.eclipse.swtbot.swt.finder.SWTBot;
import org.eclipse.swtbot.swt.finder.junit.SWTBotJunit4ClassRunner;
import org.eclipse.swtbot.swt.finder.utils.SWTBotPreferences;
import org.eclipse.swtbot.swt.finder.waits.Conditions;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotShell;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotTable;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotToolbarButton;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotTree;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotTreeItem;
import org.eclipse.tracecompass.analysis.profiling.ui.views.flamechart.FlameChartView;
import org.eclipse.tracecompass.testtraces.ctf.CtfTestTrace;
import org.eclipse.tracecompass.tmf.core.signal.TmfSelectionRangeUpdatedSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager;
import org.eclipse.tracecompass.tmf.core.timestamp.ITmfTimestamp;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimeRange;
import org.eclipse.tracecompass.tmf.core.timestamp.TmfTimestamp;
import org.eclipse.tracecompass.tmf.ctf.core.tests.shared.CtfTmfTestTraceUtils;
import org.eclipse.tracecompass.tmf.ui.dialog.TmfFileDialogFactory;
import org.eclipse.tracecompass.tmf.ui.swtbot.tests.shared.ConditionHelpers;
import org.eclipse.tracecompass.tmf.ui.swtbot.tests.shared.SWTBotTimeGraph;
import org.eclipse.tracecompass.tmf.ui.swtbot.tests.shared.SWTBotTimeGraphEntry;
import org.eclipse.tracecompass.tmf.ui.swtbot.tests.shared.SWTBotUtils;
import org.eclipse.tracecompass.tmf.ui.tests.shared.WaitUtils;
import org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView;
import org.eclipse.ui.IWorkbenchPart;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;

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

/**
 * Test for the Call Stack view in trace compass
 */
@RunWith(SWTBotJunit4ClassRunner.class)
public class FlameChartViewTest {

    private static final String UST_ID = "org.eclipse.linuxtools.lttng2.ust.tracetype";

    private static final String PROJECT_NAME = "TestForCallstack";
    private static final String TRACE = "glxgears-cyg-profile";
    private static final String PROCESS = "UNKNOWN";
    private static final @NonNull String THREAD = "glxgears-16073";

    /** The Log4j logger instance. */
    private static final Logger sfLogger = Logger.getRootLogger();
    private static SWTWorkbenchBot sfBot;

    /**
     * Start time of the trace
     */
    private static final @NonNull ITmfTimestamp START_TIME = TmfTimestamp.fromNanos(1378850463596911581l);

    /**
     * Timestamp for loading mapping files test
     */
    private static final long TIMESTAMP = 1378850463804917148l;

    /**
     * Timestamps of consecutive events in the trace
     */
    private static final long TIMESTAMPS[] = new long[] {
            1378850463804898643l,
            1378850463804899057l,
            1378850463804900219l,
            1378850463804900678l,
            1378850463804901308l,
            1378850463804901909l,
            1378850463804902763l,
            1378850463804903168l,
            1378850463804903766l,
            1378850463804904165l,
            1378850463804904970l
    };

    /**
     * Stack frames of consecutive events in the trace
     */
    private static final String[] STACK_FRAMES[] = new String[][] {
            { "0x40472b", "0x4045c8", "0x404412" },
            { "0x40472b", "0x4045c8", "0x404412", "0x40392b" },
            { "0x40472b", "0x4045c8", "0x404412" },
            { "0x40472b", "0x4045c8" },
            { "0x40472b", "0x4045c8", "0x404412" },
            { "0x40472b", "0x4045c8", "0x404412", "0x40392b" },
            { "0x40472b", "0x4045c8", "0x404412" },
            { "0x40472b", "0x4045c8" },
            { "0x40472b", "0x4045c8", "0x404412" },
            { "0x40472b", "0x4045c8", "0x404412", "0x40392b" },
            { "0x40472b", "0x4045c8", "0x404412" },
    };

    /** Tooltips of the toolbar buttons */

    private static final @NonNull String CONFIGURE_SYMBOL_PROVIDERS = "Configure how the addresses are mapped to function names";
    // Separator
    private static final @NonNull String SORT_BY_NAME = "Name";
    private static final @NonNull String SORT_BY_ID = "PID/TID";
    private static final @NonNull String SORT_BY_START = "Start time";
    // Separator
    private static final @NonNull String SHOW_VIEW_FILTERS = "Show View Filters";
    // Separator
    private static final @NonNull String RESET_TIME_SCALE = "Reset the Time Scale to Default";
    private static final @NonNull String SELECT_PREVIOUS_STATE_CHANGE = "Select Previous State Change";
    private static final @NonNull String SELECT_NEXT_STATE_CHANGE = "Select Next State Change";
    // Separator
    private static final @NonNull String ADD_BOOKMARK = "Add Bookmark...";
    private static final @NonNull String PREVIOUS_MARKER = "Previous Marker";
    private static final @NonNull String NEXT_MARKER = "Next Marker";
    // Separator
    private static final @NonNull String SELECT_PREVIOUS_ITEM = "Select Previous Item";
    private static final @NonNull String SELECT_NEXT_ITEM = "Select Next Item";
    private static final @NonNull String ZOOM_IN = "Zoom In";
    private static final @NonNull String ZOOM_OUT = "Zoom Out";
    // Separator
    private static final @NonNull String PIN_VIEW = "Pin View";

    private static final List<String> TOOLBAR_BUTTONS_TOOLTIPS = ImmutableList.of(
            CONFIGURE_SYMBOL_PROVIDERS,
            "",
            SHOW_VIEW_FILTERS,
            "",
            RESET_TIME_SCALE, SELECT_PREVIOUS_STATE_CHANGE, SELECT_NEXT_STATE_CHANGE,
            "",
            ADD_BOOKMARK, PREVIOUS_MARKER, NEXT_MARKER,
            "",
            SELECT_PREVIOUS_ITEM, SELECT_NEXT_ITEM, ZOOM_IN, ZOOM_OUT,
            "",
            PIN_VIEW);

    /**
     * Initialization
     */
    @BeforeClass
    public static void init() {
        SWTBotUtils.initialize();

        Thread.currentThread().setName("SWTBot Thread"); // for the debugger
        /* set up for swtbot */
        SWTBotPreferences.TIMEOUT = 20000; /* 20 second timeout */
        sfLogger.removeAllAppenders();
        sfLogger.addAppender(new ConsoleAppender(new SimpleLayout()));
        sfBot = new SWTWorkbenchBot();

        SWTBotUtils.closeView("Statistics", sfBot);
        /* finish waiting for eclipse to load */
        WaitUtils.waitForJobs();
    }

    /**
     * Clean up
     */
    @AfterClass
    public static void afterClass() {
        sfLogger.removeAllAppenders();
    }

    /**
     * Open a trace in an editor
     */
    @Before
    public void beforeTest() {
        SWTBotUtils.createProject(PROJECT_NAME);
        SWTBotTreeItem treeItem = SWTBotUtils.selectTracesFolder(sfBot, PROJECT_NAME);
        assertNotNull(treeItem);
        final CtfTestTrace cygProfile = CtfTestTrace.CYG_PROFILE;
        final File file = new File(CtfTmfTestTraceUtils.getTrace(cygProfile).getPath());
        CtfTmfTestTraceUtils.dispose(cygProfile);
        SWTBotUtils.openTrace(PROJECT_NAME, file.getAbsolutePath(), UST_ID);
        SWTBotUtils.activateEditor(sfBot, file.getName());
        SWTBotUtils.openView(FlameChartView.ID);
        WaitUtils.waitForJobs();
        final SWTBotView viewBot = sfBot.viewById(FlameChartView.ID);
        IWorkbenchPart part = viewBot.getViewReference().getPart(false);
        sfBot.waitUntil(ConditionHelpers.timeGraphIsReadyCondition((AbstractTimeGraphView) part, new TmfTimeRange(START_TIME, START_TIME), START_TIME));
    }

    /**
     * Close the editor
     */
    @After
    public void tearDown() {
        sfBot.closeAllEditors();
        SWTBotUtils.deleteProject(PROJECT_NAME, sfBot);
    }

    /**
     * Test if callstack is populated
     */
    @Test
    public void testOpenCallstack() {
        SWTBotView viewBot = sfBot.viewById(FlameChartView.ID);
        viewBot.setFocus();
        waitForSymbolNames("0x40472b");
    }

    /**
     * Test check callstack at a time
     */
    @Test
    public void testGoToTimeAndCheckStack() {
        final SWTBotView viewBot = sfBot.viewById(FlameChartView.ID);
        viewBot.setFocus();
        WaitUtils.waitForJobs();

        goToTime(TIMESTAMPS[0]);
        waitForSymbolNames(STACK_FRAMES[0]);
    }

    /**
     * Test check callstack at a time after navigating
     */
    @Test
    public void testGoToTimeGoBackAndForthAndCheckStack() {
        int currentEventOffset = 0;
        final SWTBotView viewBot = sfBot.viewById(FlameChartView.ID);

        goToTime(TIMESTAMPS[currentEventOffset]);

        // forward 10 times
        for (int i = 0; i < 10; i++) {
            viewBot.toolbarPushButton(SELECT_NEXT_STATE_CHANGE).click();
            currentEventOffset++;
            sfBot.waitUntil(ConditionHelpers.selectionInEventsTable(sfBot, TIMESTAMPS[currentEventOffset]));
            WaitUtils.waitForJobs();
            waitForSymbolNames(STACK_FRAMES[currentEventOffset]);
        }

        // back twice
        for (int i = 0; i < 2; i++) {
            viewBot.toolbarPushButton(SELECT_PREVIOUS_STATE_CHANGE).click();
            currentEventOffset--;
            sfBot.waitUntil(ConditionHelpers.selectionInEventsTable(sfBot, TIMESTAMPS[currentEventOffset]));
            WaitUtils.waitForJobs();
            waitForSymbolNames(STACK_FRAMES[currentEventOffset]);
        }
        // move up and down once to make sure it doesn't explode
        viewBot.toolbarPushButton(SELECT_PREVIOUS_ITEM).click();
        WaitUtils.waitForJobs();
        viewBot.toolbarPushButton(SELECT_NEXT_ITEM).click();
        WaitUtils.waitForJobs();

        // Zoom in and out too
        viewBot.toolbarPushButton(ZOOM_IN).click();
        WaitUtils.waitForJobs();
        viewBot.toolbarPushButton(ZOOM_OUT).click();
        WaitUtils.waitForJobs();
    }

    /**
     * Test check callstack at a time with sorting, the trace is not sortable,
     * this is a smoke test
     */
    @Test
    public void testGoToTimeSortAndCheckStack() {
        goToTime(TIMESTAMPS[0]);
        final SWTBotView viewBot = sfBot.viewById(FlameChartView.ID);
        viewBot.setFocus();
        SWTBotTree tree = viewBot.bot().tree();
        tree.header(SORT_BY_NAME).click();
        tree.header(SORT_BY_ID).click();
        tree.header(SORT_BY_START).click();
        viewBot.setFocus();
        WaitUtils.waitForJobs();
        waitForSymbolNames(STACK_FRAMES[0]);
    }

    private static void waitForSymbolNames(String... symbolNames) {
        final SWTBotView viewBot = sfBot.viewById(FlameChartView.ID);
        List<String> symbolNameList = Lists.newArrayList(symbolNames);
        WaitUtils.waitUntil(vBot -> symbolNameList.equals(getVisibleStackFrames(vBot)),
                viewBot, "Wrong symbol names, expected:" + symbolNameList
                + ", got: " + getVisibleStackFrames(viewBot));
    }

    private static List<String> getVisibleStackFrames(final SWTBotView viewBot) {
        SWTBotTimeGraph timeGraph = new SWTBotTimeGraph(viewBot.bot());
        List<String> stackFrames = new ArrayList<>();
        for (SWTBotTimeGraphEntry entry : timeGraph.getEntry(TRACE, PROCESS, THREAD).getEntries()) {
            String name = entry.getText();
            if (!name.isEmpty()) {
                stackFrames.add(name);
            }
        }
        return stackFrames;
    }

    private static void goToTime(long timestamp) {
        ITmfTimestamp time = TmfTimestamp.fromNanos(timestamp);
        SWTBotTable table = sfBot.activeEditor().bot().table();
        table.setFocus();
        WaitUtils.waitForJobs();
        TmfSignalManager.dispatchSignal(new TmfSelectionRangeUpdatedSignal(table.widget, time));
        sfBot.waitUntil(ConditionHelpers.selectionInEventsTable(sfBot, timestamp));
        final SWTBotView viewBot = sfBot.viewById(FlameChartView.ID);
        IWorkbenchPart part = viewBot.getViewReference().getPart(false);
        sfBot.waitUntil(ConditionHelpers.timeGraphIsReadyCondition((AbstractTimeGraphView) part, new TmfTimeRange(time, time), time));
    }

    /**
     * Test manipulating valid and invalid mapping files (add, remove and change
     * priority of files)
     *
     * @throws IOException
     *             Missing file
     */
    @Test
    public void testManipulatingMappingFiles() throws IOException {

        // 1- Open valid mapping files and invalid mapping file
        URL mapUrlA = CtfTmfTestTraceUtils.class.getResource("cyg-profile-mapping.txt");
        URL mapUrlB = CtfTmfTestTraceUtils.class.getResource("dummy-mapping.txt");
        URL mapUrlC = CtfTmfTestTraceUtils.class.getResource("invalid-cyg-profile-mapping.txt");
        URL mapUrlD = CtfTmfTestTraceUtils.class.getResource("random.out");
        URL mapUrlE = CtfTmfTestTraceUtils.class.getResource("win32Random.exe");

        String absoluteFileA = FileLocator.toFileURL(mapUrlA).getFile();
        String absoluteFileB = FileLocator.toFileURL(mapUrlB).getFile();
        String absoluteFileC = FileLocator.toFileURL(mapUrlC).getFile();
        String absoluteFileD = FileLocator.toFileURL(mapUrlD).getFile();
        String absoluteFileE = FileLocator.toFileURL(mapUrlE).getFile();
        String[] overrideFiles = { absoluteFileA, absoluteFileA, absoluteFileB, absoluteFileC, absoluteFileD, absoluteFileE };
        TmfFileDialogFactory.setOverrideFiles(overrideFiles);

        SWTBotShell shell = openSymbolProviderDialog();
        final SWTBot symbolDialog = shell.bot();
        symbolDialog.button("Add...").click();
        final SWTBot errorDialog = sfBot.shell("Import failure").bot();
        errorDialog.button("OK").click();
        final SWTBotTable table = symbolDialog.table();
        assertEquals(4, table.rowCount());
        assertEquals(absoluteFileA, table.getTableItem(0).getText());
        assertEquals(absoluteFileB, table.getTableItem(1).getText());
        assertEquals(absoluteFileD, table.getTableItem(2).getText());
        assertEquals(absoluteFileE, table.getTableItem(3).getText());

        // 2- Change priority of mapping files
        table.select(0);
        symbolDialog.button("Down").click().click().click();
        assertEquals(absoluteFileB, table.getTableItem(0).getText());
        assertEquals(absoluteFileD, table.getTableItem(1).getText());
        assertEquals(absoluteFileE, table.getTableItem(2).getText());
        assertEquals(absoluteFileA, table.getTableItem(3).getText());

        symbolDialog.button("Up").click().click().click();
        assertEquals(absoluteFileA, table.getTableItem(0).getText());
        assertEquals(absoluteFileB, table.getTableItem(1).getText());
        assertEquals(absoluteFileD, table.getTableItem(2).getText());
        assertEquals(absoluteFileE, table.getTableItem(3).getText());

        // 3- Remove multiple mapping files
        table.select(0, 1);
        symbolDialog.button("Remove").click();
        assertEquals(2, table.rowCount());

        // 4- Close symbol provider dialog
        symbolDialog.button("Cancel").click();
    }

    /**
     * Test loading conflicting mapping files.
     *
     * @throws IOException
     *             Missing file
     */
    @Test
    public void testLoadingMappingFiles() throws IOException {

        // 1- Open conflicting mapping files
        URL mapUrlA = CtfTmfTestTraceUtils.class.getResource("cyg-profile-mapping.txt");
        URL mapUrlB = CtfTmfTestTraceUtils.class.getResource("dummy-mapping.txt");
        String absoluteFileA = FileLocator.toFileURL(mapUrlA).getFile();
        String absoluteFileB = FileLocator.toFileURL(mapUrlB).getFile();
        String[] overrideFiles = { absoluteFileA, absoluteFileB };
        TmfFileDialogFactory.setOverrideFiles(overrideFiles);

        SWTBotShell shell = openSymbolProviderDialog();
        final SWTBot symbolDialog = shell.bot();
        symbolDialog.button("Add...").click();
        symbolDialog.button("OK").click();

        // 2- Ensure symbols are loaded and prioritized
        sfBot.viewById(FlameChartView.ID).setFocus();
        WaitUtils.waitForJobs();
        goToTime(TIMESTAMP);
        waitForSymbolNames("main", "event_loop", "draw_frame", "draw_gears", "drawB");
    }

    private static SWTBotShell openSymbolProviderDialog() {
        final SWTBotView viewBot = sfBot.viewById(FlameChartView.ID);
        viewBot.setFocus();
        viewBot.toolbarButton(CONFIGURE_SYMBOL_PROVIDERS).click();
        return sfBot.shell("Symbol mapping");
    }

    /**
     * Test check callstack at a time with function map
     *
     * @throws IOException
     *             Missing file
     */
    @Test
    public void testGoToTimeAndCheckStackWithNames() throws IOException {
        goToTime(TIMESTAMPS[0]);
        final SWTBotView viewBot = sfBot.viewById(FlameChartView.ID);
        viewBot.setFocus();

        URL mapUrl = CtfTmfTestTraceUtils.class.getResource("cyg-profile-mapping.txt");
        String absoluteFile = FileLocator.toFileURL(mapUrl).getFile();
        TmfFileDialogFactory.setOverrideFiles(absoluteFile);

        SWTBotShell shell = openSymbolProviderDialog();
        SWTBot shellBot = shell.bot();
        shellBot.button("Add...").click();
        shellBot.button("OK").click();
        shellBot.waitUntil(Conditions.shellCloses(shell));
        SWTBotTimeGraph timeGraph = new SWTBotTimeGraph(viewBot.bot());
        SWTBotTimeGraphEntry[] threads = timeGraph.getEntry(TRACE, PROCESS).getEntries();
        assertEquals(1, threads.length);
        assertEquals(THREAD, threads[0].getText(0));
        waitForSymbolNames("main", "event_loop", "handle_event");
    }

    /**
     * Test Call Stack tool bar
     */
    @Test
    public void testCallStackToolBar() {
        SWTBotView viewBot = sfBot.viewById(FlameChartView.ID);
        viewBot.setFocus();
        List<String> buttons = Lists.transform(viewBot.getToolbarButtons(), SWTBotToolbarButton::getToolTipText);
        assertEquals(TOOLBAR_BUTTONS_TOOLTIPS, buttons);
    }
}
