/*******************************************************************************
 * Copyright (c) 2016, 2018 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
 *******************************************************************************/
package org.eclipse.tracecompass.lttng2.kernel.ui.swtbot.tests;

import static org.junit.Assert.assertTrue;

import java.io.IOException;

import org.apache.commons.io.FileUtils;
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.jface.bindings.keys.KeyStroke;
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.finders.UIThreadRunnable;
import org.eclipse.swtbot.swt.finder.junit.SWTBotJunit4ClassRunner;
import org.eclipse.swtbot.swt.finder.keyboard.Keystrokes;
import org.eclipse.swtbot.swt.finder.matchers.WidgetOfType;
import org.eclipse.swtbot.swt.finder.utils.SWTBotPreferences;
import org.eclipse.swtbot.swt.finder.utils.SWTUtils;
import org.eclipse.swtbot.swt.finder.utils.TableCollection;
import org.eclipse.swtbot.swt.finder.waits.Conditions;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotButton;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotCanvas;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotCheckBox;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotCombo;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotLabel;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotRadio;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotShell;
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.ui.swtbot.tests.shared.ConditionHelpers;
import org.eclipse.tracecompass.tmf.ui.swtbot.tests.shared.SWTBotTimeGraph;
import org.eclipse.tracecompass.tmf.ui.swtbot.tests.shared.SWTBotUtils;
import org.eclipse.tracecompass.tmf.ui.views.timegraph.AbstractTimeGraphView;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.widgets.TimeGraphControl;
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;

/**
 * Abstract class to build SWTBot test for time graph find dialog. Test the time
 * graph view find dialog and its options.
 *
 * @author Jean-Christian Kouame
 */
@RunWith(SWTBotJunit4ClassRunner.class)
public abstract class FindDialogTestBase {

    /** LTTng kernel trace type */
    protected static final String KERNEL_TRACE_TYPE = "org.eclipse.linuxtools.lttng2.kernel.tracetype";
    /** LTTng kernel perspective */
    protected static final String KERNEL_PERSPECTIVE_ID = "org.eclipse.linuxtools.lttng2.kernel.ui.perspective";
    /** Default project name */
    protected static final String TRACE_PROJECT_NAME = "test";

    /** The workbench bot */
    protected static SWTWorkbenchBot fBot;

    /** The Log4j logger instance. */
    private static final Logger fLogger = Logger.getRootLogger();
    private static final @NonNull ITmfTimestamp START_TIME = TmfTimestamp.create(1412670961211260539L, ITmfTimestamp.NANOSECOND_SCALE);
    private static final @NonNull String SPACE = " ";
    private static final String REGEX_PREFIX = "\\A";
    private static final String DIALOG_TITLE = "Find";
    private String fFindText;
    private SWTBotView fViewBot;
    private SWTBotTimeGraph fTimeGraphBot;

    private int fSelectionIndex;

    /**
     * Get the title of the time graph view the find dialog will use
     *
     * @return The view title
     */
    protected abstract String getViewTitle();

    /**
     * Get the string that will be used for the search
     *
     * @return The string
     */
    protected abstract String getFindText();

    /**
     * Before Class
     *
     * @throws IOException
     *             When the trace could not be opened
     */
    @BeforeClass
    public static void beforeClass() throws IOException {
        SWTBotUtils.initialize();

        /* 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));
        fBot = new SWTWorkbenchBot();
        SWTBotUtils.closeView("welcome", fBot);
        /* Switch perspectives */
        SWTBotUtils.switchToPerspective(KERNEL_PERSPECTIVE_ID);
        /* Create the trace project */
        SWTBotUtils.createProject(TRACE_PROJECT_NAME);
        /* Open the trace */
        String tracePath = FileUtils.toFile(FileLocator.toFileURL(CtfTestTrace.ARM_64_BIT_HEADER.getTraceURL())).getAbsolutePath();
        SWTBotUtils.openTrace(TRACE_PROJECT_NAME, tracePath, KERNEL_TRACE_TYPE);
        /* Finish waiting for eclipse to load */
        SWTBotUtils.activateEditor(fBot, CtfTestTrace.ARM_64_BIT_HEADER.getTraceURL().getPath().replaceAll("/", ""));
    }

    /**
     * After Class
     */
    @AfterClass
    public static void afterClass() {
        SWTBotUtils.deleteProject(TRACE_PROJECT_NAME, fBot);
        fLogger.removeAllAppenders();
    }

    /**
     * Initialize the test and open the timegraph view and the find dialog
     */
    @Before
    public void before() {
        String title = getViewTitle();
        fViewBot = fBot.viewByTitle(title);
        fViewBot.show();
        fTimeGraphBot = new SWTBotTimeGraph(fViewBot.bot());

        TmfSignalManager.dispatchSignal(new TmfSelectionRangeUpdatedSignal(this, START_TIME));
        fBot.waitUntil(ConditionHelpers.timeGraphIsReadyCondition((AbstractTimeGraphView) fViewBot.getViewReference().getPart(false), new TmfTimeRange(START_TIME, START_TIME), START_TIME));
        fViewBot.setFocus();
        openDialog(fViewBot);

        fFindText = getFindText();
    }

    /**
     * After method to close the dialog
     */
    @After
    public void after() {
        SWTBotShell shell = getDialogShell();
        closeDialog(getDialogBot());
        fBot.waitUntil(Conditions.shellCloses(shell));
        SWTBotUtils.closeSecondaryShells(fBot);
    }

    private static void openDialog(SWTBotView view) {
        SWTBotCanvas canvas = view.bot().canvas(1);
        view.setFocus();
        canvas.pressShortcut(Keystrokes.HOME);
        if (SWTUtils.isMac()) {
            canvas.pressShortcut(Keystrokes.COMMAND, KeyStroke.getInstance('F'));
        } else {
            canvas.pressShortcut(Keystrokes.CTRL, KeyStroke.getInstance('F'));
        }
        fBot.shell(DIALOG_TITLE).activate();
    }

    /**
     * Test wrap search option
     */
    @Test
    public void testWrapSearch() {
        SWTBot bot = getDialogBot();
        SWTBotButton findButton = bot.button("Find");

        SearchOptions options = getOptions(false, false, true, false, false);
        search(fFindText, options, findButton, bot);
        assertTrue(isWrapped(bot));
        verifySelection(fFindText, options, fViewBot, isWrapped(bot));
        options = getOptions(true, false, true, false, false);
        search(fFindText, options, findButton, bot);
        assertTrue(isWrapped(bot));
    }

    /**
     * Test the direction search option
     */
    @Test
    public void testDirection() {
        SWTBot bot = getDialogBot();
        SWTBotButton findButton = bot.button("Find");

        // forward
        testDirectionSearch(true, fFindText, bot, findButton, fViewBot);
        testDirectionSearch(false, fFindText, bot, findButton, fViewBot);
    }

    private void testDirectionSearch(boolean forward, String findText, SWTBot bot, SWTBotButton findButton, SWTBotView view) {
        SearchOptions options = getOptions(forward, false, true, false, false);
        fSelectionIndex = getSelectionIndex(view);
        search(findText, options, findButton, bot);
        verifySelection(findText, options, view, isWrapped(bot));
    }

    /**
     * Test the case sensitive search option
     */
    @Test
    public void testCaseSensitive() {
        SWTBot bot = getDialogBot();
        SWTBotButton findButton = bot.button("Find");
        String upper = fFindText.toUpperCase();
        String lower = fFindText.toLowerCase();

        SearchOptions options = getOptions(true, true, false, false, false);
        search(fFindText, options, findButton, bot);
        verifyStatusLabel(bot, true);
        search(fFindText.equals(upper) ? lower : upper, options, findButton, bot);
        verifyStatusLabel(bot, false);
    }

    /**
     * Test the whole word search option
     */
    @Test
    public void testWholeWord() {
        SWTBot bot = getDialogBot();
        SWTBotButton findButton = bot.button("Find");

        @NonNull
        String text = fFindText.split(SPACE)[0];
        SearchOptions options = getOptions(true, false, false, true, false);
        search(text, options, findButton, bot);
        verifyStatusLabel(bot, true);
        search(text.substring(0, text.length() - 1), options, findButton, bot);
        verifyStatusLabel(bot, false);
    }

    /**
     * Test the regular expression search option
     */
    @Test
    public void testRegEx() {
        SWTBot bot = getDialogBot();
        SWTBotButton findButton = bot.button("Find");

        final String text = REGEX_PREFIX + fFindText.split(SPACE)[0];
        SearchOptions options = getOptions(true, false, false, false, true);
        search(text, options, findButton, bot);
        verifyStatusLabel(bot, true);
        options = getOptions(true, false, false, false, false);
        search(text, options, findButton, bot);
        verifyStatusLabel(bot, false);
    }

    /**
     * Test open/close the find dialog
     */
    @Test
    public void testOpenCloseDialog() {
        SWTBotShell shell = getDialogShell();
        closeDialog(getDialogBot());
        fBot.waitUntil(Conditions.shellCloses(shell));
        openDialog(fViewBot);
    }

    private static void verifyStatusLabel(SWTBot bot, boolean shouldBeFound) {
        // Get the second label in the dialog
        SWTBotLabel statusLabel = bot.label(1);
        String label = statusLabel.getText();
        assertTrue("status label : \"" + label + '\"', shouldBeFound == !label.equals("Entry not found"));
    }

    /**
     * Get the find dialog bot
     *
     * @return The bot
     */
    private static SWTBot getDialogBot() {
        return getDialogShell().bot();
    }

    /**
     * Get the find dialog shell bot
     *
     * @return The shell bot
     */
    private static SWTBotShell getDialogShell() {
        return fBot.shell(DIALOG_TITLE);
    }

    private static void closeDialog(SWTBot bot) {
        bot.button("Close").click();
    }

    private static void search(String findText, SearchOptions options, SWTBotButton findButton, SWTBot bot) {
        // set the text to search
        SWTBotCombo findFieldCombo = bot.comboBox();
        findFieldCombo.setText(findText);
        assertTrue("Find combo", findFieldCombo.getText().equals(findText));

        // set the options
        SWTBotRadio directions = options.forwardSearch ? bot.radio("Forward").click() : bot.radio("Backward").click();
        assertTrue("direction", directions.isSelected());

        setCheckButton("Case sensitive", options.caseSensitive, bot);
        setCheckButton("Wrap search", options.wrapSearch, bot);
        setCheckButton("Whole word", options.wholeWord, bot);
        setCheckButton("Regular expression", options.regExSearch, bot);

        findButton.click();
    }

    private SearchOptions getOptions(boolean forward, boolean caseSensitive, boolean wrapSearch, boolean wholeWord, boolean regEx) {
        SearchOptions options = new SearchOptions();
        options.forwardSearch = forward;
        options.caseSensitive = caseSensitive;
        options.wrapSearch = wrapSearch;
        options.wholeWord = wholeWord;
        options.regExSearch = regEx;
        return options;
    }

    private static void setCheckButton(String mnemonic, boolean option, SWTBot bot) {
        final SWTBotCheckBox checkBox = bot.checkBox(mnemonic);
        if (checkBox.isEnabled()) {
            if (option) {
                checkBox.select();
            } else {
                checkBox.deselect();
            }
        }
    }

    private void verifySelection(String name, SearchOptions options, SWTBotView view, boolean isWrapped) {
        final String entryName = getTimegraphSelectionText(view);
        assertTrue("entry name : \"" + entryName + '\"', entryName != null && entryName.contains(name));

        final int selectionIndex = getSelectionIndex(view);
        if (!isWrapped) {
            assertTrue("selection index", options.forwardSearch ? selectionIndex > fSelectionIndex : selectionIndex < fSelectionIndex);
        } else {
            assertTrue("selection index", options.forwardSearch ? selectionIndex <= fSelectionIndex : selectionIndex >= fSelectionIndex);
        }
        fSelectionIndex = selectionIndex;
    }

    private static boolean isWrapped(final SWTBot bot) {
        return bot.label(1).getText().equals("Wrapped search");
    }

    /**
     * Get the timegraph view selected entry
     *
     * @param viewBot
     *            The timegraph view bot
     * @return The selected entry
     */
    private String getTimegraphSelectionText(final SWTBotView view) {
        String text = "";
        TableCollection selection = fTimeGraphBot.selection();
        if (selection.rowCount() > 0) {
            for (int col = 0; col < selection.columnCount(); col++) {
                text += ' ' + selection.get(0, col);
            }
        }
        return text;
    }

    /**
     * Get the index of the entry selected in the timegraph view
     *
     * @param viewBot
     *            The timegraph view bot
     * @return
     */
    private static Integer getSelectionIndex(SWTBotView viewBot) {
        final TimeGraphControl timegraph = viewBot.bot().widget(WidgetOfType.widgetOfType(TimeGraphControl.class));
        return UIThreadRunnable.syncExec(() -> {
            return timegraph.getSelectedIndex();
        });
    }

    private class SearchOptions {
        boolean forwardSearch;
        boolean caseSensitive;
        boolean wrapSearch;
        boolean wholeWord;
        boolean regExSearch;
    }
}
