package com.njcn.rdms.module.project.service.project.task.assignee;

import com.njcn.rdms.framework.common.exception.ServiceException;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.assignee.TaskAssigneeInactiveReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.assignee.TaskAssigneeLogPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.assignee.TaskAssigneeLogRespVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.assignee.TaskAssigneeRespVO;
import com.njcn.rdms.module.project.controller.admin.project.task.vo.assignee.TaskAssigneeSaveReqVO;
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ExecutionAssigneeDO;
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.ProjectTaskDO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskAssigneeDO;
import com.njcn.rdms.module.project.dal.dataobject.project.task.TaskAssigneeLogDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
import com.njcn.rdms.module.project.dal.mysql.audit.BizAuditLogMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
import com.njcn.rdms.module.project.dal.mysql.project.execution.ExecutionAssigneeMapper;
import com.njcn.rdms.module.project.dal.mysql.project.execution.ProjectExecutionMapper;
import com.njcn.rdms.module.project.dal.mysql.project.task.ProjectTaskMapper;
import com.njcn.rdms.module.project.dal.mysql.project.task.TaskAssigneeLogMapper;
import com.njcn.rdms.module.project.dal.mysql.project.task.TaskAssigneeMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.system.api.user.AdminUserApi;
import com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;

import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class TaskAssigneeServiceImplTest extends BaseMockitoUnitTest {

    @InjectMocks
    private TaskAssigneeServiceImpl taskAssigneeService;

    @Mock
    private ProjectMapper projectMapper;
    @Mock
    private ProjectExecutionMapper projectExecutionMapper;
    @Mock
    private ProjectTaskMapper projectTaskMapper;
    @Mock
    private ExecutionAssigneeMapper executionAssigneeMapper;
    @Mock
    private TaskAssigneeMapper taskAssigneeMapper;
    @Mock
    private TaskAssigneeLogMapper taskAssigneeLogMapper;
    @Mock
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Mock
    private BizAuditLogMapper bizAuditLogMapper;
    @Mock
    private AdminUserApi adminUserApi;

    private static final Long PROJECT_ID = 2001L;
    private static final Long EXECUTION_ID = 5001L;
    private static final Long TASK_ID = 9001L;
    private static final Long OWNER_ID = 3001L;
    private static final Long USER_ID = 3002L;

    // -------------------- createAssignee --------------------

    @Test
    void createAssignee_shouldInsertActiveRowAndJoinLog() {
        TaskAssigneeSaveReqVO reqVO = new TaskAssigneeSaveReqVO();
        reqVO.setUserId(USER_ID);

        stubEditableContext();
        when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(PROJECT_ID, EXECUTION_ID, TASK_ID))
                .thenReturn(createTask(OWNER_ID));
        when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(EXECUTION_ID, USER_ID))
                .thenReturn(createExecutionAssignee(EXECUTION_ID, USER_ID));
        when(taskAssigneeMapper.selectActiveByTaskIdAndUserId(TASK_ID, USER_ID)).thenReturn(null);
        when(taskAssigneeMapper.insert(any(TaskAssigneeDO.class))).thenAnswer(invocation -> {
            TaskAssigneeDO assignee = invocation.getArgument(0);
            assignee.setId(7001L);
            return 1;
        });

        Long assigneeId = taskAssigneeService.createAssignee(PROJECT_ID, EXECUTION_ID, TASK_ID, reqVO);

        assertEquals(7001L, assigneeId);
        ArgumentCaptor<TaskAssigneeDO> activeCaptor = ArgumentCaptor.forClass(TaskAssigneeDO.class);
        verify(taskAssigneeMapper).insert(activeCaptor.capture());
        assertEquals(TASK_ID, activeCaptor.getValue().getTaskId());
        assertEquals(USER_ID, activeCaptor.getValue().getUserId());
        assertNotNull(activeCaptor.getValue().getJoinedAt());

        ArgumentCaptor<TaskAssigneeLogDO> logCaptor = ArgumentCaptor.forClass(TaskAssigneeLogDO.class);
        verify(taskAssigneeLogMapper).insert(logCaptor.capture());
        assertEquals(TASK_ID, logCaptor.getValue().getTaskId());
        assertEquals(USER_ID, logCaptor.getValue().getUserId());
        assertEquals("join", logCaptor.getValue().getActionType());
        assertNull(logCaptor.getValue().getReason());
        assertNotNull(logCaptor.getValue().getActionTime());
    }

    @Test
    void createAssignee_whenUserIsOwner_shouldThrow() {
        TaskAssigneeSaveReqVO reqVO = new TaskAssigneeSaveReqVO();
        reqVO.setUserId(OWNER_ID);

        stubEditableContext();
        when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(PROJECT_ID, EXECUTION_ID, TASK_ID))
                .thenReturn(createTask(OWNER_ID));

        ServiceException ex = assertThrows(ServiceException.class,
                () -> taskAssigneeService.createAssignee(PROJECT_ID, EXECUTION_ID, TASK_ID, reqVO));
        assertEquals(ErrorCodeConstants.PROJECT_TASK_ASSIGNEE_OWNER_CONFLICT.getCode(), ex.getCode());
        verify(taskAssigneeMapper, never()).insert(any(TaskAssigneeDO.class));
    }

    @Test
    void createAssignee_whenUserNotActiveExecutionAssignee_shouldThrow() {
        TaskAssigneeSaveReqVO reqVO = new TaskAssigneeSaveReqVO();
        reqVO.setUserId(USER_ID);

        stubEditableContext();
        when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(PROJECT_ID, EXECUTION_ID, TASK_ID))
                .thenReturn(createTask(OWNER_ID));
        when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(EXECUTION_ID, USER_ID))
                .thenReturn(null);

        ServiceException ex = assertThrows(ServiceException.class,
                () -> taskAssigneeService.createAssignee(PROJECT_ID, EXECUTION_ID, TASK_ID, reqVO));
        assertEquals(ErrorCodeConstants.PROJECT_TASK_ASSIGNEE_INVALID_MEMBER.getCode(), ex.getCode());
        verify(taskAssigneeMapper, never()).insert(any(TaskAssigneeDO.class));
    }

    @Test
    void createAssignee_whenAlreadyActive_shouldThrow() {
        TaskAssigneeSaveReqVO reqVO = new TaskAssigneeSaveReqVO();
        reqVO.setUserId(USER_ID);

        stubEditableContext();
        when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(PROJECT_ID, EXECUTION_ID, TASK_ID))
                .thenReturn(createTask(OWNER_ID));
        when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(EXECUTION_ID, USER_ID))
                .thenReturn(createExecutionAssignee(EXECUTION_ID, USER_ID));
        when(taskAssigneeMapper.selectActiveByTaskIdAndUserId(TASK_ID, USER_ID))
                .thenReturn(createActiveAssignee(7001L, TASK_ID, USER_ID));

        ServiceException ex = assertThrows(ServiceException.class,
                () -> taskAssigneeService.createAssignee(PROJECT_ID, EXECUTION_ID, TASK_ID, reqVO));
        assertEquals(ErrorCodeConstants.PROJECT_TASK_ASSIGNEE_ALREADY_ACTIVE.getCode(), ex.getCode());
        verify(taskAssigneeMapper, never()).insert(any(TaskAssigneeDO.class));
    }

    // -------------------- inactiveAssignee --------------------

    @Test
    void inactiveAssignee_shouldMarkDeletedAndWriteInactiveLog() {
        Long assigneeId = 7001L;
        TaskAssigneeInactiveReqVO reqVO = new TaskAssigneeInactiveReqVO();
        reqVO.setReason("项目阶段调整");

        stubEditableContext();
        when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(PROJECT_ID, EXECUTION_ID, TASK_ID))
                .thenReturn(createTask(OWNER_ID));
        when(taskAssigneeMapper.selectByIdAndTaskId(assigneeId, TASK_ID))
                .thenReturn(createActiveAssignee(assigneeId, TASK_ID, USER_ID));

        taskAssigneeService.inactiveAssignee(PROJECT_ID, EXECUTION_ID, TASK_ID, assigneeId, reqVO);

        ArgumentCaptor<TaskAssigneeDO> activeCaptor = ArgumentCaptor.forClass(TaskAssigneeDO.class);
        verify(taskAssigneeMapper).updateById(activeCaptor.capture());
        assertNotNull(activeCaptor.getValue().getRemovedAt());

        ArgumentCaptor<TaskAssigneeLogDO> logCaptor = ArgumentCaptor.forClass(TaskAssigneeLogDO.class);
        verify(taskAssigneeLogMapper).insert(logCaptor.capture());
        assertEquals("inactive", logCaptor.getValue().getActionType());
        assertEquals("项目阶段调整", logCaptor.getValue().getReason());
        assertEquals(USER_ID, logCaptor.getValue().getUserId());
    }

    @Test
    void inactiveAssignee_whenAssigneeNotExists_shouldThrow() {
        TaskAssigneeInactiveReqVO reqVO = new TaskAssigneeInactiveReqVO();
        reqVO.setReason("调整");

        stubEditableContext();
        when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(PROJECT_ID, EXECUTION_ID, TASK_ID))
                .thenReturn(createTask(OWNER_ID));
        when(taskAssigneeMapper.selectByIdAndTaskId(7001L, TASK_ID)).thenReturn(null);

        ServiceException ex = assertThrows(ServiceException.class,
                () -> taskAssigneeService.inactiveAssignee(PROJECT_ID, EXECUTION_ID, TASK_ID, 7001L, reqVO));
        assertEquals(ErrorCodeConstants.PROJECT_TASK_ASSIGNEE_NOT_EXISTS.getCode(), ex.getCode());
    }

    @Test
    void inactiveAssignee_whenAlreadyInactive_shouldThrow() {
        TaskAssigneeInactiveReqVO reqVO = new TaskAssigneeInactiveReqVO();
        reqVO.setReason("调整");

        stubEditableContext();
        when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(PROJECT_ID, EXECUTION_ID, TASK_ID))
                .thenReturn(createTask(OWNER_ID));
        TaskAssigneeDO inactive = createActiveAssignee(7001L, TASK_ID, USER_ID);
        inactive.setRemovedAt(LocalDateTime.now());
        when(taskAssigneeMapper.selectByIdAndTaskId(7001L, TASK_ID)).thenReturn(inactive);

        ServiceException ex = assertThrows(ServiceException.class,
                () -> taskAssigneeService.inactiveAssignee(PROJECT_ID, EXECUTION_ID, TASK_ID, 7001L, reqVO));
        assertEquals(ErrorCodeConstants.PROJECT_TASK_ASSIGNEE_NOT_ACTIVE.getCode(), ex.getCode());
    }

    @Test
    void inactiveAssignee_whenReasonBlank_shouldThrow() {
        TaskAssigneeInactiveReqVO reqVO = new TaskAssigneeInactiveReqVO();
        reqVO.setReason("   ");

        stubEditableContext();
        when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(PROJECT_ID, EXECUTION_ID, TASK_ID))
                .thenReturn(createTask(OWNER_ID));
        when(taskAssigneeMapper.selectByIdAndTaskId(7001L, TASK_ID))
                .thenReturn(createActiveAssignee(7001L, TASK_ID, USER_ID));

        ServiceException ex = assertThrows(ServiceException.class,
                () -> taskAssigneeService.inactiveAssignee(PROJECT_ID, EXECUTION_ID, TASK_ID, 7001L, reqVO));
        assertEquals(ErrorCodeConstants.PROJECT_TASK_ASSIGNEE_REASON_REQUIRED.getCode(), ex.getCode());
    }

    // -------------------- getAssigneeList --------------------

    @Test
    void getAssigneeList_shouldReturnActiveAssigneesWithNickname() {
        when(projectExecutionMapper.selectByProjectIdAndId(PROJECT_ID, EXECUTION_ID))
                .thenReturn(createExecution());
        when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(PROJECT_ID, EXECUTION_ID, TASK_ID))
                .thenReturn(createTask(OWNER_ID));
        when(taskAssigneeMapper.selectActiveListByTaskId(TASK_ID))
                .thenReturn(List.of(createActiveAssignee(7001L, TASK_ID, USER_ID)));
        AdminUserRespDTO user = new AdminUserRespDTO();
        user.setId(USER_ID);
        user.setNickname("张三");
        when(adminUserApi.getUserMap(anyCollection())).thenReturn(Map.of(USER_ID, user));

        List<TaskAssigneeRespVO> list = taskAssigneeService.getAssigneeList(PROJECT_ID, EXECUTION_ID, TASK_ID);

        assertEquals(1, list.size());
        assertEquals(7001L, list.get(0).getId());
        assertEquals(USER_ID, list.get(0).getUserId());
        assertEquals("张三", list.get(0).getUserNickname());
    }

    // -------------------- getAssigneeLogPage --------------------

    @Test
    void getAssigneeLogPage_shouldDelegateToMapper() {
        TaskAssigneeLogPageReqVO reqVO = new TaskAssigneeLogPageReqVO();
        TaskAssigneeLogDO logDO = new TaskAssigneeLogDO();
        logDO.setId(8001L);
        logDO.setTaskId(TASK_ID);
        logDO.setUserId(USER_ID);
        logDO.setUserNicknameSnapshot("张三");
        logDO.setActionType("join");
        logDO.setOperatorUserId(OWNER_ID);
        logDO.setOperatorNicknameSnapshot("项目经理");
        logDO.setActionTime(LocalDateTime.now());
        PageResult<TaskAssigneeLogDO> page = new PageResult<>(List.of(logDO), 1L);

        when(projectExecutionMapper.selectByProjectIdAndId(PROJECT_ID, EXECUTION_ID))
                .thenReturn(createExecution());
        when(projectTaskMapper.selectByProjectIdAndExecutionIdAndId(PROJECT_ID, EXECUTION_ID, TASK_ID))
                .thenReturn(createTask(OWNER_ID));
        when(taskAssigneeLogMapper.selectPageByTaskId(TASK_ID, reqVO)).thenReturn(page);

        PageResult<TaskAssigneeLogRespVO> result =
                taskAssigneeService.getAssigneeLogPage(PROJECT_ID, EXECUTION_ID, TASK_ID, reqVO);

        assertEquals(1L, result.getTotal());
        assertEquals(1, result.getList().size());
        assertEquals("join", result.getList().get(0).getActionType());
        assertEquals(USER_ID, result.getList().get(0).getUserId());
    }

    // -------------------- initializeAssignees --------------------

    @Test
    void initializeAssignees_whenListEmpty_shouldNoop() {
        taskAssigneeService.initializeAssignees(TASK_ID, OWNER_ID, EXECUTION_ID, Collections.emptyList());
        verify(taskAssigneeMapper, never()).insert(any(TaskAssigneeDO.class));
        verify(taskAssigneeLogMapper, never()).insert(any(TaskAssigneeLogDO.class));
    }

    @Test
    void initializeAssignees_shouldDedupAndWriteAll() {
        Long userA = 4001L;
        Long userB = 4002L;
        when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(EXECUTION_ID, userA))
                .thenReturn(createExecutionAssignee(EXECUTION_ID, userA));
        when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(EXECUTION_ID, userB))
                .thenReturn(createExecutionAssignee(EXECUTION_ID, userB));
        when(taskAssigneeMapper.selectActiveByTaskIdAndUserId(TASK_ID, userA)).thenReturn(null);
        when(taskAssigneeMapper.selectActiveByTaskIdAndUserId(TASK_ID, userB)).thenReturn(null);
        when(taskAssigneeMapper.insert(any(TaskAssigneeDO.class))).thenReturn(1);

        // 列表里 userA 重复出现两次，期望去重为单次写入
        taskAssigneeService.initializeAssignees(TASK_ID, OWNER_ID, EXECUTION_ID, List.of(userA, userB, userA));

        verify(taskAssigneeMapper, org.mockito.Mockito.times(2)).insert(any(TaskAssigneeDO.class));
        verify(taskAssigneeLogMapper, org.mockito.Mockito.times(2)).insert(any(TaskAssigneeLogDO.class));
    }

    @Test
    void initializeAssignees_whenContainsOwnerId_shouldThrow() {
        ServiceException ex = assertThrows(ServiceException.class,
                () -> taskAssigneeService.initializeAssignees(TASK_ID, OWNER_ID, EXECUTION_ID, List.of(OWNER_ID)));
        assertEquals(ErrorCodeConstants.PROJECT_TASK_ASSIGNEE_OWNER_CONFLICT.getCode(), ex.getCode());
        verify(taskAssigneeMapper, never()).insert(any(TaskAssigneeDO.class));
    }

    // -------------------- helpers --------------------

    private void stubEditableContext() {
        when(projectMapper.selectById(PROJECT_ID)).thenReturn(createEditableProject());
        when(projectExecutionMapper.selectByProjectIdAndId(PROJECT_ID, EXECUTION_ID))
                .thenReturn(createExecution());
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
                .thenReturn(createStatus("project", "pending", true));
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("execution", "pending"))
                .thenReturn(createStatus("execution", "pending", true));
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("task", "pending"))
                .thenReturn(createStatus("task", "pending", true));
    }

    private ProjectDO createEditableProject() {
        ProjectDO project = new ProjectDO();
        project.setId(PROJECT_ID);
        project.setStatusCode("pending");
        return project;
    }

    private ProjectExecutionDO createExecution() {
        ProjectExecutionDO execution = new ProjectExecutionDO();
        execution.setId(EXECUTION_ID);
        execution.setProjectId(PROJECT_ID);
        execution.setStatusCode("pending");
        return execution;
    }

    private ProjectTaskDO createTask(Long ownerId) {
        ProjectTaskDO task = new ProjectTaskDO();
        task.setId(TASK_ID);
        task.setProjectId(PROJECT_ID);
        task.setExecutionId(EXECUTION_ID);
        task.setOwnerId(ownerId);
        task.setStatusCode("pending");
        return task;
    }

    private ExecutionAssigneeDO createExecutionAssignee(Long executionId, Long userId) {
        ExecutionAssigneeDO member = new ExecutionAssigneeDO();
        member.setExecutionId(executionId);
        member.setUserId(userId);
        return member;
    }

    private TaskAssigneeDO createActiveAssignee(Long id, Long taskId, Long userId) {
        TaskAssigneeDO assignee = new TaskAssigneeDO();
        assignee.setId(id);
        assignee.setTaskId(taskId);
        assignee.setUserId(userId);
        assignee.setJoinedAt(LocalDateTime.now());
        assignee.setRemovedAt(null);
        return assignee;
    }

    private ObjectStatusModelDO createStatus(String objectType, String statusCode, boolean allowEdit) {
        ObjectStatusModelDO status = new ObjectStatusModelDO();
        status.setObjectType(objectType);
        status.setStatusCode(statusCode);
        status.setAllowEdit(allowEdit);
        return status;
    }
}
