/*******************************************************************************
 * Copyright (c) 2013, 2016 École Polytechnique de Montréal
 *
 * 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.tmf.core.tests.statesystem;

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

import java.io.File;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
import org.eclipse.tracecompass.statesystem.core.interval.ITmfStateInterval;
import org.eclipse.tracecompass.statesystem.core.tests.shared.utils.StateIntervalStub;
import org.eclipse.tracecompass.statesystem.core.tests.shared.utils.StateSystemTestUtils;
import org.eclipse.tracecompass.tmf.core.event.ITmfEvent;
import org.eclipse.tracecompass.tmf.core.exceptions.TmfAnalysisException;
import org.eclipse.tracecompass.tmf.core.signal.TmfTraceOpenedSignal;
import org.eclipse.tracecompass.tmf.core.statesystem.ITmfStateProvider;
import org.eclipse.tracecompass.tmf.core.statesystem.ITmfStateProvider.FutureEventType;
import org.eclipse.tracecompass.tmf.core.statesystem.Messages;
import org.eclipse.tracecompass.tmf.core.statesystem.TmfStateSystemAnalysisModule;
import org.eclipse.tracecompass.tmf.core.tests.TmfCoreTestPlugin;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.tests.stubs.analysis.TestStateSystemModule;
import org.eclipse.tracecompass.tmf.tests.stubs.analysis.TestStateSystemProvider;
import org.eclipse.tracecompass.tmf.tests.stubs.trace.xml.TmfXmlTraceStub;
import org.eclipse.tracecompass.tmf.tests.stubs.trace.xml.TmfXmlTraceStubNs;
import org.junit.After;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;

import com.google.common.collect.ImmutableList;

/**
 * Test the {@link TmfStateSystemAnalysisModule} class
 *
 * @author Geneviève Bastien
 */
public class StateSystemAnalysisModuleTest {

    private final class BreakingTest extends TestStateSystemProvider {

        private final int fEventNo;
        private int fEventCount = 0;
        private String fExceptionMessage;

        private BreakingTest(@NonNull ITmfTrace trace, int eventToFail, String msg) {
            super(trace, 1, 2);
            fEventNo = eventToFail;
            fExceptionMessage = msg;
        }

        @Override
        protected void eventHandle(@NonNull ITmfEvent event) {
            fEventCount++;
            if (fEventCount == fEventNo) {
                throw new IllegalArgumentException(fExceptionMessage);
            }
        }
    }

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

    /** ID of the test state system analysis module */
    public static final String MODULE_SS = "org.eclipse.linuxtools.tmf.core.tests.analysis.sstest";
    private static final String XML_TRACE = "testfiles/stub_xml_traces/valid/analysis_dependency.xml";

    private TestStateSystemModule fModule;
    private ITmfTrace fTrace;

    /**
     * Setup test trace
     */
    @Before
    public void setupTraces() {
        TmfXmlTraceStub trace = TmfXmlTraceStubNs.setupTrace(TmfCoreTestPlugin.getAbsoluteFilePath(XML_TRACE));
        trace.traceOpened(new TmfTraceOpenedSignal(this, trace, null));
        fTrace = trace;

        fModule = (TestStateSystemModule) trace.getAnalysisModule(MODULE_SS);
    }

    /**
     * Some tests use traces, let's clean them here
     */
    @After
    public void cleanupTraces() {
        fTrace.dispose();
    }

    /**
     * Test the state system module execution and result
     */
    @Test
    public void testSsModule() {
        ITmfStateSystem ss = fModule.getStateSystem();
        assertNull(ss);
        fModule.schedule();
        if (fModule.waitForCompletion()) {
            ss = fModule.getStateSystem();
            assertNotNull(ss);
        } else {
            fail("Module did not complete properly");
        }
    }

    /**
     * Make sure that the state system is initialized after calling
     * {@link TmfStateSystemAnalysisModule#waitForInitialization()}.
     */
    @Test
    public void testInitialization() {
        assertNull(fModule.getStateSystem());
        fModule.schedule();

        assertTrue("Initialization succeeded", fModule.waitForInitialization());
        assertNotNull(fModule.getStateSystem());
    }

    /**
     * Test that helper returns the right properties
     */
    @Test
    public void testProperties() {

        /* The stub state system has in mem backend 2 properties */
        Map<String, String> properties = fModule.getProperties();
        assertEquals(fModule.getBackendName(), properties.get(Messages.TmfStateSystemAnalysisModule_PropertiesBackend));
        assertEquals(fModule.getId(), properties.get(org.eclipse.tracecompass.tmf.core.analysis.Messages.TmfAbstractAnalysisModule_LabelId));
    }

    private static final String CRUCIAL_EVENT = "crucialEvent";
    private static final String CRUCIAL_FIELD = "crucialInfo";

    private static void setupDependentAnalysisHandler(CyclicBarrier barrier) {
        TestStateSystemProvider.setEventHandler((ss, provider, event) -> {
            try {
                /* Wait before processing the current event */
                barrier.await();
                if (event.getName().equals(CRUCIAL_EVENT)) {
                    String crucialInfo = (String) event.getContent().getField(CRUCIAL_FIELD).getValue();
                    int quark = ss.getQuarkAbsoluteAndAdd(CRUCIAL_FIELD);
                    try {
                        ss.modifyAttribute(event.getTimestamp().toNanos(), crucialInfo, quark);
                    } catch (Exception e) {
                        fail(e.getMessage());
                    }
                }
                /* Wait before processing the next event */
                barrier.await();
                return true;
            } catch (InterruptedException | BrokenBarrierException e1) {
                return false;
            }

        });
    }

    /**
     * Test the {@link TmfStateSystemAnalysisModule#isQueryable(long)} method
     */
    @Test
    public void testIsQueryable() {

        CyclicBarrier barrier = new CyclicBarrier(2);
        setupDependentAnalysisHandler(barrier);

        TestStateSystemModule module = fModule;
        assertNotNull(module);

        /* Module is not started, it should be queriable */
        assertTrue(module.isQueryable(1));
        assertTrue(module.isQueryable(4));
        assertTrue(module.isQueryable(5));
        assertTrue(module.isQueryable(7));
        assertTrue(module.isQueryable(10));

        module.schedule();

        assertTrue(module.waitForInitialization());

        assertFalse(module.isQueryable(1));

        try {
            /* 2 waits for a barrier for one event */
            // event 1
            barrier.await();
            barrier.await();
            // event 2
            barrier.await();
            assertTrue(module.isQueryable(1));
            assertTrue(module.isQueryable(4));
            assertFalse(module.isQueryable(5));
            barrier.await();
            // event 3
            barrier.await();
            assertTrue(module.isQueryable(1));
            assertTrue(module.isQueryable(4));
            assertFalse(module.isQueryable(5));
            barrier.await();
            // event 4
            barrier.await();
            assertTrue(module.isQueryable(1));
            assertTrue(module.isQueryable(4));
            assertFalse(module.isQueryable(5));
            barrier.await();
            // event 5
            barrier.await();
            assertTrue(module.isQueryable(1));
            assertTrue(module.isQueryable(4));
            assertTrue(module.isQueryable(5));
            assertFalse(module.isQueryable(7));
            barrier.await();
            // event 6
            barrier.await();
            assertTrue(module.isQueryable(1));
            assertTrue(module.isQueryable(4));
            assertTrue(module.isQueryable(5));
            assertFalse(module.isQueryable(7));
            barrier.await();
            // event 7
            barrier.await();
            assertTrue(module.isQueryable(1));
            assertTrue(module.isQueryable(4));
            assertTrue(module.isQueryable(5));
            assertTrue(module.isQueryable(7));
            assertFalse(module.isQueryable(10));
            barrier.await();

            fModule.waitForCompletion();
            assertTrue(module.isQueryable(1));
            assertTrue(module.isQueryable(4));
            assertTrue(module.isQueryable(5));
            assertTrue(module.isQueryable(7));
            assertTrue(module.isQueryable(10));

            // Should return true only if later than trace time
            assertTrue(module.isQueryable(100));

        } catch (InterruptedException | BrokenBarrierException e1) {
            fail(e1.getMessage());
            fModule.cancel();
        } finally {
            TestStateSystemProvider.setEventHandler(null);
        }
    }

    /**
     * Test the {@link TmfStateSystemAnalysisModule#isQueryable(long)} method
     * when the analysis is cancelled
     */
    @Ignore("Hangs very often")
    @Test
    public void testIsQueryableCancel() {

        TestStateSystemModule module = fModule;
        assertNotNull(module);
        /* Set the queue to 1 to limit the number of events buffered */
        module.setPerEventSignalling(true);

        /* Module is not started, it should be queriable */
        assertTrue(module.isQueryable(1));
        assertTrue(module.isQueryable(4));
        assertTrue(module.isQueryable(5));
        assertTrue(module.isQueryable(7));
        assertTrue(module.isQueryable(10));

        fModule.schedule();

        assertTrue(module.waitForInitialization());

        assertFalse(module.isQueryable(1));

        // Process 2 events, then cancel
        module.signalNextEvent();
        module.signalNextEvent();
        module.cancel();
        module.setPerEventSignalling(false);

        fModule.waitForCompletion();
        assertTrue(module.isQueryable(1));
        assertTrue(module.isQueryable(4));
        assertTrue(module.isQueryable(5));
        assertTrue(module.isQueryable(7));
        assertTrue(module.isQueryable(10));
    }

    /**
     * Test that an analysis with full backend is re-read correctly
     *
     * @throws TmfAnalysisException
     *             Propagates exceptions
     */
    @Test
    public void testReReadFullAnalysis() throws TmfAnalysisException {
        TestStateSystemModule module = new TestStateSystemModule(true);
        TestStateSystemModule module2 = new TestStateSystemModule(true);
        try {
            ITmfTrace trace = fTrace;
            assertNotNull(trace);
            module.setTrace(trace);
            module2.setTrace(trace);

            // Execute the first module
            module.schedule();
            assertTrue(module.waitForCompletion());

            // Execute the second module, it should read the state system file
            File ssFile = module2.getSsFile();
            assertNotNull(ssFile);
            assertTrue(ssFile.exists());
            module2.schedule();
            assertTrue(module2.waitForCompletion());
        } finally {
            module.dispose();
            module2.dispose();
        }
    }

    /**
     * Test that an analysis whose event request throws an exception is failed
     * correctly
     *
     * @throws TmfAnalysisException
     *             Propagates exceptions
     */
    @Test
    public void testRequestFailure() throws TmfAnalysisException {
        TestStateSystemModule module = new TestStateSystemModule();
        module.setRequestAction(e -> {
            throw new IllegalArgumentException("This exception is desired and part of the test");
        });
        try {
            ITmfTrace trace = fTrace;
            assertNotNull(trace);
            module.setTrace(trace);

            // Execute the module that should throw the exception
            module.schedule();
            assertFalse(module.waitForCompletion());

        } finally {
            module.dispose();
        }
    }

    /**
     * Test the behavior of a state provider that causes a runtime exception at
     * different moments of the analysis. The analyses should be marked as
     * failed
     *
     * @throws TmfAnalysisException
     *             An exception when setting the trace
     */
    @Test
    public void testFaultyStateProvider() throws TmfAnalysisException {
        ITmfTrace trace = fTrace;
        assertNotNull(trace);

        // Test failure on the last event
        TmfStateSystemAnalysisModule module = new TestStateSystemModule() {

            @Override
            protected @NonNull ITmfStateProvider createStateProvider() {
                return new BreakingTest(trace, 7, "Expected exception: should be caught by the analysis itself");
            }

        };
        try {
            module.setTrace(trace);
            module.schedule();
            assertFalse(module.waitForCompletion());
        } finally {
            module.dispose();
        }

        // Test failure when the analysis the request finishes before the queue
        // is full
        module = new TestStateSystemModule() {

            @Override
            protected @NonNull ITmfStateProvider createStateProvider() {
                return new BreakingTest(trace, 5, "Expected exception: should be caught by either the analysis or the event request");
            }

        };
        try {
            module.setTrace(trace);
            module.schedule();
            assertFalse(module.waitForCompletion());
        } finally {
            module.dispose();
        }

        // Test failure when the queue should be full
        module = new TestStateSystemModule() {

            @Override
            protected @NonNull ITmfStateProvider createStateProvider() {
                return new BreakingTest(trace, 1, "Expected exception: should be caught by the event request thread");
            }

        };
        try {
            module.setTrace(trace);
            module.schedule();
            assertFalse(module.waitForCompletion());
        } finally {
            module.dispose();
        }
    }

    /**
     * Test a module that causes an exception before the module is initialized
     *
     * @throws TmfAnalysisException
     *             An exception when setting the trace
     */
    @Test
    public void testFailBeforeInitialization() throws TmfAnalysisException {
        ITmfTrace trace = fTrace;
        assertNotNull(trace);

        /* This module will throw an exception before it is initialized */
        TmfStateSystemAnalysisModule module = new TestStateSystemModule() {

            @Override
            protected boolean executeAnalysis(@Nullable IProgressMonitor monitor) {
                throw new IllegalStateException("This exception happens before initialization");
            }

        };
        try {
            module.setTrace(trace);
            module.schedule();
            assertFalse(module.waitForInitialization());
            assertFalse(module.waitForCompletion());
        } finally {
            module.dispose();
        }
    }

    /**
     * Test adding future values to the state system
     */
    @Test
    public void testFutureEvents() {
        String futureVal = "futureVal";
        String futureStack = "futureStack";
        int oneValue = 100;
        String stack1 = "stack1";
        String stack2 = "stack2";
        try {
            TestStateSystemProvider.setEventHandler((ss, provider, event) -> {
                // Add future events
                if (event.getTimestamp().toNanos() == 1) {
                    // initialize a value quark and modify it in future
                    int valueQuark = ss.getQuarkAbsoluteAndAdd(futureVal);
                    ss.modifyAttribute(event.getTimestamp().toNanos(), oneValue, valueQuark);
                    provider.addFutureEvent(5, null, valueQuark, FutureEventType.MODIFICATION);

                    // Initialize a stack and modify it in future
                    int stackQuark = ss.getQuarkAbsoluteAndAdd(futureStack);
                    ss.pushAttribute(event.getTimestamp().toNanos(), stack1, stackQuark);
                    provider.addFutureEvent(4, stack2, stackQuark, FutureEventType.PUSH);
                    provider.addFutureEvent(7, stack2, stackQuark, FutureEventType.POP);
                    provider.addFutureEvent(8, stack1, stackQuark, FutureEventType.POP);
                }
                return true;

            });

            TestStateSystemModule module = fModule;
            module.schedule();
            assertTrue(module.waitForCompletion());

            ITmfStateSystem ss = module.getStateSystem();
            assertNotNull(ss);

            List<@NonNull ITmfStateInterval> expected = ImmutableList.of(new StateIntervalStub(1, 4, oneValue), new StateIntervalStub(5, 10, (Object) null));
            StateSystemTestUtils.testIntervalForAttributes(ss, expected, futureVal);

            expected = ImmutableList.of(new StateIntervalStub(1, 7, stack1), new StateIntervalStub(8, 10, (Object) null));
            StateSystemTestUtils.testIntervalForAttributes(ss, expected, futureStack, "1");

            expected = ImmutableList.of( new StateIntervalStub(1, 3, (Object) null),
                    new StateIntervalStub(4, 6, stack2), new StateIntervalStub(7, 10, (Object) null));
            StateSystemTestUtils.testIntervalForAttributes(ss, expected, futureStack, "2");
        } finally {
            TestStateSystemProvider.setEventHandler(null);
        }
    }
}
