/*******************************************************************************
 * (c) Copyright 2016 Hewlett-Packard Development Company, L.P.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License v2.0 which accompany this distribution.
 *
 * The Apache License is available at
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 *******************************************************************************/
package io.cloudslang.lang.systemtests;

import com.google.common.collect.Sets;
import io.cloudslang.lang.compiler.SlangSource;
import io.cloudslang.lang.compiler.SlangTextualKeys;
import io.cloudslang.lang.compiler.modeller.model.Executable;
import io.cloudslang.lang.compiler.modeller.model.Flow;
import io.cloudslang.lang.entities.CompilationArtifact;
import io.cloudslang.lang.entities.SystemProperty;
import io.cloudslang.lang.entities.bindings.Output;
import io.cloudslang.lang.entities.bindings.values.Value;
import io.cloudslang.lang.entities.bindings.values.ValueFactory;
import io.cloudslang.lang.entities.utils.ExpressionUtils;
import org.apache.commons.lang.StringUtils;
import org.junit.Assert;
import org.junit.Test;

import java.io.Serializable;
import java.net.URI;
import java.net.URL;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;

/**
 * @author Bonczidai Levente
 * @since 3/18/2016
 */
public class BindingScopeTest extends SystemsTestsParent {

    @Test
    public void testStepPublishValues() throws Exception {
        URL resource = getClass().getResource("/yaml/binding_scope_flow.sl");
        URI operation = getClass().getResource("/yaml/binding_scope_op.sl").toURI();
        Set<SlangSource> path = Sets.newHashSet(SlangSource.fromFile(operation));
        CompilationArtifact compilationArtifact = slang.compile(SlangSource.fromFile(resource.toURI()), path);

        Map<String, Value> userInputs = Collections.emptyMap();
        Set<SystemProperty> systemProperties = Collections.emptySet();

        // trigger ExecutionPlan
        RuntimeInformation runtimeInformation = triggerWithData(compilationArtifact, userInputs, systemProperties);

        Map<String, StepData> executionData = runtimeInformation.getSteps();

        StepData stepData = executionData.get(FIRST_STEP_PATH);
        Assert.assertNotNull("step data is null", stepData);

        verifyStepPublishValues(stepData);
    }

    @Test
    public void testInputMissing() throws Exception {
        URL resource = getClass().getResource("/yaml/check_weather_missing_input.sl");
        CompilationArtifact compilationArtifact = slang
                .compile(SlangSource.fromFile(resource.toURI()), new HashSet<SlangSource>());

        Map<String, Value> userInputs = Collections.emptyMap();
        Set<SystemProperty> systemProperties = Collections.emptySet();

        RuntimeException exception = assertThrows(RuntimeException.class, () ->
                triggerWithData(compilationArtifact, userInputs, systemProperties));
        Assert.assertEquals("Error running: 'check_weather_missing_input'.\n" +
                "\t Error binding input: 'input_get_missing_input', \n" +
                "\tError in evaluating expression: 'missing_input',\n" +
                "\tError in running script expression: 'missing_input',\n" +
                "\tException is: name 'missing_input' is not defined", exception.getMessage());
    }

    @Test
    public void testInputWithDefaultValue() throws Exception {
        URL resource = getClass().getResource("/yaml/check_weather_required_input_with_default.sl");
        CompilationArtifact compilationArtifact = slang
                .compile(SlangSource.fromFile(resource.toURI()), new HashSet<SlangSource>());

        Map<String, Value> userInputs = new HashMap<>();
        userInputs.put("city", ValueFactory.create(""));
        userInputs.put("input_with_default_value", ValueFactory.create(""));
        Set<SystemProperty> systemProperties = Collections.emptySet();

        Map<String, StepData> steps = triggerWithData(compilationArtifact, userInputs, systemProperties).getSteps();

        assertEquals("default_value", steps.get(EXEC_START_PATH).getInputs().get("input_with_default_value"));
        assertEquals("", steps.get(EXEC_START_PATH).getInputs().get("city"));
    }

    @Test
    public void testInputNotRequiredNull() throws Exception {
        URL resource = getClass().getResource("/yaml/check_weather_required_input_with_default.sl");
        CompilationArtifact compilationArtifact = slang
                .compile(SlangSource.fromFile(resource.toURI()), new HashSet<SlangSource>());

        Map<String, Value> userInputs = new HashMap<>();
        userInputs.put("city", ValueFactory.create(null));
        userInputs.put("input_with_default_value", ValueFactory.create(""));
        Set<SystemProperty> systemProperties = Collections.emptySet();

        Map<String, StepData> steps = triggerWithData(compilationArtifact, userInputs, systemProperties).getSteps();

        assertEquals("default_value", steps.get(EXEC_START_PATH).getInputs().get("input_with_default_value"));
        assertEquals(null, steps.get(EXEC_START_PATH).getInputs().get("city"));
    }

    @Test
    public void testInputRequiredWithEmptyValue() throws Exception {
        URL resource = getClass().getResource("/yaml/check_weather_input_required.sl");
        final CompilationArtifact compilationArtifact = slang
                .compile(SlangSource.fromFile(resource.toURI()), new HashSet<SlangSource>());

        Map<String, Value> userInputs = new HashMap<>();
        userInputs.put("city", ValueFactory.create(""));
        Set<SystemProperty> systemProperties = Collections.emptySet();

        RuntimeException exception = assertThrows(RuntimeException.class, () ->
                triggerWithData(compilationArtifact, userInputs, systemProperties));
        Assert.assertEquals("Error running: 'check_weather_input_required'.\n" +
                "\t Input with name: 'city' is Required, but value is empty", exception.getMessage());
    }

    @Test
    public void testInputRequiredWithNullValue() throws Exception {
        URL resource = getClass().getResource("/yaml/check_weather_input_required.sl");
        final CompilationArtifact compilationArtifact = slang
                .compile(SlangSource.fromFile(resource.toURI()), new HashSet<SlangSource>());

        Map<String, Value> userInputs = new HashMap<>();
        userInputs.put("city", ValueFactory.create(null));
        Set<SystemProperty> systemProperties = Collections.emptySet();

        RuntimeException exception = assertThrows(RuntimeException.class, () ->
                triggerWithData(compilationArtifact, userInputs, systemProperties));
        Assert.assertEquals("Error running: 'check_weather_input_required'.\n" +
                "\t Input with name: 'city' is Required, but value is empty", exception.getMessage());
    }

    @Test
    public void testInputOptionalWithEmptyValue() throws Exception {
        URL resource = getClass().getResource("/yaml/check_weather_optional_input_with_default.sl");
        CompilationArtifact compilationArtifact = slang
                .compile(SlangSource.fromFile(resource.toURI()), new HashSet<SlangSource>());

        Map<String, Value> userInputs = new HashMap<>();
        userInputs.put("input_with_default_value", ValueFactory.create(""));
        Set<SystemProperty> systemProperties = Collections.emptySet();

        Map<String, StepData> steps = triggerWithData(compilationArtifact, userInputs, systemProperties).getSteps();

        assertEquals("default_value", steps.get(EXEC_START_PATH).getInputs().get("input_with_default_value"));
    }

    @Test
    public void testInputOptionalWithNullValue() throws Exception {
        URL resource = getClass().getResource("/yaml/check_weather_optional_input_with_default.sl");
        CompilationArtifact compilationArtifact = slang
                .compile(SlangSource.fromFile(resource.toURI()), new HashSet<SlangSource>());

        Map<String, Value> userInputs = new HashMap<>();
        userInputs.put("input_with_default_value", ValueFactory.create(null));
        Set<SystemProperty> systemProperties = Collections.emptySet();

        Map<String, StepData> steps = triggerWithData(compilationArtifact, userInputs, systemProperties).getSteps();

        assertEquals("default_value", steps.get(EXEC_START_PATH).getInputs().get("input_with_default_value"));
    }

    @Test
    public void testStepInputRequiredWithEmptyValue() throws Exception {
        URL resource = getClass().getResource("/yaml/check_weather_flow.sl");
        URI operation1 = getClass().getResource("/yaml/check_weather_required_input_with_default.sl").toURI();
        Set<SlangSource> path = Sets.newHashSet(SlangSource.fromFile(operation1));

        CompilationArtifact compilationArtifact = slang
                .compile(SlangSource.fromFile(resource.toURI()), path);

        Map<String, Value> userInputs = new HashMap<>();
        Set<SystemProperty> systemProperties = Collections.emptySet();

        Map<String, StepData> steps = triggerWithData(compilationArtifact, userInputs, systemProperties).getSteps();

        assertEquals("weather thing default_value", steps.get(FIRST_STEP_PATH).getOutputs().get("kuku"));
    }

    @Test
    public void testSensitiveInputAndOutputsWithAndWithoutDefault() throws Exception {
        URL resource = getClass().getResource("/yaml/check_weather_flow_sensitive.sl");
        URI operation1 = getClass().getResource("/yaml/check_weather_required_input_sensitive.sl").toURI();
        Set<SlangSource> path = Sets.newHashSet(SlangSource.fromFile(operation1));

        CompilationArtifact compilationArtifact = slang
                .compile(SlangSource.fromFile(resource.toURI()), path);

        Map<String, Value> userInputs = new HashMap<>();
        userInputs.put("flow_input_sensitive", ValueFactory.create("sensitiveValue2", true));
        Set<SystemProperty> systemProperties = Collections.emptySet();

        Map<String, StepData> steps = triggerWithData(compilationArtifact, userInputs, systemProperties).getSteps();

        assertEquals("********", steps.get(EXEC_START_PATH).getInputs().get("flow_input_sensitive"));
        assertEquals("********", steps.get(EXEC_START_PATH).getInputs().get("flow_input_0"));
        assertEquals("defaultValue", steps.get(EXEC_START_PATH).getInputs().get("flow_input_1"));
        assertEquals("********", steps.get(EXEC_START_PATH).getOutputs().get("flow_output_0"));
        assertEquals("weather thing default_value sensitiveValue",
                steps.get(EXEC_START_PATH).getOutputs().get("flow_output_1"));
        assertEquals("sensitiveValue", steps.get(FIRST_STEP_PATH).getInputs().get("input_with_sensitive_no_default"));
    }

    @Test
    public void testPublishedSensitiveOutputs() throws Exception {
        URL resource = getClass().getResource("/yaml/check_weather_flow_sensitive_step_outputs.sl");
        URI operation1 = getClass().getResource("/yaml/check_weather.sl").toURI();
        Set<SlangSource> path = Sets.newHashSet(SlangSource.fromFile(operation1));
        CompilationArtifact compilationArtifact = slang.compile(SlangSource.fromFile(resource.toURI()), path);
        Map<String, Value> userInputs = new HashMap<>();
        Set<SystemProperty> systemProperties = Collections.emptySet();

        Map<String, StepData> steps = triggerWithData(compilationArtifact, userInputs, systemProperties).getSteps();
        assertEquals("********", steps.get(EXEC_START_PATH).getOutputs().get("sensitive_flow_output"));
        assertEquals("********", steps.get(FIRST_STEP_PATH).getOutputs().get("sensitive_step_output"));
    }

    @Test
    public void testSensitiveStepOutputs() throws Exception {
        URL resource = getClass().getResource("/yaml/check_weather_flow_sensitive_outputs.sl");
        URI operation1 = getClass().getResource("/yaml/check_weather_required_input_sensitive.sl").toURI();
        Set<SlangSource> path = Sets.newHashSet(SlangSource.fromFile(operation1));

        CompilationArtifact compilationArtifact = slang.compile(SlangSource.fromFile(resource.toURI()), path);

        Map<String, Value> userInputs = new HashMap<>();
        Set<SystemProperty> systemProperties = Collections.emptySet();

        Map<String, StepData> steps = triggerWithData(compilationArtifact, userInputs, systemProperties).getSteps();

        assertEquals("********", steps.get(EXEC_START_PATH).getOutputs().get("flow_output_0"));
        assertEquals("********", steps.get(EXEC_START_PATH).getOutputs().get("flow_output_1"));
        assertEquals("sensitive", steps.get(FIRST_STEP_PATH).getInputs().get("input_with_sensitive_no_default"));
    }

    @Test
    public void testStepInputOptionalWithEmptyValue() throws Exception {
        URL resource = getClass().getResource("/yaml/check_weather_flow_optional.sl");
        URI operation1 = getClass().getResource("/yaml/check_weather_optional_input_with_default.sl").toURI();
        Set<SlangSource> path = Sets.newHashSet(SlangSource.fromFile(operation1));

        CompilationArtifact compilationArtifact = slang.compile(SlangSource.fromFile(resource.toURI()), path);

        Map<String, Value> userInputs = new HashMap<>();
        Set<SystemProperty> systemProperties = Collections.emptySet();

        Map<String, StepData> steps = triggerWithData(compilationArtifact, userInputs, systemProperties).getSteps();

        assertEquals("weather thing default_value", steps.get(FIRST_STEP_PATH).getOutputs().get("kuku"));
    }

    private void verifyStepPublishValues(StepData stepData) {
        Map<String, Serializable> expectedPublishValues = new LinkedHashMap<>();
        expectedPublishValues.put("step1_publish_1", "op_output_1_value op_input_1_step step_arg_1_value");
        expectedPublishValues.put("step1_publish_2_conflict", "op_output_2_value");
        Map<String, Serializable> actualPublishValues = stepData.getOutputs();
        assertEquals("step publish values not as expected", expectedPublishValues, actualPublishValues);
    }

    @Test
    public void testFlowContextInStepPublishSection() throws Exception {
        URL resource = getClass().getResource("/yaml/binding_scope_flow_context_in_step_publish.sl");
        URI operation = getClass().getResource("/yaml/binding_scope_op.sl").toURI();
        Set<SlangSource> path = Sets.newHashSet(SlangSource.fromFile(operation));

        // pre-validation - step expression uses flow var name
        SlangSource flowSource = SlangSource.fromFile(resource.toURI());
        Executable flowExecutable = slangCompiler.preCompile(flowSource);
        String flowVarName = "flow_var";
        assertEquals(
                "Input name should be: " + flowVarName,
                flowVarName,
                flowExecutable.getInputs().get(0).getName()
        );
        @SuppressWarnings("unchecked")
        List<Output> stepPublishValues = (List<Output>) ((Flow) flowExecutable)
                .getWorkflow()
                .getSteps()
                .getFirst()
                .getPostStepActionData()
                .get(SlangTextualKeys.PUBLISH_KEY);
        assertEquals(
                "Step expression should contain: " + flowVarName,
                flowVarName,
                StringUtils.trim(ExpressionUtils.extractExpression(stepPublishValues.get(0).getValue().get()))
        );

        final CompilationArtifact compilationArtifact = slang.compile(flowSource, path);

        final Map<String, Value> userInputs = Collections.emptyMap();
        final Set<SystemProperty> systemProperties = Collections.emptySet();

        // trigger ExecutionPlan
        RuntimeException exception = assertThrows(RuntimeException.class, () ->
                triggerWithData(compilationArtifact, userInputs, systemProperties));
        Assert.assertEquals("Error running: 'step1': Binding output: 'step1_publish_1 failed',\n" +
                "\tError in evaluating expression: 'flow_var',\n" +
                "\tError in running script expression: 'flow_var',\n" +
                "\tException is: name 'flow_var' is not defined", exception.getMessage());
    }

}
