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

import com.njcn.rdms.framework.common.exception.ServiceException;
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberInactiveReqVO;
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.member.ProjectMemberUpdateReqVO;
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.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.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.permission.ObjectPermissionApi;
import com.njcn.rdms.module.system.api.permission.dto.ObjectRoleRespDTO;
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.List;
import java.util.Map;
import java.util.Set;

import static com.njcn.rdms.framework.common.pojo.CommonResult.success;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
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.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class ProjectMemberServiceImplTest extends BaseMockitoUnitTest {

    @InjectMocks
    private ProjectMemberServiceImpl projectMemberService;
    @Mock
    private ProjectMapper projectMapper;
    @Mock
    private UserObjectRoleMapper userObjectRoleMapper;
    @Mock
    private ObjectPermissionApi objectPermissionApi;
    @Mock
    private BizAuditLogMapper bizAuditLogMapper;
    @Mock
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Mock
    private AdminUserApi adminUserApi;
    @Mock
    private ProjectExecutionMapper projectExecutionMapper;

    @Test
    void getProjectMemberList_shouldFillRoleAndUserInfo() {
        Long projectId = 1001L;
        ProjectDO project = createProject(projectId, 2001L);
        UserObjectRoleDO manager = createMember(9001L, projectId, 2001L, 3101L, 0);
        UserObjectRoleDO member = createMember(9002L, projectId, 2002L, 3102L, 0);
        when(projectMapper.selectById(projectId)).thenReturn(project);
        when(userObjectRoleMapper.selectListByObject("project", projectId)).thenReturn(List.of(manager, member));
        when(objectPermissionApi.getObjectRoleList(Set.of(3101L, 3102L), "object", "project"))
                .thenReturn(success(List.of(
                        createRole(3101L, "project_manager", "项目经理"),
                        createRole(3102L, "project_member_configured", "项目成员")
                )));
        when(adminUserApi.getUserMap(Set.of(2001L, 2002L))).thenReturn(Map.of(
                2001L, createUser("经理甲"),
                2002L, createUser("成员乙")
        ));

        List<ProjectMemberRespVO> respVOList = projectMemberService.getProjectMemberList(projectId);

        assertEquals(2, respVOList.size());
        assertEquals("项目经理", respVOList.get(0).getRoleName());
        assertEquals("project_manager", respVOList.get(0).getRoleCode());
        assertEquals(Boolean.TRUE, respVOList.get(0).getManagerFlag());
        assertEquals("项目成员", respVOList.get(1).getRoleName());
        assertFalse(respVOList.get(1).getManagerFlag());
    }

    @Test
    void createProjectMember_whenUserAlreadyActive_shouldThrowAlreadyExists() {
        Long projectId = 1002L;
        ProjectMemberSaveReqVO reqVO = new ProjectMemberSaveReqVO();
        reqVO.setUserId(2002L);
        reqVO.setRoleId(3102L);
        when(projectMapper.selectById(projectId)).thenReturn(createProject(projectId, 2001L));
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "active"))
                .thenReturn(createStatus("active", true));
        when(adminUserApi.validateUserList(any())).thenReturn(success(true));
        when(objectPermissionApi.getObjectRoleById(3102L, "object", "project"))
                .thenReturn(success(createRole(3102L, "project_member_configured", "项目成员")));
        when(userObjectRoleMapper.selectByObjectUserAndRole("project", projectId, 2002L, 3102L))
                .thenReturn(createMember(9002L, projectId, 2002L, 3102L, 0));

        ServiceException ex = assertThrows(ServiceException.class,
                () -> projectMemberService.createProjectMember(projectId, reqVO));

        assertEquals(ErrorCodeConstants.PROJECT_MEMBER_ALREADY_EXISTS.getCode(), ex.getCode());
        verify(userObjectRoleMapper, never()).insert(any(UserObjectRoleDO.class));
        verify(bizAuditLogMapper, never()).insert(any(BizAuditLogDO.class));
    }

    @Test
    void createProjectMember_whenRoleInvalid_shouldThrowRoleInvalid() {
        Long projectId = 1003L;
        ProjectMemberSaveReqVO reqVO = new ProjectMemberSaveReqVO();
        reqVO.setUserId(2003L);
        reqVO.setRoleId(3999L);
        when(projectMapper.selectById(projectId)).thenReturn(createProject(projectId, 2001L));
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "active"))
                .thenReturn(createStatus("active", true));
        when(adminUserApi.validateUserList(any())).thenReturn(success(true));
        when(objectPermissionApi.getObjectRoleById(3999L, "object", "project")).thenReturn(success(null));

        ServiceException ex = assertThrows(ServiceException.class,
                () -> projectMemberService.createProjectMember(projectId, reqVO));

        assertEquals(ErrorCodeConstants.PROJECT_MEMBER_ROLE_INVALID.getCode(), ex.getCode());
    }

    @Test
    void createProjectMember_whenManagerTransfer_shouldKeepPreviousManagerActiveWithNewRole() {
        Long projectId = 1004L;
        ProjectMemberSaveReqVO reqVO = new ProjectMemberSaveReqVO();
        reqVO.setUserId(2002L);
        reqVO.setRoleId(3101L);
        reqVO.setPreviousManagerUserId(2001L);
        reqVO.setPreviousManagerRoleId(3102L);
        ProjectDO project = createProject(projectId, 2001L);
        UserObjectRoleDO previousManager = createMember(9001L, projectId, 2001L, 3101L, 0);
        previousManager.setLeftTime(LocalDateTime.now());
        when(projectMapper.selectById(projectId)).thenReturn(project);
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "active"))
                .thenReturn(createStatus("active", true));
        when(adminUserApi.validateUserList(any())).thenReturn(success(true));
        when(objectPermissionApi.getObjectRoleById(3101L, "object", "project"))
                .thenReturn(success(createRole(3101L, "project_manager", "项目经理")));
        when(objectPermissionApi.getObjectRoleById(3102L, "object", "project"))
                .thenReturn(success(createRole(3102L, "project_member_configured", "项目成员")));
        when(userObjectRoleMapper.selectByObjectUserAndRole("project", projectId, 2002L, 3101L)).thenReturn(null);
        // transferPreviousManager 内部：先校验目标 role（previousManagerRoleId=3102L）无任何行冲突（含 INACTIVE 历史）
        when(userObjectRoleMapper.selectByObjectUserAndRole("project", projectId, 2001L, 3102L)).thenReturn(null);
        // 再按 (user, object, manager_role_id=3101L) 三元组定位旧 manager 行
        when(userObjectRoleMapper.selectActiveByObjectUserAndRole("project", projectId, 2001L, 3101L))
                .thenReturn(previousManager);
        // resolveProjectManagerRoleId() 需要拿 manager 内置角色
        when(objectPermissionApi.getObjectRoleByCode("project_manager", "object", "project"))
                .thenReturn(success(createRole(3101L, "project_manager", "项目经理")));

        projectMemberService.createProjectMember(projectId, reqVO);

        ArgumentCaptor<UserObjectRoleDO> previousManagerCaptor = ArgumentCaptor.forClass(UserObjectRoleDO.class);
        verify(userObjectRoleMapper).updateById(previousManagerCaptor.capture());
        assertEquals(2001L, previousManagerCaptor.getValue().getUserId());
        assertEquals(3102L, previousManagerCaptor.getValue().getRoleId());
        assertEquals(0, previousManagerCaptor.getValue().getStatus());
        assertNull(previousManagerCaptor.getValue().getLeftTime());
        assertEquals(2002L, project.getManagerUserId());
        verify(projectMapper).updateById(project);
    }

    @Test
    void updateProjectMember_whenManagerTransferInfoMissing_shouldThrowInfoRequired() {
        Long projectId = 1005L;
        Long memberId = 9002L;
        ProjectMemberUpdateReqVO reqVO = new ProjectMemberUpdateReqVO();
        reqVO.setRoleId(3101L);
        UserObjectRoleDO member = createMember(memberId, projectId, 2002L, 3102L, 0);
        when(projectMapper.selectById(projectId)).thenReturn(createProject(projectId, 2001L));
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "active"))
                .thenReturn(createStatus("active", true));
        when(userObjectRoleMapper.selectByIdAndObject(memberId, "project", projectId)).thenReturn(member);
        when(objectPermissionApi.getObjectRoleById(3101L, "object", "project"))
                .thenReturn(success(createRole(3101L, "project_manager", "项目经理")));

        ServiceException ex = assertThrows(ServiceException.class,
                () -> projectMemberService.updateProjectMember(projectId, memberId, reqVO));

        assertEquals(ErrorCodeConstants.PROJECT_MANAGER_TRANSFER_INFO_REQUIRED.getCode(), ex.getCode());
    }

    @Test
    void updateProjectMember_whenCurrentManagerDowngrade_shouldThrowNotAllowDowngrade() {
        Long projectId = 1006L;
        Long memberId = 9001L;
        ProjectMemberUpdateReqVO reqVO = new ProjectMemberUpdateReqVO();
        reqVO.setRoleId(3102L);
        UserObjectRoleDO member = createMember(memberId, projectId, 2001L, 3101L, 0);
        when(projectMapper.selectById(projectId)).thenReturn(createProject(projectId, 2001L));
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "active"))
                .thenReturn(createStatus("active", true));
        when(userObjectRoleMapper.selectByIdAndObject(memberId, "project", projectId)).thenReturn(member);
        when(objectPermissionApi.getObjectRoleById(3102L, "object", "project"))
                .thenReturn(success(createRole(3102L, "project_member_configured", "项目成员")));

        ServiceException ex = assertThrows(ServiceException.class,
                () -> projectMemberService.updateProjectMember(projectId, memberId, reqVO));

        assertEquals(ErrorCodeConstants.PROJECT_MANAGER_MEMBER_NOT_ALLOW_DOWNGRADE.getCode(), ex.getCode());
        verify(userObjectRoleMapper, never()).updateById(any(UserObjectRoleDO.class));
    }

    @Test
    void inactiveProjectMember_whenStillOwnsOpenExecution_shouldThrowHandoffRequired() {
        Long projectId = 1001L;
        Long memberId = 9002L;
        UserObjectRoleDO member = createMember(memberId, projectId, 2002L, 3102L, 0);
        when(projectMapper.selectById(projectId)).thenReturn(createProject(projectId, 2001L));
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "active"))
                .thenReturn(createStatus("active", true));
        when(userObjectRoleMapper.selectByIdAndObject(memberId, "project", projectId)).thenReturn(member);
        when(objectStatusModelMapper.selectListByObjectType("execution"))
                .thenReturn(List.of(createExecutionStatus("completed", true), createExecutionStatus("cancelled", true)));
        when(projectExecutionMapper.countNonTerminalByProjectIdAndOwnerId(projectId, 2002L,
                List.of("completed", "cancelled"))).thenReturn(1);

        ProjectMemberInactiveReqVO reqVO = new ProjectMemberInactiveReqVO();
        reqVO.setReason("退出项目");

        ServiceException ex = assertThrows(ServiceException.class,
                () -> projectMemberService.inactiveProjectMember(projectId, memberId, reqVO));

        assertEquals(ErrorCodeConstants.PROJECT_EXECUTION_OWNER_HANDOFF_REQUIRED.getCode(), ex.getCode());
    }

    @Test
    void createProjectMember_sameUserDifferentRole_shouldAllow() {
        Long projectId = 1010L;
        ProjectMemberSaveReqVO reqVO = new ProjectMemberSaveReqVO();
        reqVO.setUserId(2002L);
        reqVO.setRoleId(3102L);
        when(projectMapper.selectById(projectId)).thenReturn(createProject(projectId, 2001L));
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "active"))
                .thenReturn(createStatus("active", true));
        when(adminUserApi.validateUserList(any())).thenReturn(success(true));
        when(objectPermissionApi.getObjectRoleById(3102L, "object", "project"))
                .thenReturn(success(createRole(3102L, "project_member_configured", "项目成员")));
        // user=2002L 在项目里 (2002L, 3102L) 这个 role 不存在 —— 即便 user 已有别的 role，新 role 应允许
        when(userObjectRoleMapper.selectByObjectUserAndRole("project", projectId, 2002L, 3102L))
                .thenReturn(null);

        projectMemberService.createProjectMember(projectId, reqVO);

        // 不抛 ALREADY_EXISTS，新加一行
        verify(userObjectRoleMapper).insert(any(UserObjectRoleDO.class));
        verify(userObjectRoleMapper, never()).updateById(any(UserObjectRoleDO.class));
    }

    @Test
    void createProjectMember_inactiveSameRoleExists_shouldReactivate() {
        Long projectId = 1011L;
        Long userId = 2002L;
        Long roleId = 3102L;
        ProjectMemberSaveReqVO reqVO = new ProjectMemberSaveReqVO();
        reqVO.setUserId(userId);
        reqVO.setRoleId(roleId);
        UserObjectRoleDO inactiveOld = createMember(9099L, projectId, userId, roleId, 1);
        inactiveOld.setLeftTime(LocalDateTime.now().minusDays(7));
        when(projectMapper.selectById(projectId)).thenReturn(createProject(projectId, 2001L));
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "active"))
                .thenReturn(createStatus("active", true));
        when(adminUserApi.validateUserList(any())).thenReturn(success(true));
        when(objectPermissionApi.getObjectRoleById(roleId, "object", "project"))
                .thenReturn(success(createRole(roleId, "project_member_configured", "项目成员")));
        when(userObjectRoleMapper.selectByObjectUserAndRole("project", projectId, userId, roleId))
                .thenReturn(inactiveOld);

        projectMemberService.createProjectMember(projectId, reqVO);

        // 复活：updateById 被调，status=ACTIVE，leftTime=null；不调 insert
        ArgumentCaptor<UserObjectRoleDO> captor = ArgumentCaptor.forClass(UserObjectRoleDO.class);
        verify(userObjectRoleMapper).updateById(captor.capture());
        assertEquals(0, captor.getValue().getStatus());
        assertNull(captor.getValue().getLeftTime());
        verify(userObjectRoleMapper, never()).insert(any(UserObjectRoleDO.class));
    }

    private ProjectDO createProject(Long projectId, Long managerUserId) {
        ProjectDO project = new ProjectDO();
        project.setId(projectId);
        project.setManagerUserId(managerUserId);
        project.setStatusCode("active");
        project.setProjectName("测试项目");
        project.setProjectCode("CNPJ2026001");
        return project;
    }

    private UserObjectRoleDO createMember(Long memberId, Long projectId, Long userId, Long roleId, Integer status) {
        UserObjectRoleDO member = new UserObjectRoleDO();
        member.setId(memberId);
        member.setObjectType("project");
        member.setObjectId(projectId);
        member.setUserId(userId);
        member.setRoleId(roleId);
        member.setStatus(status);
        member.setJoinedTime(LocalDateTime.now());
        return member;
    }

    private ObjectRoleRespDTO createRole(Long roleId, String roleCode, String roleName) {
        ObjectRoleRespDTO role = new ObjectRoleRespDTO();
        role.setId(roleId);
        role.setCode(roleCode);
        role.setName(roleName);
        role.setScopeType("object");
        role.setObjectType("project");
        return role;
    }

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

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

    private ObjectStatusModelDO createExecutionStatus(String statusCode, boolean terminalFlag) {
        ObjectStatusModelDO statusModel = new ObjectStatusModelDO();
        statusModel.setObjectType("execution");
        statusModel.setStatusCode(statusCode);
        statusModel.setTerminalFlag(terminalFlag);
        return statusModel;
    }

    // ============== review 遗留 4 条 issue 修复后的回归用例 ==============

    /** Issue 1：transferPreviousManager 时原经理在目标 role 上有 INACTIVE 历史行 → 抛 DUPLICATE 异常（不是撞 SQL）。 */
    @Test
    void transferPreviousManager_targetRoleHasInactiveHistory_shouldThrowDuplicate() {
        Long projectId = 1020L;
        ProjectMemberSaveReqVO reqVO = new ProjectMemberSaveReqVO();
        reqVO.setUserId(2002L);
        reqVO.setRoleId(3101L);
        reqVO.setPreviousManagerUserId(2001L);
        reqVO.setPreviousManagerRoleId(3102L);
        ProjectDO project = createProject(projectId, 2001L);
        UserObjectRoleDO inactiveOldRow = createMember(9077L, projectId, 2001L, 3102L, 1);
        inactiveOldRow.setLeftTime(LocalDateTime.now().minusDays(30));
        when(projectMapper.selectById(projectId)).thenReturn(project);
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "active"))
                .thenReturn(createStatus("active", true));
        when(adminUserApi.validateUserList(any())).thenReturn(success(true));
        when(objectPermissionApi.getObjectRoleById(3101L, "object", "project"))
                .thenReturn(success(createRole(3101L, "project_manager", "项目经理")));
        when(objectPermissionApi.getObjectRoleById(3102L, "object", "project"))
                .thenReturn(success(createRole(3102L, "project_member_configured", "项目成员")));
        when(userObjectRoleMapper.selectByObjectUserAndRole("project", projectId, 2002L, 3101L)).thenReturn(null);
        // 关键：原经理 (2001L, 3102L) 上有 INACTIVE 历史行 —— pre-check 用不带 status 的 select 命中
        when(userObjectRoleMapper.selectByObjectUserAndRole("project", projectId, 2001L, 3102L))
                .thenReturn(inactiveOldRow);

        ServiceException ex = assertThrows(ServiceException.class,
                () -> projectMemberService.createProjectMember(projectId, reqVO));

        assertEquals(ErrorCodeConstants.PROJECT_MANAGER_TRANSFER_TARGET_ROLE_DUPLICATE.getCode(), ex.getCode());
    }

    /** Issue 2：updateProjectMember 改 roleId 时目标 role 已被该 user 的另一行占据 → 抛 ALREADY_EXISTS（不是撞 SQL）。 */
    @Test
    void updateProjectMember_targetRoleHasOtherRow_shouldThrowAlreadyExists() {
        Long projectId = 1021L;
        Long memberId = 9011L;
        ProjectMemberUpdateReqVO reqVO = new ProjectMemberUpdateReqVO();
        reqVO.setRoleId(3103L);
        UserObjectRoleDO member = createMember(memberId, projectId, 2002L, 3102L, 0);
        // 该 user 已有另一行持有目标 role 3103L —— 直接 update 会撞唯一索引
        UserObjectRoleDO conflictRow = createMember(9012L, projectId, 2002L, 3103L, 0);
        when(projectMapper.selectById(projectId)).thenReturn(createProject(projectId, 2001L));
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "active"))
                .thenReturn(createStatus("active", true));
        when(userObjectRoleMapper.selectByIdAndObject(memberId, "project", projectId)).thenReturn(member);
        when(objectPermissionApi.getObjectRoleById(3103L, "object", "project"))
                .thenReturn(success(createRole(3103L, "project_dev", "项目开发")));
        when(userObjectRoleMapper.selectByObjectUserAndRole("project", projectId, 2002L, 3103L))
                .thenReturn(conflictRow);

        ServiceException ex = assertThrows(ServiceException.class,
                () -> projectMemberService.updateProjectMember(projectId, memberId, reqVO));

        assertEquals(ErrorCodeConstants.PROJECT_MEMBER_ALREADY_EXISTS.getCode(), ex.getCode());
        verify(userObjectRoleMapper, never()).updateById(any(UserObjectRoleDO.class));
    }

    /** Issue 3：复活分支审计动作应为 REACTIVATE，不是 ADD（语义区分新增 vs 软失效后再激活）。 */
    @Test
    void createProjectMember_inactiveSameRoleExists_auditActionShouldBeReactivate() {
        Long projectId = 1022L;
        Long userId = 2002L;
        Long roleId = 3102L;
        ProjectMemberSaveReqVO reqVO = new ProjectMemberSaveReqVO();
        reqVO.setUserId(userId);
        reqVO.setRoleId(roleId);
        UserObjectRoleDO inactiveOld = createMember(9088L, projectId, userId, roleId, 1);
        inactiveOld.setLeftTime(LocalDateTime.now().minusDays(7));
        when(projectMapper.selectById(projectId)).thenReturn(createProject(projectId, 2001L));
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "active"))
                .thenReturn(createStatus("active", true));
        when(adminUserApi.validateUserList(any())).thenReturn(success(true));
        when(objectPermissionApi.getObjectRoleById(roleId, "object", "project"))
                .thenReturn(success(createRole(roleId, "project_member_configured", "项目成员")));
        when(userObjectRoleMapper.selectByObjectUserAndRole("project", projectId, userId, roleId))
                .thenReturn(inactiveOld);

        projectMemberService.createProjectMember(projectId, reqVO);

        ArgumentCaptor<BizAuditLogDO> auditCaptor = ArgumentCaptor.forClass(BizAuditLogDO.class);
        verify(bizAuditLogMapper).insert(auditCaptor.capture());
        // reactivate_member 来自 ObjectActivityConstants.MEMBER_ACTION_REACTIVATE，硬编码字面量避免引入额外 import
        assertEquals("reactivate_member", auditCaptor.getValue().getActionType());
    }

    /** Issue 4：内置 manager 角色未在 system_role 找到 → 抛业务 ServiceException（不是 IllegalStateException 透出 500）。 */
    @Test
    void resolveProjectManagerRoleId_internalRoleMissing_shouldThrowBusinessException() {
        Long projectId = 1023L;
        ProjectMemberSaveReqVO reqVO = new ProjectMemberSaveReqVO();
        reqVO.setUserId(2002L);
        reqVO.setRoleId(3101L);
        reqVO.setPreviousManagerUserId(2001L);
        reqVO.setPreviousManagerRoleId(3102L);
        ProjectDO project = createProject(projectId, 2001L);
        when(projectMapper.selectById(projectId)).thenReturn(project);
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "active"))
                .thenReturn(createStatus("active", true));
        when(adminUserApi.validateUserList(any())).thenReturn(success(true));
        when(objectPermissionApi.getObjectRoleById(3101L, "object", "project"))
                .thenReturn(success(createRole(3101L, "project_manager", "项目经理")));
        when(objectPermissionApi.getObjectRoleById(3102L, "object", "project"))
                .thenReturn(success(createRole(3102L, "project_member_configured", "项目成员")));
        when(userObjectRoleMapper.selectByObjectUserAndRole("project", projectId, 2002L, 3101L)).thenReturn(null);
        when(userObjectRoleMapper.selectByObjectUserAndRole("project", projectId, 2001L, 3102L)).thenReturn(null);
        // 关键：内置 project_manager 角色查询返回 null —— resolveProjectManagerRoleId 应抛业务异常
        when(objectPermissionApi.getObjectRoleByCode("project_manager", "object", "project"))
                .thenReturn(success(null));

        ServiceException ex = assertThrows(ServiceException.class,
                () -> projectMemberService.createProjectMember(projectId, reqVO));

        assertEquals(ErrorCodeConstants.PROJECT_INTERNAL_ROLE_NOT_CONFIGURED.getCode(), ex.getCode());
    }

}
