/*
 * Copyright (C) 2014 Stratio (http://stratio.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.stratio.qa.aspects;

import com.stratio.qa.cucumber.runner.Glue;
import com.stratio.qa.cucumber.testng.CucumberReporter;
import com.stratio.qa.cucumber.testng.TestSourcesModelUtil;
import com.stratio.qa.exceptions.NonReplaceableException;
import com.stratio.qa.specs.CommonG;
import com.stratio.qa.specs.HookGSpec;
import com.stratio.qa.utils.ExceptionList;
import com.stratio.qa.utils.StepException;
import com.stratio.qa.utils.ThreadProperty;
import cucumber.api.PickleStepTestStep;
import cucumber.api.Result.Type;
import cucumber.api.Scenario;
import cucumber.runner.AmbiguousStepDefinitionsException;
import cucumber.runner.EventBus;
import cucumber.runtime.Backend;
import cucumber.runtime.CucumberException;
import cucumber.runtime.RuntimeOptions;
import cucumber.runtime.StepDefinitionMatch;
import gherkin.events.PickleEvent;
import gherkin.pickles.*;
import gherkin.pickles.Argument;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class ReplacementAspect {

    private final Logger logger = LoggerFactory.getLogger(this.getClass().getCanonicalName());

    private Glue glue;

    private List<String> undefinedSteps = new ArrayList<>();

    private String lastEchoedStep = "";

    @Pointcut("execution (cucumber.runner.Runner.new(..)) && "
            + "args (bus, backends, runtimeOptions)")
    protected void runnerInit(EventBus bus, Collection<? extends Backend> backends, RuntimeOptions runtimeOptions) {
    }

    @After(value = "runnerInit(bus, backends, runtimeOptions)")
    public void runnerInitGlue(JoinPoint jp, EventBus bus, Collection<? extends Backend> backends, RuntimeOptions runtimeOptions) throws Throwable {
        glue = new Glue(bus);
        for (Backend backend : backends) {
            backend.loadGlue(glue, runtimeOptions.getGlue());
        }
    }

    @Pointcut("execution (* cucumber.runtime.formatter.DefaultSummaryPrinter.printSnippets(..))")
    protected void print() {
    }

    @Around(value = "print()")
    public void printSnippets(ProceedingJoinPoint pjp) throws Throwable {
        if (!undefinedSteps.isEmpty()) {
            logger.error("The following steps are undefined:");
            for (String undefinedStep:undefinedSteps) {
                logger.error("    {}", undefinedStep);
            }
        }
    }

    @Pointcut("execution (* cucumber.runner.Runner.runPickle(..)) && "
            + "args (pickle)")
    protected void replacementScenarios(PickleEvent pickle) {
    }

    @Before(value = "replacementScenarios(pickle)")
    public void aroundScenarios(JoinPoint jp, PickleEvent pickle) throws Throwable {
        String scenarioName = pickle.pickle.getName();
        String newScenarioName;
        try {
            newScenarioName = replacedElement(scenarioName, jp);
        } catch (Exception e) {
            ExceptionList.INSTANCE.getExceptions().add(e);
            newScenarioName = scenarioName + " | Placeholder not replaced -- " + e.toString();
        }
        if (!newScenarioName.equals(pickle.pickle.getName())) {
            Pickle pickle1 = pickle.pickle;
            Field field = null;
            Class current = pickle1.getClass();
            do {
                try {
                    field = current.getDeclaredField("name");
                } catch (Exception e) { }
            } while ((current = current.getSuperclass()) != null);

            field.setAccessible(true);
            field.set(pickle1, newScenarioName);
        }
    }

    @Pointcut("execution (* cucumber.runner.TestStep.executeStep(..)) && "
            + "args (scenario, skipSteps)")
    protected void replacementStar(Scenario scenario, boolean skipSteps) {
    }

    @Around(value = "replacementStar(scenario, skipSteps)")
    public Type aroundReplacementStar(ProceedingJoinPoint pjp, Scenario scenario, boolean skipSteps) throws Throwable {
        if (pjp.getTarget() instanceof PickleStepTestStep) {
            if (StepException.INSTANCE.getException() != null) {
                return Type.SKIPPED;
            } else {
                if (scenario.getSourceTagNames().contains("@error")) {
                    for (String tag: scenario.getSourceTagNames()) {
                        if (tag.contains("errorMessage")) {
                            String msg = tag.substring((tag.lastIndexOf("(") + 1), (tag.length()) - 1).replaceAll("__", " ");
                            logger.error("-> " + msg);
                            throw new CucumberException(msg);
                        }
                    }
                }
                try {
                    PickleStepTestStep pickleTestStep = (PickleStepTestStep) pjp.getTarget();
                    PickleStep step = pickleTestStep.getPickleStep();

                    // Replace elements in datatable
                    StringBuilder sbDataTable = new StringBuilder();
                    List<Argument> argumentList = new ArrayList<>(step.getArgument());
                    for (int a = 0; a < argumentList.size(); a++) {
                        if (argumentList.get(a) instanceof PickleTable) {
                            PickleTable pickleTable = (PickleTable) argumentList.get(a);
                            List<PickleRow> pickleRowList = new ArrayList<>(pickleTable.getRows());
                            for (int r = 0; r < pickleRowList.size(); r++) {
                                PickleRow pickleRow = pickleRowList.get(r);
                                List<PickleCell> pickleCellList = new ArrayList<>(pickleRow.getCells());
                                sbDataTable.append("| ");
                                for (int c = 0; c < pickleCellList.size(); c++) {
                                    PickleCell pickleCell = pickleCellList.get(c);
                                    pickleCellList.set(c, new PickleCell(pickleCell.getLocation(), replacedElement(pickleCell.getValue(), pjp)));
                                    sbDataTable.append(pickleCellList.get(c).getValue()).append(" | ");
                                }
                                pickleRowList.set(r, new PickleRow(pickleCellList));
                                sbDataTable.append("\n");
                            }
                            pickleTable = new PickleTable(pickleRowList);
                            argumentList.set(a, pickleTable);
                        }
                    }

                    // Set new datatable in PickleStep object
                    step = new PickleStep(step.getText(), argumentList, step.getLocations());

                    // Replace elements in step
                    String uri = pickleTestStep.getStepLocation().split(":")[0];
                    String stepName = step.getText();
                    String newName = replacedElement(stepName, pjp);
                    String keyword = TestSourcesModelUtil.INSTANCE.getTestSourcesModel().getKeywordFromSource(scenario.getUri(), pickleTestStep.getStepLine());
                    if (!stepName.equals(newName)) {
                        //field up to BasicStatement, from Step and ExampleStep
                        Field field = null;
                        Class current = step.getClass();
                        do {
                            try {
                                field = current.getDeclaredField("text");
                            } catch (Exception e) {
                            }
                        } while ((current = current.getSuperclass()) != null);

                        field.setAccessible(true);
                        field.set(step, newName.replaceAll("(\\r|\\n|\\r\\n)+", "\\\\n"));
                        scenario.write(newName);
                    }
                    step = new PickleStep(step.getText(), argumentList, step.getLocations());

                    TestSourcesModelUtil.INSTANCE.getTestSourcesModel().addReplacedStep(scenario.getUri(), pickleTestStep.getStepLine(), step);
                    lastEchoedStep = pickleTestStep.getStepText();
                    if (HookGSpec.loggerEnabled) {
                        logger.info("   {}{}", keyword, newName);
                        if (!sbDataTable.toString().isEmpty()) {
                            logger.info("  {}", sbDataTable.toString());
                        }
                    }

                    // Run step
                    try {
                        StepDefinitionMatch definitionMatch = glue.stepDefinitionMatch(uri, step);
                        if (definitionMatch != null) {
                            if (!skipSteps) {
                                definitionMatch.runStep(scenario);
                                return Type.PASSED;
                            } else {
                                definitionMatch.dryRunStep(scenario);
                                return Type.SKIPPED;
                            }
                        } else {
                            logger.error("Undefined step!! {}", newName);
                            String undefinedStep = scenario.getUri() + " # " + newName;
                            if (!undefinedSteps.contains(undefinedStep)) {
                                undefinedSteps.add(undefinedStep);
                            }
                            return Type.UNDEFINED;
                        }
                    } catch (AmbiguousStepDefinitionsException asde) {
                        return (Type) pjp.proceed();
                    }
                } catch (Exception e) {
                    StepException.INSTANCE.setException(e);
                    throw e;
                }
            }
        }
        return (Type) pjp.proceed();
    }

    protected String replacedElement(String el, JoinPoint jp) throws NonReplaceableException {
        if (el.contains("${")) {
            el = replaceEnvironmentPlaceholders(el, jp);
        }
        if (el.contains("!{")) {
            el = replaceReflectionPlaceholders(el, jp);
        }
        if (el.contains("@{")) {
            el = replaceCodePlaceholders(el, jp);
        }
        return el;
    }

    /**
     * Replaces every placeholded element, enclosed in @{} with the
     * corresponding attribute value in local Common class
     * <p>
     * If the element starts with:
     * - IP: We expect it to be followed by '.' + interface name (i.e. IP.eth0). It can contain other replacements.
     * <p>
     * If the element starts with:
     * - JSON: We expect it to be followed by '.' + path_to_json_file (relative to src/test/resources or
     * target/test-classes). The json is read and its content is returned as a string
     * <p>
     * If the element starts with:
     * - FILE: We expect it to be followed by '.' + path_to_file (relative to src/test/resources or
     * target/test-classes). The file is read and its content is returned as a string
     *
     * @param element element to be replaced
     * @param pjp JoinPoint
     * @return String
     * @throws NonReplaceableException exception
     */
    protected String replaceCodePlaceholders(String element, JoinPoint pjp) throws NonReplaceableException {
        String newVal = element;
        while (newVal.contains("@{")) {
            String placeholder = newVal.substring(newVal.indexOf("@{"), newVal.indexOf("}", newVal.indexOf("@{")) + 1);
            String property = placeholder.substring(2, placeholder.length() - 1).toLowerCase();
            String subproperty = "";
            CommonG commonJson;
            if (placeholder.contains(".")) {
                property = placeholder.substring(2, placeholder.indexOf(".")).toLowerCase();
                subproperty = placeholder.substring(placeholder.indexOf(".") + 1, placeholder.length() - 1);
            } else {
                if (pjp.getThis() instanceof CucumberReporter.TestMethod) {
                    return newVal;
                } else {
                    logger.error("{} -> {} placeholded element has not been replaced previously.", element, property);
                    throw new NonReplaceableException("Unreplaceable placeholder: " + placeholder);
                }
            }

            switch (property) {
                case "ip":
                    boolean found = false;
                    if (!subproperty.isEmpty()) {
                        Enumeration<InetAddress> ifs = null;
                        try {
                            ifs = NetworkInterface.getByName(subproperty).getInetAddresses();
                        } catch (SocketException e) {
                            this.logger.error(e.getMessage());
                        }
                        while (ifs.hasMoreElements() && !found) {
                            InetAddress itf = ifs.nextElement();
                            if (itf instanceof Inet4Address) {
                                String ip = itf.getHostAddress();
                                newVal = newVal.replace(placeholder, ip);
                                found = true;
                            }
                        }
                    }
                    if (!found) {
                        throw new NonReplaceableException("Interface " + subproperty + " not available");
                    }
                    break;
                case "json":
                case "file":
                    commonJson = new CommonG();
                    newVal = newVal.replace(placeholder, commonJson.retrieveData(subproperty, property));
                    break;
                default:
                    commonJson = new CommonG();
                    commonJson.getLogger().error("Replacement with an undefined option ({})", property);
                    newVal = newVal.replace(placeholder, "");
            }
        }
        return newVal;
    }


    /**
     * Replaces every placeholded element, enclosed in !{} with the
     * corresponding attribute value in local Common class
     *
     * @param element element to be replaced
     * @param pjp JoinPoint
     * @return String
     * @throws NonReplaceableException exception
     */
    protected String replaceReflectionPlaceholders(String element, JoinPoint pjp) throws NonReplaceableException {
        String newVal = element;
        while (newVal.contains("!{")) {
            String placeholder = newVal.substring(newVal.indexOf("!{"),
                    newVal.indexOf("}", newVal.indexOf("!{")) + 1);
            String attribute = placeholder.substring(2, placeholder.length() - 1);
            // we want to use value previously saved
            String prop = ThreadProperty.get(attribute);

            if (prop == null && (pjp.getThis() instanceof CucumberReporter.TestMethod)) {
                return element;
            } else if (prop == null) {
                logger.error("{} -> {} local var has not been saved correctly previously.", element, attribute);
                throw new NonReplaceableException("Unreplaceable placeholder: " + placeholder);
            } else {
                newVal = newVal.replace(placeholder, prop);
            }
        }
        return newVal;
    }


    /**
     * Replaces every placeholded element, enclosed in ${} with the
     * corresponding java property
     *
     * @param element element to be replaced
     * @param jp JoinPoint
     * @return String
     * @throws NonReplaceableException exception
     */
    protected String replaceEnvironmentPlaceholders(String element, JoinPoint jp) throws NonReplaceableException {
        String newVal = element;
        while (newVal.contains("${")) {
            String placeholder = newVal.substring(newVal.indexOf("${"),
                    newVal.indexOf("}", newVal.indexOf("${")) + 1);
            String modifier = "";
            String sysProp;
            String defaultValue = "";
            String prop;
            String placeholderAux = "";
            Boolean emptyDefault = false;

            if (placeholder.contains(":-")) {
                defaultValue = placeholder.substring(placeholder.indexOf(":-") + 2, placeholder.length() - 1);
                if ("''".equals(defaultValue)) {
                    emptyDefault = true;
                    defaultValue = "";
                }
                placeholderAux = placeholder.substring(0, placeholder.indexOf(":-")) + "}";
            }

            if (placeholderAux.contains(".")) {
                if (placeholder.contains(":-")) {
                    sysProp = placeholderAux.substring(2, placeholderAux.indexOf("."));
                    modifier = placeholderAux.substring(placeholderAux.indexOf(".") + 1, placeholderAux.length() - 1);
                } else {
                    sysProp = placeholder.substring(2, placeholder.indexOf("."));
                    modifier = placeholder.substring(placeholder.indexOf(".") + 1, placeholder.length() - 1);
                }
            } else {
                if (defaultValue.isEmpty() && !emptyDefault) {
                    if (placeholder.contains(".")) {
                        modifier = placeholder.substring(placeholder.indexOf(".") + 1, placeholder.length() - 1);
                        sysProp = placeholder.substring(2, placeholder.indexOf("."));
                    } else {
                        sysProp = placeholder.substring(2, placeholder.length() - 1);
                    }
                } else {
                    sysProp = placeholder.substring(2, placeholder.indexOf(":-"));
                }
            }

            if (defaultValue.isEmpty()) {
                if (emptyDefault) {
                    prop = System.getProperty(sysProp, defaultValue);
                } else {
                    prop = System.getProperty(sysProp);
                }

            } else {
                prop = System.getProperty(sysProp, defaultValue);
            }

            if (prop == null && (jp.getThis() instanceof CucumberReporter.TestMethod)) {
                return element;
            } else if (prop == null) {
                logger.error("{} -> {} env var has not been defined.", element, sysProp);
                throw new NonReplaceableException("Unreplaceable placeholder: " + placeholder);
            }

            if ("toLower".equals(modifier)) {
                prop = prop.toLowerCase();
            } else if ("toUpper".equals(modifier)) {
                prop = prop.toUpperCase();
            }
            newVal = newVal.replace(placeholder, prop);
        }

        // Allow setting empty string as default value
        if (newVal.equalsIgnoreCase("\"\"")) {
            newVal = "";
        }
        return newVal;
    }
}