/*
 * 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.utils.ThreadProperty;
import gherkin.events.PickleEvent;
import gherkin.pickles.PickleLocation;
import gherkin.pickles.PickleTag;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;

@Aspect
public class RunOnTagAspect {

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

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

    /**
     * Allows conditional scenario execution.
     * If the scenario contains the following tag:
     * <dl>
     *    <dt>\@runOnEnv(param)</dt>
     *<dd>The scenario will only be executed if the param is defined when test is launched. Configuration map object.
     * More than one param can be passed in the tag. To do so, the params must be comma separated:
     * \@runOnEnv(param): The scenario will only be executed if the param is defined when test is launched.
     * \@runOnEnv(param1,param2,param3): The scenario will only be executed if ALL the params are defined.
     * </dd>
     * </dl>
     * Additionally, if the scenario contains the following tag:
     * <dl>
     *    <dt>\@skipOnEnv(param)</dt>
     *<dd>The scenario will be omitted if the param is defined when test is launched.
     * More than one param can be passed in the tag. To do so, the params must be comma separated.
     * The scenario will omitted if ANY of params are defined. (OR)</dd>
     *
     *<dd>Or in separated lines to force ALL of the params to be defined in order to omit the execution</dd>
     *    <dt>  \@skipOnEnv(param1)
     *          \@skipOnEnv(param2)
     *          \@skipOnEnv(param3)</dt>
     *<dd>The scenario will omitted if ALL of params are defined. (AND)</dd>
     *</dl>
     *
     * @param pickleEvent pickleEvent
     * @throws Throwable exception
     */
    @Before(value = "AddRunOnTagPointcutScenario(pickleEvent)")
    public void aroundAddRunOnTagPointcut(JoinPoint jp, PickleEvent pickleEvent) throws Throwable {
        int line = 0;
        if (!pickleEvent.pickle.getLocations().isEmpty()) {
            line = pickleEvent.pickle.getLocations().get(0).getLine();
        }
        try {
            Boolean exit = tagsIteration(pickleEvent.pickle.getTags(), line);
            if (exit) {
                ThreadProperty.set("skippedOnParams" + pickleEvent.pickle.getName() + line, "true");
            }
        } catch (Exception e) {
            logger.error(e.getMessage());
            pickleEvent.pickle.getTags().add(new PickleTag(new PickleLocation(line, 0), "@ignore"));
            pickleEvent.pickle.getTags().add(new PickleTag(new PickleLocation(line, 0), "@envCondition"));
        }
    }

    public boolean tagsIteration(List<PickleTag> tags, Integer line) throws Exception {
        PickleLocation pickleLocation = new PickleLocation(line, 0);
        for (PickleTag tag : tags) {
            if (tag.getName().contains("@runOnEnv")) {
                if (!checkParams(getParams(tag.getName()))) {
                    tags.add(new PickleTag(pickleLocation, "@ignore"));
                    tags.add(new PickleTag(pickleLocation, "@envCondition"));
                    return true;
                }
            } else if (tag.getName().contains("@skipOnEnv")) {
                if (checkParams(getParams(tag.getName()))) {
                    tags.add(new PickleTag(pickleLocation, "@ignore"));
                    tags.add(new PickleTag(pickleLocation, "@envCondition"));
                    return true;
                }
            }
        }
        return false;
    }

    /*
    * Returns a string array of params
    */
    public String[][] getParams(String s) throws Exception {
        String delimeters = ",|&&|\\|\\|";
        String[] val = s.substring((s.lastIndexOf("(") + 1), (s.length()) - 1).split(delimeters);
        if (val[0].startsWith("@")) {
            throw new Exception ("Error while parsing params. Format is: \"runOnEnv(PARAM)\", but found: " + s);
        }

        String[] ops;
        // Only valid operators: AND -> &&; OR -> ||
        // Valid variables names and values can only contain: characters, numbers, underscores, hyphens, dots and equal
        if (s.contains("&&") || s.contains("||")) {
            ops = s.substring((s.lastIndexOf("(") + 1), (s.length()) - 1).split("[a-zA-Z._\\-0-9=<>]+");
            if (ops.length > 0) {
                ops = Arrays.copyOfRange(ops, 1, ops.length);
            }
            if (ops.length != val.length - 1) {
                throw new Exception("Error in expression. Number of conditional operators plus 1 should be equal to the number of expressions.");
            }
            for (String op: ops) {
                if (!("||".equals(op) || "&&".equals(op))) {
                    throw new Exception("Error in conditional operators. Operators should be && or ||.");
                }
            }
        } else {
            ops = new String[] {};
        }

        String[][] result = new String[][] {val, ops};
        return result;
    }

   /*
    * Checks if every param in the array of strings is defined
    */
    public boolean checkParams(String[][] params) throws Exception {
        if ("".equals(params[0][0])) {
            throw new Exception("Error while parsing params. Params must be at least one");
        }

        boolean result = true;
        // Primer elemento
        if (params[0][0].contains("=")) {
            result = firstElementOperator(params[0][0], "=");
        } else if (params[0][0].contains(">")) {
            result = firstElementOperator(params[0][0], ">");
        } else if (params[0][0].contains("<")) {
            result = firstElementOperator(params[0][0], "<");
        } else {
            if (System.getProperty(params[0][0], "").isEmpty() && ThreadProperty.get(params[0][0]) == null) {
                result = false;
            }
        }

        // Elementos intermedios
        for (int j = 1; j < params[0].length - 1; j++) {
            if (params[0][j].contains("=")) {
                result = elementOperator(params[0][j], "=", params[1], j - 1, result);
            } else if (params[0][j].contains(">")) {
                result = elementOperator(params[0][j], ">", params[1], j - 1, result);
            } else if (params[0][j].contains("<")) {
                result = elementOperator(params[0][j], "<", params[1], j - 1, result);
            } else {
                if (System.getProperty(params[0][j], "").isEmpty() && ThreadProperty.get(params[0][j]) == null) {
                    result = updateResultOperation(params[1], j - 1, result, false);
                } else {
                    result = updateResultOperation(params[1], j - 1, result, true);
                }

            }
        }

        // Ăšltimo elemento
        if (params[0].length > 1) {
            if (params[0][params[0].length - 1].contains("=")) {
                result = elementOperator(params[0][params[0].length - 1], "=", params[1], params[1].length - 1, result);
            } else if (params[0][params[0].length - 1].contains(">")) {
                result = elementOperator(params[0][params[0].length - 1], ">", params[1], params[1].length - 1, result);
            } else if (params[0][params[0].length - 1].contains("<")) {
                result = elementOperator(params[0][params[0].length - 1], "<", params[1], params[1].length - 1, result);
            } else {
                if (System.getProperty(params[0][params[0].length - 1], "").isEmpty() && ThreadProperty.get(params[0][params[0].length - 1]) == null) {
                    result = updateResultOperation(params[1], params[1].length - 1, result, false);
                } else {
                    result = updateResultOperation(params[1], params[1].length - 1, result, true);
                }
            }
        }
        return result;
    }

    private boolean firstElementOperator(String element, String operador) throws Exception {
        boolean result = true;
        String param = element.split(operador)[0];
        String value = element.split(operador)[1];
        String property = System.getProperty(param) != null ? System.getProperty(param, "") : ThreadProperty.get(param) != null ? ThreadProperty.get(param) : "";
        if (property.isEmpty()) {
            result = false;
        } else if (value.contains(".") && property.contains(".")) {
            if (!checkVersion(operador.charAt(0), param, value)) {
                result = false;
            }
        } else if (operador.equals("=") && !value.equals(property)) {
            result = false;
        } else if (operador.equals(">") && !(property.compareTo(value) > 0)) {
            result = false;
        } else if (operador.equals("<") && !(property.compareTo(value) < 0)) {
            result = false;
        }
        return result;
    }

    private boolean elementOperator(String element, String operador, String[] operations, int posop, boolean result) throws Exception {
        boolean res = result;
        String param = element.split(operador)[0];
        String value = element.split(operador)[1];
        String property = System.getProperty(param) != null ? System.getProperty(param, "") : ThreadProperty.get(param) != null ? ThreadProperty.get(param) : "";
        if (property.isEmpty()) {
            res =  updateResultOperation(operations, posop, result, false);
        } else if (value.contains(".") && property.contains(".")) {
            if (!checkVersion(operador.charAt(0), param, value)) {
                res =  updateResultOperation(operations, posop, result, false);
            } else {
                res =  updateResultOperation(operations, posop, result, true);
            }
        } else if (operador.equals("=")) {
            if (!value.equals(property)) {
                res =  updateResultOperation(operations, posop, result, false);
            } else {
                res =  updateResultOperation(operations, posop, result, true);
            }
        } else if (operador.equals(">")) {
            if (!(property.compareTo(value) > 0)) {
                res =  updateResultOperation(operations, posop, result, false);
            } else {
                res =  updateResultOperation(operations, posop, result, true);
            }
        } else if (operador.equals("<")) {
            if (!(property.compareTo(value) < 0)) {
                res =  updateResultOperation(operations, posop, result, false);
            } else {
                res =  updateResultOperation(operations, posop, result, true);
            }
        }
        return res;
    }

    private boolean updateResultOperation (String[] param, int pos, boolean result, boolean valor) throws Exception {
        if (param.length == 0) {
            return result && valor;
        } else if ("&&".equals(param[pos])) {
            return result && valor;
        } else {
            return result || valor;
        }
    }

    private boolean checkVersion(char operador, String param, String value) throws Exception {
        boolean result = true;
        String regexp = "^[[[0-9]+.]+[0-9]+][-[[0-9]+.]+[0-9]+]*";
        String property = System.getProperty(param) != null ? System.getProperty(param, "") : ThreadProperty.get(param) != null ? ThreadProperty.get(param) : "";
        String envVarValue = property.replaceAll("-(SNAPSHOT|[a-zA-Z0-9]{7}|M[1-9]|RC[1-9])[0-9]", "error").replaceAll("-(SNAPSHOT|[a-zA-Z0-9]{7}|M[1-9]|RC[1-9])", "");
        if (!Pattern.matches(regexp, envVarValue) || !Pattern.matches(regexp, value)) {
            throw new Exception("Error while parsing params. The versions have some characters that are not numbers, '.' or '-' or an invalid format");
        } else if (operador == '=') {
            if (value.contains("-") || envVarValue.contains("-")) {
                String[] paramversion = envVarValue.split("-");
                String[] valueversion = value.split("-");
                if (paramversion.length != valueversion.length) {
                    result = false;
                } else {
                    int j = 0;
                    while (j < paramversion.length && result) {
                        String[] parver = paramversion[j].split("\\.");
                        String[] valver = valueversion[j].split("\\.");
                        if (parver.length != valver.length) {
                            result = false;
                        } else {
                            int z = 0;
                            while (z < parver.length && result) {
                                if (Integer.parseInt(parver[z]) != Integer.parseInt(valver[z])) {
                                    result = false;
                                }
                                z++;
                            }
                        }
                        j++;
                    }
                }
            } else {
                String[] parver = envVarValue.split("\\.");
                String[] valver = value.split("\\.");
                if (parver.length != valver.length) {
                    result = false;
                } else {
                    int z = 0;
                    while (z < parver.length && result) {
                        if (Integer.parseInt(parver[z]) != Integer.parseInt(valver[z])) {
                            result = false;
                        }
                        z++;
                    }
                }
            }
        } else {
            if ((value.contains("-") || envVarValue.contains("-"))) {
                String[] paramversion = envVarValue.split("-");
                String[] valueversion = value.split("-");
                if (operador == '>' && paramversion.length < valueversion.length) {
                    result = false;
                } else if (operador == '<' && paramversion.length > valueversion.length) {
                    result = false;
                } else {
                    int size = paramversion.length;
                    if (valueversion.length < size) {
                        size = valueversion.length;
                    }
                    int countversion = 0;
                    int j = 0;
                    while (j < size && result) {
                        String[] parver = paramversion[j].split("\\.");
                        String[] valver = valueversion[j].split("\\.");
                        if (parver.length != valver.length) {
                            throw new Exception("Error while parsing params. The versions must have the same number of elements");
                        } else {
                            int count = 0;
                            int z = 0;
                            while (z < parver.length && result) {
                                if (operador == '>' && Integer.parseInt(parver[z]) < Integer.parseInt(valver[z])) {
                                    result = false;
                                } else if (operador == '<' && Integer.parseInt(parver[z]) > Integer.parseInt(valver[z])) {
                                    result = false;
                                } else if (Integer.parseInt(parver[z]) == Integer.parseInt(valver[z])) {
                                    count = count + 1;
                                } else {
                                    z = parver.length;
                                    j = size;
                                }
                                z++;
                            }
                            if (count == parver.length) {
                                countversion = countversion + 1;
                            }
                        }
                        j++;
                    }
                    if (countversion == size && paramversion.length == valueversion.length) {
                        result = false;
                    }
                }
            } else {
                String[] parver = envVarValue.split("\\.");
                String[] valver = value.split("\\.");
                if (parver.length != valver.length) {
                    throw new Exception("Error while parsing params. The versions must have the same number of elements");
                }
                int count = 0;
                int z = 0;
                while (z < parver.length && result) {
                    if (operador == '>' && Integer.parseInt(parver[z]) < Integer.parseInt(valver[z])) {
                        result = false;
                    } else if (operador == '<' && Integer.parseInt(parver[z]) > Integer.parseInt(valver[z])) {
                        result = false;
                    } else if (Integer.parseInt(parver[z]) == Integer.parseInt(valver[z])) {
                        count = count + 1;
                    } else {
                        z = parver.length;
                    }
                    z++;
                }
                if (count == parver.length) {
                    result = false;
                }
            }
        }
        return result;
    }

}