package tech.easyflow.approval.service.impl;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.MockitoJUnitRunner;
import tech.easyflow.approval.entity.ApprovalInstance;
import tech.easyflow.approval.entity.ApprovalLog;
import tech.easyflow.approval.entity.ApprovalTask;
import tech.easyflow.approval.entity.vo.ApprovalAssigneeTargetVo;
import tech.easyflow.approval.entity.vo.ApprovalFlowDetailVo;
import tech.easyflow.approval.entity.vo.ApprovalFlowStepVo;
import tech.easyflow.approval.entity.vo.ApprovalSubmitRequest;
import tech.easyflow.approval.enums.ApprovalAssigneeType;
import tech.easyflow.approval.enums.ApprovalInstanceStatus;
import tech.easyflow.approval.enums.ApprovalTaskStatus;
import tech.easyflow.approval.mapper.ApprovalFlowStepMapper;
import tech.easyflow.approval.mapper.ApprovalInstanceMapper;
import tech.easyflow.approval.mapper.ApprovalLogMapper;
import tech.easyflow.approval.mapper.ApprovalTaskMapper;
import tech.easyflow.approval.service.ApprovalAssigneeService;
import tech.easyflow.approval.service.ApprovalMatchService;
import tech.easyflow.common.constant.enums.EnumDataStatus;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.satoken.util.SaTokenUtil;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.entity.SysDept;
import tech.easyflow.system.service.SysAccountService;
import tech.easyflow.system.service.SysDeptService;

import java.math.BigInteger;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * {@link ApprovalInstanceServiceImpl} 多审批对象冻结测试。
 */
@RunWith(MockitoJUnitRunner.class)
public class ApprovalInstanceMultiAssigneeTest {

    @Mock
    private ApprovalMatchService approvalMatchService;

    @Mock
    private ApprovalInstanceMapper approvalInstanceMapper;

    @Mock
    private ApprovalTaskMapper approvalTaskMapper;

    @Mock
    private ApprovalLogMapper approvalLogMapper;

    @Mock
    private ApprovalFlowStepMapper approvalFlowStepMapper;

    @Mock
    private ApprovalAssigneeService approvalAssigneeService;

    @Mock
    private SysAccountService sysAccountService;

    @Mock
    private SysDeptService sysDeptService;

    @InjectMocks
    private ApprovalInstanceServiceImpl service;

    /**
     * 验证提审时会把全部审批对象写入实例快照和任务关联。
     */
    @Test
    @SuppressWarnings("unchecked")
    public void submitApprovalShouldFreezeAllAssigneeTargets() {
        BigInteger applicantId = BigInteger.valueOf(7);
        BigInteger tenantId = BigInteger.valueOf(42);
        BigInteger applicantDeptId = BigInteger.valueOf(43);
        BigInteger instanceId = BigInteger.valueOf(101);
        BigInteger taskId = BigInteger.valueOf(201);
        ApprovalSubmitRequest request = new ApprovalSubmitRequest();
        request.setApplicantId(applicantId);
        request.setResourceId(BigInteger.valueOf(501));
        request.setSummary("多用户审批");

        ApprovalFlowStepVo step = new ApprovalFlowStepVo();
        step.setStepNo(1);
        step.setStepName("审核");
        step.setAssigneeType(ApprovalAssigneeType.USER.getCode());
        step.setRestrictToApplicantDept(1);
        step.setAssigneeTargets(List.of(
                target(11, "u11", "用户甲"),
                target(12, "u12", "用户乙")));
        ApprovalFlowDetailVo flow = new ApprovalFlowDetailVo();
        flow.setId(BigInteger.valueOf(301));
        flow.setVersion(3);
        flow.setResourceType("WORKFLOW");
        flow.setActionType("PUBLISH");
        flow.setSteps(List.of(step));
        when(approvalMatchService.matchFlow(request)).thenReturn(flow);

        LoginAccount loginAccount = new LoginAccount();
        loginAccount.setId(applicantId);
        loginAccount.setTenantId(tenantId);
        SysAccount applicant = new SysAccount();
        applicant.setId(applicantId);
        applicant.setTenantId(tenantId);
        applicant.setDeptId(applicantDeptId);
        when(sysAccountService.getById(applicantId)).thenReturn(applicant);
        SysDept applicantDept = new SysDept();
        applicantDept.setId(applicantDeptId);
        applicantDept.setDeptName("技术部");
        applicantDept.setStatus(EnumDataStatus.AVAILABLE.getCode());
        when(sysDeptService.getById(applicantDeptId)).thenReturn(applicantDept);
        doAnswer(invocation -> {
            ((ApprovalInstance) invocation.getArgument(0)).setId(instanceId);
            return 1;
        }).when(approvalInstanceMapper).insert(any(ApprovalInstance.class));
        doAnswer(invocation -> {
            ((ApprovalTask) invocation.getArgument(0)).setId(taskId);
            return 1;
        }).when(approvalTaskMapper).insert(any(ApprovalTask.class));

        try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
            saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount);
            assertEquals(instanceId, service.submitApproval(request));
        }

        ArgumentCaptor<ApprovalInstance> instanceCaptor = ArgumentCaptor.forClass(ApprovalInstance.class);
        verify(approvalInstanceMapper).insert(instanceCaptor.capture());
        List<Map<String, Object>> frozenSteps =
                (List<Map<String, Object>>) instanceCaptor.getValue().getSnapshotJson().get("steps");
        List<Map<String, Object>> frozenTargets =
                (List<Map<String, Object>>) frozenSteps.get(0).get("assigneeTargets");
        assertEquals(2, frozenTargets.size());
        assertEquals(BigInteger.valueOf(12), frozenTargets.get(1).get("targetId"));
        assertEquals(1, frozenSteps.get(0).get("restrictToApplicantDept"));
        assertEquals(applicantDeptId, instanceCaptor.getValue().getApplicantDeptId());
        assertEquals("技术部", instanceCaptor.getValue().getApplicantDeptName());
        assertEquals(applicantDeptId,
                instanceCaptor.getValue().getSnapshotJson().get("applicantDeptId"));
        ArgumentCaptor<ApprovalTask> taskCaptor = ArgumentCaptor.forClass(ApprovalTask.class);
        verify(approvalTaskMapper).insert(taskCaptor.capture());
        assertEquals(applicantDeptId, taskCaptor.getValue().getRequiredDeptId());
        verify(approvalAssigneeService).validateRestrictedStepCandidates(
                eq(List.of(step)), eq(applicantDeptId));
        verify(approvalAssigneeService).saveTaskAssigneeTargets(
                eq(taskId),
                eq(ApprovalAssigneeType.USER.getCode()),
                argThat(targets -> targets.size() == 2),
                eq(applicantId),
                any(Date.class));
        verify(approvalLogMapper, org.mockito.Mockito.times(2)).insert(any(ApprovalLog.class));
    }

    /**
     * 验证发起人没有直属部门时，会在创建实例前拒绝受限流程提审。
     */
    @Test
    public void submitApprovalShouldRejectRestrictedFlowWhenApplicantHasNoDepartment() {
        BigInteger applicantId = BigInteger.valueOf(7);
        BigInteger tenantId = BigInteger.valueOf(42);
        ApprovalSubmitRequest request = new ApprovalSubmitRequest();
        request.setApplicantId(applicantId);
        request.setResourceId(BigInteger.valueOf(501));

        ApprovalFlowStepVo step = new ApprovalFlowStepVo();
        step.setStepNo(1);
        step.setStepName("审核");
        step.setAssigneeType(ApprovalAssigneeType.USER.getCode());
        step.setAssigneeTargets(List.of(target(11, "u11", "用户甲")));
        step.setRestrictToApplicantDept(1);
        ApprovalFlowDetailVo flow = new ApprovalFlowDetailVo();
        flow.setId(BigInteger.valueOf(301));
        flow.setVersion(3);
        flow.setResourceType("WORKFLOW");
        flow.setActionType("PUBLISH");
        flow.setSteps(List.of(step));
        when(approvalMatchService.matchFlow(request)).thenReturn(flow);

        LoginAccount loginAccount = new LoginAccount();
        loginAccount.setId(applicantId);
        loginAccount.setTenantId(tenantId);
        SysAccount applicant = new SysAccount();
        applicant.setId(applicantId);
        applicant.setTenantId(tenantId);
        when(sysAccountService.getById(applicantId)).thenReturn(applicant);

        try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
            saToken.when(SaTokenUtil::getLoginAccount).thenReturn(loginAccount);
            assertThrows(BusinessException.class, () -> service.submitApproval(request));
        }

        verify(approvalInstanceMapper, never()).insert(any(ApprovalInstance.class));
        verify(approvalTaskMapper, never()).insert(any(ApprovalTask.class));
    }

    /**
     * 验证进入下一步骤时继续使用实例快照中冻结的全部审批对象。
     */
    @Test
    public void approveShouldCreateNextTaskFromFrozenAssigneeTargets() {
        BigInteger operatorId = BigInteger.valueOf(7);
        BigInteger tenantId = BigInteger.valueOf(42);
        BigInteger instanceId = BigInteger.valueOf(101);
        BigInteger currentTaskId = BigInteger.valueOf(201);
        BigInteger nextTaskId = BigInteger.valueOf(202);
        BigInteger firstDeptId = BigInteger.valueOf(31);
        BigInteger secondDeptId = BigInteger.valueOf(32);

        ApprovalInstance instance = new ApprovalInstance();
        instance.setId(instanceId);
        instance.setTenantId(tenantId);
        instance.setFlowId(BigInteger.valueOf(301));
        instance.setStatus(ApprovalInstanceStatus.PENDING.getCode());
        instance.setCurrentStepNo(1);
        instance.setSnapshotJson(Map.of(
                "steps", List.of(
                        frozenStep(1, ApprovalAssigneeType.USER.getCode(),
                                List.of(frozenTarget(11, 0))),
                        frozenStep(2, ApprovalAssigneeType.DEPT.getCode(), List.of(
                                frozenTarget(firstDeptId.longValue(), 0),
                                frozenTarget(secondDeptId.longValue(), 1))))));

        ApprovalTask currentTask = new ApprovalTask();
        currentTask.setId(currentTaskId);
        currentTask.setInstanceId(instanceId);
        currentTask.setStepNo(1);
        currentTask.setStatus(ApprovalTaskStatus.PENDING.getCode());
        currentTask.setAssigneeType(ApprovalAssigneeType.USER.getCode());

        SysAccount operator = new SysAccount();
        operator.setId(operatorId);
        operator.setTenantId(tenantId);
        when(sysAccountService.getById(operatorId)).thenReturn(operator);
        when(approvalInstanceMapper.selectOneByQuery(any())).thenReturn(instance);
        when(approvalTaskMapper.selectOneByQuery(any())).thenReturn(currentTask);
        when(approvalAssigneeService.getAvailableRoleIds(operatorId)).thenReturn(Set.of());
        when(approvalAssigneeService.canHandleTask(currentTask, operatorId, Set.of())).thenReturn(true);
        when(approvalFlowStepMapper.selectListByQuery(any())).thenReturn(List.of());
        doAnswer(invocation -> {
            ((ApprovalTask) invocation.getArgument(0)).setId(nextTaskId);
            return 1;
        }).when(approvalTaskMapper).insert(any(ApprovalTask.class));

        service.approve(instanceId, "同意", operatorId);

        verify(approvalAssigneeService).saveTaskAssigneeTargets(
                eq(nextTaskId),
                eq(ApprovalAssigneeType.DEPT.getCode()),
                argThat(targets -> targets.size() == 2
                        && firstDeptId.equals(targets.get(0).getTargetId())
                        && secondDeptId.equals(targets.get(1).getTargetId())
                        && Integer.valueOf(1).equals(targets.get(1).getIncludeChildren())),
                eq(operatorId),
                any(Date.class));
        assertEquals(ApprovalInstanceStatus.PROCESSING.getCode(), instance.getStatus());
        assertEquals(Integer.valueOf(2), instance.getCurrentStepNo());
    }

    /**
     * 验证下一受限步骤没有合法候选人时，当前任务保持待审批。
     */
    @Test
    public void approveShouldKeepCurrentTaskPendingWhenNextRestrictedStepHasNoCandidate() {
        BigInteger operatorId = BigInteger.valueOf(7);
        BigInteger tenantId = BigInteger.valueOf(42);
        BigInteger instanceId = BigInteger.valueOf(101);
        BigInteger applicantDeptId = BigInteger.valueOf(21);
        ApprovalInstance instance = new ApprovalInstance();
        instance.setId(instanceId);
        instance.setTenantId(tenantId);
        instance.setFlowId(BigInteger.valueOf(301));
        instance.setStatus(ApprovalInstanceStatus.PENDING.getCode());
        instance.setCurrentStepNo(1);
        instance.setApplicantDeptId(applicantDeptId);
        instance.setSnapshotJson(Map.of(
                "steps", List.of(
                        frozenStep(1, ApprovalAssigneeType.USER.getCode(),
                                List.of(frozenTarget(11, 0))),
                        frozenRestrictedStep(2, ApprovalAssigneeType.ROLE.getCode(),
                                List.of(frozenTarget(31, 0))))));

        ApprovalTask currentTask = new ApprovalTask();
        currentTask.setId(BigInteger.valueOf(201));
        currentTask.setInstanceId(instanceId);
        currentTask.setStepNo(1);
        currentTask.setStatus(ApprovalTaskStatus.PENDING.getCode());
        currentTask.setAssigneeType(ApprovalAssigneeType.USER.getCode());
        SysAccount operator = new SysAccount();
        operator.setId(operatorId);
        operator.setTenantId(tenantId);
        when(sysAccountService.getById(operatorId)).thenReturn(operator);
        when(approvalInstanceMapper.selectOneByQuery(any())).thenReturn(instance);
        when(approvalTaskMapper.selectOneByQuery(any())).thenReturn(currentTask);
        when(approvalAssigneeService.getAvailableRoleIds(operatorId)).thenReturn(Set.of());
        when(approvalAssigneeService.canHandleTask(currentTask, operatorId, Set.of())).thenReturn(true);
        when(approvalFlowStepMapper.selectListByQuery(any())).thenReturn(List.of());
        doThrow(new BusinessException("下一步没有候选人"))
                .when(approvalAssigneeService)
                .validateRestrictedStepCandidates(any(), eq(applicantDeptId));

        assertThrows(BusinessException.class,
                () -> service.approve(instanceId, "同意", operatorId));

        assertEquals(ApprovalTaskStatus.PENDING.getCode(), currentTask.getStatus());
        verify(approvalTaskMapper, never()).update(any(ApprovalTask.class));
        verify(approvalTaskMapper, never()).insert(any(ApprovalTask.class));
        verify(approvalInstanceMapper, never()).update(any(ApprovalInstance.class));
    }

    /**
     * 构造冻结步骤。
     *
     * @param stepNo 步骤序号
     * @param assigneeType 审批对象类型
     * @param targets 审批对象
     * @return 冻结步骤
     */
    private Map<String, Object> frozenStep(int stepNo, String assigneeType, List<Map<String, Object>> targets) {
        Map<String, Object> primary = targets.get(0);
        return Map.of(
                "stepNo", stepNo,
                "stepName", "第" + stepNo + "步",
                "assigneeType", assigneeType,
                "assigneeTargetId", primary.get("targetId"),
                "assigneeTargetCode", "target-" + primary.get("targetId"),
                "assigneeTargetName", "对象" + primary.get("targetId"),
                "restrictToApplicantDept", 0,
                "assigneeTargets", targets);
    }

    /**
     * 构造受发起人部门限制的冻结步骤。
     *
     * @param stepNo 步骤序号
     * @param assigneeType 审批对象类型
     * @param targets 审批对象
     * @return 冻结步骤
     */
    private Map<String, Object> frozenRestrictedStep(int stepNo, String assigneeType,
                                                     List<Map<String, Object>> targets) {
        Map<String, Object> primary = targets.get(0);
        return Map.of(
                "stepNo", stepNo,
                "stepName", "第" + stepNo + "步",
                "assigneeType", assigneeType,
                "assigneeTargetId", primary.get("targetId"),
                "assigneeTargetCode", "target-" + primary.get("targetId"),
                "assigneeTargetName", "对象" + primary.get("targetId"),
                "restrictToApplicantDept", 1,
                "assigneeTargets", targets);
    }

    /**
     * 构造冻结审批对象。
     *
     * @param id 对象 ID
     * @param includeChildren 是否包含子部门
     * @return 冻结审批对象
     */
    private Map<String, Object> frozenTarget(long id, int includeChildren) {
        return Map.of(
                "targetId", BigInteger.valueOf(id),
                "targetCode", "target-" + id,
                "targetName", "对象" + id,
                "includeChildren", includeChildren);
    }

    /**
     * 构造审批对象。
     *
     * @param id 用户 ID
     * @param code 用户编码
     * @param name 用户名称
     * @return 审批对象
     */
    private ApprovalAssigneeTargetVo target(long id, String code, String name) {
        ApprovalAssigneeTargetVo target = new ApprovalAssigneeTargetVo();
        target.setTargetId(BigInteger.valueOf(id));
        target.setTargetCode(code);
        target.setTargetName(name);
        target.setIncludeChildren(0);
        return target;
    }
}
