/*******************************************************************************
 * Copyright (c) 2014, 2017 Ericsson
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License 2.0 which
 * accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *   Alexandre Montplaisir - Initial API and implementation
 *******************************************************************************/

package org.eclipse.tracecompass.lttng2.ust.core.tests.callstack;

import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;
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.File;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.tracecompass.analysis.profiling.core.callstack.CallStackStateProvider;
import org.eclipse.tracecompass.internal.lttng2.ust.core.callstack.LttngUstCallStackProvider;
import org.eclipse.tracecompass.lttng2.ust.core.tests.shared.LttngUstTestTraceUtils;
import org.eclipse.tracecompass.lttng2.ust.core.trace.LttngUstTrace;
import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
import org.eclipse.tracecompass.statesystem.core.StateSystemUtils;
import org.eclipse.tracecompass.statesystem.core.exceptions.AttributeNotFoundException;
import org.eclipse.tracecompass.statesystem.core.exceptions.StateSystemDisposedException;
import org.eclipse.tracecompass.statesystem.core.interval.ITmfStateInterval;
import org.eclipse.tracecompass.statesystem.core.statevalue.ITmfStateValue;
import org.eclipse.tracecompass.testtraces.ctf.CtfTestTrace;
import org.eclipse.tracecompass.tmf.core.exceptions.TmfAnalysisException;
import org.eclipse.tracecompass.tmf.core.statesystem.ITmfStateProvider;
import org.eclipse.tracecompass.tmf.core.statesystem.TmfStateSystemAnalysisModule;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.core.trace.TmfTraceManager;
import org.eclipse.tracecompass.tmf.ctf.core.tests.shared.CtfTmfTestTraceUtils;
import org.eclipse.tracecompass.tmf.ctf.core.trace.CtfTmfTrace;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;

/**
 * Base class for the UST callstack state provider tests.
 *
 * @author Alexandre Montplaisir
 */
public abstract class AbstractProviderTest {

    /** Time-out tests after 1 minute. */
    @Rule
    public TestRule globalTimeout = new Timeout(1, TimeUnit.MINUTES);

    // ------------------------------------------------------------------------
    // Attributes
    // ------------------------------------------------------------------------

    private static final @NonNull CtfTestTrace otherUstTrace = CtfTestTrace.HELLO_LOST;

    private ITmfTrace fTrace = null;
    private ITmfStateSystem fSS = null;
    private TestLttngCallStackModule fModule;


    // ------------------------------------------------------------------------
    // Abstract methods
    // ------------------------------------------------------------------------

    /**
     * @return The test trace to use for this test
     */
    protected abstract @NonNull CtfTestTrace getTestTrace();

    /**
     * @return The ID of the process the desired thread belongs to
     */
    protected abstract int getProcessId();

    /**
     * @return The name of the executable process in that particular trace
     */
    protected abstract String getThreadName();

    /**
     * Get the list of timestamps to query in that trace.
     *
     * @param index
     *            Which of the test timestamps?
     * @return That particular timestamp
     */
    protected abstract long getTestTimestamp(int index);

    // ------------------------------------------------------------------------
    // Maintenance
    // ------------------------------------------------------------------------

    /**
     * Perform pre-class initialization.
     */
    @Before
    public void setUp() {
        CtfTestTrace testTrace = getTestTrace();

        LttngUstTrace trace = LttngUstTestTraceUtils.getTrace(testTrace);
        fTrace = trace;
        fModule = new TestLttngCallStackModule();
        try {
            assertTrue(fModule.setTrace(trace));
        } catch (TmfAnalysisException e) {
            fail();
        }
        fModule.schedule();
        assertTrue(fModule.waitForCompletion());

        fSS = fModule.getStateSystem();
        assertNotNull(fSS);
    }

    /**
     * Perform post-class clean-up.
     */
    @After
    public void tearDown() {
        fModule.dispose();
        ITmfTrace trace = fTrace;
        if (trace != null) {
            LttngUstTestTraceUtils.dispose(getTestTrace());
            File suppDir = new File(TmfTraceManager.getSupplementaryFileDir(trace));
            deleteDirectory(suppDir);
        }
    }

    // ------------------------------------------------------------------------
    // Test methods
    // ------------------------------------------------------------------------

    /**
     * Test the handling of generic UST traces who do not contain the required
     * information.
     */
    @Test
    public void testOtherUstTrace() {
        /* Initialize the trace and analysis module */
        File suppDir;
        CtfTmfTrace ustTrace = CtfTmfTestTraceUtils.getTrace(otherUstTrace);
        TestLttngCallStackModule module = null;
        try {
            module = new TestLttngCallStackModule();
            try {
                assertTrue(module.setTrace(ustTrace));
            } catch (TmfAnalysisException e) {
                fail();
            }
            module.schedule();
            assertTrue(module.waitForCompletion());

            /* Make sure the generated state system exists, but is empty */
            ITmfStateSystem ss = module.getStateSystem();
            assertNotNull(ss);
            assertTrue(ss.getStartTime() >= ustTrace.getStartTime().toNanos());
            assertEquals(0, ss.getNbAttributes());
        } finally {
            if (module != null) {
                module.dispose();
            }
        }
        suppDir = new File(TmfTraceManager.getSupplementaryFileDir(ustTrace));

        ustTrace.dispose();
        deleteDirectory(suppDir);
        assertFalse(suppDir.exists());
    }

    /**
     * Test that the callstack state system is there and contains data.
     */
    @Test
    public void testConstruction() {
        assertNotNull(fSS);
        assertTrue(fSS.getNbAttributes() > 0);
    }

    /**
     * Test the callstack at the beginning of the state system.
     */
    @Test
    public void testCallStackBegin() {
        long start = fSS.getStartTime();
        String[] cs = getCallStack(fSS, getProcessId(), getThreadName(), start);
        assertEquals(1, cs.length);

        assertEquals("40472b", cs[0]);
    }

    /**
     * Test the callstack somewhere in the trace.
     */
    @Test
    public void testCallStack1() {
        String[] cs = getCallStack(fSS, getProcessId(), getThreadName(), getTestTimestamp(0));
        assertEquals(2, cs.length);

        assertEquals("40472b", cs[0]);
        assertEquals("403d60", cs[1]);
    }

    /**
     * Test the callstack somewhere in the trace.
     */
    @Test
    public void testCallStack2() {
        String[] cs = getCallStack(fSS, getProcessId(), getThreadName(), getTestTimestamp(1));
        assertEquals(3, cs.length);

        assertEquals("40472b", cs[0]);
        assertEquals("403b14", cs[1]);
        assertEquals("401b23", cs[2]);
    }

    /**
     * Test the callstack somewhere in the trace.
     */
    @Test
    public void testCallStack3() {
        String[] cs = getCallStack(fSS, getProcessId(), getThreadName(), getTestTimestamp(2));
        assertEquals(4, cs.length);

        assertEquals("40472b", cs[0]);
        assertEquals("4045c8", cs[1]);
        assertEquals("403760", cs[2]);
        assertEquals("401aac", cs[3]);
    }

    /**
     * Test the callstack at the end of the trace/state system.
     */
    @Test
    public void testCallStackEnd() {
        long end = fSS.getCurrentEndTime();
        String[] cs = getCallStack(fSS, getProcessId(), getThreadName(), end);
        assertEquals(3, cs.length);

        assertEquals("40472b", cs[0]);
        assertEquals("4045c8", cs[1]);
        assertEquals("403760", cs[2]);
    }

    // ------------------------------------------------------------------------
    // Utility methods
    // ------------------------------------------------------------------------

    /** Empty and delete a directory */
    private static void deleteDirectory(File dir) {
        /* Assuming the dir only contains file or empty directories */
        for (File file : dir.listFiles()) {
            file.delete();
        }
        dir.delete();
    }

    /** Get the callstack for the given timestamp, for this particular trace */
    private static String[] getCallStack(ITmfStateSystem ss, int pid, String threadName, long timestamp) {
        try {
            String processName = (pid == CallStackStateProvider.UNKNOWN_PID) ? CallStackStateProvider.UNKNOWN : Integer.toString(pid);
            int stackAttribute = ss.getQuarkAbsolute("Processes", processName, threadName, "CallStack");
            List<ITmfStateInterval> state = ss.queryFullState(timestamp);
            int depth = state.get(stackAttribute).getStateValue().unboxInt();

            int stackTop = ss.getQuarkRelative(stackAttribute, String.valueOf(depth));
            ITmfStateValue expectedValue = state.get(stackTop).getStateValue();
            ITmfStateInterval interval = StateSystemUtils.querySingleStackTop(ss, timestamp, stackAttribute);
            assertNotNull(interval);
            assertEquals(expectedValue, interval.getStateValue());

            String[] ret = new String[depth];
            for (int i = 0; i < depth; i++) {
                int quark = ss.getQuarkRelative(stackAttribute, String.valueOf(i + 1));
                ret[i] = Long.toHexString(state.get(quark).getStateValue().unboxLong());
            }
            return ret;

        } catch (AttributeNotFoundException e) {
            fail(e.getMessage());
        } catch (StateSystemDisposedException e) {
            fail(e.getMessage());
        }
        fail();
        return null;
    }

    private class TestLttngCallStackModule extends TmfStateSystemAnalysisModule {

        @Override
        protected ITmfStateProvider createStateProvider() {
            return new LttngUstCallStackProvider(checkNotNull(getTrace()));
        }
    }
}
