package org.matrix.actuators.move;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.matrix.actuators.Actuator;
import org.matrix.actuators.httpclient.HttpClientActuator;
import org.matrix.actuators.httpclient.HttpRequestDetail;
import org.matrix.actuators.httpclient.HttpResponseDetail;
import org.matrix.actuators.sql.SqlExpActuator;
import org.matrix.actuators.sql.SqlExpDetail;
import org.matrix.actuators.usecase.CaseActuator;
import org.matrix.actuators.util.CompleteExpressionUtil;
import org.matrix.database.entity.Action;
import org.matrix.database.entity.Move;
import org.matrix.database.entity.TestCaseBTO;
import org.matrix.database.service.IMoveService;
import org.matrix.database.service.ITestDataService;
import org.matrix.database.service.impl.ActionServiceImpl;
import org.matrix.enums.ActionType;
import org.matrix.enums.MoveType;
import org.matrix.exception.GlobalException;
import org.matrix.socket.queue.LogQueueRuntime;
import org.matrix.util.BeanFlattener;
import org.matrix.util.JsonFormatUtil;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static org.matrix.actuators.move.MoveStrategy.*;
import static org.matrix.enums.ActionType.*;
import static org.matrix.enums.ModuleType.MOVE_ACTUATOR;
import static org.matrix.enums.ModuleType.SYNTAX_PARSE;

/**
 * MoveActuator. 行为执行器
 *
 * @author Matrix <xhyrzldf@gmail.com>
 * @since 2022/2/25 at 3:32 PM
 * Suffering is the most powerful teacher of life.
 */
@SuppressWarnings("AlibabaLowerCamelCaseVariableNaming")
@Slf4j
@Component
public class MoveActuator implements Actuator {

    /**
     * 识别行为语法的正则字符串
     */
    private static final String MOVE_POOL_REG = "\\$\\{(?<col>[\\w|.]*)}(\\[(?<row>\\d*)])?(?<jp><(\\s)*\\$.*?>)?";

    private static final List<String> STRATEGY_LIST = Arrays.asList(PRE_MOVE.name(), MID_MOVE.name(), AFT_MOVE.name());
    private final CompleteExpressionUtil resultUtil;
    private final ActionServiceImpl actionService;
    private final SqlExpActuator sqlExpActuator;
    private final HttpClientActuator httpActuator;
    private final ITestDataService dataService;
    private final IMoveService moveService;
    private final CaseActuator caseActuator;
    private final CompleteExpressionUtil expressionUtil;
    /**
     * 动作临时结果保存的的ThreadLocal的结果集保存,key = strategy_actionId (例如PRE_MOVE_1)，value 是 数据表，如果存在则add，如果不存在则put
     */
    private ThreadLocal<Map<String, List<Map<String, Object>>>> resSet = ThreadLocal.withInitial(HashMap::new);

    public MoveActuator(CompleteExpressionUtil resultUtil, ActionServiceImpl actionService, SqlExpActuator sqlExpActuator, HttpClientActuator httpActuator, ITestDataService dataService, IMoveService moveService, CaseActuator caseActuator, CompleteExpressionUtil expressionUtil) {
        this.resultUtil = resultUtil;
        this.actionService = actionService;
        this.sqlExpActuator = sqlExpActuator;
        this.httpActuator = httpActuator;
        this.dataService = dataService;
        this.moveService = moveService;
        this.caseActuator = caseActuator;
        this.expressionUtil = expressionUtil;
    }

    /**
     * 将含有动作结果池语法 ${pre1.name}[0]<$.book> 这样的语法 ${a}[b]<c>
     * 其中a是定位列,b是定位行数,c是内容的二次JSON解析
     * 可能出现的情况是
     * <blockquote>
     * <pre>
     *         a -> 默认第一行,直接返回某个列的数据 例如 {pre1.name}
     *         ab -> 第a列第b行，直接返回 例如 {pre1.name}[1]
     *         ac -> 第a列第一行，取到的数据用c去解析 {pre1.name}<$.book>
     *         abc -> 第a列第b行，取到的数据用c去解析 {pre1.name}[1]<$.book>
     *     </pre>
     * </blockquote>
     *
     * @param dynamicString 含有动作结果池的字段 ${pre1.name}[0]<$.book> 简写就是 ${a}[b]<c>
     * @return 找出动作结果池语法里其中的变量名与对应的下标序列值(默认为0)
     */
    public String parseMoveVar(String dynamicString) {
        Pattern pattern = Pattern.compile(MOVE_POOL_REG);
        Matcher matcher = pattern.matcher(dynamicString);
        String col, row, jsonPath;
        int rowIndex;
        boolean pathResolution;
        // 提取、提取、补全 col,row,json_path内容
        if (matcher.find()) {
            col = matcher.group("col");
            if (StringUtils.isEmpty(col)) {
                throw new GlobalException("[行为执行器]动作语法里必须包含形如${pre1.name}这样的字符串!您提供的是: " + dynamicString);
            }

            row = matcher.group("row");
            if (StringUtils.isEmpty(row)) {
                rowIndex = 0;
            } else {
                rowIndex = Integer.parseInt(row);
            }

            jsonPath = matcher.group("jp");
            if (StringUtils.isEmpty(jsonPath)) {
                pathResolution = false;
            } else {
                pathResolution = true;
            }
        } else {
            throw new GlobalException("[行为执行器]提供的语法不正确！正确的语法应当是形如 ${pre1.name}[0]<$.book> 这样的，您提供的是 :" + dynamicString);
        }

        // 解析,解释 col、row、jp
        MoveRegularObject mro = col2RegularObj(col);
        List<Map<String, Object>> valueMap = resSet.get().get(mro.getKey());
        if (valueMap.size() <= rowIndex) {
            throw new GlobalException(String.format("没有找到指定行的数据，您要求的数据行数为%d,实际存在的数据行数为%d", rowIndex, valueMap.size()));
        }

        String finalValue = null;
        if (valueMap.get(rowIndex).get(mro.getColName()) == null) {
            log.warn("[行为执行器] {}语法下的 {} 列 数据为 null", dynamicString, mro.getColName());
        } else {
            finalValue = valueMap.get(rowIndex).get(mro.getColName()).toString();
        }
        // 是否需要继续解析jsonPath
        if (pathResolution) {
            finalValue = expressionUtil.completeJsonPathExpression(jsonPath, finalValue);
        }
        LogQueueRuntime.addNewLog(this.getClass(), SYNTAX_PARSE, String.format("初始语法 = %s ,解析结果 = %s", dynamicString, finalValue));
        return finalValue;
    }

    /**
     * 中间行为执行器，根据运行时环境决定使用哪一个动作，并根据动作的类型调用不同的动作执行器(该方法用于中间动作与后置动作)
     * 其中中间动作与后置动作需要进行结果集数据的替换，然后根据动作类型，策略，ID 来制作主键
     * key = actionType_strategy_actionId
     * 当 动作类型是 SQL 类型时，value  = 数据表
     * 当 动作类型是 HTTP/CASE 类型时, value = detail对象的属性(key=属性名,value=属性数值)
     *
     * @param moveId         行为id
     * @param envId          环境id
     * @param projectId      项目id
     * @param caseResultData 测试用例执行后保留下的结果集,该参数在前置策略中可以为null,在中间/后置策略中必须要提供合理的结果集对象
     * @param strategy       动作的策略，即是前置/中间/后置,具体查看{@link MoveStrategy}
     */
    @SuppressWarnings("DuplicatedCode")
    public void runMove(Long moveId, Long envId, Long projectId, String caseResultData, MoveStrategy strategy) {
        Move move = Optional.ofNullable(moveService.getById(moveId))
                .orElseThrow(() -> new GlobalException(String.format("没有找到指定的action动作对象，提供查询的moveId = %d,envId = %d", moveId, envId)));

        Action action = actionService.findByMoveAndEnv(moveId, envId)
                .orElseThrow(() -> new GlobalException(String.format("没有找到指定的action动作对象，提供查询的moveId = %d,envId = %d", moveId, envId)));
        LogQueueRuntime.addNewLog(this.getClass(), MOVE_ACTUATOR, String.format("准备执行行为动作,动作策略: %s,行为Id =%d ,动作id = %d", strategy, moveId, action.getId()));

        // 如果是 mid 或者 aft 动作，则需要进行结果集的语法替换 与 空值判断
        if (strategy != PRE_MOVE) {
            if (Objects.isNull(caseResultData)) {
                throw new GlobalException(String.format("中间与后置动作必须要提供结果集! moveId = %d,envId = %d", moveId, envId));
            }
            // 尝试解析并替换动作中要用到用例结果集的情况
            String runtimeDetail = resultUtil.completeJsonPathExpression(action.getDetail(), caseResultData);
            action.setDetail(runtimeDetail);
        }

        // 拼接key,准备数据容器
        ActionType actionType = action.getType();
        String runtimeDetail = action.getDetail();
        String key = Lists.newArrayList(strategy, moveId)
                .stream().map(Object::toString)
                .collect(Collectors.joining("_"));
        Map<String, List<Map<String, Object>>> res = resSet.get();

        // 分类处理 + 结果集处理(如果不存在则put数据，如果存在则替换数据)
        LogQueueRuntime.addNewLog(this.getClass(), MOVE_ACTUATOR, String.format("正在执行动作 actionId = %d,动作类型 = %s,动作策略 = %s,动作参数 = %s", action.getId(), action, strategy, runtimeDetail));
        if (actionType == SQL_ACTION) {
            List<Map<String, Object>> resultMap = sqlActionHandler(envId, projectId, runtimeDetail);
            res.put(key, resultMap);
        } else if (actionType == HTTP_ACTION) {
            HttpResponseDetail detail = httpActionHandler(envId, projectId, runtimeDetail);
            Map<String, Object> detailMap = detail2Map(detail);
            moveTypeHandler(strategy, move, detail, detailMap);
            res.put(key, Lists.newArrayList(detailMap));
        } else if (actionType == CASE_ACTION) {
            HttpResponseDetail detail = caseActionHandler(envId, projectId, runtimeDetail);
            Map<String, Object> detailMap = detail2Map(detail);
            moveTypeHandler(strategy, move, detail, detailMap);
            res.put(key, Lists.newArrayList(detailMap));
        } else if (actionType == WAIT_ACTION) {
            waitActionHandler(runtimeDetail);
        } else {
            throw new GlobalException("[行为执行器]不支持的行为类型: " + actionType);
        }

        LogQueueRuntime.addNewLog(getClass(),MOVE_ACTUATOR, String.format(
                "当前执行的行为策略 = %s, 行为名 = %s, 动作名 = %s , 动作id = %d , 动作池里的变量 = %s",
                strategy.getDes(),move.getName(),action.getName(),action.getId(), JsonFormatUtil.formatJson(JSONObject.toJSONString(res))));
        log.info("[动作执行器] 动作执行完毕");
    }

    /**
     * 依据行为的动作类型来决定操作逻辑
     *
     * @param strategy  行为策略
     * @param move      行为
     * @param detail    动作执行后的返回值对象
     * @param detailMap 用于存放到动作结果池里的Map
     */
    private void moveTypeHandler(MoveStrategy strategy, Move move, HttpResponseDetail detail, Map<String, Object> detailMap) {
        if (strategy == PRE_MOVE && move.getMoveType() == MoveType.LOGIN) {
            Arrays.stream(detail.getResponse().getHeaders("SET_COOKIE"))
                    .filter(header -> header.getValue().contains("JSESSIONID"))
                    .findFirst()
                    .ifPresent(session -> detailMap.put("SESSION_ID", session));
        }
    }

    /**
     * 清理结果集
     */
    public void clearMoveRes() {
        log.info("[动作执行器] 清理动作结果池缓存");
        resSet = ThreadLocal.withInitial(HashMap::new);
    }

    /**
     * 线程等待执行器
     *
     * @param runtimeDetail 要等待的时间，单位毫秒ms
     */
    private void waitActionHandler(String runtimeDetail) {
        try {
            int sleepMills = Integer.parseInt(runtimeDetail);
            Thread.sleep(sleepMills);
        } catch (InterruptedException e) {
            log.error("[线程异常] 线程休眠时发生异常,异常信息 : " + e.getMessage());
        } catch (Exception e) {
            log.error("[参数/转换异常] 执行行为时参数抓换时发生异常，异常信息 " + e.getMessage());
        }
    }

    /**
     * 将行为语法里的col部分转化为threadLocal里的key值
     *
     * @param col 原始col字符串
     * @return threadLocal里的key值
     */
    private MoveRegularObject col2RegularObj(String col) {
        String[] colArray = col.split("\\.");
        if (colArray.length != 2) {
            throw new GlobalException("[行为执行器]行为语法里定位部分的语法不符合规范，您提供的部分是:  " + col);
        }
        String strategy = colArray[0].substring(0, 3).toUpperCase() + "_MOVE";
        if (!STRATEGY_LIST.contains(strategy)) {
            throw new GlobalException("[行为执行器]行为语法里定位部分的语法不符合规范(需要至少包含pre/mid/aft)，您提供的部分是:" + col);
        }
        int actionId;
        try {
            actionId = Integer.parseInt(colArray[0].substring(3));
        } catch (NumberFormatException e) {
            throw new GlobalException("[行为执行器]行为语法里定位部分的语法没有提供合适的actionId!例如需要提供pre1,pre2，您提供的部分是: " + col);
        }
        String key = strategy + "_" + actionId;
        LogQueueRuntime.addNewLog(this.getClass(), MOVE_ACTUATOR, "KEY值拼接完成，KEY值 = " + key);
        return new MoveRegularObject(key, colArray[1]);
    }

    /**
     * 用例行为解析器
     *
     * @param envId         环境ID
     * @param projectId     项目ID
     * @param runtimeDetail 用例行为解析器
     */
    private HttpResponseDetail caseActionHandler(Long envId, Long projectId, String runtimeDetail) {
        long caseDataId = Long.parseLong(runtimeDetail);
        TestCaseBTO caseBTO = dataService.toCaseBTO(caseDataId);
        return (HttpResponseDetail) caseActuator.executeTestCase(caseBTO, envId, projectId)
                .getBaseTestCaseResponseDetail();
    }

    /**
     * http行为解析器
     *
     * @param envId         环境ID
     * @param projectId     项目ID
     * @param runtimeDetail http url 与 参数字符串
     */
    private HttpResponseDetail httpActionHandler(Long envId, Long projectId, String runtimeDetail) {
        HttpRequestDetail httpRequestDetail = JSON.parseObject(runtimeDetail, HttpRequestDetail.class);
        return httpActuator.sendHttpRequest(httpRequestDetail, envId, projectId);
    }

    /**
     * SQL动作解析器
     *
     * @param envId         环境ID
     * @param projectId     项目ID
     * @param runtimeDetail SQL字符串
     */
    private List<Map<String, Object>> sqlActionHandler(Long envId, Long projectId, String runtimeDetail) {
        SqlExpDetail sqlExpDetail = JSON.parseObject(runtimeDetail, SqlExpDetail.class);
        return sqlExpActuator.parseSql(sqlExpDetail, envId, projectId);
    }

    private Map<String, Object> detail2Map(HttpResponseDetail detail) {
        return BeanFlattener.deepToMap(detail);
    }
}

