package com.njcn.rdms.module.project.service.project.execution;

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.execution.vo.assignee.ExecutionAssigneeInactiveReqVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeLogPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeLogRespVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeRespVO;
import com.njcn.rdms.module.project.controller.admin.project.execution.vo.assignee.ExecutionAssigneeSaveReqVO;
import com.njcn.rdms.module.project.dal.dataobject.audit.BizAuditLogDO;
import com.njcn.rdms.module.project.dal.dataobject.member.UserObjectRoleDO;
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.ExecutionAssigneeLogDO;
import com.njcn.rdms.module.project.dal.dataobject.project.execution.ProjectExecutionDO;
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.member.UserObjectRoleMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
import com.njcn.rdms.module.project.dal.mysql.project.execution.ExecutionAssigneeLogMapper;
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.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.math.BigDecimal;
import java.time.LocalDateTime;
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.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class ProjectExecutionAssigneeServiceImplTest extends BaseMockitoUnitTest {

    @InjectMocks
    private ProjectExecutionAssigneeServiceImpl projectExecutionAssigneeService;
    @Mock
    private ProjectMapper projectMapper;
    @Mock
    private ProjectExecutionMapper projectExecutionMapper;
    @Mock
    private ExecutionAssigneeMapper executionAssigneeMapper;
    @Mock
    private ExecutionAssigneeLogMapper executionAssigneeLogMapper;
    @Mock
    private UserObjectRoleMapper userObjectRoleMapper;
    @Mock
    private BizAuditLogMapper bizAuditLogMapper;
    @Mock
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Mock
    private AdminUserApi adminUserApi;

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

    // -------------------- getExecutionAssigneeList --------------------

    @Test
    void getExecutionAssigneeList_shouldReturnOnlyActiveSegments() {
        when(projectMapper.selectById(PROJECT_ID)).thenReturn(createEditableProject());
        when(projectExecutionMapper.selectByProjectIdAndId(PROJECT_ID, EXECUTION_ID))
                .thenReturn(createExecution());
        when(executionAssigneeMapper.selectActiveListByExecutionId(EXECUTION_ID))
                .thenReturn(List.of(createMember(7001L, USER_ID, null)));

        List<ExecutionAssigneeRespVO> respVOList = projectExecutionAssigneeService
                .getExecutionAssigneeList(PROJECT_ID, EXECUTION_ID);

        assertEquals(1, respVOList.size());
        assertEquals(USER_ID, respVOList.get(0).getUserId());
        assertNull(respVOList.get(0).getRemovedAt());
        verify(executionAssigneeMapper, never()).selectListByExecutionId(any());
    }

    // -------------------- createExecutionAssignee --------------------

    @Test
    void createExecutionAssignee_byUserNeverJoined_shouldInsertActiveAndWriteJoinLog() {
        stubEditableContext();
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", PROJECT_ID, USER_ID))
                .thenReturn(List.of(createProjectMember()));
        when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(EXECUTION_ID, USER_ID)).thenReturn(null);
        when(executionAssigneeMapper.insert(any(ExecutionAssigneeDO.class))).thenAnswer(inv -> {
            ExecutionAssigneeDO m = inv.getArgument(0);
            m.setId(7001L);
            return 1;
        });

        ExecutionAssigneeSaveReqVO reqVO = new ExecutionAssigneeSaveReqVO();
        reqVO.setUserId(USER_ID);

        Long memberId = projectExecutionAssigneeService.createExecutionAssignee(PROJECT_ID, EXECUTION_ID, reqVO);

        assertEquals(7001L, memberId);
        ArgumentCaptor<ExecutionAssigneeDO> activeCaptor = ArgumentCaptor.forClass(ExecutionAssigneeDO.class);
        verify(executionAssigneeMapper).insert(activeCaptor.capture());
        assertNull(activeCaptor.getValue().getRemovedAt());
        assertNull(activeCaptor.getValue().getRemovedReason());
        ArgumentCaptor<ExecutionAssigneeLogDO> logCaptor = ArgumentCaptor.forClass(ExecutionAssigneeLogDO.class);
        verify(executionAssigneeLogMapper).insert(logCaptor.capture());
        assertEquals("join", logCaptor.getValue().getActionType());
        assertEquals(USER_ID, logCaptor.getValue().getUserId());
        assertNull(logCaptor.getValue().getReason());
        // 旧 BizAuditLog 仍写一条（兼容现有审计链路）
        verify(bizAuditLogMapper).insert(any(BizAuditLogDO.class));
    }

    @Test
    void createExecutionAssignee_whenInactiveSegmentExists_shouldInsertNewSegmentAndPreserveHistoricalReason() {
        // B 模型：用户曾失效，重新加入新插一段，旧段不动
        stubEditableContext();
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", PROJECT_ID, USER_ID))
                .thenReturn(List.of(createProjectMember()));
        // 当前没有活跃段（旧段已失效），通过 active-only 校验
        when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(EXECUTION_ID, USER_ID)).thenReturn(null);
        when(executionAssigneeMapper.insert(any(ExecutionAssigneeDO.class))).thenAnswer(inv -> {
            ExecutionAssigneeDO m = inv.getArgument(0);
            m.setId(7002L);
            return 1;
        });

        ExecutionAssigneeSaveReqVO reqVO = new ExecutionAssigneeSaveReqVO();
        reqVO.setUserId(USER_ID);

        Long memberId = projectExecutionAssigneeService.createExecutionAssignee(PROJECT_ID, EXECUTION_ID, reqVO);

        assertEquals(7002L, memberId);
        // 不应触碰旧段（updateById 不应被调用）
        verify(executionAssigneeMapper, never()).updateById(any(ExecutionAssigneeDO.class));
        verify(executionAssigneeMapper).insert(any(ExecutionAssigneeDO.class));
    }

    @Test
    void createExecutionAssignee_whenAlreadyActive_shouldThrowAlreadyExists() {
        stubEditableContext();
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", PROJECT_ID, USER_ID))
                .thenReturn(List.of(createProjectMember()));
        when(executionAssigneeMapper.selectActiveByExecutionIdAndUserId(EXECUTION_ID, USER_ID))
                .thenReturn(createMember(7001L, USER_ID, null));

        ExecutionAssigneeSaveReqVO reqVO = new ExecutionAssigneeSaveReqVO();
        reqVO.setUserId(USER_ID);

        ServiceException ex = assertThrows(ServiceException.class,
                () -> projectExecutionAssigneeService.createExecutionAssignee(PROJECT_ID, EXECUTION_ID, reqVO));
        assertEquals(ErrorCodeConstants.PROJECT_EXECUTION_ASSIGNEE_ALREADY_EXISTS.getCode(), ex.getCode());
        verify(executionAssigneeMapper, never()).insert(any(ExecutionAssigneeDO.class));
    }

    @Test
    void createExecutionAssignee_whenExecutionPaused_shouldThrowNotAllowEdit() {
        ProjectExecutionDO execution = createExecution();
        execution.setStatusCode("paused");
        when(projectMapper.selectById(PROJECT_ID)).thenReturn(createEditableProject());
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
                .thenReturn(createStatus("project", "pending", true));
        when(projectExecutionMapper.selectByProjectIdAndId(PROJECT_ID, EXECUTION_ID)).thenReturn(execution);
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("execution", "paused"))
                .thenReturn(createStatus("execution", "paused", false));

        ExecutionAssigneeSaveReqVO reqVO = new ExecutionAssigneeSaveReqVO();
        reqVO.setUserId(USER_ID);

        ServiceException ex = assertThrows(ServiceException.class,
                () -> projectExecutionAssigneeService.createExecutionAssignee(PROJECT_ID, EXECUTION_ID, reqVO));
        assertEquals(ErrorCodeConstants.PROJECT_EXECUTION_STATUS_NOT_ALLOW_EDIT.getCode(), ex.getCode());
    }

    // -------------------- inactiveExecutionAssignee --------------------

    @Test
    void inactiveExecutionAssignee_shouldSetRemovedAtAndWriteInactiveLog() {
        ExecutionAssigneeDO member = createMember(7001L, USER_ID, null);
        stubEditableContext();
        when(executionAssigneeMapper.selectByIdAndExecutionId(7001L, EXECUTION_ID)).thenReturn(member);

        ExecutionAssigneeInactiveReqVO reqVO = new ExecutionAssigneeInactiveReqVO();
        reqVO.setReason("阶段性退出");

        projectExecutionAssigneeService.inactiveExecutionAssignee(PROJECT_ID, EXECUTION_ID, 7001L, reqVO);

        assertNotNull(member.getRemovedAt());
        assertEquals("阶段性退出", member.getRemovedReason());
        verify(executionAssigneeMapper).updateById(member);
        ArgumentCaptor<ExecutionAssigneeLogDO> logCaptor = ArgumentCaptor.forClass(ExecutionAssigneeLogDO.class);
        verify(executionAssigneeLogMapper).insert(logCaptor.capture());
        assertEquals("inactive", logCaptor.getValue().getActionType());
        assertEquals(USER_ID, logCaptor.getValue().getUserId());
        assertEquals("阶段性退出", logCaptor.getValue().getReason());
        assertNotNull(logCaptor.getValue().getActionTime());
    }

    @Test
    void inactiveExecutionAssignee_whenAlreadyInactive_shouldThrowNotActive() {
        ExecutionAssigneeDO inactiveMember = createMember(7001L, USER_ID, LocalDateTime.now().minusDays(1));
        stubEditableContext();
        when(executionAssigneeMapper.selectByIdAndExecutionId(7001L, EXECUTION_ID)).thenReturn(inactiveMember);

        ExecutionAssigneeInactiveReqVO reqVO = new ExecutionAssigneeInactiveReqVO();
        reqVO.setReason("再次失效");

        ServiceException ex = assertThrows(ServiceException.class,
                () -> projectExecutionAssigneeService.inactiveExecutionAssignee(PROJECT_ID, EXECUTION_ID, 7001L, reqVO));
        assertEquals(ErrorCodeConstants.PROJECT_EXECUTION_ASSIGNEE_NOT_ACTIVE.getCode(), ex.getCode());
        verify(executionAssigneeMapper, never()).updateById(any(ExecutionAssigneeDO.class));
    }

    @Test
    void inactiveExecutionAssignee_whenExecutionPaused_shouldThrowNotAllowEdit() {
        ProjectExecutionDO execution = createExecution();
        execution.setStatusCode("paused");
        when(projectMapper.selectById(PROJECT_ID)).thenReturn(createEditableProject());
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
                .thenReturn(createStatus("project", "pending", true));
        when(projectExecutionMapper.selectByProjectIdAndId(PROJECT_ID, EXECUTION_ID)).thenReturn(execution);
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("execution", "paused"))
                .thenReturn(createStatus("execution", "paused", false));

        ExecutionAssigneeInactiveReqVO reqVO = new ExecutionAssigneeInactiveReqVO();
        reqVO.setReason("暂停态退出");

        ServiceException ex = assertThrows(ServiceException.class,
                () -> projectExecutionAssigneeService.inactiveExecutionAssignee(PROJECT_ID, EXECUTION_ID, 7001L, reqVO));
        assertEquals(ErrorCodeConstants.PROJECT_EXECUTION_STATUS_NOT_ALLOW_EDIT.getCode(), ex.getCode());
    }

    // -------------------- getExecutionAssigneeLogPage --------------------

    @Test
    void getExecutionAssigneeLogPage_shouldDelegateToMapperAndConvertVO() {
        when(projectMapper.selectById(PROJECT_ID)).thenReturn(createEditableProject());
        when(projectExecutionMapper.selectByProjectIdAndId(PROJECT_ID, EXECUTION_ID))
                .thenReturn(createExecution());
        ExecutionAssigneeLogDO logRow = new ExecutionAssigneeLogDO();
        logRow.setId(12001L);
        logRow.setExecutionId(EXECUTION_ID);
        logRow.setUserId(USER_ID);
        logRow.setActionType("join");
        logRow.setActionTime(LocalDateTime.now());
        when(executionAssigneeLogMapper.selectPageByExecutionId(eq(EXECUTION_ID), any(ExecutionAssigneeLogPageReqVO.class)))
                .thenReturn(new PageResult<>(List.of(logRow), 1L));

        ExecutionAssigneeLogPageReqVO reqVO = new ExecutionAssigneeLogPageReqVO();
        PageResult<ExecutionAssigneeLogRespVO> page = projectExecutionAssigneeService
                .getExecutionAssigneeLogPage(PROJECT_ID, EXECUTION_ID, reqVO);

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

    @Test
    void getExecutionAssigneeLogPage_whenSnapshotExists_shouldUseCurrentNicknamesByAdminUserApi() {
        when(projectMapper.selectById(PROJECT_ID)).thenReturn(createEditableProject());
        when(projectExecutionMapper.selectByProjectIdAndId(PROJECT_ID, EXECUTION_ID))
                .thenReturn(createExecution());
        ExecutionAssigneeLogDO logRow = new ExecutionAssigneeLogDO();
        logRow.setId(12002L);
        logRow.setExecutionId(EXECUTION_ID);
        logRow.setUserId(USER_ID);
        logRow.setUserNicknameSnapshot("旧成员名");
        logRow.setActionType("join");
        logRow.setOperatorUserId(OWNER_ID);
        logRow.setOperatorNicknameSnapshot("旧操作人名");
        logRow.setActionTime(LocalDateTime.now());
        when(executionAssigneeLogMapper.selectPageByExecutionId(eq(EXECUTION_ID), any(ExecutionAssigneeLogPageReqVO.class)))
                .thenReturn(new PageResult<>(List.of(logRow), 1L));
        when(adminUserApi.getUserMap(anyCollection())).thenReturn(Map.of(
                USER_ID, createUser("陈道飞"),
                OWNER_ID, createUser("灿能源码")));

        ExecutionAssigneeLogPageReqVO reqVO = new ExecutionAssigneeLogPageReqVO();
        PageResult<ExecutionAssigneeLogRespVO> page = projectExecutionAssigneeService
                .getExecutionAssigneeLogPage(PROJECT_ID, EXECUTION_ID, reqVO);

        assertEquals("陈道飞", page.getList().get(0).getUserNicknameSnapshot());
        assertEquals("灿能源码", page.getList().get(0).getOperatorNicknameSnapshot());
        verify(adminUserApi).getUserMap(anyCollection());
    }

    // -------------------- writeAssigneeLog 直接调用 --------------------

    @Test
    void writeAssigneeLog_shouldInsertWithProvidedFields() {
        projectExecutionAssigneeService.writeAssigneeLog(EXECUTION_ID, USER_ID, "owner_transfer_in", "test reason");

        ArgumentCaptor<ExecutionAssigneeLogDO> captor = ArgumentCaptor.forClass(ExecutionAssigneeLogDO.class);
        verify(executionAssigneeLogMapper).insert(captor.capture());
        assertEquals(EXECUTION_ID, captor.getValue().getExecutionId());
        assertEquals(USER_ID, captor.getValue().getUserId());
        assertEquals("owner_transfer_in", captor.getValue().getActionType());
        assertEquals("test reason", captor.getValue().getReason());
        assertNotNull(captor.getValue().getActionTime());
    }

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

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

    private ProjectDO createEditableProject() {
        ProjectDO project = new ProjectDO();
        project.setId(PROJECT_ID);
        project.setProjectCode("CNPJ2026001");
        project.setProjectName("测试项目");
        project.setStatusCode("pending");
        project.setManagerUserId(OWNER_ID);
        return project;
    }

    private ProjectExecutionDO createExecution() {
        ProjectExecutionDO execution = new ProjectExecutionDO();
        execution.setId(EXECUTION_ID);
        execution.setProjectId(PROJECT_ID);
        execution.setExecutionName("接口联调");
        execution.setOwnerId(OWNER_ID);
        execution.setStatusCode("pending");
        execution.setProgressRate(BigDecimal.ZERO);
        execution.setDeleted(Boolean.FALSE);
        return execution;
    }

    private ExecutionAssigneeDO createMember(Long id, Long userId, LocalDateTime removedAt) {
        ExecutionAssigneeDO member = new ExecutionAssigneeDO();
        member.setId(id);
        member.setExecutionId(EXECUTION_ID);
        member.setUserId(userId);
        member.setJoinedAt(LocalDateTime.now().minusDays(2));
        member.setRemovedAt(removedAt);
        member.setRemovedReason(removedAt == null ? null : "历史退出");
        return member;
    }

    private UserObjectRoleDO createProjectMember() {
        UserObjectRoleDO role = new UserObjectRoleDO();
        role.setId(99L);
        role.setObjectType("project");
        role.setObjectId(PROJECT_ID);
        role.setUserId(USER_ID);
        role.setRoleId(3102L);
        role.setStatus(0);
        role.setJoinedTime(LocalDateTime.now());
        return role;
    }

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

    private AdminUserRespDTO createUser(String nickname) {
        AdminUserRespDTO user = new AdminUserRespDTO();
        user.setNickname(nickname);
        return user;
    }

}
