/*******************************************************************************
 * Copyright (c) 2013, 2014 É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
 *
 * Contributors:
 *   Geneviève Bastien - Initial API and implementation
 *******************************************************************************/

package org.eclipse.tracecompass.tmf.core.tests.analysis;

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.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.osgi.util.NLS;
import org.eclipse.tracecompass.tmf.core.analysis.IAnalysisModule;
import org.eclipse.tracecompass.tmf.core.analysis.Messages;
import org.eclipse.tracecompass.tmf.core.analysis.TmfAbstractAnalysisModule;
import org.eclipse.tracecompass.tmf.core.exceptions.TmfAnalysisException;
import org.eclipse.tracecompass.tmf.core.tests.shared.TmfTestHelper;
import org.eclipse.tracecompass.tmf.core.tests.shared.TmfTestTrace;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.tests.stubs.analysis.TestAnalysis;
import org.eclipse.tracecompass.tmf.tests.stubs.analysis.TestAnalysis2;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;

import com.google.common.collect.ImmutableSet;

/**
 * Test suite for the {@link TmfAbstractAnalysisModule} class
 */
public class AnalysisModuleTest {

    /** Test timeout */
    @Rule
    public TestRule timeoutRule = new Timeout(1, TimeUnit.MINUTES);

    private static final @NonNull String MODULE_GENERIC_ID = "test.id";
    private static final @NonNull String MODULE_GENERIC_NAME = "Test analysis";

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

    /**
     * Test suite for analysis module getters and setters
     */
    @Test
    public void testGettersSetters() {
        IAnalysisModule module = new TestAnalysis();

        module.setName(MODULE_GENERIC_NAME);
        module.setId(MODULE_GENERIC_ID);
        assertEquals(MODULE_GENERIC_ID, module.getId());
        assertEquals(MODULE_GENERIC_NAME, module.getName());

        module.setAutomatic(false);
        assertFalse(module.isAutomatic());
        module.setAutomatic(true);
        assertTrue(module.isAutomatic());
        module.addParameter(TestAnalysis.PARAM_TEST);
        assertNull(module.getParameter(TestAnalysis.PARAM_TEST));
        module.setParameter(TestAnalysis.PARAM_TEST, 1);
        assertEquals(1, module.getParameter(TestAnalysis.PARAM_TEST));

        assertEquals(0, module.getDependencyLevel());

        /* Try to set and get wrong parameter */
        String wrongParam = "abc";
        Exception exception = null;
        try {
            module.setParameter(wrongParam, 1);
        } catch (RuntimeException e) {
            exception = e;
            assertEquals(NLS.bind(Messages.TmfAbstractAnalysisModule_InvalidParameter, wrongParam, module.getName()), e.getMessage());
        }
        assertNotNull(exception);
        assertNull(module.getParameter(wrongParam));

        module.dispose();
    }

    private static TestAnalysis setUpAnalysis() {
        TestAnalysis module = new TestAnalysis();

        module.setName(MODULE_GENERIC_NAME);
        module.setId(MODULE_GENERIC_ID);
        module.addParameter(TestAnalysis.PARAM_TEST);

        return module;
    }

    /**
     * Test suite for analysis module
     * {@link TmfAbstractAnalysisModule#waitForCompletion} with successful
     * execution
     */
    @Test
    public void testWaitForCompletionSuccess() {
        TestAnalysis module = setUpAnalysis();

        IStatus status = module.schedule();
        assertEquals(IStatus.ERROR, status.getSeverity());

        /* Set a stub trace for analysis */
        try {
            assertTrue(module.setTrace(TmfTestTrace.A_TEST_10K.getTrace()));
        } catch (TmfAnalysisException e) {
            fail(e.getMessage());
        }

        /* Default execution, with output 1 */
        module.setParameter(TestAnalysis.PARAM_TEST, 1);
        status = module.schedule();
        assertEquals(Status.OK_STATUS, status);
        boolean completed = module.waitForCompletion();

        assertTrue(completed);
        assertEquals(1, module.getAnalysisOutput());

        module.dispose();
    }

    /**
     * Test suite for {@link TmfAbstractAnalysisModule#waitForCompletion} with
     * cancellation
     */
    @Test
    public void testWaitForCompletionCancelled() {
        TestAnalysis module = setUpAnalysis();
        try {
            /* Set a stub trace for analysis */
            ITmfTrace trace = TmfTestTrace.A_TEST_10K.getTrace();
            try {
                assertTrue(module.setTrace(trace));
            } catch (TmfAnalysisException e) {
                fail(e.getMessage());
            }

            module.setParameter(TestAnalysis.PARAM_TEST, 0);
            IStatus status = module.schedule();
            assertEquals(Status.OK_STATUS, status);
            boolean completed = module.waitForCompletion();

            assertFalse(completed);
            assertEquals(0, module.getAnalysisOutput());
        } finally {
            module.dispose();
        }
    }

    /**
     * Test the {@link TmfAbstractAnalysisModule#setTrace(ITmfTrace)} method
     * with wrong trace
     */
    @Test
    public void testSetWrongTrace() {
        IAnalysisModule module = new TestAnalysis2();

        module.setName(MODULE_GENERIC_NAME);
        module.setId(MODULE_GENERIC_ID);
        assertEquals(MODULE_GENERIC_ID, module.getId());
        assertEquals(MODULE_GENERIC_NAME, module.getName());

        try {
            assertFalse(module.setTrace(TmfTestTrace.A_TEST_10K.getTrace()));
        } catch (TmfAnalysisException e) {
            fail();
        }

        module.dispose();
    }

    /**
     * Test the {@link TmfAbstractAnalysisModule#setTrace(ITmfTrace)} method
     * with wrong trace
     */
    @Test
    public void testSetTraceTwice() {
        IAnalysisModule module = new TestAnalysis();

        module.setName(MODULE_GENERIC_NAME);
        module.setId(MODULE_GENERIC_ID);

        try {
            assertTrue(module.setTrace(TmfTestTrace.A_TEST_10K.getTrace()));
        } catch (TmfAnalysisException e) {
            fail();
        }
        TmfAnalysisException exception = null;
        try {
            module.setTrace(TmfTestTrace.A_TEST_10K.getTrace());
        } catch (TmfAnalysisException e) {
            exception = e;
        }
        assertNotNull(exception);

        module.dispose();
    }

    /**
     * Test suite for the {@link TmfAbstractAnalysisModule#cancel()} method
     */
    @Test
    public void testCancel() {
        TestAnalysis module = setUpAnalysis();

        module.setParameter(TestAnalysis.PARAM_TEST, 999);
        try {
            assertTrue(module.setTrace(TmfTestTrace.A_TEST_10K.getTrace()));
        } catch (TmfAnalysisException e) {
            fail(e.getMessage());
        }

        IStatus schedule = module.schedule();
        assertEquals(Status.OK_STATUS, schedule);

        /* Give the job a chance to start */
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            fail(e.getMessage());
        }

        module.cancel();
        assertFalse(module.waitForCompletion());
        assertEquals(-1, module.getAnalysisOutput());

        module.dispose();
    }

    /**
     * Test suite for the {@link IAnalysisModule#notifyParameterChanged(String)}
     * method
     */
    @Test
    public void testParameterChanged() {
        TestAnalysis module = setUpAnalysis();

        try {
            assertTrue(module.setTrace(TmfTestTrace.A_TEST_10K.getTrace()));
        } catch (TmfAnalysisException e) {
            fail(e.getMessage());
        }

        /* Check exception if no wrong parameter name */
        Exception exception = null;
        try {
            module.notifyParameterChanged("aaa");
        } catch (RuntimeException e) {
            exception = e;
        }
        assertNotNull(exception);

        /*
         * Cannot test anymore of this method, need a parameter provider to do
         * this
         */
        module.dispose();
    }

    /**
     * Test the {@link TmfTestHelper#executeAnalysis(IAnalysisModule)} method
     */
    @Test
    public void testHelper() {
        TestAnalysis module = setUpAnalysis();

        try {
            assertTrue(module.setTrace(TmfTestTrace.A_TEST_10K.getTrace()));
        } catch (TmfAnalysisException e) {
            fail(e.getMessage());
        }

        module.setParameter(TestAnalysis.PARAM_TEST, 1);
        boolean res = TmfTestHelper.executeAnalysis(module);
        assertTrue(res);

        module.setParameter(TestAnalysis.PARAM_TEST, 0);
        res = TmfTestHelper.executeAnalysis(module);
        assertFalse(res);

        module.dispose();
    }

    /**
     * Test the {@link TmfAbstractAnalysisModule} also executes the dependent
     * analyses
     */
    @Test
    public void testDependentAnalyses() {

        ITmfTrace trace = TmfTestTrace.A_TEST_10K.getTrace();
        int paramAndResult = 5;

        /* Setup the dependent module */
        final String suffix = " dep";
        final TestAnalysis depModule = new TestAnalysis() {

            @Override
            protected boolean executeAnalysis(IProgressMonitor monitor) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    return false;
                }
                return super.executeAnalysis(monitor);
            }

        };
        depModule.setName(MODULE_GENERIC_NAME + suffix);
        depModule.setId(MODULE_GENERIC_ID + suffix);
        depModule.addParameter(TestAnalysis.PARAM_TEST);
        depModule.setParameter(TestAnalysis.PARAM_TEST, paramAndResult);

        /* Prepare the main analysis with a dependent analysis */
        TestAnalysis module = new TestAnalysis() {

            @Override
            protected Iterable<IAnalysisModule> getDependentAnalyses() {
                Set<IAnalysisModule> modules = new HashSet<>();
                modules.add(depModule);
                return modules;
            }

        };

        module.setName(MODULE_GENERIC_NAME);
        module.setId(MODULE_GENERIC_ID);
        module.addParameter(TestAnalysis.PARAM_TEST);
        module.setParameter(TestAnalysis.PARAM_TEST, paramAndResult);

        try {
            assertTrue(depModule.setTrace(trace));
            assertTrue(module.setTrace(trace));
        } catch (TmfAnalysisException e) {
            fail(e.getMessage());
        }

        /* Verify none of the module has run */
        assertEquals(0, module.getAnalysisOutput());
        assertEquals(0, depModule.getAnalysisOutput());

        module.schedule();
        assertTrue(module.waitForCompletion());
        assertEquals(paramAndResult, module.getAnalysisOutput());

        /* Make sure the dependent analysis has run and completed */
        assertEquals(paramAndResult, depModule.getAnalysisOutput());

        /* Check the dependency level of both analyses */
        assertEquals(0, depModule.getDependencyLevel());
        assertEquals(1, module.getDependencyLevel());

        module.dispose();
        depModule.dispose();
        trace.dispose();

    }

    /**
     * Test that the dependency level is consistent with a case where
     * B depends on A, and C depends on A and B
     */
    @Test
    public void testMultipleDependencies() {

        ITmfTrace trace = TmfTestTrace.A_TEST_10K.getTrace();

        /* Prepare module A with no dependency */
        IAnalysisModule moduleA = new TestAnalysis();
        moduleA.setName(MODULE_GENERIC_NAME);
        moduleA.setId(MODULE_GENERIC_ID);
        moduleA.addParameter(TestAnalysis.PARAM_TEST);
        moduleA.setParameter(TestAnalysis.PARAM_TEST, 1);

        /* Prepare module B depending on A */
        String suffix = " B";
        IAnalysisModule moduleB = new TestAnalysis() {

            @Override
            protected Iterable<IAnalysisModule> getDependentAnalyses() {
                return ImmutableSet.of(moduleA);
            }

        };
        moduleB.setName(MODULE_GENERIC_NAME + suffix);
        moduleB.setId(MODULE_GENERIC_ID + suffix);
        moduleB.addParameter(TestAnalysis.PARAM_TEST);
        moduleB.setParameter(TestAnalysis.PARAM_TEST, 1);

        /* Prepare module C depending on A and B */
        suffix = " C";
        IAnalysisModule moduleC = new TestAnalysis() {

            @Override
            protected Iterable<IAnalysisModule> getDependentAnalyses() {
                return ImmutableSet.of(moduleA, moduleB);
            }

        };
        moduleC.setName(MODULE_GENERIC_NAME + suffix);
        moduleC.setId(MODULE_GENERIC_ID + suffix);
        moduleC.addParameter(TestAnalysis.PARAM_TEST);
        moduleC.setParameter(TestAnalysis.PARAM_TEST, 1);

        try {
            assertTrue(moduleA.setTrace(trace));
            assertTrue(moduleB.setTrace(trace));
            assertTrue(moduleC.setTrace(trace));
        } catch (TmfAnalysisException e) {
            fail(e.getMessage());
        }

        moduleC.schedule();
        assertTrue(moduleC.waitForCompletion());

        /* Check the dependency level of the analyses */
        assertEquals(0, moduleA.getDependencyLevel());
        assertEquals(1, moduleB.getDependencyLevel());
        assertEquals(3, moduleC.getDependencyLevel());

        moduleA.dispose();
        moduleB.dispose();
        moduleC.dispose();
        trace.dispose();

    }
}
