/*******************************************************************************
 * Copyright (c) 2013, 2018 Red Hat.
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Red Hat - initial implementation
 *******************************************************************************/

package org.eclipse.linuxtools.systemtap.ui.ide.test.swtbot;

import static org.eclipse.swtbot.swt.finder.matchers.WidgetMatcherFactory.allOf;
import static org.eclipse.swtbot.swt.finder.matchers.WidgetMatcherFactory.widgetOfType;
import static org.eclipse.swtbot.swt.finder.matchers.WidgetMatcherFactory.withStyle;
import static org.eclipse.swtbot.swt.finder.matchers.WidgetMatcherFactory.withText;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.MessageFormat;
import java.util.List;

import org.eclipse.linuxtools.internal.systemtap.ui.ide.handlers.ImportDataSetHandler;
import org.eclipse.linuxtools.internal.systemtap.ui.ide.launcher.Messages;
import org.eclipse.linuxtools.internal.systemtap.ui.ide.structures.TapsetLibrary;
import org.eclipse.linuxtools.internal.systemtap.ui.ide.structures.TreeSettings;
import org.eclipse.linuxtools.internal.systemtap.ui.ide.structures.nodedata.FuncparamNodeData;
import org.eclipse.linuxtools.internal.systemtap.ui.ide.structures.nodedata.FunctionNodeData;
import org.eclipse.linuxtools.internal.systemtap.ui.ide.structures.nodedata.ProbeNodeData;
import org.eclipse.linuxtools.internal.systemtap.ui.ide.structures.nodedata.ProbevarNodeData;
import org.eclipse.linuxtools.systemtap.graphing.core.datasets.IDataEntry;
import org.eclipse.linuxtools.systemtap.graphing.core.datasets.IFilteredDataSet;
import org.eclipse.linuxtools.systemtap.graphing.core.datasets.row.FilteredRowDataSet;
import org.eclipse.linuxtools.systemtap.graphing.core.datasets.row.RowEntry;
import org.eclipse.linuxtools.systemtap.graphing.core.structures.GraphData;
import org.eclipse.linuxtools.systemtap.graphing.ui.charts.AbstractChartBuilder;
import org.eclipse.linuxtools.systemtap.graphing.ui.wizards.graph.GraphFactory;
import org.eclipse.linuxtools.systemtap.structures.TreeDefinitionNode;
import org.eclipse.linuxtools.systemtap.structures.TreeNode;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot;
import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotEclipseEditor;
import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotEditor;
import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotView;
import org.eclipse.swtbot.swt.finder.exceptions.WidgetNotFoundException;
import org.eclipse.swtbot.swt.finder.finders.ContextMenuHelper;
import org.eclipse.swtbot.swt.finder.finders.UIThreadRunnable;
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.waits.DefaultCondition;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotButton;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotCTabItem;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotCombo;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotMenu;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotScale;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotShell;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotSlider;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotTable;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotText;
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.swtbot.swt.finder.widgets.TimeoutException;
import org.hamcrest.Matcher;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.swtchart.Chart;
import org.swtchart.IAxis;
import org.swtchart.ISeries;
import org.swtchart.Range;

@RunWith(SWTBotJunit4ClassRunner.class)
@Ignore("fails with Oxygen")
public class TestCreateSystemtapScript {
    private static final String SYSTEMTAP_PROJECT_NAME = "SystemtapTest";

    private static SWTWorkbenchBot bot;
    private static SWTBotView projectExplorer;
    private static SWTBotShell mainShell;

    // dummy probe/function information
    private static File probeDef, funcDef;
    private static final String probeCategoryFull = "Static Probes";
    private static final String probeCategoryEmpty = "Probe Aliases";
    private static final String probeGroup = "probegroup";
    private static final String probeSingleWithoutDef = "testprobe";
    private static final String funcNodeName = "ftest";

    private static class NodeAvailableAndSelect extends DefaultCondition {

        private SWTBotTree tree;
        private String parent;
        private String node;

        /**
         * Wait for a tree node (with a known parent) to become visible, and select it
         * when it does. Note that this wait condition should only be used after having
         * made an attempt to reveal the node.
         * @param tree The SWTBotTree that contains the node to select.
         * @param parent The text of the parent node that contains the node to select.
         * @param node The text of the node to select.
         */
        NodeAvailableAndSelect(SWTBotTree tree, String parent, String node) {
            this.tree = tree;
            this.node = node;
            this.parent = parent;
        }

        @Override
        public boolean test() {
            try {
                SWTBotTreeItem parentNode = tree.getTreeItem(parent);
                parentNode.getNode(node).select();
                return true;
            } catch (WidgetNotFoundException e) {
                return false;
            }
        }

        @Override
        public String getFailureMessage() {
            return "Timed out waiting for " + node; //$NON-NLS-1$
        }
    }

    private static class TreeItemPopulated extends DefaultCondition {

        private SWTBotTreeItem parent;

        TreeItemPopulated(SWTBotTreeItem parent) {
            this.parent = parent;
        }

        @Override
        public boolean test() {
            return this.parent.getItems().length > 0;
        }

        @Override
        public String getFailureMessage() {
            return "Timed out waiting for tree to populate.";
        }
    }

    private static class TableHasUpdated extends DefaultCondition {

        final private int expectedRows;
        final private boolean exact;
        final private SWTBotTable table;
        /**
         * Wait for the provided GraphSelectorEditor table to be fully updated.
         * @param graphTable Which graph set table to watch for updates.
         * @param expectedRows How many entries/rows are expected to be in the table when it's fully updated.
         * @param exact Set this to <code>true</code> if the number of graph columns should exactly match the
         * expected amount, or <code>false</code> if it may be greater than the expected amount.
         */
        public TableHasUpdated(SWTBotTable graphTable, int expectedRows, boolean exact) {
            this.expectedRows = expectedRows;
            this.exact = exact;
            table = graphTable;
        }
        @Override
        public boolean test() {
            if (!exact) {
                return table.rowCount() >= expectedRows;
            } else {
                return table.rowCount() == expectedRows;
            }
        }

        @Override
        public String getFailureMessage() {
            return "Timed out waiting for data table to update";
        }
    }

    private static class ChartHasUpdated extends DefaultCondition {

        private Chart chart;
        int oldCount;
        int expectedCount;
        /**
         * Wait for the provided chart to become updated.
         * @param chart The chart to watch for an update.
         * @param expectedCount The expected number of series points. Set to -1 to instead
         * check for when there are more points than there were at the beginning.
         */
        public ChartHasUpdated(Chart chart, int expectedCount) {
            this.chart = chart;
            ISeries[] seriesSet = chart.getSeriesSet().getSeries();
            this.oldCount = seriesSet.length > 0 ? seriesSet[0].getXSeries().length : 0;
            this.expectedCount = expectedCount;
        }
        @Override
        public boolean test() {
            ISeries[] seriesSet = chart.getSeriesSet().getSeries();
            int newCount = seriesSet.length > 0 ? seriesSet[0].getXSeries().length : 0;
            return expectedCount < 0 ? newCount > oldCount : newCount == expectedCount;
        }

        @Override
        public String getFailureMessage() {
            return "Timed out waiting for chart to update";
        }

    }

    private static class EditorIsActive extends DefaultCondition {

        private String editorName;
        public EditorIsActive(String editorName) {
            this.editorName = editorName;
        }

        @Override
        public boolean test() {
            return TestCreateSystemtapScript.bot.activeEditor().getTitle().equals(editorName);
        }

        @Override
        public String getFailureMessage() {
            return "Timed out waiting for editor with name \"" + editorName + "\" to become active.";
        }
    }

    /**
     * Click an item from the main Eclipse menu, with a guarantee that the main
     * shell will be in focus.
     * @param items The names of each item in the path to the target item to click.
     * For example, to click "File->New->Project...", the items would be "File",
     * "New", and "Project...".
     */
    public static void clickMainMenu(String... items) {
        if (items.length == 0) {
            return;
        }
        mainShell.setFocus();
        SWTBotMenu menu = bot.menu(items[0]);
        for (int i = 1; i < items.length; i++) {
            menu = menu.menu(items[i]);
        }
        menu.click();
    }

    @BeforeClass
    public static void beforeClass() {
        SWTBotPreferences.TIMEOUT = 20000;
        bot = new SWTWorkbenchBot();

        try {
            bot.viewByTitle("Welcome").close();
            // hide Subclipse Usage stats popup if present/installed
            bot.shell("Subclipse Usage").activate();
            bot.button("Cancel").click();
        } catch (WidgetNotFoundException e) {
            //ignore
        	e.printStackTrace();
        }

        prepareTreeSettings();

        // Set SystemTap IDE perspective.
        bot.perspectiveByLabel("SystemTap IDE").activate();
        bot.sleep(5000);
        for (SWTBotShell sh : bot.shells()) {
            if (sh.getText().contains("SystemTap IDE")) {
                mainShell = sh;
                sh.activate();
                bot.sleep(500);
                break;
            }
        }

        // Dismiss "Systemtap not installed" dialog(s) if present.
        try {
            SWTBotShell shell = bot.shell("Cannot Run SystemTap").activate();
            shell.close();

            shell = bot.shell("Cannot Run SystemTap").activate();
            shell.close();
        } catch (WidgetNotFoundException e) {
            //ignore
        }

        // Create a Systemtap project.
        clickMainMenu("File", "New", "Project...");
        SWTBotShell shell = bot.shell("New Project");
        shell.setFocus();
        shell.bot().text().setText("Project");
        bot.waitUntil(new NodeAvailableAndSelect(bot.tree(), "General", "Project"));
        bot.button("Next >").click();
        bot.textWithLabel("Project name:").setText(SYSTEMTAP_PROJECT_NAME);
        bot.button("Finish").click();
        bot.waitUntil(Conditions.shellCloses(shell));
        projectExplorer = bot.viewByTitle("Project Explorer");
        projectExplorer.setFocus();
        projectExplorer.bot().tree().select(SYSTEMTAP_PROJECT_NAME)
            .contextMenu("Go Into").click();
    }

    /**
     * Load custom contents into the Function/Probe views.
     */
    private static void prepareTreeSettings() {
        TapsetLibrary.stop();

        try {
            probeDef = File.createTempFile("probeDef", ".stp");
            funcDef = File.createTempFile("funcDef", ".stp");
            probeDef.deleteOnExit();
            funcDef.deleteOnExit();
        } catch (IOException e) {}
        // Leave one of the files blank to test token search failures.
        try (PrintWriter writer = new PrintWriter(probeDef)) {
            writer.print("test file\nptest\nlast line\n");
        } catch (FileNotFoundException e) {}

        TreeNode testProbNodeParent = new TreeNode(null, false);
        // Have static/alias folders to comply with convention (change this later).
        testProbNodeParent.add(new TreeNode(probeCategoryEmpty, true));
        testProbNodeParent.add(new TreeNode(probeCategoryFull, true));
        TreeNode testProbNodeGroup = new TreeNode(probeGroup, true);
        String innerProbe = "probegroup.inner";
        TreeNode testProbNode = new TreeDefinitionNode(new ProbeNodeData(innerProbe), innerProbe, probeDef.getPath(), true);
        testProbNode.add(new TreeNode(new ProbevarNodeData("s:string"), false));
        testProbNodeGroup.add(testProbNode);
        testProbNodeParent.getChildAt(1).add(testProbNodeGroup);
        TreeNode testProbNode2 = new TreeDefinitionNode(new ProbeNodeData(probeSingleWithoutDef), probeSingleWithoutDef, null, true);
        testProbNodeParent.getChildAt(1).add(testProbNode2);

        TreeNode testFuncNodeParent = new TreeNode(null, false);
        TreeNode testFuncNode = new TreeDefinitionNode(new FunctionNodeData("function ftest(x:long)", null), funcNodeName, funcDef.getPath(), true);
        testFuncNode.add(new TreeNode(new FuncparamNodeData("long"), "x", false));
        testFuncNodeParent.add(testFuncNode);

        TreeSettings.setTrees(testFuncNodeParent, testProbNodeParent);
        TapsetLibrary.readTreeFile();
    }

    @After
    public void cleanUp() {
        SWTBotShell[] shells = bot.shells();
        for (final SWTBotShell shell : shells) {
            String shellTitle = shell.getText();
            if (shellTitle.length() > 0
                    && !shellTitle.startsWith("SystemTap IDE")
                    && !shellTitle.startsWith("Quick Access")) {
				UIThreadRunnable.syncExec(() -> {
					if (shell.widget.getParent() != null) {
						shell.close();
					}
				});
            }
        }
        bot.closeAllEditors();
        mainShell.activate();
    }

    @AfterClass
    public static void finalCleanUp() {
        projectExplorer.setFocus();
        SWTBotToolbarButton forwardButton = projectExplorer.toolbarPushButton("Forward");
        projectExplorer.toolbarPushButton("Back to Workspace").click();
        bot.waitUntil(Conditions.widgetIsEnabled(forwardButton));

        projectExplorer.bot().tree().select(SYSTEMTAP_PROJECT_NAME)
            .contextMenu("Delete").click();
        SWTBotShell deleteShell = bot.shell("Delete Resources");
        deleteShell.bot().button("OK").click();
        bot.waitUntil(Conditions.shellCloses(deleteShell));
    }

    public static void createScript(SWTWorkbenchBot bot, String scriptName) {
        clickMainMenu("File", "New", "Other...");
        SWTBotShell shell = bot.shell("New");
        shell.setFocus();
        shell.bot().text().setText("SystemTap");
        bot.waitUntil(new NodeAvailableAndSelect(bot.tree(), "SystemTap", "SystemTap Script"));
        bot.button("Next >").click();

        SWTBotText text = bot.textWithLabel("Script Name:").setText(scriptName);
        assertEquals(scriptName, text.getText());
        text = bot.textWithLabel("Project:").setText(SYSTEMTAP_PROJECT_NAME);
        assertEquals(SYSTEMTAP_PROJECT_NAME, text.getText());

        bot.button("Finish").click();
        bot.waitUntil(Conditions.shellCloses(shell));
        assertEquals(scriptName, bot.activeEditor().getTitle());
    }

    private static SWTBotShell prepareScript(String scriptName, String scriptContents) {
        createScript(bot, scriptName);
        if (scriptContents != null) {
            SWTBotEclipseEditor editor = bot.editorByTitle(scriptName).toTextEditor();
            editor.setText(scriptContents);
            editor.save();
        }

        openRunConfigurations(scriptName);
        SWTBotShell shell = bot.shell("Run Configurations");
        shell.setFocus();
        bot.tree().select("SystemTap").contextMenu("New").click();

        // Select the "Graphing" tab and enable output graphing.
        bot.cTabItem(Messages.SystemTapScriptGraphOptionsTab_graphingTitle).activate();
        bot.checkBox(Messages.SystemTapScriptGraphOptionsTab_graphOutputRun).click();
        return shell;
    }

    @Test
    public void testCreateScript() {
        String scriptName = "testScript.stp";
        createScript(bot, scriptName);

        // Type a script
        SWTBotEclipseEditor editor = bot.editorByTitle(scriptName).toTextEditor();
        editor.typeText(0, editor.getText().length(), "\nprobe begin{log(\"began");
        editor.typeText(0, editor.getText().length() - 1, "); exit(");
        editor.typeText(0, editor.getText().length(), "}");
        editor.save();

        openRunConfigurations(scriptName);
        SWTBotShell shell = bot.shell("Run Configurations");
        shell.setFocus();
        bot.tree().select("SystemTap").contextMenu("New").click();
    }

    @Test
    public void testTapsetContents() {
        // Create a blank script and add a function to it while it's open.
        String scriptName = "probeScript.stp";
        createScript(bot, scriptName);

        SWTBotView funcView = bot.viewByTitle("Function");
        funcView.setFocus();
        SWTBotTree funcTree = funcView.bot().tree();

        SWTBotTreeItem item = funcTree.getTreeItem(funcNodeName);
        item.doubleClick();
        SWTBotEclipseEditor editor = bot.activeEditor().toTextEditor();
        assertTrue(editor.getText().contains(item.getText()));

        // Open a non-stap file and add a probe. This should bring up a dialog
        // asking if the function should be added to the only open .stp file.
        clickMainMenu("File", "New", "Other...");
        SWTBotShell shell = bot.shell("New");
        shell.setFocus();
        shell.bot().text().setText("Untitled Text File");
        bot.waitUntil(new NodeAvailableAndSelect(bot.tree(), "General", "Untitled Text File"));
        bot.button("Finish").click();
        bot.waitUntil(Conditions.shellCloses(shell));

        SWTBotView probeView = bot.viewByTitle("Probe Alias");
        probeView.setFocus();
        SWTBotTree probeTree = probeView.bot().tree();
        SWTBotTreeItem probeCategory = probeTree.getTreeItem(probeCategoryFull);
        probeCategory.expand();
        bot.waitUntil(new TreeItemPopulated(probeCategory));

        String dialogTitle = "Select Script";
        item = probeCategory.getNode(probeGroup);
        item.expand();
        bot.waitUntil(new TreeItemPopulated(item));
        item = item.getNode(0);
        item.doubleClick();

        {
            Matcher<Shell> withText = withText(dialogTitle);
            bot.waitUntil(Conditions.waitForShell(withText));
        }

        shell = bot.shell(dialogTitle);
        shell.setFocus();
        bot.button("Yes").click();
        bot.waitUntil(Conditions.shellCloses(shell));

        // The editor containing the script should now be in focus.
        bot.waitUntil(new EditorIsActive(scriptName));
        assertTrue(wasProbeInserted(editor, item, false));

        // Open the probe's definition file (an .stp script).
        probeView.show();
        item.contextMenu("View Definition").click();
        bot.waitUntil(new EditorIsActive(probeDef.getName()));

        // Adding a probe while an .stp editor is in focus should always add it
        // to that editor, even if multiple .stp editors are open.
        item = probeCategory.getNode(probeGroup);
        item.doubleClick();
        assertTrue(wasProbeInserted(bot.activeEditor().toTextEditor(), item, true));
        assertFalse(wasProbeInserted(editor, item, true));

        // Switch to the non-stp editor, and add a probe. A dialog should appear
        // to let the user choose which of the open files to add to.
        editor = bot.editorByTitle("Untitled 1").toTextEditor();
        editor.show();
        item = probeCategory.getNode(probeSingleWithoutDef);
        item.doubleClick();
        shell = bot.shell(dialogTitle);
        shell.setFocus();
        SWTBotTable table = bot.table();
        assertTrue(table.containsItem(scriptName));
        assertTrue(table.containsItem(probeDef.getName()));
        table.select(scriptName);
        bot.button("OK").click();
        bot.waitUntil(Conditions.shellCloses(shell));

        bot.waitUntil(new EditorIsActive(scriptName));
        assertTrue(wasProbeInserted(bot.activeEditor().toTextEditor(), item, false));
    }

    private static boolean wasProbeInserted(SWTBotEclipseEditor editor, SWTBotTreeItem probeNode, boolean isGroup) {
        String scriptText = editor.getText();
        int entryIndex = scriptText.indexOf("probe " + probeNode.getText() + (isGroup ? ".*\n" : "\n"));
        if (entryIndex == -1) {
            return false;
        }
        String probeText = scriptText.substring(entryIndex);
        if (!isGroup) {
            SWTBotTreeItem[] variables = probeNode.getItems();
            if (variables.length > 0) {
                // If the probe has variables, each one should be mentioned in comments.
                for (SWTBotTreeItem variable : probeNode.getItems()) {
                    if (!probeText.contains(variable.getText())) {
                        return false;
                    }
                }
            } else if (probeText.contains("variables")) {
                // If the probe has no variables, no mention of variables should be added in comments.
                return false;
            }
        }
        return true;
    }

    @Test
    public void testGraphErrors() {
        SWTBotShell shell = prepareScript("missingColumns.stp", null);

        SWTBotButton runButton = bot.button("Run");
        SWTBotButton addButton = bot.button(Messages.SystemTapScriptGraphOptionsTab_AddGraphButton);
        SWTBotButton editButton = bot.button(Messages.SystemTapScriptGraphOptionsTab_EditGraphButton);
        SWTBotButton dupButton = bot.button(Messages.SystemTapScriptGraphOptionsTab_DuplicateGraphButton);
        SWTBotButton remButton = bot.button(Messages.SystemTapScriptGraphOptionsTab_RemoveGraphButton);
        String graphID = "org.eclipse.linuxtools.systemtap.graphing.ui.charts.scatterchartbuilder";

        // As soon as the Graphing tab is entered, no regular expression exists & nothing can be run.
        SWTBotCombo combo = bot.comboBoxWithLabel(Messages.SystemTapScriptGraphOptionsTab_regexLabel);
        assertEquals("", combo.getText());
        assertFalse(runButton.isEnabled());
        assertFalse(addButton.isEnabled());
        combo.setText("(1)(2)");
        assertEquals("(1)(2)", combo.getText());
        assertTrue(runButton.isEnabled());
        assertTrue(addButton.isEnabled());

        setupGraphWithTests("Graph", false);
        assertTrue(runButton.isEnabled());

        // Removing groups from the regex disables graphs that rely on those groups.
        combo = bot.comboBoxWithLabel(Messages.SystemTapScriptGraphOptionsTab_regexLabel);
        combo.setText("(1)");
        assertFalse(runButton.isEnabled());
        combo.setText("(1)(2)(3)");
        assertTrue(runButton.isEnabled());

        final SWTBotTable table = bot.table();
        table.select(0);
        dupButton.click();
        assertEquals(2, table.rowCount());
        assertTrue(runButton.isEnabled());

        combo.setText("(1)");
        assertFalse(runButton.isEnabled());
        for (int i = 0, n = table.rowCount(); i < n; i++) {
            String itemTitle = table.getTableItem(i).getText();
            assertTrue("Graph " + i + " should be invalid, but it's not: " + itemTitle,
                    table.getTableItem(i).getText().contains(Messages.SystemTapScriptGraphOptionsTab_invalidGraph));
        }

        setupGraphGeneral("Safe", 1, graphID, true, false);
        assertFalse(table.getTableItem(2).getText().contains(Messages.SystemTapScriptGraphOptionsTab_invalidGraph));
        combo.setText("(1)(2)(3)");
        assertTrue(runButton.isEnabled());

        setupGraphGeneral("Unsafe", 3, graphID, true, false);
        assertTrue(runButton.isEnabled());
        combo.setText("(1)(2)");
        assertFalse(runButton.isEnabled());
        for (int i = 0, n = table.rowCount(); i < n; i++) {
            String itemTitle = table.getTableItem(i).getText();
            assertTrue("Graph " + i + " has incorrect validity: " + itemTitle,
                    !itemTitle.contains(Messages.SystemTapScriptGraphOptionsTab_invalidGraph)
                    || itemTitle.contains("Unsafe"));
        }

        table.select(3);
        dupButton.click();
        remButton.click();
        assertTrue(!runButton.isEnabled());

        table.select(3);
        editButton.click();
        SWTBotShell graphShell = bot.shell("Edit Graph").activate();
        SWTBotButton finishButton = bot.button("Finish");
        assertTrue(!finishButton.isEnabled());
        bot.comboBox("<Deleted>").setSelection("NA");
        finishButton.click();
        bot.waitUntil(Conditions.shellCloses(graphShell));
        shell.setFocus();
        assertTrue(runButton.isEnabled());

        // Perform tests when graphs have an invalid graphID.
		UIThreadRunnable.syncExec(() -> {
			GraphData gd = (GraphData) table.getTableItem(0).widget.getData();
			gd.graphID = "invalidID";
			table.getTableItem(0).widget.setData(gd);
		});

        combo.setText(combo.getText().concat(" ")); // Just to refresh the dialog
        assertFalse(runButton.isEnabled());
        assertTrue(table.getTableItem(0).getText().contains(Messages.SystemTapScriptGraphOptionsTab_invalidGraphID));

        table.select(0);
        dupButton.click();
        remButton.click();
        assertFalse(runButton.isEnabled());
        assertTrue(table.getTableItem(table.rowCount() - 1).getText().contains(Messages.SystemTapScriptGraphOptionsTab_invalidGraphID));

        table.select(table.rowCount() - 1);
        editButton.click();
        graphShell = bot.shell("Edit Graph").activate();
        finishButton = bot.button("Finish");
        assertFalse(finishButton.isEnabled());
        bot.radio(0).click();
        finishButton.click();
        bot.waitUntil(Conditions.shellCloses(graphShell));
        shell.setFocus();
        assertTrue(runButton.isEnabled());

        // Removing all invalid graphs should restore validity.
        combo.setText("(1)");
        assertFalse(runButton.isEnabled());
        for (int i = table.rowCount() - 1; i >= 0; i--) {
            if (table.getTableItem(i).getText().contains(Messages.SystemTapScriptGraphOptionsTab_invalidGraph)) {
                table.select(i);
                remButton.click();
            }
        }
        assertTrue(runButton.isEnabled());
        while (table.rowCount() > 0) {
            table.select(0);
            remButton.click();
        }

        bot.button("Apply").click();
    }

    @Test
    public void testDeleteBlankRegex() {
        SWTBotShell shell = prepareScript("blank.stp", null);

        // Confirm that adding a new regex when the current one is blank has no effect.
        SWTBotCombo combo = bot.comboBoxWithLabel(Messages.SystemTapScriptGraphOptionsTab_regexLabel);
        assertEquals(2, combo.itemCount());
        combo.setSelection(1);
        assertEquals(2, combo.itemCount());
        assertEquals(0, combo.selectionIndex());

        // Confirm that adding a regex works when the current regex is not empty.
        combo.setText("(a) b (c)");
        assertEquals("(a) b (c)", combo.getText());
        combo.setSelection(1);
        assertEquals(3, combo.itemCount());
        assertEquals(1, combo.selectionIndex());

        // Confirm that a blank regex is not removed when other data is not empty.
        combo.setText("(a)");
        bot.button(Messages.SystemTapScriptGraphOptionsTab_AddGraphButton).click();
        SWTBotShell graphShell = bot.shell("Create Graph").activate();
        graphShell.setFocus();
        bot.textWithLabel("Title:").setText("Test");
        bot.button("Finish").click();
        bot.waitUntil(Conditions.shellCloses(graphShell));
        shell.setFocus();
        combo.setText("");
        assertEquals("", combo.getText());
        combo.setSelection(0);
        assertEquals(3, combo.itemCount());

        // Confirm that auto-deleting a blank regex in the middle of the regex list works properly.
        combo.setSelection(2);
        assertEquals(4, combo.itemCount());
        combo.setText("sample");
        combo.setSelection(1);
        assertEquals(4, combo.itemCount());
        SWTBotTable table = bot.table();
        SWTBotButton remButton = bot.button(Messages.SystemTapScriptGraphOptionsTab_RemoveGraphButton);
        assertTrue(!remButton.isEnabled());
        table.select(0);
        assertTrue(remButton.isEnabled());
        remButton.click();
        assertTrue(!remButton.isEnabled());
        combo.setSelection(2);
        assertEquals(3, combo.itemCount());
        assertEquals("sample", combo.getText());
        assertEquals("(a) b (c)", combo.items()[0]);
        assertEquals("sample", combo.items()[1]);

        // Confirm that auto-deleting a regex from the beginning of the list works properly.
        combo.setSelection(2);
        combo.setText("another sample");
        combo.setSelection(0);
        combo.setText("");
        combo.setSelection(1);
        assertEquals(3, combo.itemCount());
        assertEquals("sample", combo.getText());
        assertEquals("sample", combo.items()[0]);
        assertEquals("another sample", combo.items()[1]);
    }

    @Test
    public void testGraphConfig() {
        String scriptName = "testGraph.stp";
        createScript(bot, scriptName);

        final String val0 = "i";
        final String val1 = "j";
        final String val2 = "k";

        openRunConfigurations(scriptName);
        SWTBotShell shell = bot.shell("Run Configurations");
        shell.setFocus();
        bot.tree().select("SystemTap").contextMenu("New").click();
        bot.textWithLabel("Name:").setText(scriptName);

        // Select the "Graphing" tab.
        SWTBotCTabItem tab = bot.cTabItem(Messages.SystemTapScriptGraphOptionsTab_graphingTitle);
        tab.activate();
        bot.checkBox(Messages.SystemTapScriptGraphOptionsTab_graphOutputRun).click();

        // Create first regex.
        SWTBotCombo combo = bot.comboBoxWithLabel(Messages.SystemTapScriptGraphOptionsTab_regexLabel);
        combo.setText("Value:(\\d+) (\\d+)");
        assertEquals("Value:(\\d+) (\\d+)", combo.getText());
        SWTBotText text = bot.textWithLabel(Messages.SystemTapScriptGraphOptionsTab_sampleOutputLabel);
        text.setText("Value:1 2");
        assertEquals("Value:1 2", text.getText());

        text = bot.text(MessageFormat.format(Messages.SystemTapScriptGraphOptionsTab_defaultColumnTitleBase, 1));
        text.setText(val0);
        assertEquals(val0, text.getText());
        text = bot.text(MessageFormat.format(Messages.SystemTapScriptGraphOptionsTab_defaultColumnTitleBase, 2));
        text.setText(val1);
        assertEquals(val1, text.getText());
        setupGraphWithTests("Values", false);

        // Make a second regex, and a graph for it.
        shell.setFocus();
        combo = bot.comboBoxWithLabel(Messages.SystemTapScriptGraphOptionsTab_regexLabel);
        assertEquals(2, combo.itemCount());
        combo.setSelection(combo.selectionIndex() + 1);
        assertEquals(3, combo.itemCount());
        assertEquals("", combo.getText());
        combo.setText("Other:(\\d+) (\\d+)");
        assertEquals("Other:(\\d+) (\\d+)", combo.getText());

        text = bot.text(MessageFormat.format(Messages.SystemTapScriptGraphOptionsTab_defaultColumnTitleBase, 1));
        text.setText(val0);
        assertEquals(val0, text.getText());
        text = bot.text(MessageFormat.format(Messages.SystemTapScriptGraphOptionsTab_defaultColumnTitleBase, 2));
        text.setText(val2);
        assertEquals(val2, text.getText());

        text = bot.textWithLabel(Messages.SystemTapScriptGraphOptionsTab_sampleOutputLabel);
        assertEquals("", text.getText());
        setupGraphWithTests("Others", false);

        // Apply the changes, then close the menu & reopen it to make sure settings were saved.
        shell.setFocus();
        bot.button("Apply").click();
        bot.button("Close").click();
        bot.waitUntil(Conditions.shellCloses(shell));
        openRunConfigurations(scriptName);
        shell = bot.shell("Run Configurations");
        shell.setFocus();
        shell.bot().text().setText(scriptName); // Set the filter text to show configs of the current script
        bot.waitUntil(new NodeAvailableAndSelect(bot.tree(), "SystemTap", scriptName));
        tab = bot.cTabItem(Messages.SystemTapScriptGraphOptionsTab_graphingTitle);
        tab.activate();

        combo = bot.comboBoxWithLabel(Messages.SystemTapScriptGraphOptionsTab_regexLabel);
        text = bot.textWithLabel(Messages.SystemTapScriptGraphOptionsTab_sampleOutputLabel);
        SWTBotTable table = bot.table();
        assertEquals(3, combo.itemCount());
        assertEquals("Value:(\\d+) (\\d+)", combo.getText());
        assertEquals("Value:1 2", text.getText());
        assertEquals(1, table.rowCount());
        String graphName = GraphFactory.getGraphName("org.eclipse.linuxtools.systemtap.graphing.ui.charts.scatterchartbuilder");
        assertTrue(table.containsItem(graphName.concat(":Values")));
        combo.setSelection(1);
        assertEquals("Other:(\\d+) (\\d+)", combo.getText());
        assertEquals("", text.getText());
        assertEquals(1, table.rowCount());
        assertTrue(table.containsItem(graphName.concat(":Others")));
    }

    @Test
    public void testGraphContents() {
        final String valA1 = "A1";
        final String valB1 = "B1";
        createAndViewDummyData(
                new String[]{valA1, valB1},
                new Integer[]{
                        0,0, 1,2, 2,4, 3,6, 4,8, 5,10, 6,12, 7,14, 8,16, 9,18});
        SWTBotEditor graphEditorA = bot.activeEditor();

        final String valA2 = "A2";
        final String valB2 = "B2";
        createAndViewDummyData(
                new String[]{valA2, valB2},
                new Integer[]{
                        2,0, 5,1, 7,2, 10,3});
        SWTBotEditor graphEditorB = bot.activeEditor();

        // Add graphs.
        setupGraphWithTests("Others", true);
        String graphTitle2 = "Others - Scatter Graph";
        graphEditorA.show();
        setupGraphWithTests("Values", true);
        String graphTitle1 = "Values - Scatter Graph";

        // Test table & graph contents.
        graphEditorA.bot().cTabItem("Data View").activate();
        SWTBotTable dataTable = bot.table();
        List<String> colNames = dataTable.columns();
        assertEquals(3, colNames.size());
        assertEquals(valA1, colNames.get(1));
        assertEquals(valB1, colNames.get(2));
        assertEquals("2", dataTable.cell(2, 1));
        assertEquals("4", dataTable.cell(2, 2));

        graphEditorA.bot().cTabItem(graphTitle1).activate();
        Matcher<AbstractChartBuilder> matcher = widgetOfType(AbstractChartBuilder.class);
        AbstractChartBuilder cb = bot.widget(matcher);
        ISeries[] series = cb.getChart().getSeriesSet().getSeries();
        assertEquals(2, series.length);
        assertEquals(10, series[0].getXSeries().length);
        assertEquals(10, series[1].getXSeries().length);
        assertEquals(2, (int) series[0].getYSeries()[2]);
        assertEquals(4, (int) series[1].getYSeries()[2]);

        graphEditorB.show();
        graphEditorB.bot().cTabItem("Data View").activate();
        dataTable = bot.table();
        colNames = dataTable.columns();
        assertEquals(3, colNames.size());
        assertEquals(valA2, colNames.get(1));
        assertEquals(valB2, colNames.get(2));
        assertEquals("7", dataTable.cell(2, 1));
        assertEquals("2", dataTable.cell(2, 2));

        graphEditorB.bot().cTabItem(graphTitle2).activate();
        cb = bot.widget(matcher);
        series = cb.getChart().getSeriesSet().getSeries();
        assertEquals(2, series.length);
        assertEquals(4, series[0].getXSeries().length);
        assertEquals(4, series[1].getXSeries().length);
        assertEquals(7, (int) series[0].getYSeries()[2]);
        assertEquals(2, (int) series[1].getYSeries()[2]);

        // Test filters on the data table & graphs.
        graphEditorA.show();
        graphEditorA.bot().cTabItem("Data View").activate();
        dataTable = bot.table();
        new SWTBotMenu(ContextMenuHelper.contextMenu(dataTable, "Add filter...")).click();
        SWTBotShell shell = bot.shell("Create Filter");
        shell.setFocus();

        // Match Filter - Remove a matching
        bot.button("Match Filter").click();
        bot.button("Next >").click();
        bot.text().setText("2");
        deselectDefaultSelection(0);
        bot.radio(1).click();
        bot.button("Finish").click();
        bot.waitUntil(Conditions.shellCloses(shell));
        bot.waitUntil(new TableHasUpdated(graphEditorA.bot().table(), 9, true));
        assertEquals("3", dataTable.cell(2, 1));
        assertEquals("6", dataTable.cell(2, 2));

        // Filters should be applied to graphs as well as data tables.
        graphEditorA.bot().cTabItem(graphTitle1).activate();
        cb = bot.widget(matcher);
        series = cb.getChart().getSeriesSet().getSeries();
        bot.waitUntil(new ChartHasUpdated(cb.getChart(), 9));
        assertEquals(3, (int) series[0].getYSeries()[2]);
        assertEquals(6, (int) series[1].getYSeries()[2]);

        // Each graph set should have its own filters.
        graphEditorB.show();
        graphEditorB.bot().cTabItem("Data View").activate();
        dataTable = bot.table();
        assertEquals(4, dataTable.rowCount());
        assertEquals("2", dataTable.cell(0, 1));

        // Test removing a filter.
        graphEditorA.show();
        graphEditorA.bot().cTabItem("Data View").activate();
        dataTable = bot.table();
        new SWTBotMenu(ContextMenuHelper.contextMenu(dataTable,
                "Remove filter...",
                "Match Filter: \"" + valA1 + "\" removing \"2\"")).click();
        bot.waitUntil(new TableHasUpdated(graphEditorA.bot().table(), 10, true));
        assertEquals("2", dataTable.cell(2, 1));
        assertEquals("4", dataTable.cell(2, 2));
    }

    @Test
    public void testGenerateFromPrintf() {
        SWTBotShell shell = prepareScript("testGenerates.stp", "#!/usr/bin/env stap"
                + "\nglobal i,j,k,a"
                + "\nprobe begin{i=0;j=5;k=20;a=65}"
                + "\n#probe begin{printf(\"%1b%1b%1blo %1b%1brld\\n\", 72,101,108,87,111)}"
                + "\nprobe timer.ms(100){printf(\"%5i|\\n%10.5d|\\n%-10.5i|\\n%05c\\n\",i,j,k,a);i++;j+=5;k+=20;a++}"
                + "\n//printf(\"this is a comment\\n\");"
                + "\n/*printf(\"this is a...\\n\");"
                + "\nprintf(\"...multiline comment\\n\");*/"
                + "\nprobe begin{b = sprintf(\"Here\"); printf(\"-->%s<--\\n\", b)}"
                + "\nprobe timer.ms(100){printf(\"%x - %#x - %#X\\n\",i,i,i);}"
                + "\nprobe timer.ms(100){printf(\"%o - %#o\\n\",i,i);}"
                + "\nprobe begin{printf(\"%1b-\\\\n-%p\\n\", 65, 0x8000000002345678)}");

        // Generate regexs.
        bot.button(Messages.SystemTapScriptGraphOptionsTab_generateFromPrintsButton).click();

        SWTBotShell dialogShell = bot.shell(Messages.SystemTapScriptGraphOptionsTab_generateFromPrintsTitle);
        dialogShell.setFocus();
        bot.button("Yes").click();
        bot.waitUntil(Conditions.shellCloses(dialogShell));
        shell.setFocus();

        SWTBotCombo combo = bot.comboBoxWithLabel(Messages.SystemTapScriptGraphOptionsTab_regexLabel);
        assertEquals(9, combo.itemCount()); // One extra entry for "Add New Regex"

        String[] expectedRegexs = new String[]{
                " {0,4}(-?\\d+)\\|",
                " {0,9}(-?\\d+)\\|",
                "(-?\\d+) {0,9}\\|",
                " {0,4}(.)",
                "-->(.+)<--",
                "([a-f0-9]+) - (0x[a-f0-9]+) - (0X[A-F0-9]+)",
                "(\\d+) - (0\\d+)",
                "(.)-\\\\n-(0x[a-f0-9]+)",
                Messages.SystemTapScriptGraphOptionsTab_regexAddNew
        };
        for (int i = 0, n = combo.itemCount(); i < n; i++) {
            assertEquals(expectedRegexs[i], combo.items()[i]);
        }
        bot.button("Apply").click();
    }

    @Test
    public void testLabelledGraphScript() {
        final int numItems = 13;
        final int numNumberItems = 4;
        final int numCategories = 3;
        createAndViewDummyData(
                new String[] {
                "Fruit", "Number", "Freshness", "Tastiness"},

                new Object[] {
                "Apples", 2, 14, 16,
                1, 1, 2, 3,
                "Bananas (2)", 10, 10, 10,
                "Cherries", 10, 20, 30,
                2, 2, 4, 6,
                "Apples", 12, 5, 16,
                "Bananas", 0, 1, 0,
                3, 3, 6, 9,
                "Dates", 12, 5, 16,
                "Bananas", 2, 1, 2,
                4, 4, 8, 12,
                "Apples", 12, 5, 16,
                "Bananas (2)", 3, 1, 3
                });
        SWTBotEditor graphEditor = bot.activeEditor();

        // Add bar, pie, and line graphs that use the same column data.
        String title = "Fruit Info";
        setupGraphGeneral(title, 4, "org.eclipse.linuxtools.systemtap.graphing.ui.charts.barchartbuilder", false, true);
        setupGraphGeneral(title, 4, "org.eclipse.linuxtools.systemtap.graphing.ui.charts.piechartbuilder", false, true);
        setupGraphGeneral(title, 4, "org.eclipse.linuxtools.systemtap.graphing.ui.charts.linechartbuilder", false, true);

        // Confirm that the bar & pie charts display the String categories, but the line chart ignores them.
        String titleBar = title + " - Bar Graph";
        String titlePie = title + " - Pie Chart";
        String titleLine = title + " - Line Graph";
        graphEditor.bot().cTabItem(titleLine).activate();
        Matcher<AbstractChartBuilder> matcher = widgetOfType(AbstractChartBuilder.class);
        AbstractChartBuilder cb = bot.widget(matcher);
        assertEquals(numNumberItems, cb.getChart().getSeriesSet().getSeries()[0].getXSeries().length);

        graphEditor.bot().cTabItem(titlePie).activate();
        cb = bot.widget(matcher);
        assertEquals(numItems, cb.getChart().getSeriesSet().getSeries().length);

        graphEditor.bot().cTabItem(titleBar).activate();
        cb = bot.widget(matcher);
        assertEquals(numItems, cb.getChart().getSeriesSet().getSeries()[0].getXSeries().length);

        // Test graph scaling & scrolling
        discreteXControlTests(cb, numItems); //Bar Chart
        continuousControlTests(cb, false);
        graphEditor.bot().cTabItem(titlePie).activate();
        cb = bot.widget(matcher);
        discreteXControlTests(cb, numCategories);
        graphEditor.bot().cTabItem(titleLine).activate();
        cb = bot.widget(matcher);
        continuousControlTests(cb, true);
        continuousControlTests(cb, false);
    }

    private static void discreteXControlTests(AbstractChartBuilder cb, int numAxisItems) {
        // Check that default range shows 100% of data.
        IAxis axis = cb.getChart().getAxisSet().getXAxis(0);
        Range range = axis.getRange();
        double scale = cb.getScale();
        double scroll = cb.getScroll();
        assertTrue(range.upper - range.lower == axis.getCategorySeries().length - 1 && range.upper - range.lower == numAxisItems - 1);
        assertTrue(scale == 1.0 && scroll == 1.0);

        // Check that scroll buttons are disabled at 100% range.
        SWTBotButton firstButton = bot.button(org.eclipse.linuxtools.systemtap.graphing.ui.widgets.Messages.GraphDiscreteXControl_First);
        SWTBotButton leftButton = bot.button(org.eclipse.linuxtools.systemtap.graphing.ui.widgets.Messages.GraphDiscreteXControl_Left);
        SWTBotButton rightButton = bot.button(org.eclipse.linuxtools.systemtap.graphing.ui.widgets.Messages.GraphDiscreteXControl_Right);
        SWTBotButton lastButton = bot.button(org.eclipse.linuxtools.systemtap.graphing.ui.widgets.Messages.GraphDiscreteXControl_Last);
        assertFalse(firstButton.isEnabled());
        assertFalse(leftButton.isEnabled());
        assertFalse(rightButton.isEnabled());
        assertFalse(lastButton.isEnabled());

        // Test zooming in. The amount of zoom is arbitrary for this test--just make sure zooming happened.
        SWTBotButton zoomInButton = bot.button(org.eclipse.linuxtools.systemtap.graphing.ui.widgets.Messages.GraphDiscreteXControl_ZoomIn);
        SWTBotButton zoomOutButton = bot.button(org.eclipse.linuxtools.systemtap.graphing.ui.widgets.Messages.GraphDiscreteXControl_ZoomOut);
        SWTBotButton allButton = bot.button(org.eclipse.linuxtools.systemtap.graphing.ui.widgets.Messages.GraphDiscreteXControl_All);
        assertTrue(zoomInButton.isEnabled());
        assertFalse(zoomOutButton.isEnabled());
        assertFalse(allButton.isEnabled());
        zoomInButton.click();
        assertTrue(zoomOutButton.isEnabled());
        assertTrue(allButton.isEnabled());

        // By default, zooming in should zoom in on the end of the axis (newest data).
        range = axis.getRange();
        assertTrue(range.upper == numAxisItems - 1 && range.lower > 0 && cb.getScale() < scale && cb.getScroll() == 1.0);

        // Left scrolling should now be enabled.
        assertTrue(firstButton.isEnabled());
        assertTrue(leftButton.isEnabled());
        assertFalse(rightButton.isEnabled());
        assertFalse(lastButton.isEnabled());

        // Test scrolling left. Again, the specific amount is arbitrary, just make sure scrolling happened.
        leftButton.click();
        range = axis.getRange();
        assertTrue(range.upper < numAxisItems - 1 && cb.getScroll() < scroll);
        int rstore = (int) range.lower;
        assertTrue(rightButton.isEnabled());
        assertTrue(lastButton.isEnabled());

        // Zooming out should bring the range back to 100%.
        zoomOutButton.click();
        range = axis.getRange();
        assertTrue(range.upper - range.lower == numAxisItems - 1 && cb.getScale() == 1.0 && cb.getScroll() < scroll);
        assertTrue(zoomInButton.isEnabled());
        assertFalse(zoomOutButton.isEnabled());
        assertFalse(allButton.isEnabled());
        assertFalse(firstButton.isEnabled());
        assertFalse(leftButton.isEnabled());
        assertFalse(rightButton.isEnabled());
        assertFalse(lastButton.isEnabled());

        // For convenience, zooming out after having scrolled somewhere should make zooming in
        // zoom back to the area that was scrolled to.
        scroll = cb.getScroll();
        zoomInButton.click();
        assertTrue(rstore == axis.getRange().lower && scroll == cb.getScroll());

        // Scrolling right should take the range back to the end of the axis.
        rightButton.click();
        range = axis.getRange();
        assertTrue(range.upper == numAxisItems - 1 && range.lower > 0 && cb.getScroll() > scroll);
        assertTrue(firstButton.isEnabled());
        assertTrue(leftButton.isEnabled());
        assertFalse(rightButton.isEnabled());
        assertFalse(lastButton.isEnabled());

        // Zoom in as much as possible (range should show only one item),
        // and step right/left. Add a loop limit for safety.
        for (int i = 0; i < numAxisItems; i++) {
            range = axis.getRange();
            if (range.upper == range.lower) {
                break;
            }
            zoomInButton.click();
        }
        range = axis.getRange();
        assertTrue(range.upper == range.lower && range.upper == numAxisItems - 1);
        assertTrue(!zoomInButton.isEnabled());
        for (int i = 0; i < numAxisItems; i++) {
            if (axis.getRange().lower == 0) {
                break;
            }
            leftButton.click();
            assertTrue(axis.getRange().lower < range.lower);
            range = axis.getRange();
            assertEquals(range.lower, range.upper, 0.0);
        }
        assertEquals(axis.getRange().lower, 0, 0.0);
        for (int i = 0; i < numAxisItems; i++) {
            if (axis.getRange().upper == numAxisItems - 1) {
                break;
            }
            rightButton.click();
            assertTrue(axis.getRange().upper > range.upper);
            range = axis.getRange();
            assertEquals(range.lower, range.upper, 0.0);
        }
        assertEquals(axis.getRange().upper, numAxisItems - 1, 0);

        firstButton.click();
        assertEquals(axis.getRange().lower, 0, 0);
        assertFalse(firstButton.isEnabled());
        assertFalse(leftButton.isEnabled());
        assertTrue(rightButton.isEnabled());
        assertTrue(lastButton.isEnabled());

        lastButton.click();
        assertEquals(axis.getRange().upper, numAxisItems - 1, 0);
        assertTrue(firstButton.isEnabled());
        assertTrue(leftButton.isEnabled());
        assertFalse(rightButton.isEnabled());
        assertFalse(lastButton.isEnabled());
    }

    private static double getAxisScale(AbstractChartBuilder cb, boolean isXAxis) {
        return isXAxis ? cb.getScale() : cb.getScaleY();
    }

    private static double getAxisScroll(AbstractChartBuilder cb, boolean isXAxis) {
        return isXAxis ? cb.getScroll() : cb.getScrollY();
    }

    private static void continuousControlTests(AbstractChartBuilder cb, boolean isXAxis) {
        // Continuous scaling/scrolling is less strict/predictable than discrete scrolling,
        // so just check that the controls perform their intended actions.
        IAxis axis;
        SWTBotButton zoomInButton, zoomOutButton;
        SWTBotScale zoomScale;
        SWTBotSlider scrollBar;
        int flipSign;
        if (isXAxis) {
            axis = cb.getChart().getAxisSet().getXAxis(0);
            zoomInButton = bot.buttonWithTooltip(org.eclipse.linuxtools.systemtap.graphing.ui.widgets.Messages.GraphContinuousXControl_ZoomInTooltip);
            zoomOutButton = bot.buttonWithTooltip(org.eclipse.linuxtools.systemtap.graphing.ui.widgets.Messages.GraphContinuousXControl_ZoomOutTooltip);
            zoomScale = bot.scaleWithTooltip(org.eclipse.linuxtools.systemtap.graphing.ui.widgets.Messages.GraphContinuousXControl_ScaleMessage);
            scrollBar = bot.sliderWithTooltip(org.eclipse.linuxtools.systemtap.graphing.ui.widgets.Messages.GraphContinuousXControl_ScrollMessage);
            flipSign = 1;
        } else {
            axis = cb.getChart().getAxisSet().getYAxis(0);
            zoomInButton = bot.buttonWithTooltip(org.eclipse.linuxtools.systemtap.graphing.ui.widgets.Messages.GraphContinuousYControl_ZoomInTooltip);
            zoomOutButton = bot.buttonWithTooltip(org.eclipse.linuxtools.systemtap.graphing.ui.widgets.Messages.GraphContinuousYControl_ZoomOutTooltip);
            zoomScale = bot.scaleWithTooltip(org.eclipse.linuxtools.systemtap.graphing.ui.widgets.Messages.GraphContinuousYControl_ScaleMessage);
            scrollBar = bot.sliderWithTooltip(org.eclipse.linuxtools.systemtap.graphing.ui.widgets.Messages.GraphContinuousYControl_ScrollMessage);
            flipSign = -1;
        }
        double scale = getAxisScale(cb, isXAxis);
        double scroll = getAxisScroll(cb, isXAxis);
        int thumb = scrollBar.getThumb();

        // Default range should be 100%, so zooming out shouldn't have an effect yet.
        assertEquals(scale, 1.0, 0);
        int zoomValue = zoomScale.getValue();
        Range range = axis.getRange();
        zoomOutButton.click();
        Range range2 = axis.getRange();
        assertTrue(range.upper == range2.upper && range.lower == range2.lower && zoomScale.getValue() == zoomValue && getAxisScale(cb, isXAxis) == scale && scrollBar.getThumb() == thumb);

        // Zoom in & back out with the zoom buttons.
        zoomInButton.click();
        range2 = axis.getRange();
        assertTrue(range2.upper - range2.lower < range.upper - range.lower && flipSign * (zoomScale.getValue() - zoomValue) > 0 && getAxisScale(cb, isXAxis) < scale && scrollBar.getThumb() < thumb);
        zoomOutButton.click();
        range2 = axis.getRange();
        assertTrue(range.upper == range2.upper && range.lower == range2.lower && zoomScale.getValue() == zoomValue && getAxisScale(cb, isXAxis) == scale && scrollBar.getThumb() == thumb);

        // Zoom in with the Scale control.
        int controlRange = zoomScale.getMaximum() - zoomScale.getMinimum();
        zoomScale.setValue(zoomScale.getValue() + controlRange / 2 * flipSign);
        // Note: the charts need some time to be updated after using the scale/slider controls.
        // Sleeping for a brief moment is faster than using a bot wait condition.
        bot.sleep(100);
        range2 = axis.getRange();
        assertTrue(range2.upper - range2.lower < range.upper - range.lower && getAxisScale(cb, isXAxis) < scale && scrollBar.getThumb() < thumb);

        range = range2;
        thumb = scrollBar.getThumb();
        scale = getAxisScale(cb, isXAxis);
        zoomScale.setValue(zoomScale.getValue() - controlRange / 4 * flipSign);
        bot.sleep(100);
        range2 = axis.getRange();
        assertTrue(range2.upper - range2.lower > range.upper - range.lower && getAxisScale(cb, isXAxis) > scale && scrollBar.getThumb() > thumb);

        // Test scrolling. Don't assume an initial scroll position, as it may be changed
        // in future versions (it's more likely to change than default zoom, at least).
        thumb = scrollBar.getThumb();
        controlRange = scrollBar.getMaximum() - scrollBar.getThumb() - scrollBar.getMinimum();
        scrollBar.setSelection(controlRange / 2);
        bot.sleep(100);
        assertEquals(scrollBar.getThumb(), thumb);

        // Scroll towards origin.
        range = axis.getRange();
        scrollBar.setSelection(scrollBar.getSelection() - controlRange / 4 * flipSign);
        bot.sleep(100);
        range2 = axis.getRange();
        assertTrue(range2.upper - range2.lower == range.upper - range.lower && range2.upper < range.upper && getAxisScroll(cb, isXAxis) < scroll);

        // Scroll away from origin.
        range = range2;
        scroll = getAxisScroll(cb, isXAxis);
        scrollBar.setSelection(scrollBar.getSelection() + controlRange / 8 * flipSign);
        bot.sleep(100);
        range2 = axis.getRange();
        assertTrue(range2.upper - range2.lower == range.upper - range.lower && range2.upper > range.upper && getAxisScroll(cb, isXAxis) > scroll);
    }

    @Test @Ignore
    public void testGraphTooltips() {
        createAndViewDummyData(new String[]{"Column 1"}, new Integer[]{1,2,3,4,5});

        // Add bar, pie, and line graphs that use the same column data.
        String title = "Info";
        setupGraphGeneral(title, 1, "org.eclipse.linuxtools.systemtap.graphing.ui.charts.linechartbuilder", true, true);
        setupGraphGeneral(title, 1, "org.eclipse.linuxtools.systemtap.graphing.ui.charts.barchartbuilder", true, true);

        // Perform mouse hover tests on graphs.
        bot.activeEditor().bot().cTabItem(title.concat(" - Bar Graph")).activate();
        final Matcher<AbstractChartBuilder> matcher = widgetOfType(AbstractChartBuilder.class);
        AbstractChartBuilder cb = bot.widget(matcher);
        String tooltipFormat = "{0}: {1}";
        checkTooltipAtDataPoint(cb, 0, MessageFormat.format(tooltipFormat, "Column 1", "1"), true);

        bot.activeEditor().bot().cTabItem(title.concat(" - Line Graph")).activate();
        cb = bot.widget(matcher);
        tooltipFormat = "Series: {0}\nx: {1}\ny: {2}";
        String lineChartTooltip = MessageFormat.format(tooltipFormat, "Column 1", "2", "2");
        checkTooltipAtDataPoint(cb, 1, lineChartTooltip, true);

        // The tooltip should disappear when a point moves away from the mouse, without need for mouse movement.
        cb.setScale(0.2);
        checkTooltipAtDataPoint(cb, -1, lineChartTooltip, false);
    }

    /**
     * May move the mouse to a desired data point on a chart and test for the tooltip that appears.
     * @param cb The AbstractChartBuilder containing the chart to test.
     * @param dataPoint The data point of the series to move the mouse to. Set this to -1
     * or less if the mouse should stay where it is.
     * @param expectedTooltip The expected contents of the tooltip.
     * @param shellShouldExist Set to <code>false</code> if the tooltip should not be found.
     */
    private static void checkTooltipAtDataPoint(final AbstractChartBuilder cb, final int dataPoint, final String expectedTooltip,
            final boolean shellShouldExist) {

        for (int retries = 5; retries > 0; retries--) {

            if (dataPoint >= 0) {
                final Event event = new Event();
                event.type = SWT.MouseMove;
                // Jitter the mouse before moving to the data point
				UIThreadRunnable.syncExec(() -> {
					event.x = 0;
					event.y = 0;
					bot.getDisplay().post(event);
				});
                bot.sleep(100);
				UIThreadRunnable.syncExec(() -> {
					Point mousePoint = cb.getChart().getPlotArea()
							.toDisplay(cb.getChart().getSeriesSet().getSeries()[0].getPixelCoordinates(dataPoint));
					event.x = mousePoint.x;
					event.y = mousePoint.y;
					bot.getDisplay().post(event);
				});
            }

            bot.sleep(500); // Give some time for the tooltip to appear/change
            if (expectedTooltip.equals(cb.getMouseMessage()) == shellShouldExist) {
                return;
            }
        }

        if (shellShouldExist) {
            throw new AssertionError("Didn't find the expected tooltip: " + expectedTooltip);
        } else {
            throw new AssertionError("Did not expect to find this tooltip, but found it: " + expectedTooltip);
        }
    }

    private static void openRunConfigurations(String scriptName) {
        // Focus on project explorer view.
        projectExplorer.setFocus();
        new SWTBotMenu(ContextMenuHelper.contextMenu(
                projectExplorer.bot().tree().select(scriptName),
                "Run As", "Run Configurations...")).click();
    }

    private static void setupGraphWithTests(String title, boolean isTab) {
        SWTBotShell firstShell = bot.activeShell();

        openGraphMenu(isTab);
        SWTBotShell shell = bot.shell("Create Graph");
        shell.setFocus();

        SWTBotText text = bot.textWithLabel("Title:");
        text.setText(title);
        assertEquals(title, text.getText());

        SWTBotCombo comboX = bot.comboBoxWithLabel("X Series:");
        assertEquals(3, comboX.itemCount()); // X Series includes "Row Num" as a selection
        SWTBotCombo comboY0 = bot.comboBoxWithLabel("Y Series 0:");
        assertEquals(2, comboY0.itemCount()); // Y Series 0 only includes series entries
        comboY0.setSelection(0);
        SWTBotCombo comboY1 = bot.comboBoxWithLabel("Y Series 1:");
        assertEquals(3, comboY1.itemCount()); // Y Series (i>0) has extra "NA" option as first entry
        comboY1.setSelection(1);
        assertFalse(bot.button("Finish").isEnabled()); // Don't allow duplicate selections
        comboY1.setSelection(2);
        bot.button("Finish").click();

        bot.waitUntil(Conditions.shellCloses(shell));
        firstShell.setFocus();
    }

    private static void setupGraphGeneral(String title, int numItems, String graphID, boolean useRowNum, boolean isTab) {
        int offset = useRowNum ? 0 : 1;
        SWTBotShell firstShell = bot.activeShell();

        openGraphMenu(isTab);
        SWTBotShell shell = bot.shell("Create Graph");
        shell.setFocus();

        deselectDefaultSelection(0);
        bot.radioWithTooltip(GraphFactory.getGraphName(graphID) + "\n\n" +
                GraphFactory.getGraphDescription(graphID)).click();

        shell.setFocus();
        SWTBotText text = bot.textWithLabel("Title:");
        text.setText(title);

        bot.comboBoxWithLabel("X Series:").setSelection(offset);
        bot.comboBoxWithLabel("Y Series 0:").setSelection(offset);
        for (int i = 1; i < numItems - offset; i++) {
            bot.comboBoxWithLabel(MessageFormat.format("Y Series {0}:", i)).setSelection(i + 1 + offset);
        }
        if (!useRowNum) {
            bot.comboBoxWithLabel(MessageFormat.format("Y Series {0}:", numItems - 1)).setSelection(0);
        }
        bot.button("Finish").click();
        bot.waitUntil(Conditions.shellCloses(shell));
        firstShell.setFocus();
    }

    private static void openGraphMenu(boolean isTab) {
        if (!isTab) {
            bot.button(Messages.SystemTapScriptGraphOptionsTab_AddGraphButton).click();
        } else {
            // The "Add Graph" button is actually a tab that doesn't get activated when clicked.
            // Use a background thread to supress the wait for tab activation.
            new Thread(() -> {
			    try {
			        bot.activeEditor().bot().cTabItem(1).activate();
			    } catch (TimeoutException e) {}
			}).start();
        }
    }

    private static void createAndViewDummyData(String[] titles, Object[] data) {
        if (data.length % titles.length != 0) {
            throw new IllegalArgumentException("data.length must be a multiple of titles.length.");
        }
        final int numRows = data.length / titles.length;
        IFilteredDataSet dataset = new FilteredRowDataSet(titles);
        for (int i = 0; i < numRows; i++) {
            IDataEntry dataEntry = new RowEntry();
            Object[] values = new Object[titles.length];
            for (int v = 0; v < titles.length; v++) {
                values[v] = data[titles.length * i + v];
            }
            dataEntry.putRow(0, values);
            dataset.setData(dataEntry);
        }
        final File dataFile;
        try {
            dataFile = File.createTempFile("testSet", ".set");
            dataFile.deleteOnExit();
            if (!dataset.writeToFile(dataFile)) {
                throw new IOException();
            }
        } catch (IOException e) {
            fail("Could not create dummy data set.");
            return;
        }

		UIThreadRunnable.syncExec(() -> {
			new ImportDataSetHandler().execute(dataFile.getPath());
		});
        String editorName = dataFile.getName().concat(" Graphs");
        bot.waitUntil(new EditorIsActive(editorName));
    }

    /**
     * Deselects a radio button.
     * Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=344484
     * @param currSelection The index of the radiobutton to deselect
     */
    private static void deselectDefaultSelection(final int currSelection) {
		UIThreadRunnable.syncExec(() -> {
			@SuppressWarnings("unchecked")
			Matcher<Button> matcher = allOf(widgetOfType(Button.class), withStyle(SWT.RADIO, "SWT.RADIO"));
			Button b = bot.widget(matcher, currSelection);
			b.setSelection(false);
		});
    }

}
