/*
 * Copyright 2006-2022 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */

package de.rcenvironment.components.parametricstudy.execution;

import static org.easymock.EasyMock.anyObject;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.easymock.EasyMock;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import de.rcenvironment.components.parametricstudy.common.ParametricStudyComponentConstants;
import de.rcenvironment.components.parametricstudy.common.ParametricStudyService;
import de.rcenvironment.components.parametricstudy.common.StudyDataset;
import de.rcenvironment.components.parametricstudy.common.StudyPublisher;
import de.rcenvironment.components.parametricstudy.common.StudyStructure;
import de.rcenvironment.core.component.api.ComponentException;
import de.rcenvironment.core.component.api.LoopComponentConstants;
import de.rcenvironment.core.component.execution.api.Component;
import de.rcenvironment.core.component.execution.api.Component.FinalComponentState;
import de.rcenvironment.core.component.testutils.ComponentContextMock;
import de.rcenvironment.core.component.testutils.ComponentTestWrapper;
import de.rcenvironment.core.datamodel.api.DataType;
import de.rcenvironment.core.datamodel.api.EndpointCharacter;
import de.rcenvironment.core.datamodel.api.TypedDatum;
import de.rcenvironment.core.datamodel.api.TypedDatumFactory;
import de.rcenvironment.core.datamodel.api.TypedDatumService;
import de.rcenvironment.core.datamodel.types.api.BooleanTD;
import de.rcenvironment.core.datamodel.types.api.FloatTD;
import de.rcenvironment.core.datamodel.types.api.IntegerTD;
import de.rcenvironment.core.toolkitbridge.transitional.ConcurrencyUtils;

/**
 * Integration test for {@link ParametricStudyComponent}.
 * 
 * @author Oliver Seebach
 * @author Doreen Seider
 * @author Jascha Riedel
 */
public class ParametricStudyComponentTest {

    private static final int CANCEL_TEST_TIMEOUT_MSEC = 1000;

    private static final String ONE_ONE = "1.1";

    private static final String DONE = "Done";

    private static final Double SOME_DOUBLE = 5.0;

    private static final int LARGE_NUMBER = 100000;

    private static final String FIT_STEP_SIZE_TO_BOUNDS = "fitStepSizeToBounds";

    private static final String FIVE = "5";

    private static final String TEN = "10";

    private static final String ONE = "1";

    private static final String TWO = "2";

    private static final String MINUS = "-";

    private static final String RETURN_VALUE = "ReturnValue";

    private static final String RETURN_VALUE_2 = "AnotherReturnValue";

    private static final String STEP_SIZE = "StepSize";

    private static final String TO_VALUE = "ToValue";

    private static final String FROM_VALUE = "FromValue";

    private static final String DESIGN_VARIABLE = "Design variable";

    private static final String N = "n";

    /** Exception rule. */
    @Rule
    public ExpectedException exception = ExpectedException.none();

    /**
     * Context mockup for parametric study component tests.
     * 
     * @author Oliver Seebach
     */
    private final class ParametricStudyComponentContextMock extends ComponentContextMock {

        private static final long serialVersionUID = -6574116384120957764L;

    }

    private ComponentTestWrapper component;

    private ParametricStudyComponentContextMock context;

    private ParametricStudyService parametricStudyServiceMock;

    private TypedDatumFactory typedDatumFactory;

    /**
     * Set up Parametric Study tests.
     * 
     * @throws Exception e
     */
    @Before
    public void setUp() throws Exception {
        context = new ParametricStudyComponentContextMock();
        component = new ComponentTestWrapper(new ParametricStudyComponent(), context);
        typedDatumFactory = context.getService(TypedDatumService.class).getFactory();

        // Create StudyPublisher mock required by ParametricStudyService mock
        StudyPublisher studyPublisherMock = EasyMock.createMock(StudyPublisher.class);
        EasyMock.expect(studyPublisherMock.getStudy()).andReturn(null);
        studyPublisherMock.add(anyObject(StudyDataset.class));
        EasyMock.expectLastCall().anyTimes();
        studyPublisherMock.clearStudy();
        EasyMock.replay(studyPublisherMock);

        // Create ParametricStudyService mock
        parametricStudyServiceMock = EasyMock.createNiceMock(ParametricStudyService.class);
        EasyMock.expect(parametricStudyServiceMock.createPublisher(anyObject(String.class), anyObject(String.class),
            anyObject(StudyStructure.class))).andReturn(studyPublisherMock).anyTimes();
        EasyMock.replay(parametricStudyServiceMock);
        context.addService(ParametricStudyService.class, parametricStudyServiceMock);
    }

    /**
     * Generates meta data for parametric study test.
     * 
     * @param from Start value of parametric study range
     * @param to End value of parametric study range
     * @param stepSize Step size of parametric study range
     * @return
     */
    private Map<String, String> generateParametricStudyMetadata(String from, Boolean useInputAsFrom, String to, Boolean useInputAsTo,
        String stepSize, Boolean useInputAsStepSize, Boolean fitStepSizeToBounds) {
        Map<String, String> metadata = new HashMap<>();
        metadata.put(FROM_VALUE, from);
        metadata.put(ParametricStudyComponentConstants.OUTPUT_METADATA_USE_INPUT_AS_FROM_VALUE, useInputAsFrom.toString());
        metadata.put(TO_VALUE, to);
        metadata.put(ParametricStudyComponentConstants.OUTPUT_METADATA_USE_INPUT_AS_TO_VALUE, useInputAsTo.toString());
        metadata.put(STEP_SIZE, stepSize);
        metadata.put(ParametricStudyComponentConstants.OUTPUT_METADATA_USE_INPUT_AS_STEPSIZE_VALUE, useInputAsStepSize.toString());
        metadata.put(FIT_STEP_SIZE_TO_BOUNDS, fitStepSizeToBounds.toString());
        return metadata;
    }

    /**
     * Test with no input and output from 1-10.
     * 
     * @throws ComponentException ce
     */
    @Test
    public void testNoInputsSimple() throws ComponentException {
        Map<String, String> metadata = generateParametricStudyMetadata(ONE, false, TEN, false, ONE, false, false);
        addSimulatedOutputs(metadata);
        component.start();

        assertEquals(10, context.getCapturedOutput(DESIGN_VARIABLE).size());
        final Double[] expectedValues = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 };
        assertEquals(true, assertListsEqual(context.getCapturedOutput(DESIGN_VARIABLE), expectedValues));

        checkDoneOutputs(true);

        component.tearDownAndDispose(Component.FinalComponentState.FINISHED);
    }

    /**
     * Test with no input and output from 1-10.
     * 
     * @throws ComponentException ce
     */
    @Test
    public void testNoInputsSimpleDescending() throws ComponentException {
        Map<String, String> metadata = generateParametricStudyMetadata(TEN, false, ONE, false, ONE, false, false);
        addSimulatedOutputs(metadata);
        component.start();

        assertEquals(10, context.getCapturedOutput(DESIGN_VARIABLE).size());
        final Double[] expectedValues = { 10.0, 9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0 };
        assertEquals(true, assertListsEqual(context.getCapturedOutput(DESIGN_VARIABLE), expectedValues));

        checkDoneOutputs(true);
        component.tearDownAndDispose(Component.FinalComponentState.FINISHED);
    }

    /**
     * Test with one input and output from 1-5.
     * 
     * @throws ComponentException ce
     */
    @Test
    public void test1InputSimple() throws ComponentException {
        Map<String, String> metadata = generateParametricStudyMetadata(ONE, false, FIVE, false, ONE, false, false);
        addSimulatedOutputs(metadata);
        context.addSimulatedInput(RETURN_VALUE, ParametricStudyComponentConstants.DYNAMIC_INPUT_IDENTIFIER, DataType.Float, true, null);

        component.start();
        checkDoneOutputs(false);

        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(context.getCapturedOutput(DESIGN_VARIABLE).get(0), typedDatumFactory.createFloat(1.0));

        context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(SOME_DOUBLE));
        component.processInputs();
        checkDoneOutputs(false);
        assertEquals(context.getCapturedOutput(DESIGN_VARIABLE).get(0), typedDatumFactory.createFloat(2.0));

        context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(SOME_DOUBLE));
        component.processInputs();
        checkDoneOutputs(false);
        assertEquals(context.getCapturedOutput(DESIGN_VARIABLE).get(0), typedDatumFactory.createFloat(3.0));

        context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(SOME_DOUBLE));
        component.processInputs();
        checkDoneOutputs(false);
        assertEquals(context.getCapturedOutput(DESIGN_VARIABLE).get(0), typedDatumFactory.createFloat(4.0));

        context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(SOME_DOUBLE));
        component.processInputs();
        checkDoneOutputs(false);
        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(context.getCapturedOutput(DESIGN_VARIABLE).get(0), typedDatumFactory.createFloat(5.0));

        context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(SOME_DOUBLE));
        component.processInputs();
        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());

        checkDoneOutputs(true);
        component.tearDownAndDispose(Component.FinalComponentState.FINISHED);
    }

    /**
     * Test with one input and output from 1-5.
     * 
     * @throws ComponentException ce
     */
    @Test
    public void test1InputSimpleDescending() throws ComponentException {
        Map<String, String> metadata = generateParametricStudyMetadata(FIVE, false, ONE, false, ONE, false, false);
        addSimulatedOutputs(metadata);
        context.addSimulatedInput(RETURN_VALUE, ParametricStudyComponentConstants.DYNAMIC_INPUT_IDENTIFIER, DataType.Float, true, null);

        component.start();
        checkDoneOutputs(false);

        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(typedDatumFactory.createFloat(5.0), context.getCapturedOutput(DESIGN_VARIABLE).get(0));

        for (double i = 4; i > 0; i--) {
            context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(SOME_DOUBLE));
            component.processInputs();
            checkDoneOutputs(false);
            assertEquals(typedDatumFactory.createFloat(i), context.getCapturedOutput(DESIGN_VARIABLE).get(0));
        }

        context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(SOME_DOUBLE));
        component.processInputs();
        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());
        checkDoneOutputs(true);

        component.tearDownAndDispose(Component.FinalComponentState.FINISHED);
    }

    /**
     * Test with one input and output from 1-5.
     * 
     * @throws ComponentException ce
     */
    @Test
    public void test2InputsSimple() throws ComponentException {
        Map<String, String> metadata = generateParametricStudyMetadata(ONE, false, FIVE, false, ONE, false, false);
        addSimulatedOutputs(metadata);
        context.addSimulatedInput(RETURN_VALUE, ParametricStudyComponentConstants.DYNAMIC_INPUT_IDENTIFIER, DataType.Float, true, null);
        context.addSimulatedInput(RETURN_VALUE_2, ParametricStudyComponentConstants.DYNAMIC_INPUT_IDENTIFIER, DataType.Float, true, null);

        component.start();
        checkDoneOutputs(false);
        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(context.getCapturedOutput(DESIGN_VARIABLE).get(0), typedDatumFactory.createFloat(1.0));

        for (double i = 2; i <= 5; i++) {
            context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(SOME_DOUBLE));
            context.setInputValue(RETURN_VALUE_2, typedDatumFactory.createFloat(SOME_DOUBLE));
            component.processInputs();
            checkDoneOutputs(false);
            assertEquals(context.getCapturedOutput(DESIGN_VARIABLE).get(0), typedDatumFactory.createFloat(i));
        }

        context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(SOME_DOUBLE));
        context.setInputValue(RETURN_VALUE_2, typedDatumFactory.createFloat(SOME_DOUBLE));
        component.processInputs();
        checkDoneOutputs(true);

        component.tearDownAndDispose(Component.FinalComponentState.FINISHED);
    }

    /**
     * Test with no input and output from 1-10.
     * 
     * @throws ComponentException ce
     */
    @Test
    public void testNoInputsManyIterations() throws ComponentException {
        Map<String, String> metadata = generateParametricStudyMetadata(ONE, false, String.valueOf(LARGE_NUMBER), false, ONE, false, false);
        addSimulatedOutputs(metadata);
        component.start();

        assertEquals(LARGE_NUMBER, context.getCapturedOutput(DESIGN_VARIABLE).size());

        checkDoneOutputs(true);
        component.tearDownAndDispose(Component.FinalComponentState.FINISHED);
    }

    /**
     * Test with one input and output from 1-5.
     * 
     * @throws ComponentException ce
     */
    @Test
    public void test1InputManyIterations() throws ComponentException {
        Map<String, String> metadata = generateParametricStudyMetadata(ONE, false, String.valueOf(LARGE_NUMBER), false, ONE, false, false);
        addSimulatedOutputs(metadata);
        context.addSimulatedInput(RETURN_VALUE, ParametricStudyComponentConstants.DYNAMIC_INPUT_IDENTIFIER, DataType.Float, true, null);

        component.start();
        checkDoneOutputs(false);
        for (int i = 2; i <= LARGE_NUMBER; i++) {
            context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(5.0));
            component.processInputs();
            checkDoneOutputs(false);
            assertEquals(typedDatumFactory.createFloat(i), context.getCapturedOutput(DESIGN_VARIABLE).get(0));
        }

        context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(5.0));
        component.processInputs();
        checkDoneOutputs(true);

        component.tearDownAndDispose(Component.FinalComponentState.FINISHED);
    }

    /**
     * Test with no input and output from 1-10.
     * 
     * @throws ComponentException ce
     */
    @Test
    public void testInvalidStepSize() throws ComponentException {
        Map<String, String> metadata = generateParametricStudyMetadata(TEN, false, ONE, false, "-1.1", false, true);
        context.addSimulatedOutput(DESIGN_VARIABLE, "", DataType.Float, false, metadata);

        exception.expect(ComponentException.class);

        component.start();

        component.tearDownAndDispose(Component.FinalComponentState.FAILED);
    }

    /**
     * Test with one input and output from 1-5.
     * 
     * @throws ComponentException ce
     */
    @Test
    public void test1InputLargeNumberNonIntegerStepWidth() throws ComponentException {
        final double stepWidth = 0.123;
        Map<String, String> metadata = generateParametricStudyMetadata("0", false, String.valueOf(LARGE_NUMBER), false,
            String.valueOf(stepWidth), false, false);
        addSimulatedOutputs(metadata);
        context.addSimulatedInput(RETURN_VALUE, ParametricStudyComponentConstants.DYNAMIC_INPUT_IDENTIFIER, DataType.Float, true, null);

        component.start();

        for (double i = stepWidth; i <= LARGE_NUMBER; i += stepWidth) {
            context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(5.0));
            component.processInputs();
            checkDoneOutputs(false);
            final double delta = 0.1;
            assertEquals(i, ((FloatTD) context.getCapturedOutput(DESIGN_VARIABLE).get(0)).getFloatValue(), delta);
        }

        context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(5.0));
        component.processInputs();
        checkDoneOutputs(true, true, 0);

        component.tearDownAndDispose(Component.FinalComponentState.FINISHED);
    }

    /**
     * Test without input and fit to step width.
     * 
     * @throws ComponentException ce
     */
    @Test
    public void testNoInputsFitToStepWidth() throws ComponentException {
        Map<String, String> metadata = generateParametricStudyMetadata(ONE, false, TEN, false, ONE_ONE, false, true);
        addSimulatedOutputs(metadata);
        component.start();

        // Component takes step size one size bigger to match the given end value
        // I.e. in this case 1.125 instead of 1.1

        assertEquals(9, context.getCapturedOutput(DESIGN_VARIABLE).size());
        final Double[] expectedValues = { 1.0, 2.125, 3.25, 4.375, 5.5, 6.625, 7.75, 8.875, 10.0 };
        assertEquals(true, assertListsEqual(context.getCapturedOutput(DESIGN_VARIABLE), expectedValues));

        component.tearDownAndDispose(Component.FinalComponentState.FINISHED);
    }

    /**
     * Test with 1 input and fit to step width.
     * 
     * @throws ComponentException ce
     */
    @Test
    public void test1InputFitToStepWidth() throws ComponentException {
        Map<String, String> metadata = generateParametricStudyMetadata(ONE, false, TEN, false, ONE_ONE, false, true);
        addSimulatedOutputs(metadata);
        context.addSimulatedInput(RETURN_VALUE, ParametricStudyComponentConstants.DYNAMIC_INPUT_IDENTIFIER, DataType.Float, true, null);
        component.start();

        // Component takes step size one size bigger to match the given end value
        // I.e. in this case 1.125 instead of 1.1

        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(context.getCapturedOutput(DESIGN_VARIABLE).get(0), typedDatumFactory.createFloat(1.0));

        final double stepWidth = 1.125;

        for (double i = (1 + stepWidth); i < 10; i += stepWidth) {
            context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(5.0));
            component.processInputs();
            assertEquals(true, assertListsEqual(context.getCapturedOutput(DESIGN_VARIABLE), i));
        }

        component.tearDownAndDispose(Component.FinalComponentState.FINISHED);
    }

    /**
     * Test with no inputs, fit to step width and descending values.
     * 
     * @throws ComponentException ce
     */
    @Test
    public void testNoInputsFitToStepWidthDescending() throws ComponentException {
        Map<String, String> metadata = generateParametricStudyMetadata(TEN, false, ONE, false, ONE_ONE, false, true);
        addSimulatedOutputs(metadata);
        component.start();

        // Component takes step size one size bigger to match the given end value
        // I.e. in this case -1.125 instead of -1.1

        assertEquals(9, context.getCapturedOutput(DESIGN_VARIABLE).size());
        final Double[] expectedValues = { 10.0, 8.875, 7.75, 6.625, 5.5, 4.375, 3.25, 2.125, 1.0 };
        assertEquals(true, assertListsEqual(context.getCapturedOutput(DESIGN_VARIABLE), expectedValues));

        component.tearDownAndDispose(Component.FinalComponentState.FINISHED);
    }

    /**
     * Tests if values are forwarded as expected.
     * 
     * @throws ComponentException on unexpected errors
     */
    @Test
    public void testForwardingValue() throws ComponentException {
        Map<String, String> metadata = generateParametricStudyMetadata(ONE, false, TWO, false, ONE, false, false);
        addSimulatedOutputs(metadata);
        context.addSimulatedInput(N, LoopComponentConstants.ENDPOINT_ID_TO_FORWARD, DataType.Integer, true, null);
        context.addSimulatedInput(N + LoopComponentConstants.ENDPOINT_STARTVALUE_SUFFIX,
            LoopComponentConstants.ENDPOINT_ID_START_TO_FORWARD, DataType.Integer, true, null, EndpointCharacter.OUTER_LOOP);
        context.addSimulatedOutput(N, LoopComponentConstants.ENDPOINT_ID_TO_FORWARD, DataType.Integer, true, null);

        component.start();
        assertEquals(0, context.getCapturedOutput(N).size());
        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());

        context.setInputValue(N + LoopComponentConstants.ENDPOINT_STARTVALUE_SUFFIX, typedDatumFactory.createInteger(3));
        component.processInputs();

        assertEquals(1, context.getCapturedOutput(N).size());
        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(1.0, ((FloatTD) context.getCapturedOutput(DESIGN_VARIABLE).get(0)).getFloatValue(), 0);
        assertEquals(3, ((IntegerTD) context.getCapturedOutput(N).get(0)).getIntValue());

        context.setInputValue(N, typedDatumFactory.createInteger(7));
        component.processInputs();

        assertEquals(1, context.getCapturedOutput(N).size());
        assertEquals(7, ((IntegerTD) context.getCapturedOutput(N).get(0)).getIntValue());
        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(2.0, ((FloatTD) context.getCapturedOutput(DESIGN_VARIABLE).get(0)).getFloatValue(), 0);

        context.setInputValue(N, typedDatumFactory.createInteger(7));
        component.processInputs();

        assertEquals(0, context.getCapturedOutput(N).size());
        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());

        checkDoneOutputs(true, true, 1);

        component.tearDownAndDispose(Component.FinalComponentState.FINISHED);
    }

    /**
     * Tests if values are forwarded as expected.
     * 
     * @throws ComponentException on unexpected errors
     */
    @Test
    public void testForwardingStartValue() throws ComponentException {
        Map<String, String> metadata = generateParametricStudyMetadata(ONE, false, TWO, false, ONE, false, false);
        addSimulatedOutputs(metadata);
        context.addSimulatedInput(N, LoopComponentConstants.ENDPOINT_ID_TO_FORWARD, DataType.Integer, true, null);
        context.addSimulatedInput(N + LoopComponentConstants.ENDPOINT_STARTVALUE_SUFFIX,
            LoopComponentConstants.ENDPOINT_ID_START_TO_FORWARD, DataType.Integer, true, null, EndpointCharacter.OUTER_LOOP);
        context.addSimulatedOutput(N, LoopComponentConstants.ENDPOINT_ID_TO_FORWARD, DataType.Integer, true, null);
        component.start();

        assertEquals(0, context.getCapturedOutput(N).size());
        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());

        context.setInputValue(N + LoopComponentConstants.ENDPOINT_STARTVALUE_SUFFIX, typedDatumFactory.createInteger(5));
        component.processInputs();

        assertEquals(1, context.getCapturedOutput(N).size());
        assertEquals(5, ((IntegerTD) context.getCapturedOutput(N).get(0)).getIntValue(), 0);
        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(1.0, ((FloatTD) context.getCapturedOutput(DESIGN_VARIABLE).get(0)).getFloatValue(), 0);

        context.setInputValue(N, typedDatumFactory.createInteger(7));
        component.processInputs();

        assertEquals(1, context.getCapturedOutput(N).size());
        assertEquals(7, ((IntegerTD) context.getCapturedOutput(N).get(0)).getIntValue());
        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(2.0, ((FloatTD) context.getCapturedOutput(DESIGN_VARIABLE).get(0)).getFloatValue(), 0);

        context.setInputValue(N, typedDatumFactory.createInteger(7));
        component.processInputs();

        assertEquals(0, context.getCapturedOutput(N).size());
        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());

        checkDoneOutputs(true, true, 1);

        component.tearDownAndDispose(Component.FinalComponentState.FINISHED);
    }

    /**
     * Tests if values are forwarded as expected.
     * 
     * @throws ComponentException on unexpected errors
     */
    @Test
    public void testReset() throws ComponentException {
        context.setConfigurationValue(LoopComponentConstants.CONFIG_KEY_IS_NESTED_LOOP, String.valueOf(true));
        Map<String, String> metadata = generateParametricStudyMetadata(ONE, false, TWO, false, ONE, false, false);
        addSimulatedOutputs(metadata);
        context.addSimulatedInput(N, LoopComponentConstants.ENDPOINT_ID_TO_FORWARD, DataType.Integer, true, null);
        context.addSimulatedInput(N + LoopComponentConstants.ENDPOINT_STARTVALUE_SUFFIX,
            LoopComponentConstants.ENDPOINT_ID_START_TO_FORWARD, DataType.Integer, true, null, EndpointCharacter.OUTER_LOOP);
        context.addSimulatedOutput(N, LoopComponentConstants.ENDPOINT_ID_TO_FORWARD, DataType.Integer, true, null);

        component.start();

        testOneLoopRun();
        testOneLoopRun();

    }

    /**
     * Tests if values are forwarded as expected.
     * 
     * @throws ComponentException on unexpected errors
     */
    @Test
    public void testDataTypes() throws ComponentException {
        context.setConfigurationValue(LoopComponentConstants.CONFIG_KEY_IS_NESTED_LOOP, String.valueOf(true));
        Map<String, String> metadata = generateParametricStudyMetadata(ONE, false, TWO, false, ONE, false, false);
        addSimulatedOutputs(metadata);

        context.addSimulatedInput(RETURN_VALUE, "", DataType.Float, true, new HashMap<String, String>());
        context.addSimulatedInput(RETURN_VALUE_2, "", DataType.Integer, true, new HashMap<String, String>());
        final double value = 5.4;
        context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(value));
        context.setInputValue(RETURN_VALUE_2, typedDatumFactory.createInteger(2));
        component.start();
        component.processInputs();
    }

    /**
     * 
     * Tests the "From Value" Input as simple test without any other Inputs.
     * 
     * @throws ComponentException on unexpected errors
     */
    @Test
    public void testInputFromValueOnlyParameterInputs() throws ComponentException {
        Map<String, String> metaData = generateParametricStudyMetadata(MINUS, true, TEN, false, ONE, false, false);
        addSimulatedOutputs(metaData);

        context.addSimulatedInput(ParametricStudyComponentConstants.INPUT_NAME_FROM_VALUE,
            ParametricStudyComponentConstants.DYNAMIC_INPUT_STUDY_PARAMETERS,
            DataType.Float, true, metaData);

        component.start();

        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());

        context.setInputValue(ParametricStudyComponentConstants.INPUT_NAME_FROM_VALUE, typedDatumFactory.createFloat(1.0));
        component.processInputs();
        assertEquals(10, context.getCapturedOutput(DESIGN_VARIABLE).size());
        final Double[] expectedValues = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 };
        assertEquals(true, assertListsEqual(context.getCapturedOutput(DESIGN_VARIABLE), expectedValues));

        checkDoneOutputs(true);

        component.tearDownAndDispose(FinalComponentState.FINISHED);
    }

    /**
     * 
     * Tests the "To Value" Input as simple test without any other Inputs.
     * 
     * @throws ComponentException on unexpected errors
     */
    @Test
    public void testInputToValueOnlyParameterInputs() throws ComponentException {
        Map<String, String> metaData = generateParametricStudyMetadata(ONE, false, MINUS, true, ONE, false, false);
        addSimulatedOutputs(metaData);

        context.addSimulatedInput(ParametricStudyComponentConstants.INPUT_NAME_TO_VALUE,
            ParametricStudyComponentConstants.DYNAMIC_INPUT_STUDY_PARAMETERS,
            DataType.Float, true, metaData);

        component.start();

        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());

        context.setInputValue(ParametricStudyComponentConstants.INPUT_NAME_TO_VALUE, typedDatumFactory.createFloat(10));
        component.processInputs();
        assertEquals(10, context.getCapturedOutput(DESIGN_VARIABLE).size());
        final Double[] expectedValues = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 };
        assertEquals(true, assertListsEqual(context.getCapturedOutput(DESIGN_VARIABLE), expectedValues));

        checkDoneOutputs(true);

        component.tearDownAndDispose(FinalComponentState.FINISHED);
    }

    /**
     * 
     * Tests the "StepSize Value" Input as simple test without any other Inputs.
     * 
     * @throws ComponentException on unexpected errors
     */
    @Test
    public void testInputStepSizeValueOnlyParameterInputs() throws ComponentException {
        Map<String, String> metaData = generateParametricStudyMetadata(ONE, false, TEN, false, MINUS, true, false);
        addSimulatedOutputs(metaData);

        context.addSimulatedInput(ParametricStudyComponentConstants.INPUT_NAME_STEPSIZE_VALUE,
            ParametricStudyComponentConstants.DYNAMIC_INPUT_STUDY_PARAMETERS,
            DataType.Float, true, metaData);

        component.start();

        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());

        context.setInputValue(ParametricStudyComponentConstants.INPUT_NAME_STEPSIZE_VALUE, typedDatumFactory.createFloat(1));
        component.processInputs();
        assertEquals(10, context.getCapturedOutput(DESIGN_VARIABLE).size());
        final Double[] expectedValues = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 };
        assertEquals(true, assertListsEqual(context.getCapturedOutput(DESIGN_VARIABLE), expectedValues));

        checkDoneOutputs(true);

        component.tearDownAndDispose(FinalComponentState.FINISHED);
    }

    /**
     * 
     * Tests all input parameters in one run as simple test without any other Inputs.
     * 
     * @throws ComponentException on unexpected errors
     */
    @Test
    public void testAllInputParametersOnlyParameterInputs() throws ComponentException {
        Map<String, String> metaData = generateParametricStudyMetadata(MINUS, true, MINUS, true, MINUS, true, false);
        addSimulatedOutputs(metaData);

        context.addSimulatedInput(ParametricStudyComponentConstants.INPUT_NAME_FROM_VALUE,
            ParametricStudyComponentConstants.DYNAMIC_INPUT_STUDY_PARAMETERS,
            DataType.Float, true, metaData);
        context.addSimulatedInput(ParametricStudyComponentConstants.INPUT_NAME_TO_VALUE,
            ParametricStudyComponentConstants.DYNAMIC_INPUT_STUDY_PARAMETERS,
            DataType.Float, true, metaData);
        context.addSimulatedInput(ParametricStudyComponentConstants.INPUT_NAME_STEPSIZE_VALUE,
            ParametricStudyComponentConstants.DYNAMIC_INPUT_STUDY_PARAMETERS,
            DataType.Float, true, metaData);

        component.start();

        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());

        context.setInputValue(ParametricStudyComponentConstants.INPUT_NAME_FROM_VALUE, typedDatumFactory.createFloat(1.0));
        context.setInputValue(ParametricStudyComponentConstants.INPUT_NAME_TO_VALUE, typedDatumFactory.createFloat(10));
        context.setInputValue(ParametricStudyComponentConstants.INPUT_NAME_STEPSIZE_VALUE, typedDatumFactory.createFloat(1));
        component.processInputs();
        assertEquals(10, context.getCapturedOutput(DESIGN_VARIABLE).size());
        final Double[] expectedValues = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0 };
        assertEquals(true, assertListsEqual(context.getCapturedOutput(DESIGN_VARIABLE), expectedValues));

        checkDoneOutputs(true);

        component.tearDownAndDispose(FinalComponentState.FINISHED);
    }

    /**
     * 
     * Test Input Parameter From Value with "waiting for evaluation result".
     * 
     * @throws ComponentException on unexpected errors.
     */
    @Test
    public void testInputParameterWithEvaluationResult() throws ComponentException {
        Map<String, String> metaData = generateParametricStudyMetadata(MINUS, true, TWO, false, ONE, false, false);
        addSimulatedOutputs(metaData);

        context.addSimulatedInput(RETURN_VALUE, ParametricStudyComponentConstants.DYNAMIC_INPUT_IDENTIFIER, DataType.Float, true, null);
        context.addSimulatedInput(ParametricStudyComponentConstants.INPUT_NAME_FROM_VALUE,
            ParametricStudyComponentConstants.DYNAMIC_INPUT_STUDY_PARAMETERS,
            DataType.Float, true, metaData);

        component.start();

        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());
        context.setInputValue(ParametricStudyComponentConstants.INPUT_NAME_FROM_VALUE, typedDatumFactory.createFloat(1));

        component.processInputs();

        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(1.0, ((FloatTD) context.getCapturedOutput(DESIGN_VARIABLE).get(0)).getFloatValue(), 0);
        context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(5.0));

        component.processInputs();
        context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(5.0));
        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(2.0, ((FloatTD) context.getCapturedOutput(DESIGN_VARIABLE).get(0)).getFloatValue(), 0);

        component.processInputs(); // processes last evaluation result

        checkDoneOutputs(true);

        component.tearDownAndDispose(FinalComponentState.FINISHED);
    }

    /**
     * 
     * Test one of the parameter inputs in combination with a forwarding endpoint.
     * 
     * @throws ComponentException on unexpected error.
     */
    @Test
    public void testInputParameterWithForwardEndpoint() throws ComponentException {
        Map<String, String> metaData = generateParametricStudyMetadata(MINUS, true, TWO, false, ONE, false, false);
        addSimulatedOutputs(metaData);

        context.addSimulatedInput(ParametricStudyComponentConstants.INPUT_NAME_FROM_VALUE,
            ParametricStudyComponentConstants.DYNAMIC_INPUT_STUDY_PARAMETERS,
            DataType.Float, true, metaData);
        context.addSimulatedInput(N, LoopComponentConstants.ENDPOINT_ID_TO_FORWARD, DataType.Float, true, null);
        context.addSimulatedInput(N + LoopComponentConstants.ENDPOINT_STARTVALUE_SUFFIX,
            LoopComponentConstants.ENDPOINT_ID_START_TO_FORWARD, DataType.Float, true, null, EndpointCharacter.OUTER_LOOP);
        context.addSimulatedOutput(N, LoopComponentConstants.ENDPOINT_ID_TO_FORWARD, DataType.Float, true, null);

        component.start();

        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(0, context.getCapturedOutput(N).size());

        context.setInputValue(ParametricStudyComponentConstants.INPUT_NAME_FROM_VALUE, typedDatumFactory.createFloat(1.0));
        context.setInputValue(N + LoopComponentConstants.ENDPOINT_STARTVALUE_SUFFIX, typedDatumFactory.createFloat(3.0));

        component.processInputs();

        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(1.0, ((FloatTD) context.getCapturedOutput(DESIGN_VARIABLE).get(0)).getFloatValue(), 0);
        assertEquals(1, context.getCapturedOutput(N).size());
        assertEquals(3.0, ((FloatTD) context.getCapturedOutput(N).get(0)).getFloatValue(), 0);

        context.setInputValue(N, typedDatumFactory.createFloat(4.0));

        component.processInputs();

        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(2.0, ((FloatTD) context.getCapturedOutput(DESIGN_VARIABLE).get(0)).getFloatValue(), 0);
        assertEquals(1, context.getCapturedOutput(N).size());
        assertEquals(4.0, ((FloatTD) context.getCapturedOutput(N).get(0)).getFloatValue(), 0);

        context.setInputValue(N, typedDatumFactory.createFloat(5.0));

        component.processInputs();

        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(0, context.getCapturedOutput(N).size());

        checkDoneOutputs(true);

        component.tearDownAndDispose(FinalComponentState.FINISHED);
    }

    /**
     * 
     * Test if input parameter originates from a driving component (i. e. nested loop)
     * 
     * @throws ComponentException on unexpected error.
     */
    @Test
    public void testInputParameterInNestedLoop() throws ComponentException {
        context.setConfigurationValue(LoopComponentConstants.CONFIG_KEY_IS_NESTED_LOOP, String.valueOf(true));
        Map<String, String> metaData = generateParametricStudyMetadata(MINUS, true, TWO, false, ONE, false, false);
        addSimulatedOutputs(metaData);

        context.addSimulatedInput(ParametricStudyComponentConstants.INPUT_NAME_FROM_VALUE,
            ParametricStudyComponentConstants.DYNAMIC_INPUT_STUDY_PARAMETERS,
            DataType.Float, true, metaData);
        context.addSimulatedInput(RETURN_VALUE, ParametricStudyComponentConstants.DYNAMIC_INPUT_IDENTIFIER, DataType.Float, true, null);

        component.start();

        // context.setInputValue(LoopComponentConstants.ENDPOINT_NAME_OUTERLOOP_DONE, typedDatumFactory.createBoolean(false));

        // first run
        context.setInputValue(ParametricStudyComponentConstants.INPUT_NAME_FROM_VALUE, typedDatumFactory.createFloat(1.0));
        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());
        component.processInputs();

        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(1.0, ((FloatTD) context.getCapturedOutput(DESIGN_VARIABLE).get(0)).getFloatValue(), 0);
        checkDoneOutputs(false);

        context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(5.0));
        component.processInputs();
        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(2.0, ((FloatTD) context.getCapturedOutput(DESIGN_VARIABLE).get(0)).getFloatValue(), 0);

        context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(5.0));
        component.processInputs();
        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());
        component.reset();
        checkDoneOutputs(true);

        // second run
        context.setInputValue(ParametricStudyComponentConstants.INPUT_NAME_FROM_VALUE, typedDatumFactory.createFloat(2.0));

        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());

        component.processInputs();

        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(2.0, ((FloatTD) context.getCapturedOutput(DESIGN_VARIABLE).get(0)).getFloatValue(), 0);

        context.setInputValue(RETURN_VALUE, typedDatumFactory.createFloat(5.0));
        component.processInputs();
        component.reset();

        checkDoneOutputs(true);

        component.tearDownAndDispose(FinalComponentState.FINISHED);
    }

    /**
     * Test if the components cancels sending new design variables in case the component run (start) was canceled.
     * 
     * @throws ComponentException on unexpected error.
     * @throws InterruptedException on test interruption
     */
    @Test(timeout = CANCEL_TEST_TIMEOUT_MSEC)
    public void testCancelStart() throws ComponentException, InterruptedException {
        final int maximumOutputCount = 1000 * 1000; // how many outputs the component should generate unless cancelled

        final int waitBeforeCancellation = 100;
        final int waitBeforeTesting = 200;

        Map<String, String> metadata =
            generateParametricStudyMetadata(ONE, false, Integer.toString(maximumOutputCount), false, ONE, false, false);
        addSimulatedOutputs(metadata);

        ConcurrencyUtils.getAsyncTaskService().execute("Cancel the tested component after a short wait", () -> {

            try {
                Thread.sleep(waitBeforeCancellation);
            } catch (InterruptedException e) {
                return; // test timeout will apply as onStartInterrupted is not called
            }
            component.onStartInterrupted(null);
        });

        component.start();
        Thread.sleep(waitBeforeTesting);

        int actualOutputCount = context.getCapturedOutput(DESIGN_VARIABLE).size();
        assertTrue("The component did not produce any output at all", actualOutputCount > 0);
        assertTrue("The component produced the configured maximum number of outputs - "
            + "either the simulated cancellation was too slow, or cancellation is actually broken", actualOutputCount < maximumOutputCount);

        checkDoneOutputs(true);

        component.tearDownAndDispose(Component.FinalComponentState.FINISHED);
    }

    private void testOneLoopRun() throws ComponentException {
        assertEquals(0, context.getCapturedOutput(N).size());
        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());

        context.setInputValue(N + LoopComponentConstants.ENDPOINT_STARTVALUE_SUFFIX, typedDatumFactory.createInteger(5));
        component.processInputs();
        assertEquals(1, context.getCapturedOutput(N).size());
        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(1, ((FloatTD) context.getCapturedOutput(DESIGN_VARIABLE).get(0)).getFloatValue(), 0);
        assertEquals(0, context.getCapturedOutput(LoopComponentConstants.ENDPOINT_NAME_LOOP_DONE).size());

        context.setInputValue(N, typedDatumFactory.createInteger(7));
        component.processInputs();
        assertEquals(1, context.getCapturedOutput(N).size());
        assertEquals(1, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(2, ((FloatTD) context.getCapturedOutput(DESIGN_VARIABLE).get(0)).getFloatValue(), 0);
        assertEquals(0, context.getCapturedOutput(LoopComponentConstants.ENDPOINT_NAME_LOOP_DONE).size());

        context.setInputValue(N, typedDatumFactory.createInteger(9));
        component.processInputs();
        assertEquals(0, context.getCapturedOutput(N).size());
        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(0, context.getCapturedOutput(LoopComponentConstants.ENDPOINT_NAME_LOOP_DONE).size());

        assertTrue(context.isResetOutputsCalled());
        component.reset();
        assertEquals(0, context.getCapturedOutput(N).size());
        assertEquals(0, context.getCapturedOutput(DESIGN_VARIABLE).size());
        assertEquals(1, context.getCapturedOutput(LoopComponentConstants.ENDPOINT_NAME_LOOP_DONE).size());
    }

    private void addSimulatedOutputs(Map<String, String> metadata) {
        context.addSimulatedOutput(DESIGN_VARIABLE, "", DataType.Float, false, metadata);
        context.addSimulatedOutput(LoopComponentConstants.ENDPOINT_NAME_LOOP_DONE, "", DataType.Boolean, false, null,
            EndpointCharacter.OUTER_LOOP);
    }

    private void checkDoneOutputs(boolean done) {
        checkDoneOutputs(done, false, 0);
    }

    private void checkDoneOutputs(boolean done, boolean outputsClosed, int dynOutputCount) {
        int countLoopDone;
        if (done) {
            countLoopDone = 1;
        } else {
            countLoopDone = 0;
        }
        assertEquals(countLoopDone, context.getCapturedOutput(DONE).size());
        if (done) {
            assertTrue(((BooleanTD) context.getCapturedOutput(DONE).get(0)).getBooleanValue());
            if (outputsClosed) {
                assertEquals(2 + dynOutputCount, context.getCapturedOutputClosings().size());
            }
        }
    }

    /**
     * Helper method that checks lists for equality.
     * 
     * @param listToCheck List of typed datums to be checked.
     * @param values Values to be compared with the list.
     * @return Whether the values are the same or not.
     */
    private boolean assertListsEqual(List<TypedDatum> listToCheck, Double... values) {
        List<Double> valuesToCheck = Arrays.asList(values);
        if (valuesToCheck.size() != listToCheck.size()) {
            return false;
        } else {
            for (int i = 0; i < valuesToCheck.size(); i++) {
                if (valuesToCheck.get(i) != ((FloatTD) listToCheck.get(i)).getFloatValue()) {
                    return false;
                }
            }
            return true;
        }
    }

}
