/*
 * Copyright 2020-2023 DLR, Germany
 * 
 * SPDX-License-Identifier: EPL-1.0
 * 
 * https://rcenvironment.de/
 */
package de.rcenvironment.core.component.integration.workflow.command;

import static org.junit.Assert.assertEquals;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import org.junit.Before;
import org.junit.Test;

import de.rcenvironment.core.command.common.CommandException;
import de.rcenvironment.core.command.spi.AbstractCommandParameter;
import de.rcenvironment.core.command.spi.CommandFlag;
import de.rcenvironment.core.command.spi.MainCommandDescription;
import de.rcenvironment.core.command.spi.NamedParameter;
import de.rcenvironment.core.component.api.ComponentConstants;
import de.rcenvironment.core.component.model.endpoint.api.EndpointDefinition.InputDatumHandling;
import de.rcenvironment.core.component.model.endpoint.api.EndpointDefinition.InputExecutionContraint;
import de.rcenvironment.core.component.workflow.execution.api.WorkflowFileException;
import de.rcenvironment.core.utils.common.StringUtils;

public class WfIntegrateCommandPluginTest {

    private static final String SOME_UUID_1 = "c6f85f3f-5dff-4940-a4d9-bb91363d5668";

    private static final String SOME_UUID_2 = "3c88f1d1-f043-4327-9413-2b2bb158b9f8";

    private static final String SOME_UUID_3 = "ba0bfcef-e70b-4040-a125-79cacc859b43";

    private static final String SOME_EXTERNAL_OUTPUT_NAME = "externalOutput";

    private static final String SOME_INTERNAL_OUTPUT_NAME = "internalOutput";

    private static final String SOME_INTERNAL_OUTPUT_NAME_2 = "internalInput2";

    private static final String VERBOSE_FLAG = "-v";

    private static final String SOME_INTERNAL_INPUT_NAME = "internalInput";

    private static final String SOME_INTERNAL_INPUT_NAME_2 = "someInput2";

    private static final String SOME_EXTERNAL_INPUT_NAME = "externalInput";

    private static final String SOME_COMPONENT_ID = "componentId";

    private static final String SOME_WORKFLOWFILE_PATH = "pathToFile";

    private static final String SOME_WORKFLOW_IDENTIFIER = "workflowIdentifier";

    private static final String SOME_INVALID_EXPOSURE_PARAMETER = "loremipsum:dolor:sit:amet";

    private static final String SOME_NODE_ID_WITH_SPACES = "some node";

    private static final String SOME_INTERNAL_INPUT_NAME_WITH_SPACES = "some input";

    private static final String SOME_INVALID_COMPONENT_ID = "LPT1";

    private WfIntegrateCommandPluginTestHarness pluginHarness;

    @Before
    public void createTestHarness() {
        this.pluginHarness = new WfIntegrateCommandPluginTestHarness(new WfIntegrateCommandPlugin());
    }

    @Test
    public void hasSingleCommandDescription() {
    	// WHEN
        final MainCommandDescription[] commandDescriptions = pluginHarness.when().getCommands();

        // THEN
        singleCommandDescriptionReturned(commandDescriptions);
        final MainCommandDescription singleCommandDescription = commandDescriptions[0];
        commandDescriptionHasStaticPart(singleCommandDescription, "wf-integrate");
        final String[] expectedPositionalParameters = new String[] { "toolname", "workflow file" };
        final String[] expectedNamedParameters = new String[] {
                "--expose",
                "--expose-inputs",
                "--expose-outputs"
            };
        final String[] expectedFlags = new String[] { "--verbose" };
        commandDescriptionHasModifiers(singleCommandDescription,
                expectedPositionalParameters, expectedNamedParameters, expectedFlags);
        commandDescriptionHasDescription(singleCommandDescription, "integrate a workflow file as a component");
    }

    private void singleCommandDescriptionReturned(final MainCommandDescription[] commandDescriptions) {
        assertEquals(1, commandDescriptions.length);
        assertEquals(0, commandDescriptions[0].getSubCommands().length);
    }

    private void commandDescriptionHasStaticPart(final MainCommandDescription commandDescription, String expectedStaticPart) {
        assertEquals(expectedStaticPart, commandDescription.getCommand());
    }

    private void commandDescriptionHasModifiers(final MainCommandDescription commandDescription,
            String[] positionalParameters, String[] namedParameters, String[] flags) {
        
        final List<AbstractCommandParameter> realPosParameters = commandDescription.getModifiers().getPositionals();
        final List<NamedParameter> realNamedParameters = commandDescription.getModifiers().getNamedParameters();
        final List<CommandFlag> realFlags = commandDescription.getModifiers().getFlags();
        
        assertEquals(realPosParameters.size(), positionalParameters.length);
        assertEquals(realNamedParameters.size(), namedParameters.length);
        assertEquals(realFlags.size(), flags.length);
        
        for (int i = 0; i < positionalParameters.length; i++) {
            final String name = positionalParameters[i];
            final String realName = realPosParameters.get(i).getName();
            
            assertEquals(name, realName);
        }
        
        final List<NamedParameter> sortedRealNamedParameters = realNamedParameters.stream()
                .sorted((a, b) -> a.getName().compareTo(b.getName())).collect(Collectors.toList());
        final List<String> sortedNamedParameters = Arrays.stream(namedParameters)
                .sorted((a, b) -> a.compareTo(b)).collect(Collectors.toList());
        
        for (int i = 0; i < sortedNamedParameters.size(); i++) {
            final String name = sortedNamedParameters.get(i);
            final String realName = sortedRealNamedParameters.get(i).getName();
            
            assertEquals(name, realName);
        }
        
        final List<CommandFlag> sortedRealFlags = realFlags.stream()
                .sorted((a, b) -> a.getShortFlag().compareTo(b.getShortFlag())).collect(Collectors.toList());
        final List<String> sortedFlags = Arrays.stream(flags)
                .sorted((a, b) -> a.compareTo(b)).collect(Collectors.toList());
        
        for (int i = 0; i < sortedFlags.size(); i++) {
            final String flag = sortedFlags.get(i);
            final String realFlag = sortedRealFlags.get(0).getLongFlag();
            
            assertEquals(flag, realFlag);
        }
    }

    private void commandDescriptionHasDescription(final MainCommandDescription commandDescription, String expectedLine) {
        assertEquals(expectedLine, commandDescription.getDescription());
    }

    @Test
    public void commandWithoutComponentId() throws CommandException, WorkflowFileException, IOException {
        // GIVEN
        pluginHarness.given()
            .workflowLoaderServiceIsAbsent()
            .workflowIntegrationServiceIsAbsent();

        // WHEN
        pluginHarness.when()
            .executeWfIntegrateCommand();

        // THEN
        pluginHarness.then()
            .outputReceiverWasNeverCalled()
            .syntaxErrorWasThrown()
            .exceptionHasCommandString(wfIntegrateCommand())
            .exceptionHasMessage("Not enough positional parameters");
    }
    
    @Test
    public void commandWithInvalidComponentId() throws CommandException, IOException {
        pluginHarness.given()
            .workflowLoaderServiceIsAbsent()
            .workflowIntegrationServiceIsAbsent();
        
        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_INVALID_COMPONENT_ID, "");
        
        pluginHarness.then()
            .syntaxErrorWasThrown()
            .exceptionHasCommandString(String.join(" ", wfIntegrateCommand(), SOME_INVALID_COMPONENT_ID))
            .exceptionHasMessage("Invalid component ID");
    }

    @Test
    public void commandWithoutWorkflowfilePath() throws CommandException, WorkflowFileException, IOException {
        // GIVEN
        this.pluginHarness.given()
            .workflowLoaderServiceIsAbsent()
            .workflowIntegrationServiceIsAbsent();

        // WHEN
        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID);

        // THEN
        pluginHarness.then()
            .syntaxErrorWasThrown()
            .exceptionHasCommandString(String.join(" ", wfIntegrateCommand(), SOME_COMPONENT_ID))
            .exceptionHasMessage("Not enough positional parameters");
    }

    @Test
    public void whenWorkflowLoadingFailsThenTheCommandFailsGracefully() throws WorkflowFileException {
        // GIVEN
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowLoaderThrowsExceptionOnLoad();

        pluginHarness.given()
            .workflowIntegrationServiceIsAbsent();

        // WHEN
        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH);

        // THEN
        pluginHarness.then()
            .loaderServiceWasNotCalled()
            .executionErrorWasThrown()
            .exceptionHasMessage("Workflow file at 'pathToFile' could not be parsed");
    }

    @Test
    public void whenExposureFlagIsNotGivenThenTheCommandFailsGracefully() throws WorkflowFileException {
        // GIVEN
        pluginHarness.given()
            .workflowLoaderServiceIsAbsent()
            .workflowIntegrationServiceIsAbsent();

        // WHEN
        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, SOME_INVALID_EXPOSURE_PARAMETER);

        // THEN
        pluginHarness.then()
            .syntaxErrorWasThrown()
            .exceptionHasMessage("Not all tokens could be parsed");
    }

    @Test
    public void whenExposureParameterParsingFailsThenTheCommandFailsGracefully() throws WorkflowFileException {
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowLoaderReturnsWorkflowDescription(SOME_WORKFLOW_IDENTIFIER);

        pluginHarness.given()
            .workflowIntegrationServiceIsAbsent();

        // WHEN
        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(), SOME_INVALID_EXPOSURE_PARAMETER);

        // THEN
        pluginHarness.then()
            .loaderServiceWasNotCalled()
            .syntaxErrorWasThrown()
            .exceptionHasMessage(exposureParameterErrorMessage(SOME_INVALID_EXPOSURE_PARAMETER));
    }

    private String unexpectedExposureFlagErrorMessage(String givenExposureFlag) {
        return "Unexpected exposure flag '" + givenExposureFlag + "'. "
            + "Expected '--expose', '--expose-input[s]', or '--expose-output[s]'. "
            + "Skipping this token.";
    }

    @Test
    public void whenWorkflowIntegrationFailsThenTheCommandFailsGracefully() throws WorkflowFileException, IOException {
        // GIVEN
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowLoaderReturnsWorkflowDescription(WfIntegrateCommandPluginTest.SOME_WORKFLOW_IDENTIFIER);

        pluginHarness.given()
            .workflowIntegrationServiceIsPresent()
            .workflowIntegrationThrowsError(SOME_COMPONENT_ID);

        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH);

        // THEN
        pluginHarness.then()
            .loaderServiceWasCalled()
            .integrationServiceWasCalled();

        pluginHarness.then()
            .executionErrorWasThrown()
            .exceptionHasMessage("Could not integrate workflow 'pathToFile' as component 'componentId'");
    }

    @Test
    public void validCommandWithoutExposureParameters() throws CommandException, WorkflowFileException, IOException {
        // GIVEN
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowLoaderReturnsWorkflowDescription(WfIntegrateCommandPluginTest.SOME_WORKFLOW_IDENTIFIER);

        pluginHarness.given()
            .workflowIntegrationServiceIsPresent()
            .workflowIntegrationSucceeds(SOME_COMPONENT_ID);

        // WHEN
        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH);

        // THEN
        pluginHarness.then()
            .noCommandExceptionWasThrown()
            .loaderServiceWasCalled()
            .integrationServiceWasCalled()
            .workflowIntegrationServiceWasCalledWithEmptyEndpointAdapters()
            .outputReceiverWasNeverCalled();
    }

    @Test
    public void validCommandWithSingleExposureParameter() throws CommandException, WorkflowFileException, IOException {
        // GIVEN
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowLoaderReturnsWorkflowDescription(WfIntegrateCommandPluginTest.SOME_WORKFLOW_IDENTIFIER);

        pluginHarness.given()
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1)
            .thatNodeHasIntegerInput(SOME_INTERNAL_INPUT_NAME, SOME_INTERNAL_INPUT_NAME);

        pluginHarness.given()
            .workflowIntegrationServiceIsPresent()
            .workflowIntegrationSucceeds(SOME_COMPONENT_ID);

        // WHEN
        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_COMPONENT_ID, SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME));

        // THEN
        pluginHarness.then().noCommandExceptionWasThrown()
            .loaderServiceWasCalled()
            .integrationServiceWasCalled()
            .workflowIntegrationServiceWasCalledWithEndpointAdapter(SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME)
            .outputReceiverWasNeverCalled();
    }

    @Test
    public void validVerboseCommandWithInputAndOutputExposureParameter() throws CommandException, WorkflowFileException, IOException {
        // GIVEN
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowLoaderReturnsWorkflowDescription(WfIntegrateCommandPluginTest.SOME_WORKFLOW_IDENTIFIER);

        pluginHarness.given()
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1)
            .thatNodeHasIntegerInput(SOME_INTERNAL_INPUT_NAME, SOME_INTERNAL_INPUT_NAME)
            .thatNodeHasIntegerOutput(SOME_INTERNAL_OUTPUT_NAME, SOME_INTERNAL_OUTPUT_NAME);

        pluginHarness.given()
            .workflowIntegrationServiceIsPresent()
            .workflowIntegrationSucceeds(SOME_COMPONENT_ID);

        // WHEN
        pluginHarness.when()
            .executeWfIntegrateCommand(VERBOSE_FLAG, SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_COMPONENT_ID, SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME + ","), //TODO: refactor into better fitting structure
                exposureParameter(SOME_COMPONENT_ID, SOME_INTERNAL_OUTPUT_NAME, SOME_EXTERNAL_OUTPUT_NAME));

        // THEN
        pluginHarness.then()
            .noCommandExceptionWasThrown()
            .loaderServiceWasCalled()
            .integrationServiceWasCalled()
            .workflowIntegrationServiceWasCalledWithEndpointAdapter(SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME)
            .workflowIntegrationServiceWasCalledWithEndpointAdapter(SOME_INTERNAL_OUTPUT_NAME, SOME_EXTERNAL_OUTPUT_NAME)
            .outputReceiverHasPrinted(verboseOutput());
    }

    @Test
    public void validCommandWithAbsentNodeThrowsException() throws WorkflowFileException {
        // GIVEN
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowLoaderReturnsWorkflowDescription(WfIntegrateCommandPluginTest.SOME_WORKFLOW_IDENTIFIER);

        // WHEN
        pluginHarness.when()
            .executeWfIntegrateCommand(VERBOSE_FLAG, SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_COMPONENT_ID, SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME));

        // THEN
        pluginHarness.then()
            .syntaxErrorWasThrown()
            .exceptionHasCommandString(String.join(" ", wfIntegrateCommand(), VERBOSE_FLAG, SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH,
                exposeFlag(), exposureParameter(SOME_COMPONENT_ID, SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME)))
            .exceptionHasMessage(nodeNotPresentErrorMessage(SOME_COMPONENT_ID));
    }

    private String wfIntegrateCommand() {
        return "wf-integrate";
    }

    @Test
    public void validCommandWithAmbiguousNodeThrowsException() throws WorkflowFileException {
        // GIVEN
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowLoaderReturnsWorkflowDescription(WfIntegrateCommandPluginTest.SOME_WORKFLOW_IDENTIFIER)
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1)
            .loadedWorkflowDescriptionHasNode(SOME_UUID_1, SOME_UUID_2);

        // WHEN
        pluginHarness.when()
            .executeWfIntegrateCommand(VERBOSE_FLAG, SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_UUID_1, SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME));

        // THEN
        pluginHarness.then()
            .syntaxErrorWasThrown()
            .exceptionHasCommandString(
                String.join(" ", wfIntegrateCommandVerbose(), SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                    exposureParameter(SOME_UUID_1, SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME)))
            .exceptionHasMessage(ambiguousNodeErrorMessage(SOME_UUID_1,
                nameWithId(SOME_UUID_1, SOME_UUID_2),
                nameWithId(SOME_COMPONENT_ID, SOME_UUID_1)));
    }

    private String nameWithId(String name, String id) {
        return StringUtils.format("%s [ID: %s]", name, id);
    }

    @Test
    public void validCommandWithAbsentEndpointsThrowsException() throws WorkflowFileException {
        // GIVEN
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowLoaderReturnsWorkflowDescription(WfIntegrateCommandPluginTest.SOME_WORKFLOW_IDENTIFIER)
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1);

        // WHEN
        pluginHarness.when()
            .executeWfIntegrateCommand(VERBOSE_FLAG, SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_UUID_1, SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME));

        // THEN
        pluginHarness.then()
            .syntaxErrorWasThrown()
            .exceptionHasCommandString(
                String.join(" ", wfIntegrateCommandVerbose(), SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                    exposureParameter(SOME_UUID_1, SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME)))
            .exceptionHasMessage(
                absentEndpointErrorMessage(SOME_INTERNAL_INPUT_NAME, SOME_COMPONENT_ID, SOME_UUID_1, presentInputs(), presentOutputs()));
    }

    @Test
    public void validCommandWithAbsentEndpointProvidesExistingInputs() throws WorkflowFileException {
        // GIVEN
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowLoaderReturnsWorkflowDescription(WfIntegrateCommandPluginTest.SOME_WORKFLOW_IDENTIFIER)
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1)
            .thatNodeHasIntegerInput(SOME_INTERNAL_INPUT_NAME, SOME_UUID_2);

        // WHEN
        pluginHarness.when()
            .executeWfIntegrateCommand(VERBOSE_FLAG, SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_UUID_1, SOME_INTERNAL_INPUT_NAME_2));

        // THEN
        pluginHarness.then()
            .syntaxErrorWasThrown()
            .exceptionHasCommandString(
                String.join(" ", wfIntegrateCommandVerbose(), SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                    exposureParameter(SOME_UUID_1, SOME_INTERNAL_INPUT_NAME_2)))
            .exceptionHasMessage(
                absentEndpointErrorMessage(SOME_INTERNAL_INPUT_NAME_2, SOME_COMPONENT_ID, SOME_UUID_1,
                    presentInputs(nameWithId(SOME_INTERNAL_INPUT_NAME, SOME_UUID_2)), presentOutputs()));
    }


    @Test
    public void validCommandWithAbsentEndpointProvidesExistingOutputs() throws WorkflowFileException {
        // GIVEN
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowLoaderReturnsWorkflowDescription(WfIntegrateCommandPluginTest.SOME_WORKFLOW_IDENTIFIER)
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1)
            .thatNodeHasIntegerOutput(SOME_INTERNAL_OUTPUT_NAME, SOME_UUID_2);

        // WHEN
        pluginHarness.when()
            .executeWfIntegrateCommand(VERBOSE_FLAG, SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_UUID_1, SOME_INTERNAL_OUTPUT_NAME_2));

        // THEN
        pluginHarness.then()
            .syntaxErrorWasThrown()
            .exceptionHasCommandString(
                String.join(" ", wfIntegrateCommandVerbose(), SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                    exposureParameter(SOME_UUID_1, SOME_INTERNAL_OUTPUT_NAME_2)))
            .exceptionHasMessage(
                absentEndpointErrorMessage(SOME_INTERNAL_OUTPUT_NAME_2, SOME_COMPONENT_ID, SOME_UUID_1,
                    presentInputs(), presentOutputs(nameWithId(SOME_INTERNAL_OUTPUT_NAME, SOME_UUID_2))));
    }

    private String presentInputs(String... inputs) {
        final String header = "  Inputs:  ";
        if (inputs.length == 0) {
            return "";
        } else {
            // We pad each output such that the list of outputs is left aligned. Hence, the number of spaces is the same as the number of
            // characters in the header
            return header + String.join("\n           ", inputs);
        }
    }

    private String presentOutputs(String... outputs) {
        final String header = "  Outputs: ";
        if (outputs.length == 0) {
            return "";
        } else {
            // We pad each output such that the list of outputs is left aligned. Hence, the number of spaces is the same as the number of
            // characters in the header
            return header + String.join("\n           ", outputs);
        }
    }

    private String wfIntegrateCommandVerbose() {
        return "wf-integrate -v";
    }

    @Test
    public void validCommandWithAmbiguousEndpointThrowsException() throws WorkflowFileException {
        // GIVEN
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowLoaderReturnsWorkflowDescription(WfIntegrateCommandPluginTest.SOME_WORKFLOW_IDENTIFIER)
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1)
            .thatNodeHasIntegerInput(SOME_INTERNAL_INPUT_NAME, SOME_UUID_2)
            .thatNodeHasIntegerOutput(SOME_INTERNAL_INPUT_NAME, SOME_UUID_3);

        // WHEN
        pluginHarness.when()
            .executeWfIntegrateCommand(VERBOSE_FLAG, SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_UUID_1, SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME));

        // THEN
        pluginHarness.then()
            .syntaxErrorWasThrown()
            .exceptionHasCommandString(String.join(" ", wfIntegrateCommandVerbose(), SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH,
                exposeFlag(), exposureParameter(SOME_UUID_1, SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME)))
            .exceptionHasMessage(ambiguousEndpointErrorMessage());
    }

    @Test
    public void metadataOfEndpointsIsParsedCorrectly() throws WorkflowFileException, IOException {
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowIntegrationServiceIsPresent()
            .workflowIntegrationSucceeds(SOME_COMPONENT_ID)
            .workflowLoaderReturnsWorkflowDescription(SOME_WORKFLOW_IDENTIFIER)
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1)
            .thatNodeHasIntegerInput(SOME_INTERNAL_INPUT_NAME, SOME_UUID_2)
            .thatEndpointHasMetadata(ComponentConstants.INPUT_METADATA_KEY_INPUT_DATUM_HANDLING, InputDatumHandling.Queue.name())
            .thatEndpointHasMetadata(ComponentConstants.INPUT_METADATA_KEY_INPUT_EXECUTION_CONSTRAINT,
                InputExecutionContraint.RequiredIfConnected.name());

        // WHEN
        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_UUID_1, SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME));

        // THEN
        pluginHarness.then()
            .noCommandExceptionWasThrown()
            .loaderServiceWasCalled()
            .integrationServiceWasCalled()
            .workflowIntegrationServiceWasCalledWithEndpointAdapter(SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME)
            .thatEndpointAdapterHasDatumHandling(InputDatumHandling.Queue)
            .thatEndpointHasExecutionConstraint(InputExecutionContraint.RequiredIfConnected)
            .outputReceiverWasNeverCalled();
    }

    @Test
    public void partiallyAbsentMetadataOfEndpointIsParsedCorrectly() throws WorkflowFileException, IOException {
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowIntegrationServiceIsPresent()
            .workflowIntegrationSucceeds(SOME_COMPONENT_ID)
            .workflowLoaderReturnsWorkflowDescription(SOME_WORKFLOW_IDENTIFIER)
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1)
            .thatNodeHasIntegerInput(
                SOME_INTERNAL_INPUT_NAME, SOME_UUID_2, InputDatumHandling.Constant, InputExecutionContraint.RequiredIfConnected)
            .thatEndpointHasMetadata(ComponentConstants.INPUT_METADATA_KEY_INPUT_DATUM_HANDLING, InputDatumHandling.Queue.name());

        // WHEN
        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_UUID_1, SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME));

        // THEN
        pluginHarness.then()
            .noCommandExceptionWasThrown()
            .loaderServiceWasCalled()
            .integrationServiceWasCalled()
            .workflowIntegrationServiceWasCalledWithEndpointAdapter(SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME)
            .thatEndpointAdapterHasDatumHandling(InputDatumHandling.Constant)
            .thatEndpointHasExecutionConstraint(InputExecutionContraint.RequiredIfConnected)
            .outputReceiverWasNeverCalled();
    }

    @Test
    public void exposeInputIsParsedCorrectly() throws WorkflowFileException, IOException {
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowIntegrationServiceIsPresent()
            .workflowIntegrationSucceeds(SOME_COMPONENT_ID)
            .workflowLoaderReturnsWorkflowDescription(SOME_WORKFLOW_IDENTIFIER)
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1)
            .thatNodeHasIntegerInput(SOME_INTERNAL_INPUT_NAME, SOME_UUID_2)
            .thatNodeHasIntegerOutput(SOME_INTERNAL_OUTPUT_NAME, SOME_UUID_3);

        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeInputFlag(),
                exposureParameter(SOME_COMPONENT_ID, SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME));

        pluginHarness.then()
            .noCommandExceptionWasThrown()
            .loaderServiceWasCalled()
            .integrationServiceWasCalled()
            .workflowIntegrationServiceWasCalledWithEndpointAdapter(SOME_INTERNAL_INPUT_NAME, SOME_EXTERNAL_INPUT_NAME)
            .thatEndpointAdapterIsInputAdapter();

    }

    @Test
    public void exposeOutputIsParsedCorrectly() throws WorkflowFileException, IOException {
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowIntegrationServiceIsPresent()
            .workflowIntegrationSucceeds(SOME_COMPONENT_ID)
            .workflowLoaderReturnsWorkflowDescription(SOME_WORKFLOW_IDENTIFIER)
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1)
            .thatNodeHasIntegerInput(SOME_INTERNAL_INPUT_NAME, SOME_UUID_2)
            .thatNodeHasIntegerOutput(SOME_INTERNAL_OUTPUT_NAME, SOME_UUID_3);

        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeOutputFlag(),
                exposureParameter(SOME_UUID_1, SOME_INTERNAL_OUTPUT_NAME, SOME_EXTERNAL_OUTPUT_NAME));

        pluginHarness.then()
            .noCommandExceptionWasThrown()
            .loaderServiceWasCalled()
            .integrationServiceWasCalled()
            .workflowIntegrationServiceWasCalledWithEndpointAdapter(SOME_INTERNAL_OUTPUT_NAME, SOME_EXTERNAL_OUTPUT_NAME)
            .thatEndpointAdapterIsOutputAdapter();
    }

    @Test
    public void simpleExposureParameterIsParsedCorrectly() throws WorkflowFileException, IOException {
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowIntegrationServiceIsPresent()
            .workflowIntegrationSucceeds(SOME_COMPONENT_ID)
            .workflowLoaderReturnsWorkflowDescription(SOME_WORKFLOW_IDENTIFIER)
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1)
            .thatNodeHasIntegerInput(SOME_INTERNAL_INPUT_NAME, SOME_UUID_2);

        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_UUID_1, SOME_INTERNAL_INPUT_NAME));

        pluginHarness.then()
            .noCommandExceptionWasThrown()
            .loaderServiceWasCalled()
            .integrationServiceWasCalled()
            .workflowIntegrationServiceWasCalledWithEndpointAdapter(SOME_INTERNAL_INPUT_NAME, SOME_INTERNAL_INPUT_NAME);
    }

    @Test
    public void componentExposureParameterIsParsedCorrectly() throws WorkflowFileException, IOException {
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowIntegrationServiceIsPresent()
            .workflowIntegrationSucceeds(SOME_COMPONENT_ID)
            .workflowLoaderReturnsWorkflowDescription(SOME_WORKFLOW_IDENTIFIER)
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1)
            .thatNodeHasIntegerInput(SOME_INTERNAL_INPUT_NAME, SOME_UUID_2)
            .thatNodeHasIntegerOutput(SOME_INTERNAL_OUTPUT_NAME, SOME_UUID_3);

        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_UUID_1));

        pluginHarness.then()
            .noCommandExceptionWasThrown()
            .loaderServiceWasCalled()
            .integrationServiceWasCalled()
            .workflowIntegrationServiceWasCalledWithEndpointAdapter(SOME_INTERNAL_INPUT_NAME, SOME_INTERNAL_INPUT_NAME)
            .thatEndpointAdapterIsInputAdapter()
            .workflowIntegrationServiceWasCalledWithEndpointAdapter(SOME_INTERNAL_OUTPUT_NAME, SOME_INTERNAL_OUTPUT_NAME)
            .thatEndpointAdapterIsOutputAdapter();
    }

    @Test
    public void whenExposeInputsFlagIsGivenAllInputsAreExposed() throws WorkflowFileException, IOException {
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowIntegrationServiceIsPresent()
            .workflowIntegrationSucceeds(SOME_COMPONENT_ID)
            .workflowLoaderReturnsWorkflowDescription(SOME_WORKFLOW_IDENTIFIER)
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1)
            .thatNodeHasIntegerInput(SOME_INTERNAL_INPUT_NAME, SOME_UUID_2)
            .thatNodeHasIntegerOutput(SOME_INTERNAL_OUTPUT_NAME, SOME_UUID_3);

        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeInputsFlag(),
                exposureParameter(SOME_UUID_1));

        pluginHarness.then()
            .noCommandExceptionWasThrown()
            .loaderServiceWasCalled()
            .integrationServiceWasCalled()
            .workflowIntegrationServiceWasCalledWithEndpointAdapter(SOME_INTERNAL_INPUT_NAME, SOME_INTERNAL_INPUT_NAME)
            .thatEndpointAdapterIsInputAdapter()
            .noOtherEndpointAdaptersWereGiven();
    }

    @Test
    public void whenExposeOutputsFlagIsGivenAllOutputsAreExposed() throws WorkflowFileException, IOException {
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowIntegrationServiceIsPresent()
            .workflowIntegrationSucceeds(SOME_COMPONENT_ID)
            .workflowLoaderReturnsWorkflowDescription(SOME_WORKFLOW_IDENTIFIER)
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1)
            .thatNodeHasIntegerInput(SOME_INTERNAL_INPUT_NAME, SOME_UUID_2)
            .thatNodeHasIntegerOutput(SOME_INTERNAL_OUTPUT_NAME, SOME_UUID_3);

        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeOutputsFlag(),
                exposureParameter(SOME_UUID_1));

        pluginHarness.then()
            .noCommandExceptionWasThrown()
            .loaderServiceWasCalled()
            .integrationServiceWasCalled()
            .workflowIntegrationServiceWasCalledWithEndpointAdapter(SOME_INTERNAL_OUTPUT_NAME, SOME_INTERNAL_OUTPUT_NAME)
            .thatEndpointAdapterIsOutputAdapter()
            .noOtherEndpointAdaptersWereGiven();
    }

    @Test
    public void componentsAndEndpointsWithSpacesAreHandledCorrectly() throws WorkflowFileException, IOException {
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowIntegrationServiceIsPresent()
            .workflowIntegrationSucceeds(SOME_COMPONENT_ID)
            .workflowLoaderReturnsWorkflowDescription(SOME_WORKFLOW_IDENTIFIER)
            .loadedWorkflowDescriptionHasNode(SOME_NODE_ID_WITH_SPACES, SOME_UUID_1)
            .thatNodeHasIntegerInput(SOME_INTERNAL_INPUT_NAME_WITH_SPACES, SOME_UUID_2);

        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_NODE_ID_WITH_SPACES, SOME_INTERNAL_INPUT_NAME_WITH_SPACES));

        pluginHarness.then()
            .noCommandExceptionWasThrown()
            .loaderServiceWasCalled()
            .integrationServiceWasCalled()
            .workflowIntegrationServiceWasCalledWithEndpointAdapter(
                SOME_INTERNAL_INPUT_NAME_WITH_SPACES, SOME_INTERNAL_INPUT_NAME_WITH_SPACES)
            .thatEndpointAdapterIsInputAdapter()
            .noOtherEndpointAdaptersWereGiven();
    }

    private String exposeInputsFlag() {
        return "--expose-inputs";
    }

    private String exposeOutputsFlag() {
        return "--expose-outputs";
    }

    @Test
    public void componentExposureOfAbsentComponentFailsGracefully() throws WorkflowFileException, IOException {
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowIntegrationServiceIsPresent()
            .workflowIntegrationSucceeds(SOME_COMPONENT_ID)
            .workflowLoaderReturnsWorkflowDescription(SOME_WORKFLOW_IDENTIFIER)
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1);

        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_COMPONENT_ID));

        pluginHarness.then()
            .syntaxErrorWasThrown()
            .exceptionHasCommandString(String.join(" ", wfIntegrateCommand(), SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_COMPONENT_ID)))
            .exceptionHasMessage(
                parseErrorMessageHeader() + StringUtils.format("There are no endpoints present to expose on node '%s'", SOME_COMPONENT_ID));
    }
    
    @Test
    public void exposureOfEndpointsWithoutDefinitionFailsGracefully() throws Exception {
        pluginHarness.given()
            .workflowLoaderServiceIsPresent()
            .workflowIntegrationServiceIsPresent()
            .workflowIntegrationSucceeds(SOME_COMPONENT_ID)
            .workflowLoaderReturnsWorkflowDescription(SOME_WORKFLOW_IDENTIFIER)
            .loadedWorkflowDescriptionHasNode(SOME_COMPONENT_ID, SOME_UUID_1)
            .thatNodeHasEndpointWithoutDefinition(SOME_INTERNAL_INPUT_NAME, SOME_UUID_2);

        pluginHarness.when()
            .executeWfIntegrateCommand(SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_COMPONENT_ID, SOME_INTERNAL_INPUT_NAME));

        pluginHarness.then()
            .syntaxErrorWasThrown()
            .exceptionHasCommandString(String.join(" ", wfIntegrateCommand(), SOME_COMPONENT_ID, SOME_WORKFLOWFILE_PATH, exposeFlag(),
                exposureParameter(SOME_COMPONENT_ID, SOME_INTERNAL_INPUT_NAME)))
            .exceptionHasMessage(
                parseErrorMessageHeader()
                + StringUtils.format(
                    "Could not determine input datum handling of endpoint '%s' [ID: %s]. "
                    + "This may be due to unavailability of the component. "
                    + "Please make sure that all components whose endpoints are adapted are available at the time of integration.",
                    SOME_INTERNAL_INPUT_NAME, SOME_UUID_2));
        
    }

    private String exposeInputFlag() {
        return "--expose-inputs";
    }

    private String exposeOutputFlag() {
        return "--expose-outputs";
    }

    private String exposureParameter(String componentName) {
        return componentName;
    }

    private String exposureParameter(String componentName, String endpoint) {
        return String.join(":", componentName, endpoint);
    }

    private String exposureParameter(String componentName, String internalEndpoint, String externalEndpoint) {
        return String.join(":", componentName, internalEndpoint, externalEndpoint);
    }

    private String exposeFlag() {
        return "--expose";
    }

    private String parseErrorMessageHeader() {
        return "Could not parse endpoint adapter definitions: \n";
    }

    private String nodeNotPresentErrorMessage(String nodeName) {
        return parseErrorMessageHeader()
            + "Given node '" + nodeName + "' is not present in workflow";
    }

    private String ambiguousNodeErrorMessage(String givenNodeId, String... candidateLines) {
        return parseErrorMessageHeader()
            + "Given node " + givenNodeId + " is ambiguous. Candidates:\n  "
            + String.join("\n  ", candidateLines);
    }

    private String exposureParameterErrorMessage(String exposureParameter) {
        return parseErrorMessageHeader()
            + "Could not parse endpoint adapter definition '" + exposureParameter + "'. "
            + "Expected format: Either '<component id>:<endpoint id>:<external name>' or '<component id>:<endpoint id>'";
    }

    private String absentEndpointErrorMessage(String expectedEndpointName, String expectedNodeName, String expectedNodeID,
        String expectedInputs, String expectedOutputs) {
        final StringBuilder returnValueBuilder = new StringBuilder(parseErrorMessageHeader());
        returnValueBuilder.append(StringUtils.format("Given endpoint '%s' is not present on node '%s' [ID: %s]. ",
            expectedEndpointName, expectedNodeName, expectedNodeID));

        if (expectedInputs.isEmpty() && expectedOutputs.isEmpty()) {
            returnValueBuilder.append("That node does not contain any endpoints.");
        } else {
            returnValueBuilder.append("The following endpoints are present on that node:");
            if (!expectedInputs.isEmpty()) {
                returnValueBuilder.append("\n");
                returnValueBuilder.append(expectedInputs);
            }
            
            if (!expectedOutputs.isEmpty()) {
                returnValueBuilder.append("\n");
                returnValueBuilder.append(expectedOutputs);
            }
        }
        return returnValueBuilder.toString();
    }

    private String ambiguousEndpointErrorMessage() {
        return parseErrorMessageHeader()
            + "Given endpoint 'internalInput' is ambiguous on node '" + SOME_COMPONENT_ID + "' [ID: " + SOME_UUID_1 + "]. Candidates:\n"
            + "  internalInput [ID: " + SOME_UUID_2 + "]\n"
            + "  internalInput [ID: " + SOME_UUID_3 + "]";

    }

    public String[] verboseOutput() {
        return new String[] {
            StringUtils.format("Input Adapter : %s --[Integer,Single,Required]-> %s @ %s", SOME_EXTERNAL_INPUT_NAME,
                SOME_INTERNAL_INPUT_NAME, SOME_UUID_1),
            StringUtils.format("Output Adapter: %s @ %s --[Integer]-> %s", SOME_INTERNAL_OUTPUT_NAME, SOME_UUID_1,
                SOME_EXTERNAL_OUTPUT_NAME)
        };
    }
}
