/*******************************************************************************
 * Copyright (c) 2016 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
 *******************************************************************************/

package org.eclipse.tracecompass.analysis.os.linux.ui.tests.view.controlflow;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.function.Function;

import org.eclipse.tracecompass.internal.analysis.os.linux.core.threadstatus.ThreadEntryModel;
import org.eclipse.tracecompass.internal.analysis.os.linux.ui.views.controlflow.NaiveOptimizationAlgorithm;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ILinkEvent;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.TimeGraphEntry;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.TimeLinkEvent;
import org.junit.Test;

import com.google.common.collect.ImmutableMap;

/**
 * Series of tests to test the <strong>Optimize</strong> function of the control
 * flow view.
 * <p>
 * The <strong>Optimize</strong> function is a meta-heuristic function and thus
 * is not guaranteed to always give the perfect result, however, this method
 * should always give a better result than before. Moreover, the result should
 * be the same for the same dataset.
 *
 * @author Matthew Khouzam
 */
public class ControlFlowOptimizerTest {

    /**
     * Overridable method to get the optimization method to test.
     *
     * @return the optimization method to test.
     */
    protected Function<Collection<ILinkEvent>, Map<Integer, Long>> getOptimizationMethod() {
        return new NaiveOptimizationAlgorithm();
    }

    /**
     * Test an empty vector.
     */
    @Test
    public void testEmpty() {
        Function<Collection<ILinkEvent>, Map<Integer, Long>> oa = getOptimizationMethod();
        Map<Integer, Long> result = oa.apply(Collections.emptyList());
        assertNotNull(result);
        assertEquals(Collections.emptyMap(), result);
    }

    /**
     * Test a single invalid link.
     */
    @Test
    public void testSingleInvalid() {
        Function<Collection<ILinkEvent>, Map<Integer, Long>> oa = getOptimizationMethod();
        Map<Integer, Long> result = oa.apply(Collections.singleton(new TimeLinkEvent(null, null, 0, 0)));
        assertNotNull(result);
        assertEquals(Collections.emptyMap(), result);
    }

    /**
     * Test a single partially invalid link.
     */
    public void testSinglePartiallyInvalid() {
        Function<Collection<ILinkEvent>, Map<Integer, Long>> oa = getOptimizationMethod();
        Map<Integer, Long> result = oa.apply(Collections.singleton(new TimeLinkEvent(new TimeGraphEntry("Hi", 0, 1), generateCFVEntry(0), 0, 0)));
        assertNotNull(result);
        assertEquals(Collections.emptyMap(), result);
        result = oa.apply(Collections.singleton(new TimeLinkEvent(generateCFVEntry(0), new TimeGraphEntry("Hi", 0, 1), 0, 0)));
        assertNotNull(result);
        assertEquals(Collections.emptyMap(), result);
        result = oa.apply(Collections.singleton(new TimeLinkEvent(generateCFVEntry(0), null, 0, 0)));
        assertNotNull(result);
        assertEquals(Collections.emptyMap(), result);
    }

    /**
     * Test a single valid link.
     */
    @Test
    public void testSingle() {
        Function<Collection<ILinkEvent>, Map<Integer, Long>> oa = getOptimizationMethod();
        Map<Integer, Long> result = oa.apply(Collections.singleton(new TimeLinkEvent(generateCFVEntry(2), generateCFVEntry(0), 0, 0)));
        assertNotNull(result);
        assertEquals(ImmutableMap.of(0, 0L, 2, 1L), result);
    }

    /**
     * Test two valid links.
     */
    @Test
    public void testDouble() {
        Function<Collection<ILinkEvent>, Map<Integer, Long>> oa = getOptimizationMethod();

        List<ILinkEvent> links = new ArrayList<>();
        links.add(new TimeLinkEvent(generateCFVEntry(2), generateCFVEntry(0), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(1), generateCFVEntry(0), 0, 0));

        Map<Integer, Long> result = oa.apply(links);
        assertNotNull(result);
        assertEquals(ImmutableMap.of(0, 0L, 1, 1L, 2, 2L), result);
    }

    /**
     * Test the same links as {@link #testDouble()} but in a different order.
     */
    @Test
    public void testDoubleOtherOrder() {
        Function<Collection<ILinkEvent>, Map<Integer, Long>> oa = getOptimizationMethod();

        List<ILinkEvent> links = new ArrayList<>();
        links.add(new TimeLinkEvent(generateCFVEntry(1), generateCFVEntry(0), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(2), generateCFVEntry(0), 0, 0));

        Map<Integer, Long> result = oa.apply(links);
        assertNotNull(result);
        assertEquals(ImmutableMap.of(0, 0L, 1, 1L, 2, 2L), result);
    }

    /**
     * Test a pre-sorted small list. The results should be the same.
     */
    @Test
    public void test9InOrder() {

        Map<Integer, Long> expected = getExpected9();

        Function<Collection<ILinkEvent>, Map<Integer, Long>> oa = getOptimizationMethod();

        List<ILinkEvent> links = getTestVector();

        Map<Integer, Long> result = oa.apply(links);
        assertNotNull(result);
        assertEquals(expected, result);
    }

    /**
     * Test a pre-sorted small list several times. The results should be the
     * same. This tests users clicking optimize many times hoping it would
     * optimize harder.
     */
    @Test
    public void test9InOrder10Times() {
        Map<Integer, Long> expected = getExpected9();

        Function<Collection<ILinkEvent>, Map<Integer, Long>> oa = getOptimizationMethod();

        List<ILinkEvent> links = getTestVector();

        List<Map<Integer, Long>> results = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            results.add(oa.apply(links));
        }
        for (Map<Integer, Long> result : results) {
            assertEquals(expected, result);
        }
    }

    /**
     * A typical use case, the links are out of order, this makes sure they
     * still are
     */
    @Test
    public void test9OutOfOrder() {
        Map<Integer, Long> expected = getExpected9();

        Random rnd = new Random();
        rnd.setSeed(0);

        Function<Collection<ILinkEvent>, Map<Integer, Long>> oa = getOptimizationMethod();

        List<ILinkEvent> links = getTestVector();

        Collections.shuffle(links, rnd);

        Map<Integer, Long> result = oa.apply(links);
        assertNotNull(result);
        assertEquals(expected, result);
    }

    /**
     * This is a smoke test, it makes sure that the algorithm scales moderately
     * well. The typical dataset is about 100-500 elements to sort, so we add a
     * factor of safety here.
     */
    @Test
    public void testMany() {
        Random rnd = new Random();
        rnd.setSeed(12345); // The same combination as my luggage

        Function<Collection<ILinkEvent>, Map<Integer, Long>> oa = getOptimizationMethod();

        List<ILinkEvent> links = new ArrayList<>();
        int count = 25000;
        for (int i = 0; i < count; i++) {
            int src = rnd.nextInt(10000);
            int dst = rnd.nextInt(1000);
            links.add(new TimeLinkEvent(generateCFVEntry(src), generateCFVEntry(dst), src, dst));
        }

        Map<Integer, Long> result = oa.apply(links);
        assertNotNull(result);
        // calculate weight
        long initialWeight = 0;
        long optimalWeight = 0;
        for (ILinkEvent link : links) {
            long src = link.getTime();
            long dst = link.getDuration();
            initialWeight += Math.abs(src - dst);
            Long newSrc = result.get((int) src);
            Long newDst = result.get((int) dst);
            assertNotNull(newSrc);
            assertNotNull(newDst);
            optimalWeight += Math.abs(newSrc - newDst);
        }
        assertTrue(optimalWeight <= initialWeight);
    }

    /**
     * Helper function to make entries to test. As they are only looking up the
     * tid, that is the only parameter that needs to be passed.
     *
     * @param tid
     *            the TID to lookup
     * @return a {@link ControlFlowEntry} with the correct tid.
     */
    private static TimeGraphEntry generateCFVEntry(int tid) {
        return new TimeGraphEntry(new ThreadEntryModel(0, -1, Collections.singletonList("exec"), Long.MIN_VALUE, Long.MAX_VALUE, tid, 0, -1));
    }

    // -------------------------------------------------------------------------
    // Small test vector
    // -------------------------------------------------------------------------

    private static List<ILinkEvent> getTestVector() {
        List<ILinkEvent> links = new ArrayList<>();
        links.add(new TimeLinkEvent(generateCFVEntry(1), generateCFVEntry(2), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(1), generateCFVEntry(2), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(1), generateCFVEntry(2), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(1), generateCFVEntry(2), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(1), generateCFVEntry(3), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(1), generateCFVEntry(3), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(1), generateCFVEntry(3), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(1), generateCFVEntry(4), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(1), generateCFVEntry(4), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(1), generateCFVEntry(5), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(2), generateCFVEntry(4), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(2), generateCFVEntry(5), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(2), generateCFVEntry(6), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(2), generateCFVEntry(7), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(2), generateCFVEntry(8), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(2), generateCFVEntry(9), 0, 0));
        links.add(new TimeLinkEvent(generateCFVEntry(2), generateCFVEntry(10), 0, 0));
        return links;
    }

    private static Map<Integer, Long> getExpected9() {
        Map<Integer, Long> expected = new HashMap<>();
        for (int i = 0; i < 10; i++) {
            expected.put(i + 1, (long) i);
        }
        return expected;
    }

}
