/*******************************************************************************
 * (C) Copyright 2010, 2018 IBM Corp. and others.
 *
 * 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:
 *    Thavidu Ranatunga (IBM) - Initial implementation.
 *******************************************************************************/
package org.eclipse.linuxtools.internal.perf.tests;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.eclipse.debug.core.ILaunchConfiguration;
import org.eclipse.debug.core.ILaunchConfigurationType;
import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.linuxtools.internal.perf.PerfCore;
import org.eclipse.linuxtools.internal.perf.PerfPlugin;
import org.eclipse.linuxtools.internal.perf.launch.PerfEventsTab;
import org.eclipse.linuxtools.internal.perf.launch.PerfOptionsTab;
import org.eclipse.linuxtools.internal.perf.model.PMCommand;
import org.eclipse.linuxtools.internal.perf.model.PMDso;
import org.eclipse.linuxtools.internal.perf.model.PMEvent;
import org.eclipse.linuxtools.internal.perf.model.PMFile;
import org.eclipse.linuxtools.internal.perf.model.PMSymbol;
import org.eclipse.linuxtools.internal.perf.model.TreeParent;
import org.eclipse.linuxtools.internal.perf.ui.PerfDoubleClickAction;
import org.eclipse.linuxtools.internal.perf.ui.PerfProfileView;
import org.eclipse.linuxtools.profiling.tests.AbstractTest;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.osgi.framework.FrameworkUtil;

public class ModelTest extends AbstractTest {
    private ILaunchConfiguration config;
    private Stack<Class<?>> stack;

    @Before
    public void setUp() throws Exception {
        proj = createProjectAndBuild(FrameworkUtil.getBundle(this.getClass()), "fibTest"); //$NON-NLS-1$
        config = createConfiguration(proj.getProject());

        Class<?>[] klassList = new Class<?>[] { PMSymbol.class, PMFile.class,
                PMDso.class, PMCommand.class, PMEvent.class };
        stack = new Stack<>();
        Collections.addAll(stack, klassList);
    }

    @After
    public void tearDown() throws Exception {
        deleteProject(proj);
    }

    @Override
    protected ILaunchConfigurationType getLaunchConfigType() {
        return getLaunchManager().getLaunchConfigurationType(PerfPlugin.LAUNCHCONF_ID);
    }

    @Override
    protected void setProfileAttributes(ILaunchConfigurationWorkingCopy wc) {
        PerfEventsTab eventsTab = new PerfEventsTab();
        PerfOptionsTab optionsTab = new PerfOptionsTab();
        wc.setAttribute(PerfPlugin.ATTR_SourceLineNumbers, false);
        eventsTab.setDefaults(wc);
        optionsTab.setDefaults(wc);
    }
    @Test
    public void testModelDefaultGenericStructure() {
        TreeParent invisibleRoot = buildModel(
                "resources/defaultevent-data/perf.data",
                "resources/defaultevent-data/perf.data.txt",
                "resources/defaultevent-data/perf.data.err.log");

        checkChildrenStructure(invisibleRoot, stack);
    }
    @Test
    public void testModelMultiEventGenericStructure() {
        TreeParent invisibleRoot = buildModel(
                "resources/multievent-data/perf.data",
                "resources/multievent-data/perf.data.txt",
                "resources/multievent-data/perf.data.err.log");

        checkChildrenStructure(invisibleRoot, stack);
    }
    @Test
    public void testPercentages() {
        TreeParent invisibleRoot = buildModel(
                "resources/defaultevent-data/perf.data",
                "resources/defaultevent-data/perf.data.txt",
                "resources/defaultevent-data/perf.data.err.log");

        checkChildrenPercentages(invisibleRoot, invisibleRoot.getPercent());
    }
    @Test
    public void testDoubleClickAction () {
        TreeParent invisibleRoot = buildModel(
                "resources/defaultevent-data/perf.data",
                "resources/defaultevent-data/perf.data.txt",
                "resources/defaultevent-data/perf.data.err.log");

        PerfPlugin.getDefault().setModelRoot(invisibleRoot);
        // update the model root for the view
        PerfCore.refreshView("resources/defaultevent-data/perf.data");

        // number of parents excluding invisibleRoot
        int numOfParents = getNumberOfParents(invisibleRoot) - 1;

        // create a double click action to act on the tree viewer
        try {
            PerfProfileView view = (PerfProfileView) PlatformUI.getWorkbench()
                    .getActiveWorkbenchWindow().getActivePage()
                    .showView(PerfPlugin.VIEW_ID);
            TreeViewer tv = view.getTreeViewer();
            PerfDoubleClickAction dblClick = new PerfDoubleClickAction(tv);

            // double click every element
            doubleClickAllChildren(invisibleRoot, tv, dblClick);

            // If all elements are expanded, this is the number of elements
            // in our model that have children.
            assertEquals(numOfParents, tv.getExpandedElements().length);
        } catch (PartInitException e) {
            fail("Failed to open the Profiling View.");
        }
    }
    @Test
    public void testParserMultiEvent() {
        TreeParent invisibleRoot = buildModel(
                "resources/multievent-data/perf.data",
                "resources/multievent-data/perf.data.txt",
                "resources/multievent-data/perf.data.err.log");

        assertEquals(invisibleRoot.getChildren().length, 5);

        String cur = null;

        for (TreeParent event : invisibleRoot.getChildren()) {

            cur = event.getName();

            // Assert specific properties extracted by the parser.
            if ("cpu-clock".equals(cur)) {
                assertTrue(event.hasChildren());
                assertEquals(event.getChildren().length, 1);

                TreeParent cmd = event.getChildren()[0];
                assertEquals(cmd.getChildren().length, 1);

                String[] cmdLabels = { "hellotest" };
                checkCommadLabels(cmdLabels, cmd);
            } else if ("task-clock".equals(cur)) {
                assertTrue(event.hasChildren());
                assertEquals(event.getChildren().length, 1);

                TreeParent cmd = event.getChildren()[0];
                assertEquals(cmd.getChildren().length, 1);

                String[] cmdLabels = { "hellotest" };
                checkCommadLabels(cmdLabels, cmd);
            } else if ("page-faults".equals(cur)) {
                assertTrue(event.hasChildren());
                assertEquals(event.getChildren().length, 1);

                TreeParent cmd = event.getChildren()[0];
                assertEquals(cmd.getChildren().length, 3);

                String[] cmdLabels = { "ld-2.14.90.so", "[kernel.kallsyms]",
                        "libc-2.14.90.so" };
                checkCommadLabels(cmdLabels, cmd);
            } else if ("minor-faults".equals(cur)) {
                assertTrue(event.hasChildren());
                assertEquals(event.getChildren().length, 1);

                TreeParent cmd = event.getChildren()[0];
                assertEquals(cmd.getChildren().length, 3);

                String[] cmdLabels = { "ld-2.14.90.so", "[kernel.kallsyms]",
                        "libc-2.14.90.so" };
                checkCommadLabels(cmdLabels, cmd);
            } else if ("major-faults".equals(cur)) {
                assertFalse(event.hasChildren());
            }

        }
    }
    @Test
    public void testParserDefaultEvent() {
        TreeParent invisibleRoot = buildModel(
                "resources/defaultevent-data/perf.data",
                "resources/defaultevent-data/perf.data.txt",
                "resources/defaultevent-data/perf.data.err.log");

        // Assert specific properties extracted by the parser.
        assertEquals(invisibleRoot.getChildren().length, 1);

        TreeParent event = invisibleRoot.getChildren()[0];
        assertEquals(event.getName(), "cycles");
        assertTrue(event.hasChildren());
        assertEquals(event.getChildren().length, 1);

        TreeParent cmd = event.getChildren()[0];
        assertTrue(cmd.hasChildren());
        assertEquals(cmd.getChildren().length, 4);

        String[] cmdLabels = { "hellotest", "[kernel.kallsyms]",
                "ld-2.14.90.so", "perf" };
        checkCommadLabels(cmdLabels, cmd);
    }
    @Test
    public void testParseEventList() throws FileNotFoundException {
        BufferedReader input = new BufferedReader(new FileReader("resources/simple-perf-event-list"));

        Map<String, List<String>> eventList = PerfCore.parseEventList(input);
        for(String key : eventList.keySet()){
            if ("Raw hardware event descriptor".equals(key)) {
                assertTrue(eventList.get(key).contains("rNNN"));
                assertTrue(eventList.get(key).contains("cpu/t1=v1"));
            } else if ("Hardware breakpoint".equals(key)) {
                assertTrue(eventList.get(key).contains("mem:<addr>"));
            } else if ("Software event".equals(key)) {
                assertTrue(eventList.get(key).contains("cpu-clock"));
                assertTrue(eventList.get(key).contains("task-clock"));
            } else if ("Hardware cache event".equals(key)) {
                assertTrue(eventList.get(key).contains("L1-dcache-loads"));
                assertTrue(eventList.get(key).contains("L1-dcache-load-misses"));
            } else if ("Tracepoint event".equals(key)) {
                assertTrue(eventList.get(key).contains("mac80211:drv_return_void"));
                assertTrue(eventList.get(key).contains("mac80211:drv_return_int"));
            } else if ("Hardware event".equals(key)) {
                assertTrue(eventList.get(key).contains("cpu-cycles"));
                assertTrue(eventList.get(key).contains("stalled-cycles-frontend"));
            }
        }
    }
    @Test
    public void testParseAnnotation() throws FileNotFoundException {
        BufferedReader input = new BufferedReader(new FileReader(
                    "resources/perf-annotation-data"));

        // Set up arguments for the annotation parser.
        IPath workingDir = Path.fromOSString("/working/directory/");
        PMCommand cmd = new PMCommand("testCommand");
        PMDso dso = new PMDso("testDso", false);
        PMFile tmpFile = new PMFile(PerfPlugin.STRINGS_UnfiledSymbols);
        PMSymbol sym = new PMSymbol("testSym", 0, 0);

        // Set children and respective parents.
        cmd.addChild(dso);
        dso.addChild(tmpFile);
        tmpFile.addChild(sym);

        dso.setParent(cmd);
        tmpFile.setParent(dso);
        sym.setParent(tmpFile);

        PerfCore.parseAnnotation(null, input, workingDir, dso, sym);

        // Expected results data.
        String expectedDsoPath = "/working/directory/fibonacci";
        String expectedFilePath = "/home/user/workspace/fibonacci/Debug/../src/fibonacci.cpp";

        assertTrue(expectedDsoPath.equals(dso.getPath()));
        assertEquals(dso.getChildren().length, 2);

        for (TreeParent dsoChild : dso.getChildren()) {
            String filePath = ((PMFile) dsoChild).getPath();

            if (PerfPlugin.STRINGS_UnfiledSymbols.equals(filePath)) {
                assertFalse(dsoChild.hasChildren());
            } else {
                assertTrue(expectedFilePath.equals(filePath));
                assertTrue(dsoChild.hasChildren());
                assertEquals(dsoChild.getChildren().length, 1);

                TreeParent curSym = dsoChild.getChildren()[0];
                assertTrue(curSym.hasChildren());
                assertEquals(curSym.getChildren().length, 5);

                float percentCount = 0;
                for (TreeParent symChild : curSym.getChildren()) {
                    percentCount += symChild.getPercent();
                }

                assertEquals(Math.ceil(percentCount), 100.0, 0.0);

            }
        }
    }

    @Test
    public void testAnnotateString() throws CoreException {
        ILaunchConfigurationWorkingCopy tempConfig = config.copy("test-config");
        tempConfig
                .setAttribute(PerfPlugin.ATTR_Kernel_Location, "/boot/kernel");
        tempConfig.setAttribute(PerfPlugin.ATTR_ModuleSymbols, true);

        String[] annotateString = PerfCore.getAnnotateString(tempConfig, "dso",
                "symbol", "resources/defaultevent-data/perf.data", false);

        String[] expectedString = new String[] { PerfPlugin.PERF_COMMAND,
                "annotate", "--stdio", "-d", "dso", "-s", "symbol", "-l", "-P",
                "--vmlinux", "/boot/kernel", "-m", "-i",
                "resources/defaultevent-data/perf.data",
                "<", "/dev/null" };

        assertArrayEquals(expectedString, annotateString);
    }

    @Test
    public void testRecordString() throws CoreException {
        ILaunchConfigurationWorkingCopy tempConfig = config.copy("test-config");
        tempConfig.setAttribute(PerfPlugin.ATTR_Record_Realtime, true);
        tempConfig.setAttribute(PerfPlugin.ATTR_Record_Realtime_Priority, 2);
        tempConfig.setAttribute(PerfPlugin.ATTR_Record_Verbose, true);
        tempConfig.setAttribute(PerfPlugin.ATTR_Multiplex, true);

        ArrayList<String> selectedEvents = new ArrayList<>();
        selectedEvents.add("cpu-cycles");
        selectedEvents.add("cache-misses");
        selectedEvents.add("cpu-clock");
        tempConfig.setAttribute(PerfPlugin.ATTR_SelectedEvents, selectedEvents);

        tempConfig.setAttribute(PerfPlugin.ATTR_DefaultEvent, false);

        String[] recordString = PerfCore.getRecordString(tempConfig);
        assertNotNull(recordString);

        String[] expectedString = { PerfPlugin.PERF_COMMAND, "record",
                "-r", "2", "-v", "-M", "-e", "cpu-cycles", "-e", "cache-misses",
                "-e", "cpu-clock" };
        assertArrayEquals(expectedString, recordString);
    }

    @Test
    public void testReportString() throws CoreException {
        ILaunchConfigurationWorkingCopy tempConfig = null;
        tempConfig = config.copy("test-config");
        tempConfig
                .setAttribute(PerfPlugin.ATTR_Kernel_Location, "/boot/kernel");
        tempConfig.setAttribute(PerfPlugin.ATTR_ModuleSymbols, true);

        String[] reportString = PerfCore.getReportString(tempConfig,
                "resources/defaultevent-data/perf.data");
        assertNotNull(reportString);

        String[] expectedString = { PerfPlugin.PERF_COMMAND, "report",
                "--sort", "comm,dso,sym", "-n", "-t", "" + (char) 1,
                "--vmlinux", "/boot/kernel", "-m", "-i",
                "resources/defaultevent-data/perf.data" };
        assertArrayEquals(expectedString, reportString);
    }

    /**
     * @param root some element that will serve as the root
     * @param sum the expected sum of the percentages of this root's
     * immediate children
     */
    private void checkChildrenPercentages (TreeParent root, float sum) {
        float actualSum = 0;
        // If a root has no children we're done
        if (root.getChildren().length != 0) {
            for (TreeParent child : root.getChildren()) {
                actualSum += child.getPercent();
                checkChildrenPercentages(child, child.getPercent());
            }
            // some top-level elements have an undefined percentage but
            // their children have defined percentages
            // eg. the invisible root, and PMCommand
            if (actualSum != 100 && sum != -1){
                assertTrue(actualSum/sum <= 1.0 && actualSum/sum >= 0.99);
            }
        }
    }

    /**
     * @param root some element that will serve as the root
     * @param stack a stack of classes
     */
    private void checkChildrenStructure (TreeParent root, Stack<Class<?>> stack){
        if (!stack.isEmpty()){
            // children of root must be instances of the top class on the stack
            Class<?> klass = stack.pop();
            for (TreeParent tp : root.getChildren()){
                // tp.getClass() instanceof klass
                assertTrue(klass.isAssignableFrom(tp.getClass()));
                // each sibling needs its own stack
                Stack<Class<?>> newStack = new Stack<>();
                newStack.addAll(stack);
                checkChildrenStructure(tp, newStack);
            }
        }
    }

    /**
     * Performs a Perf double-click action on every element in the
     * TreeViewer model.
     *
     * @param root some element that will serve as the root
     * @param tv a TreeViewer containing elements from the Perf model
     * @param dblClick the double-click action to perform on every
     * element of the TreeViewer.
     */
    private void doubleClickAllChildren(TreeParent root, TreeViewer tv,
            PerfDoubleClickAction dblClick) {

        for (TreeParent child : root.getChildren()) {
            // see PerfDoubleClickAction for IStructuredSelection
            tv.setSelection(new StructuredSelection(child));
            dblClick.run();
            doubleClickAllChildren(child, tv, dblClick);
        }
    }

    /**
     * Find the number of ancestors of the given root that have children.
     * This includes the given root in the computation.
     *
     * @param root some element that will serve as the root
     * @return the number of elements under, and including the
     * given root, that have children elements.
     */
    private int getNumberOfParents(TreeParent root) {
        int ret = root.hasChildren() ? 1 : 0;
        for (TreeParent child : root.getChildren()) {
            ret += getNumberOfParents(child);
        }
        return ret;
    }

    /**
     * Build model based on perf data file report.
     * @param perfDataLoc location of perf data file
     * @param perfTextDataLoc location of perf data text file
     * @param perfErrorDataLoc location of error log file
     * @return tree model based on perf data report.
     */
    private TreeParent buildModel(String perfDataLoc, String perfTextDataLoc,
            String perfErrorDataLoc) {
        TreeParent invisibleRoot = new TreeParent("");
        BufferedReader input = null;
        BufferedReader error = null;

        try {
            input = new BufferedReader(new FileReader(perfTextDataLoc));
            error = new BufferedReader(new FileReader(perfErrorDataLoc));
        } catch (IOException e) {
            e.printStackTrace();
            fail(e.getMessage());
        }

        PerfCore.parseReport(config, null, null, perfDataLoc, null,
                invisibleRoot, false, input, error);
        return invisibleRoot;
    }

    /**
     * Check whether the command labels in model rooted at cmd exist in
     * list of labels cmdLabels.
     * @param cmdLabels list of command labels
     * @param cmd root of tree model
     */
    private void checkCommadLabels(String[] cmdLabels, TreeParent cmd) {
        List<String> cmdList = new ArrayList<>(Arrays.asList(cmdLabels));

        for (TreeParent dso : cmd.getChildren()) {
            assertTrue(cmdList.get(0).equals(dso.getName()));
            cmdList.remove(0);
        }
    }
}
