/**
 * NoraUi is licensed under the license GNU AFFERO GENERAL PUBLIC LICENSE
 *
 * @author Nicolas HALLOUIN
 * @author Stéphane GRILLON
 */
package cucumber.runner;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import cucumber.api.Scenario;
import cucumber.runtime.CucumberException;
import cucumber.runtime.StepDefinition;
import cucumber.runtime.StepDefinitionMatch;
import cucumber.runtime.java.ParameterInfo;
import gherkin.pickles.PickleStep;
import io.cucumber.cucumberexpressions.CucumberExpressionException;
import io.cucumber.datatable.CucumberDataTableException;
import io.cucumber.datatable.UndefinedDataTableTypeException;
import io.cucumber.stepexpression.Argument;

public class PickleStepDefinitionMatch extends Match implements StepDefinitionMatch {

    private static final Logger LOGGER = LoggerFactory.getLogger(PickleStepDefinitionMatch.class.getName());

    private final StepDefinition stepDefinition;
    private final transient String featurePath;
    // The official JSON gherkin format doesn't have a step attribute, so we're marking this as transient
    // to prevent it from ending up in the JSON.
    private final transient PickleStep step;

    public PickleStepDefinitionMatch(List<Argument> arguments, StepDefinition stepDefinition, String featurePath, PickleStep step) {
        super(arguments, stepDefinition.getLocation(false));
        this.stepDefinition = stepDefinition;
        this.featurePath = featurePath;
        this.step = step;
    }

    @Override
    public void runStep(Scenario scenario) throws Throwable {
        LOGGER.debug("runStep {}", step.getText());

        List<Argument> arguments = getArguments();
        int argumentCount = arguments.size();

        Integer parameterCount = stepDefinition.getParameterCount();
        LOGGER.debug("parameterCount:{} argumentCount:{}", step.getText(), parameterCount, argumentCount);
        for (Argument ar : arguments) {
            LOGGER.debug("Argument: {}", ar);
        }

        if (parameterCount != null && (argumentCount > parameterCount || argumentCount + 1 < parameterCount)) {
            LOGGER.error("arityMismatch: {}", parameterCount);
            throw arityMismatch(parameterCount);
        }
        List<Object> result = new ArrayList<>();
        try {
            for (Argument argument : arguments) {
                LOGGER.debug("add argument {} to result", argument.getValue());
                result.add(argument.getValue());
            }
            // add List<GherkinStepCondition> or parameters Map<String, String>
            if (parameterCount != null && argumentCount + 1 == parameterCount) {
                List<?> parameters = stepDefinition.getParameters();
                Object obj;
                if (((ParameterInfo) parameters.get(parameterCount - 1)).getType().toString().startsWith("java.util.List<")) {
                    obj = new ArrayList<>();
                } else if (((ParameterInfo) parameters.get(parameterCount - 1)).getType().toString().startsWith("java.util.Map<")) {
                    obj = new HashMap<>();
                } else {
                    LOGGER.error("arityMismatch in add List<GherkinStepCondition> or parameters Map<String, String>: {}", parameterCount);
                    throw arityMismatch(parameterCount);
                }
                LOGGER.debug("add argument {} to result in add List<GherkinStepCondition> or parameters Map<String, String>", obj);
                result.add(obj);
            }
        } catch (UndefinedDataTableTypeException e) {
            LOGGER.error("UndefinedDataTableTypeException when add", e);
            throw registerTypeInConfiguration(e);
        } catch (CucumberExpressionException | CucumberDataTableException e) {
            LOGGER.error("CucumberExpressionException or CucumberDataTableException when add", e);
            throw couldNotConvertArguments(e);
        }

        try {
            LOGGER.debug("stepDefinition.execute {}", result.size());
            stepDefinition.execute(result.toArray(new Object[0]));
        } catch (CucumberException e) {
            LOGGER.error("CucumberException when stepDefinition.execute: {}", e);
            throw e;
        } catch (Throwable t) {
            LOGGER.error("Throwable when stepDefinition.execute: {}", t);
            throw removeFrameworkFramesAndAppendStepLocation(t, getStepLocation());
        }
    }

    private CucumberException registerTypeInConfiguration(Exception e) {
        return new CucumberException(
                String.format("" + "Could not convert arguments for step [%s] defined at '%s'.\n" + "It appears you did not register a data table type. The details are in the stacktrace below.", // TODO:
                                                                                                                                                                                                   // Add
                                                                                                                                                                                                   // doc
                                                                                                                                                                                                   // URL
                        stepDefinition.getPattern(), stepDefinition.getLocation(true)),
                e);
    }

    private CucumberException couldNotConvertArguments(Exception e) {
        return new CucumberException(String.format("Could not convert arguments for step [%s] defined at '%s'.\n" + "The details are in the stacktrace below.", stepDefinition.getPattern(),
                stepDefinition.getLocation(true)), e);
    }

    @Override
    public void dryRunStep(Scenario scenario) throws Throwable {
        // Do nothing
    }

    private CucumberException arityMismatch(int parameterCount) {
        List<String> arguments = createArgumentsForErrorMessage();
        return new CucumberException(String.format("Step [%s] is defined with %s parameters at '%s'.\n" + "However, the gherkin step has %s arguments%sStep text: %s", stepDefinition.getPattern(),
                parameterCount, stepDefinition.getLocation(true), arguments.size(), formatArguments(arguments), step.getText()));
    }

    private String formatArguments(List<String> arguments) {
        if (arguments.isEmpty()) {
            return ".\n";
        }

        StringBuilder formatted = new StringBuilder(":\n");
        for (String argument : arguments) {
            formatted.append(" * ").append(argument).append("\n");
        }
        return formatted.toString();
    }

    private List<String> createArgumentsForErrorMessage() {
        List<String> arguments = new ArrayList<>(getArguments().size());
        for (Argument argument : getArguments()) {
            arguments.add(argument.toString());
        }
        return arguments;
    }

    Throwable removeFrameworkFramesAndAppendStepLocation(Throwable error, StackTraceElement stepLocation) {
        StackTraceElement[] stackTraceElements = error.getStackTrace();
        if (stackTraceElements.length == 0 || stepLocation == null) {
            return error;
        }

        int newStackTraceLength;
        for (newStackTraceLength = 1; newStackTraceLength < stackTraceElements.length; ++newStackTraceLength) {
            if (stepDefinition.isDefinedAt(stackTraceElements[newStackTraceLength - 1])) {
                break;
            }
        }
        StackTraceElement[] newStackTrace = new StackTraceElement[newStackTraceLength + 1];
        System.arraycopy(stackTraceElements, 0, newStackTrace, 0, newStackTraceLength);
        newStackTrace[newStackTraceLength] = stepLocation;
        error.setStackTrace(newStackTrace);
        return error;
    }

    public String getPattern() {
        return stepDefinition.getPattern();
    }

    StackTraceElement getStepLocation() {
        return new StackTraceElement("✽", step.getText(), featurePath, getStepLine(step));
    }

    public Match getMatch() {
        return this;
    }

    StepDefinition getStepDefinition() {
        return stepDefinition;
    }

    @Override
    public String getCodeLocation() {
        return stepDefinition.getLocation(false);
    }

    private static int getStepLine(PickleStep step) {
        return step.getLocations().get(step.getLocations().size() - 1).getLine();
    }
}