package org.matrix.actuators.usecase;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.apache.commons.lang3.StringUtils;
import org.matrix.actuators.Actuator;
import org.matrix.actuators.checkpoint.CheckPointActuator;
import org.matrix.actuators.httpclient.HttpClientActuator;
import org.matrix.actuators.move.MoveActuator;
import org.matrix.actuators.move.MoveStrategy;
import org.matrix.actuators.util.ThreadUtil;
import org.matrix.database.entity.*;
import org.matrix.actuators.checkpoint.CheckPoint;
import org.matrix.actuators.checkpoint.CheckPointResult;
import org.matrix.actuators.httpclient.HttpRequestDetail;
import org.matrix.actuators.httpclient.HttpResponseDetail;
import org.matrix.database.service.IExecutionHistoryService;
import org.matrix.enums.ExecutionHistoryStatus;
import org.matrix.exception.ExecutionRunningException;
import org.matrix.exception.GlobalException;
import org.matrix.socket.ExecutionStatusMonitorSocketHandler;
import org.matrix.socket.pool.TestCaseExecuteSocketPool;
import org.matrix.socket.queue.LogQueueRuntime;
import org.matrix.socket.vo.CaseExecuteVo;
import org.matrix.socket.vo.TestExecuteLog;
import org.matrix.util.SpringUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;

import static org.matrix.enums.ModuleType.CASE_ACTUATOR;

/**
 * 测试用例执行器
 *
 * @author huangxiahao
 */
@Component
public class CaseActuator implements Actuator {

    private final CheckPointActuator checkPointActuator;

    private final HttpClientActuator httpClientActuator;

    private final IExecutionHistoryService executionHistoryService;

    private final String baseJsPath = "syntaxCheck.js";

    private final ExecutionStatusMonitorSocketHandler executionStatusMonitorSocketHandler;

    public CaseActuator(CheckPointActuator checkPointActuator, HttpClientActuator httpClientActuator, IExecutionHistoryService executionHistoryService, ExecutionStatusMonitorSocketHandler executionStatusMonitorSocketHandler) {
        this.checkPointActuator = checkPointActuator;
        this.httpClientActuator = httpClientActuator;
        this.executionHistoryService = executionHistoryService;
        this.executionStatusMonitorSocketHandler = executionStatusMonitorSocketHandler;
    }


    /**
     * @param projectId 项目ID
     * @param envId     环境ID
     *                  执行测试用例，这个方法对内使用，由其他执行器调用
     */
    public TestDataExecuteResult executeTestCase(TestCaseBTO testCaseBto, Long envId, Long projectId) {
        try {
            LogQueueRuntime.addNewLog(this.getClass(), CASE_ACTUATOR, "开始执行用例！！");
            LogQueueRuntime.addNewLog(this.getClass(), CASE_ACTUATOR, String.format("当前正在执行用例ID: %s 用例名： %s", testCaseBto.getTestCase().getId(), testCaseBto.getTestCase().getName()));
            //执行前置动作
            executeMove(testCaseBto.getTestCase().getMoveAfterCase()
                    , envId, projectId, null, MoveStrategy.PRE_MOVE);
            //向线程中设置当前正在执行的DataId
            LogQueueRuntime.setTestData(testCaseBto.getTestData().getId());
            LocalDateTime dataStartTime = LocalDateTime.now();
            //执行测试用例的本体内容
            HttpResponseDetail baseTestCaseResponseDetail = getHttpResponseDetail(
                    envId,
                    projectId,
                    testCaseBto.getTestCase(),
                    testCaseBto.getTestData());
            //执行中置动作
            executeMove(testCaseBto.getTestCase().getMoveAfterTest()
                    , envId, projectId, baseTestCaseResponseDetail.getResponseBody(), MoveStrategy.MID_MOVE);
            //进行检验
            CheckPointResult checkPointResult = getCheckPointResult(testCaseBto.getTestCase(),
                    testCaseBto.getTestData(),
                    envId,
                    projectId,
                    baseTestCaseResponseDetail);
            //执行后置动作
            executeMove(testCaseBto.getTestCase().getMoveAfterTest()
                    , envId, projectId, baseTestCaseResponseDetail.getResponseBody(), MoveStrategy.AFT_MOVE);
            TestDataExecuteResult testDataExecuteResult = new TestDataExecuteResult(
                    dataStartTime,
                    LocalDateTime.now(),
                    true,
                    testCaseBto.getTestCase().getId(),
                    testCaseBto.getTestData().getId(),
                    baseTestCaseResponseDetail,
                    checkPointResult);
            LogQueueRuntime.addNewLog(this.getClass(), CASE_ACTUATOR, JSON.toJSONString(testDataExecuteResult));
            //将线程中正在执行的DataId清除
            LogQueueRuntime.clearTestData();
            return testDataExecuteResult;
        } finally {
            LogQueueRuntime.remove(ThreadUtil.currentThreadId());
        }
    }


    /**
     * 执行一条测试用例的多条测试数据
     */
    public List<TestDataExecuteResult> executeTestCases(TestCaseListDataBto testCaseBto, Long envId, Long projectId) {
        LogQueueRuntime.addNewLog(this.getClass(), CASE_ACTUATOR, "[用例执行器] 开始执行用例！！");
        List<TestDataExecuteResult> resultList = new ArrayList<>();
        try {
            LogQueueRuntime.addNewLog(this.getClass(), CASE_ACTUATOR, String.format("[用例执行器] 当前正在执行用例ID: %s 用例名： %s", testCaseBto.getTestCase().getId(), testCaseBto.getTestCase().getName()));
            //执行前置动作
            executeMove(testCaseBto.getTestCase().getMoveAfterCase()
                    , envId, projectId, null, MoveStrategy.PRE_MOVE);
            LogQueueRuntime.addNewLog(this.getClass(), CASE_ACTUATOR, "[用例执行器] 开始执行前置动作！！");
            //执行测试用例的本体内容
            for (TestData testData : testCaseBto.getTestDataList()) {
                LocalDateTime dataStartTime = LocalDateTime.now();
                //向线程中设置当前正在执行的DataId
                LogQueueRuntime.setTestData(testData.getId());
                //try catch 一下避免发生错误后导致循环进行不下去
                try {
                    changeExecutionHistoryStatus(ExecutionHistoryStatus.RUN);
                    LogQueueRuntime.addNewLog(this.getClass(), CASE_ACTUATOR, "[用例执行器] 开始执行数据组ID：" + testData.getId());
                    HttpResponseDetail baseTestCaseResponseDetail = getHttpResponseDetail(
                            envId,
                            projectId,
                            testCaseBto.getTestCase(),
                            testData);
                    LogQueueRuntime.addNewLog(this.getClass(), CASE_ACTUATOR, "[用例执行器] 即将开始执行中置动作");
                    //执行中置动作
                    executeMove(testCaseBto.getTestCase().getMoveAfterTest()
                            , envId, projectId, baseTestCaseResponseDetail.getResponseBody(), MoveStrategy.MID_MOVE);
                    CheckPointResult checkPointResult = getCheckPointResult(testCaseBto.getTestCase(),
                            testData,
                            envId,
                            projectId,
                            baseTestCaseResponseDetail);
                    TestDataExecuteResult testDataExecuteResult = new TestDataExecuteResult(
                            dataStartTime,
                            LocalDateTime.now(),
                            checkPointResult.getFailNum()>0?false:true,
                            testCaseBto.getTestCase().getId(),
                            testData.getId(),
                            baseTestCaseResponseDetail,
                            checkPointResult
                    );
                    resultList.add(testDataExecuteResult);
                    LogQueueRuntime.addNewLog(this.getClass(), CASE_ACTUATOR, "用例执行结果：" + JSON.toJSONString(testDataExecuteResult));
                    //执行后置动作
                    executeMove(testCaseBto.getTestCase().getMoveAfterTest()
                            , envId, projectId, baseTestCaseResponseDetail.getResponseBody(), MoveStrategy.AFT_MOVE);
                    changeExecutionHistoryStatus(ExecutionHistoryStatus.FINISH);
                } catch (Exception e) {
                    e.printStackTrace();
                    TestDataExecuteResult testDataExecuteResult = new TestDataExecuteResult(
                            dataStartTime,
                            LocalDateTime.now(),
                            false,
                            testCaseBto.getTestCase().getId(),
                            testData.getId(),
                            null,
                            null
                    );
                    resultList.add(testDataExecuteResult);
                    changeExecutionHistoryStatus(ExecutionHistoryStatus.ERROR);
                    LogQueueRuntime.addNewLog(this.getClass(), CASE_ACTUATOR, String.format("[用例执行器] 执行数据组ID：%s 数据组名称：%s 产生错误：%s", testData.getId(), testData.getName(), e.getMessage()));
                }
            }
        } finally {
            clearMoveData();
            //将线程中正在执行的DataId清除
            LogQueueRuntime.clearTestData();
        }
        return resultList;
    }

    /**
     * 调用动作
     *
     * @param moveString     用例中的moveId组 例如 1,2,3,4
     * @param envId          环境id
     * @param projectId      项目id
     * @param caseResultData 测试用例执行后保留下的结果集,该参数在前置策略中可以为null,在中间/后置策略中必须要提供合理的结果集对象
     * @param strategy       动作的策略，即是前置/中间/后置,具体查看{@link MoveStrategy}
     */
    private void executeMove(String moveString, Long envId, Long projectId, String caseResultData, MoveStrategy strategy) {
        String[] moveIds = StringUtils.isEmpty(moveString) ? new String[]{} : moveString.split(",");
        for (String moveId : moveIds) {
            if (!StringUtils.isEmpty(moveId)) {
                getMoveActuator().runMove(
                        Long.valueOf(moveId),
                        envId,
                        projectId,
                        caseResultData,
                        strategy
                );
            }

        }
    }

    /**
     * 清空动作的变量吃
     */
    private void clearMoveData() {
        getMoveActuator().clearMoveRes();
    }

    private CheckPointResult getCheckPointResult(TestCase testCase
            , TestData testData
            , Long envId
            , Long projectId
            , HttpResponseDetail baseTestCaseResponseDetail) {
        if (testCase.getType().equals(TestCaseTypeEnum.HTTP.getValue())) {
            return
                    checkPointActuator.httpCheck(
                            baseTestCaseResponseDetail,
                            getCheckPointEntity(testData),
                            envId,
                            projectId
                    );
        }
        return null;
    }

    private HttpResponseDetail getHttpResponseDetail(
            Long envId, Long projectId, TestCase testCase, TestData testData) {
        if (testCase.getType().equals(TestCaseTypeEnum.HTTP.getValue())) {
            HttpRequestDetail httpRequestDetail = JSON.parseObject(testData.getDetail(), HttpRequestDetail.class);
            return httpClientActuator.sendHttpRequest(httpRequestDetail, envId, projectId);
        }
        return null;
    }

    private CheckPoint getCheckPointEntity(TestData testData) {
        CheckPoint checkPoint = new CheckPoint();
        checkPoint.setUseExceptionCheck(testData.getAbnormalCheckpoint() != 0);
        checkPoint.setUseNullCheck(testData.getNoEmptyCheckpoint() != 0);
        checkPoint.setContainCheckPoint(testData.getContainCheckpoint());
        checkPoint.setNoContainCheckPoint(testData.getNoContainCheckpoint());
        checkPoint.setJsonPathCheckPoint(testData.getJsonpathCheckpoint());
        return checkPoint;
    }

    private static MoveActuator getMoveActuator() {
        return SpringUtils.getBean("moveActuator");
    }

    /**
     * @param caseExecuteVo 执行所需要的详细信息
     * @param session       如果是从socket运行并希望获取实时日志请从这里运行
     *                      外界如果执行测试用例的话请走这个接口,执行TestCase，并控制运行态日志池，进行日志的生成。
     */
    public RunCaseResult runTestCase(WebSocketSession session, CaseExecuteVo caseExecuteVo) {
        String uniqueKey = UUID.randomUUID().toString();
        caseExecuteVo.setUniqueKey(uniqueKey);
        RunCaseResult runCaseResult = new RunCaseResult();
        runCaseResult.setStartTime(LocalDateTime.now());
        runCaseResult.setJobId(caseExecuteVo.getJobId());
        Long currentThreadId = ThreadUtil.currentThreadId();
        try {
            if (session != null && session.isOpen()) {
                session.sendMessage(new TextMessage(JSONObject.toJSONString(caseExecuteVo)));
            }
            //将websocketSession 加入到socket池子中
            TestCaseExecuteSocketPool.add(currentThreadId, session);
            List<TestCaseListDataBto> testCaseListDataBtoList = caseExecuteVo.getTestCaseListDataBtoList();
            for (TestCaseListDataBto caseListDataBto : testCaseListDataBtoList) {
                Long caseProjectId = caseListDataBto.getTestCase().getProjectId();
                if (!caseExecuteVo.getProjectId().equals(caseProjectId)) {
                    throw new GlobalException("本次执行中，存在非相同项目的测试用例");
                }
            }
            //建立执行历史（ExecutionHistory）
            if (caseExecuteVo.getJobId() != -1L) {
                if (!LogQueueRuntime.checkJobIsInRun(caseExecuteVo.getJobId(), caseExecuteVo.getUserId(), caseExecuteVo.getType())) {
                    insertExecutionHistory(uniqueKey, caseExecuteVo);
                }
            } else {
                insertExecutionHistory(uniqueKey, caseExecuteVo);
            }
            for (TestCaseListDataBto testCaseListDataBto : testCaseListDataBtoList) {
                LocalDateTime caseStartTime = LocalDateTime.now();
                try {
                    LogQueueRuntime
                            .initTestCaseLog(
                                    caseExecuteVo.getJobId(),
                                    caseExecuteVo.getUserId()
                                    , testCaseListDataBto.getTestCase().getId()
                                    , caseExecuteVo.getType()
                                    , uniqueKey
                            );
                    //执行测试用例
                    if (testCaseListDataBto.getTestDataList() != null && testCaseListDataBto.getTestDataList().size() > 0) {
                        List<TestDataExecuteResult> resultList =
                                executeTestCases(testCaseListDataBto, caseExecuteVo.getEnvId(), caseExecuteVo.getProjectId());
                        long count = resultList.stream().filter(e -> !e.getExecutionStatus()).count();
                        TestCaseReport testCaseReport = new TestCaseReport(
                                caseStartTime,
                                LocalDateTime.now(),
                                testCaseListDataBto.getTestCase().getId(),
                                testCaseListDataBto.getTestCase().getName(),
                                TestCaseTypeEnum.getTypeByValue( testCaseListDataBto.getTestCase().getType()).toString(),
                                testCaseListDataBto.getTestCase().getDetail(),
                                testCaseListDataBto.getTestCase().getDes(),
                                testCaseListDataBto.getTestCase().getMoveBefore(),
                                testCaseListDataBto.getTestCase().getMoveAfterTest(),
                                testCaseListDataBto.getTestCase().getMoveAfterCase(),
                                count <= 0,
                                resultList
                        );
                        runCaseResult.getCaseReports().add(testCaseReport);
                    }
                } catch (ExecutionRunningException runningException) {
                    if (session != null && session.isOpen()) {
                        session.sendMessage(new TextMessage("-1.-1.-1.-1." + runningException.getMessage()));
                    }
                    break;
                } catch (Exception e) {
                    TestCaseReport testCaseReport = new TestCaseReport(
                            caseStartTime,
                            LocalDateTime.now(),
                            testCaseListDataBto.getTestCase().getId(),
                            testCaseListDataBto.getTestCase().getName(),
                            TestCaseTypeEnum.getTypeByValue( testCaseListDataBto.getTestCase().getType()).toString(),
                            testCaseListDataBto.getTestCase().getDetail(),
                            testCaseListDataBto.getTestCase().getDes(),
                            testCaseListDataBto.getTestCase().getMoveBefore(),
                            testCaseListDataBto.getTestCase().getMoveAfterTest(),
                            testCaseListDataBto.getTestCase().getMoveAfterCase(),
                            false,
                            null
                    );
                    runCaseResult.getCaseReports().add(testCaseReport);
                    LogQueueRuntime.addNewLog(this.getClass(), CASE_ACTUATOR, String.format("[用例执行器] 产生错误：%s", e.getMessage()));
                }
            }
        } catch (GlobalException | IOException e) {
            e.printStackTrace();
            try {
                if (session != null && session.isOpen()) {
                    TestExecuteLog currentTestExecute = LogQueueRuntime.getCurrentTestExecute(ThreadUtil.currentThreadId());
                    if (currentTestExecute != null) {
                        LogQueueRuntime.addNewLog(this.getClass(), CASE_ACTUATOR, e.getMessage());
                    } else {
                        //这里加-1.-1.-1是用来表明这里发出去的数据是不属于任何执行中的用例的
                        session.sendMessage(new TextMessage(String.format("-1.-1.-1.%s", e.getMessage())));
                    }
                }
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        } finally {
            runCaseResult.setEndTime(LocalDateTime.now());
            //将数据库中的执行历史状态设置为完成
            endExecutionHistory();
            //将本次产生的数据清除
            TestCaseExecuteSocketPool.remove(currentThreadId);
            //将本次产生的日志从执行状态设为停止状态
            LogQueueRuntime.remove(currentThreadId);
        }
        return runCaseResult;
    }

    public void insertExecutionHistory(String uniqueKey, CaseExecuteVo caseExecuteVo) {
        List<TestCaseListDataBto> testCaseListDataBtoList = caseExecuteVo.getTestCaseListDataBtoList();
        Long firstExecutionKey = getExecutionKey(caseExecuteVo);
        List<ExecutionHistory> executionHistories = new ArrayList<>();
        for (TestCaseListDataBto testCaseListDataBto : testCaseListDataBtoList) {
            List<TestData> testDataList = testCaseListDataBto.getTestDataList();
            TestCase testCase = testCaseListDataBto.getTestCase();
            for (int i = 0; i < testDataList.size(); i++) {
                ExecutionHistory executionHistory = new ExecutionHistory();
                executionHistory.setJobId(caseExecuteVo.getJobId());
                executionHistory.setUserId(caseExecuteVo.getUserId());
                executionHistory.setCaseId(testCase.getId());
                executionHistory.setDataId(testDataList.get(i).getId());
                executionHistory.setUniqueKey(uniqueKey);
                executionHistory.setExecutionKey(firstExecutionKey);
                executionHistory.setCaseExecuteVoString(JSONObject.toJSONString(caseExecuteVo));
                if (i == 0) {
                    executionHistory.setStartTime(LocalDateTime.now());
                }
                executionHistories.add(executionHistory);
            }
        }
        Long lastExecutionKey = getExecutionKey(caseExecuteVo);
        if (!lastExecutionKey.equals(firstExecutionKey)) {
            insertExecutionHistory(uniqueKey, caseExecuteVo);
        }
        executionHistoryService.saveBatch(executionHistories);
    }

    /**
     * 根据userId,jobId,caseId 往前查一个执行号 并且+1 ，如果jobId等于-1 则根据 userId,jobId,caseId 查否则根据 userId,jobId
     *
     * @return 新的执行号
     */
    private Long getExecutionKey(CaseExecuteVo caseExecuteVo) {
        LambdaQueryWrapper<ExecutionHistory> executionHistoryLambdaQueryWrapper = Wrappers.lambdaQuery(ExecutionHistory.class);
        executionHistoryLambdaQueryWrapper.eq(ExecutionHistory::getUserId, caseExecuteVo.getUserId());
        executionHistoryLambdaQueryWrapper.eq(ExecutionHistory::getJobId, caseExecuteVo.getJobId());
        executionHistoryLambdaQueryWrapper.orderByDesc(ExecutionHistory::getId);
        executionHistoryLambdaQueryWrapper.last("limit 1");
        if (caseExecuteVo.getJobId() == -1L) {
            TestCaseListDataBto testCaseListDataBto = caseExecuteVo.getTestCaseListDataBtoList().get(0);
            if (testCaseListDataBto != null) {
                executionHistoryLambdaQueryWrapper.eq(ExecutionHistory::getCaseId, testCaseListDataBto.getTestCase().getId());
            }
        }
        ExecutionHistory one = executionHistoryService.getOne(executionHistoryLambdaQueryWrapper);
        if (one != null && one.getExecutionKey() != null) {
            return one.getExecutionKey() + 1L;
        } else {
            return 1L;
        }

    }

    public void changeExecutionHistoryStatus(
            ExecutionHistoryStatus executionHistoryStatus) {
        TestExecuteLog currentTestExecute = LogQueueRuntime.getCurrentTestExecute(ThreadUtil.currentThreadId());
        if (currentTestExecute != null) {
            ExecutionHistory executionHistory = new ExecutionHistory();
            if (executionHistoryStatus.equals(ExecutionHistoryStatus.RUN)) {
                executionHistory.setStartTime(LocalDateTime.now());
            } else if (executionHistoryStatus.equals(ExecutionHistoryStatus.FINISH) || executionHistoryStatus.equals(ExecutionHistoryStatus.ERROR)) {
                executionHistory.setEndTime(LocalDateTime.now());
            }
            executionHistory.setStatus(executionHistoryStatus);
            executionHistoryService.update(executionHistory, Wrappers
                    .lambdaUpdate(ExecutionHistory.class)
                    .eq(ExecutionHistory::getJobId, currentTestExecute.getTestJobId())
                    .eq(ExecutionHistory::getCaseId, currentTestExecute.getTestCaseId())
                    .eq(ExecutionHistory::getDataId, currentTestExecute.getTestDataId())
                    .eq(ExecutionHistory::getUniqueKey, currentTestExecute.getUniqueKey())
                    .eq(ExecutionHistory::getUserId, currentTestExecute.getUserId())
            );
            //发送执行状态
            try {
                executionStatusMonitorSocketHandler.sendOverviewCurrentStatus(currentTestExecute.getUniqueKey());
                executionStatusMonitorSocketHandler.sendDetailCurrentStatus(
                        currentTestExecute.getUserId(),
                        currentTestExecute.getTestJobId(),
                        currentTestExecute.getTestCaseId(),
                        currentTestExecute.getTestDataId(),
                        currentTestExecute.getUniqueKey(),
                        executionHistoryStatus);
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }


    /**
     * 将所有还未完成的执行历史设置为ERROR
     */
    public void endExecutionHistory() {
        TestExecuteLog currentTestExecute = LogQueueRuntime.getCurrentTestExecute(ThreadUtil.currentThreadId());
        if (currentTestExecute != null) {
            LambdaUpdateWrapper<ExecutionHistory> wrapper = Wrappers
                    .lambdaUpdate(ExecutionHistory.class)
                    .eq(ExecutionHistory::getUniqueKey, currentTestExecute.getUniqueKey())
                    .eq(ExecutionHistory::getUserId, currentTestExecute.getUserId())
                    .ne(ExecutionHistory::getStatus, ExecutionHistoryStatus.FINISH);
            ExecutionHistory executionHistory = new ExecutionHistory();
            executionHistory.setStatus(ExecutionHistoryStatus.ERROR);
            executionHistory.setEndTime(LocalDateTime.now());
            List<ExecutionHistory> list = executionHistoryService.list(wrapper);
            executionHistoryService.update(executionHistory, wrapper);
            try {
                executionStatusMonitorSocketHandler.sendOverviewCurrentStatus(currentTestExecute.getUniqueKey());
                for (ExecutionHistory history : list) {
                    executionStatusMonitorSocketHandler.sendDetailCurrentStatus(
                            history.getUserId(),
                            history.getJobId(),
                            history.getCaseId(),
                            history.getDataId(),
                            history.getUniqueKey(),
                            ExecutionHistoryStatus.ERROR);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}
