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

import com.njcn.rdms.framework.common.exception.ServiceException;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectContextRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectLifecycleActionRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectOverviewSummaryRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectPageReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectRespVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.project.vo.project.ProjectStatusActionReqVO;
import com.njcn.rdms.module.project.service.datascope.ObjectDataScope;
import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService;
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.product.ProductDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectDO;
import com.njcn.rdms.module.project.dal.dataobject.project.ProjectStatusLogDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusModelDO;
import com.njcn.rdms.module.project.dal.dataobject.status.ObjectStatusTransitionDO;
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.product.ProductMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectMapper;
import com.njcn.rdms.module.project.dal.mysql.project.ProjectStatusLogMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusModelMapper;
import com.njcn.rdms.module.project.dal.mysql.status.ObjectStatusTransitionMapper;
import com.njcn.rdms.module.project.enums.ErrorCodeConstants;
import com.njcn.rdms.module.project.enums.ProjectDictTypeConstants;
import com.njcn.rdms.module.system.api.dict.DictDataApi;
import com.njcn.rdms.module.system.api.permission.ObjectPermissionApi;
import com.njcn.rdms.module.system.api.permission.dto.ObjectMenuRespDTO;
import com.njcn.rdms.module.system.api.permission.dto.ObjectRolePermissionRespDTO;
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 org.mockito.MockedStatic;

import java.time.LocalDate;
import java.util.Collections;
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.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
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.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class ProjectServiceImplTest extends BaseMockitoUnitTest {

    @InjectMocks
    private ProjectServiceImpl projectService;
    @Mock
    private ProjectMapper projectMapper;
    @Mock
    private ProjectStatusLogMapper projectStatusLogMapper;
    @Mock
    private ProductMapper productMapper;
    @Mock
    private BizAuditLogMapper bizAuditLogMapper;
    @Mock
    private ObjectStatusTransitionMapper objectStatusTransitionMapper;
    @Mock
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Mock
    private UserObjectRoleMapper userObjectRoleMapper;
    @Mock
    private ProjectStatusViewService projectStatusViewService;
    @Mock
    private ObjectPermissionApi objectPermissionApi;
    @Mock
    private AdminUserApi adminUserApi;
    @Mock
    private DictDataApi dictDataApi;
    @Mock
    private ObjectDataScopeService objectDataScopeService;
    @Mock
    private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;

    @Test
    void getProjectDetail_shouldFillProductNameAndManagerNickname() {
        Long projectId = 3001L;
        Long productId = 1001L;
        Long managerUserId = 2001L;

        ProjectDO project = new ProjectDO();
        project.setId(projectId);
        project.setProjectCode("CNPJ2026001");
        project.setProjectName("研发管理系统");
        project.setProductId(productId);
        project.setManagerUserId(managerUserId);

        ProductDO product = new ProductDO();
        product.setId(productId);
        product.setName("统一交付平台");

        AdminUserRespDTO manager = new AdminUserRespDTO();
        manager.setId(managerUserId);
        manager.setNickname("张三");

        when(projectMapper.selectById(projectId)).thenReturn(project);
        when(productMapper.selectById(productId)).thenReturn(product);
        when(adminUserApi.getUser(managerUserId)).thenReturn(success(manager));

        ProjectRespVO result = projectService.getProjectDetail(projectId);

        assertEquals(projectId, result.getId());
        assertEquals("统一交付平台", result.getProductName());
        assertEquals("张三", result.getManagerUserNickname());
    }

    @Test
    void getProjectListByProductId_shouldReturnProjectsUnderCurrentProduct() {
        Long productId = 1001L;

        ProductDO product = new ProductDO();
        product.setId(productId);
        product.setName("统一交付平台");

        ProjectDO project1 = createProject(3001L, productId, "项目A", 2001L, "pending");
        ProjectDO project2 = createProject(3002L, productId, "项目B", 2002L, "active");

        AdminUserRespDTO manager1 = new AdminUserRespDTO();
        manager1.setId(2001L);
        manager1.setNickname("张三");
        AdminUserRespDTO manager2 = new AdminUserRespDTO();
        manager2.setId(2002L);
        manager2.setNickname("李四");

        when(productMapper.selectById(productId)).thenReturn(product);
        when(projectMapper.selectListByProductId(productId)).thenReturn(List.of(project1, project2));
        when(adminUserApi.getUser(2001L)).thenReturn(success(manager1));
        when(adminUserApi.getUser(2002L)).thenReturn(success(manager2));

        List<ProjectRespVO> result = projectService.getProjectListByProductId(productId);

        assertEquals(2, result.size());
        assertEquals(productId, result.get(0).getProductId());
        assertEquals("统一交付平台", result.get(0).getProductName());
        assertEquals("张三", result.get(0).getManagerUserNickname());
        assertEquals("李四", result.get(1).getManagerUserNickname());
    }

    @Test
    void createProject_withManualCode_shouldInsertPendingProjectInitManagerAndWriteAudit() {
        ProjectSaveReqVO reqVO = createReqVO("CNPJ-MANUAL", 1001L, "合同交付项目", 2001L);
        ProductDO product = new ProductDO();
        product.setId(1001L);
        product.setDirectionCode("product_direction");
        when(projectMapper.selectByCode("CNPJ-MANUAL")).thenReturn(null);
        when(projectMapper.selectActiveByProductIdAndName(1001L, "合同交付项目", "cancelled")).thenReturn(null);
        when(productMapper.selectById(1001L)).thenReturn(product);
        when(dictDataApi.validateDictDataList(ProjectDictTypeConstants.PROJECT_TYPE, List.of("delivery"))).thenReturn(success(true));
        when(adminUserApi.validateUserList(any())).thenReturn(success(true));
        when(objectStatusModelMapper.selectInitialByObjectTypeEnabled("project")).thenReturn(createStatus("pending", true, true));
        when(objectPermissionApi.getObjectRoleByCode("project_manager", "object", "project"))
                .thenReturn(success(createRole(3101L, "project_manager", "项目经理")));

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(3001L, "创建人")) {
            projectService.createProject(reqVO);
        }

        ArgumentCaptor<ProjectDO> projectCaptor = ArgumentCaptor.forClass(ProjectDO.class);
        verify(projectMapper).insert(projectCaptor.capture());
        assertEquals("pending", projectCaptor.getValue().getStatusCode());
        assertEquals("CNPJ-MANUAL", projectCaptor.getValue().getProjectCode());
        assertEquals("product_direction", projectCaptor.getValue().getDirectionCode());

        ArgumentCaptor<UserObjectRoleDO> memberCaptor = ArgumentCaptor.forClass(UserObjectRoleDO.class);
        verify(userObjectRoleMapper).insert(memberCaptor.capture());
        assertEquals("project", memberCaptor.getValue().getObjectType());
        assertEquals(3101L, memberCaptor.getValue().getRoleId());
        assertEquals(2001L, memberCaptor.getValue().getUserId());

        ArgumentCaptor<BizAuditLogDO> auditCaptor = ArgumentCaptor.forClass(BizAuditLogDO.class);
        verify(bizAuditLogMapper, times(3)).insert(auditCaptor.capture());
        assertTrue(auditCaptor.getAllValues().stream().anyMatch(log ->
                "project".equals(log.getBizType()) && "create".equals(log.getActionType())
                        && "pending".equals(log.getToStatus())));
    }

    @Test
    void getProjectOverviewSummary_shouldReturnFixedStatusCountsAndIgnoreUnknownStatus() {
        when(objectStatusModelMapper.selectListByObjectType("project")).thenReturn(List.of(
                createStatusModel("pending", 10, 0),
                createStatusModel("active", 20, 0),
                createStatusModel("paused", 30, 0),
                createStatusModel("disabled", 40, 1)
        ));
        when(projectMapper.selectStatusCountList()).thenReturn(List.of(
                Map.of("statusCode", "pending", "countValue", 2L),
                Map.of("statusCode", "active", "countValue", 3L),
                Map.of("statusCode", "unknown", "countValue", 99L)
        ));

        ProjectOverviewSummaryRespVO respVO = projectService.getProjectOverviewSummary();

        assertEquals(3, respVO.getStatusCounts().size());
        assertEquals(2L, respVO.getStatusCounts().get("pending"));
        assertEquals(3L, respVO.getStatusCounts().get("active"));
        assertEquals(0L, respVO.getStatusCounts().get("paused"));
    }

    @Test
    void createProject_whenNullProductNameExists_shouldThrowDuplicate() {
        ProjectSaveReqVO reqVO = createReqVO(null, null, "内部项目", 2001L);
        when(projectMapper.selectActiveByProductIdAndName(null, "内部项目", "cancelled"))
                .thenReturn(createProject(1001L, null, "内部项目", 2001L, "pending"));

        ServiceException ex = assertThrows(ServiceException.class, () -> projectService.createProject(reqVO));

        assertEquals(ErrorCodeConstants.PROJECT_NAME_DUPLICATE.getCode(), ex.getCode());
        verify(projectMapper, never()).insert(any(ProjectDO.class));
    }

    @Test
    void createProject_whenOnlyCancelledProjectNameExists_shouldAllowReuseName() {
        ProjectSaveReqVO reqVO = createReqVO("CNPJ-REUSE", 1001L, "研发管理系统", 2001L);
        ProductDO product = new ProductDO();
        product.setId(1001L);
        product.setDirectionCode("product_direction");
        when(projectMapper.selectByCode("CNPJ-REUSE")).thenReturn(null);
        when(projectMapper.selectActiveByProductIdAndName(1001L, "研发管理系统", "cancelled")).thenReturn(null);
        when(productMapper.selectById(1001L)).thenReturn(product);
        when(dictDataApi.validateDictDataList(ProjectDictTypeConstants.PROJECT_TYPE, List.of("delivery"))).thenReturn(success(true));
        when(adminUserApi.validateUserList(any())).thenReturn(success(true));
        when(objectStatusModelMapper.selectInitialByObjectTypeEnabled("project")).thenReturn(createStatus("pending", true, true));
        when(objectPermissionApi.getObjectRoleByCode("project_manager", "object", "project"))
                .thenReturn(success(createRole(3101L, "project_manager", "项目经理")));

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(3001L, "创建人")) {
            projectService.createProject(reqVO);
        }

        verify(projectMapper).insert(any(ProjectDO.class));
    }

    @Test
    void createProject_withoutProductAndDirection_shouldThrowInvalidParam() {
        ProjectSaveReqVO reqVO = createReqVO("CNPJ-DIRECTION", null, "方向校验项目", 2001L);
        reqVO.setDirectionCode(null);
        when(projectMapper.selectByCode("CNPJ-DIRECTION")).thenReturn(null);
        when(projectMapper.selectActiveByProductIdAndName(null, "方向校验项目", "cancelled")).thenReturn(null);
        when(dictDataApi.validateDictDataList(ProjectDictTypeConstants.PROJECT_TYPE, List.of("delivery"))).thenReturn(success(true));

        ServiceException ex = assertThrows(ServiceException.class, () -> projectService.createProject(reqVO));

        assertEquals("项目方向不能为空", ex.getMessage());
        verify(projectMapper, never()).insert(any(ProjectDO.class));
    }

    @Test
    void createProject_whenActiveMainlineExists_shouldThrowDuplicate() {
        ProjectSaveReqVO reqVO = createReqVO("CNPJ-MAINLINE", 1001L, "产品主线项目", 2001L);
        reqVO.setProjectType("mainline");
        ProductDO product = new ProductDO();
        product.setId(1001L);
        product.setDirectionCode("product_direction");
        when(projectMapper.selectByCode("CNPJ-MAINLINE")).thenReturn(null);
        when(projectMapper.selectActiveByProductIdAndName(1001L, "产品主线项目", "cancelled")).thenReturn(null);
        when(productMapper.selectById(1001L)).thenReturn(product);
        when(dictDataApi.validateDictDataList(ProjectDictTypeConstants.PROJECT_TYPE, List.of("mainline"))).thenReturn(success(true));
        when(projectMapper.selectActiveMainlineListByProductId(1001L,
                Set.of("mainline", "main", "product_mainline", "主线"), "cancelled"))
                .thenReturn(List.of(createProject(1001L, 1001L, "已有主线项目", 2001L, "active")));

        ServiceException ex = assertThrows(ServiceException.class, () -> projectService.createProject(reqVO));

        assertEquals(ErrorCodeConstants.PROJECT_MAINLINE_DUPLICATE.getCode(), ex.getCode());
        verify(projectMapper, never()).insert(any(ProjectDO.class));
    }

    @Test
    void updateProject_withoutProduct_shouldUpdateDirectionCode() {
        Long projectId = 1004L;
        ProjectDO project = createProject(projectId, null, "内部项目", 2001L, "pending");
        project.setDirectionCode("old_direction");
        ProjectSaveReqVO reqVO = createReqVO("CNPJ2026001", null, "内部项目", 2001L);
        reqVO.setId(projectId);
        reqVO.setDirectionCode("new_direction");
        when(projectMapper.selectById(projectId)).thenReturn(project);
        when(projectMapper.selectActiveByProductIdAndName(null, "内部项目", "cancelled")).thenReturn(project);
        when(dictDataApi.validateDictDataList(ProjectDictTypeConstants.PROJECT_TYPE, List.of("delivery"))).thenReturn(success(true));
        when(dictDataApi.validateDictDataList("rdms_object_direction", List.of("new_direction"))).thenReturn(success(true));
        when(adminUserApi.validateUserList(any())).thenReturn(success(true));
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
                .thenReturn(createStatus("pending", true, true));

        projectService.updateProject(reqVO);

        assertEquals("new_direction", project.getDirectionCode());
    }

    @Test
    void updateProject_withProduct_shouldUseProductDirectionCode() {
        Long projectId = 1005L;
        ProjectDO project = createProject(projectId, 1001L, "产品关联项目", 2001L, "pending");
        project.setDirectionCode("old_direction");
        ProjectSaveReqVO reqVO = createReqVO("CNPJ2026001", 1001L, "产品关联项目", 2001L);
        reqVO.setId(projectId);
        reqVO.setDirectionCode("manual_direction");
        ProductDO product = new ProductDO();
        product.setId(1001L);
        product.setDirectionCode("product_direction");
        when(projectMapper.selectById(projectId)).thenReturn(project);
        when(projectMapper.selectActiveByProductIdAndName(1001L, "产品关联项目", "cancelled")).thenReturn(project);
        when(productMapper.selectById(1001L)).thenReturn(product);
        when(dictDataApi.validateDictDataList(ProjectDictTypeConstants.PROJECT_TYPE, List.of("delivery"))).thenReturn(success(true));
        when(adminUserApi.validateUserList(any())).thenReturn(success(true));
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("project", "pending"))
                .thenReturn(createStatus("pending", true, true));

        projectService.updateProject(reqVO);

        assertEquals("product_direction", project.getDirectionCode());
    }

    @Test
    void getProjectContext_whenCreatorHasNoObjectRole_shouldReturnVisitorResources() {
        // 改造后：无显式角色 + scope 命中 → 返回 implicit_observer_project 上下文（原 visitor 兜底已由 scope.contains 替代）
        Long projectId = 1002L;
        Long loginUserId = 3002L;
        Long observerRoleId = 9202L;
        ProjectDO project = createProject(projectId, null, "创建人项目", 2001L, "pending");
        project.setCreator(String.valueOf(loginUserId));
        ObjectRolePermissionRespDTO detail = new ObjectRolePermissionRespDTO();
        detail.setCurrentRole(createRole(observerRoleId, "implicit_observer_project", "项目隐式观察者"));
        detail.setMenus(List.of(
                createMenu(9301L, "概览", null, 2, 10, "/project/project/overview", "mdi:home-outline", true)
        ));
        detail.setPermissions(Set.of());
        detail.setAdditionalRoleNames(Collections.emptyList());
        when(projectMapper.selectById(projectId)).thenReturn(project);
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(Collections.emptyList());
        // scope 命中该项目 id
        when(objectDataScopeService.compute(loginUserId, "project"))
                .thenReturn(ObjectDataScope.idList(Set.of(projectId), Set.of()));
        when(objectPermissionApi.getObjectRoleByCode("implicit_observer_project", "object", "project"))
                .thenReturn(success(createRole(observerRoleId, "implicit_observer_project", "项目隐式观察者")));
        when(objectPermissionApi.getObjectRolePermissionDetailMerged(List.of(observerRoleId), "object", "project"))
                .thenReturn(success(detail));
        when(projectStatusViewService.getLifecycle("pending"))
                .thenReturn(new ProjectStatusViewService.ProjectLifecycleView(
                        "待开始", false, true, List.of(
                        createLifecycleAction("cancel", "取消", true)
                )));

        ProjectContextRespVO respVO;
        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "创建人")) {
            respVO = projectService.getProjectContext(projectId);
        }

        assertNotNull(respVO.getCurrentProject());
        assertEquals("待开始", respVO.getCurrentProject().getStatusName());
        assertEquals(Boolean.FALSE, respVO.getCurrentProject().getTerminal());
        assertEquals(Boolean.TRUE, respVO.getCurrentProject().getAllowEdit());
        assertEquals(1, respVO.getCurrentProject().getAvailableActions().size());
        assertEquals("cancel", respVO.getCurrentProject().getAvailableActions().get(0).getActionCode());
        assertEquals(Boolean.TRUE, respVO.getCurrentRole().getGuestFlag());
        assertEquals(observerRoleId, respVO.getCurrentRole().getRoleId());
        assertEquals("implicit_observer_project", respVO.getCurrentRole().getRoleCode());
        assertEquals("项目隐式观察者", respVO.getCurrentRole().getRoleName());
        assertEquals(1, respVO.getNavs().size());
        assertEquals(9301L, respVO.getNavs().get(0).getId());
        assertEquals(List.of(), respVO.getButtons());
    }

    @Test
    void getProjectContext_returnsImplicitObserver_whenNoExplicitRoleButScopeContains() {
        Long projectId = 100L;
        Long loginUserId = 1L;
        Long observerRoleId = 9203L;
        ProjectDO project = createProject(projectId, null, "观察者项目", 2001L, "active");
        project.setDirectionCode("dc_obs");
        ObjectRolePermissionRespDTO detail = new ObjectRolePermissionRespDTO();
        detail.setCurrentRole(createRole(observerRoleId, "implicit_observer_project", "项目隐式观察者"));
        detail.setMenus(List.of(
                createMenu(9401L, "任务", null, 2, 20, "/project/project/task", "mdi:format-list-checks", true)
        ));
        detail.setPermissions(Set.of());
        detail.setAdditionalRoleNames(Collections.emptyList());
        when(projectMapper.selectById(projectId)).thenReturn(project);
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(Collections.emptyList());
        when(objectDataScopeService.compute(loginUserId, "project"))
                .thenReturn(ObjectDataScope.idList(Set.of(projectId), Set.of()));
        when(objectPermissionApi.getObjectRoleByCode("implicit_observer_project", "object", "project"))
                .thenReturn(success(createRole(observerRoleId, "implicit_observer_project", "项目隐式观察者")));
        when(objectPermissionApi.getObjectRolePermissionDetailMerged(List.of(observerRoleId), "object", "project"))
                .thenReturn(success(detail));
        when(projectStatusViewService.getLifecycle("active"))
                .thenReturn(new ProjectStatusViewService.ProjectLifecycleView(
                        "进行中", false, true, List.of()
                ));

        ProjectContextRespVO respVO;
        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "观察者")) {
            respVO = projectService.getProjectContext(projectId);
        }

        assertNotNull(respVO);
        assertEquals(Boolean.TRUE, respVO.getCurrentRole().getGuestFlag());
        assertEquals("项目隐式观察者", respVO.getCurrentRole().getRoleName());
        assertEquals("implicit_observer_project", respVO.getCurrentRole().getRoleCode());
        assertEquals(1, respVO.getNavs().size());
        assertEquals(List.of(), respVO.getButtons());
    }

    @Test
    void getProjectContext_throws_whenNoExplicitRoleAndScopeMisses() {
        Long projectId = 100L;
        Long loginUserId = 1L;
        ProjectDO project = createProject(projectId, null, "无权限项目", 2001L, "active");
        project.setDirectionCode("dc_miss");
        when(projectMapper.selectById(projectId)).thenReturn(project);
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("project", projectId, loginUserId))
                .thenReturn(Collections.emptyList());
        when(objectDataScopeService.compute(loginUserId, "project"))
                .thenReturn(ObjectDataScope.empty());

        ServiceException ex;
        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "无关用户")) {
            ex = assertThrows(ServiceException.class, () -> projectService.getProjectContext(projectId));
        }

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

    @Test
    void getProjectPage_returnsEmpty_whenScopeIsEmpty() {
        try (MockedStatic<SecurityFrameworkUtils> mocked = mockStatic(SecurityFrameworkUtils.class)) {
            mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(1L);
            when(objectDataScopeService.compute(1L, "project")).thenReturn(ObjectDataScope.empty());

            PageResult<ProjectDO> result = projectService.getProjectPage(new ProjectPageReqVO());

            assertThat(result.getList()).isEmpty();
            verify(projectMapper, never()).selectPage(any(ProjectPageReqVO.class), any());
        }
    }

    @Test
    void getProjectPage_passesIdListIntoSql_whenScopeIsIdList() {
        try (MockedStatic<SecurityFrameworkUtils> mocked = mockStatic(SecurityFrameworkUtils.class)) {
            mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(1L);
            when(objectDataScopeService.compute(1L, "project"))
                    .thenReturn(ObjectDataScope.idList(Set.of(101L), Set.of()));
            when(projectMapper.selectPage(any(ProjectPageReqVO.class), any())).thenReturn(new PageResult<>());

            projectService.getProjectPage(new ProjectPageReqVO());

            verify(projectMapper, times(1)).selectPage(any(ProjectPageReqVO.class), any());
        }
    }

    @Test
    void getProjectPage_skipsScopeFilter_whenScopeIsAll() {
        try (MockedStatic<SecurityFrameworkUtils> mocked = mockStatic(SecurityFrameworkUtils.class)) {
            mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(1L);
            when(objectDataScopeService.compute(1L, "project")).thenReturn(ObjectDataScope.all());
            when(projectMapper.selectPage(any(ProjectPageReqVO.class), any())).thenReturn(new PageResult<>());

            projectService.getProjectPage(new ProjectPageReqVO());

            verify(projectMapper, times(1)).selectPage(any(ProjectPageReqVO.class), any());
        }
    }

    @Test
    void changeProjectStatus_shouldUpdateStatusAndWriteStatusAndAuditLogs() {
        Long projectId = 1003L;
        ProjectDO project = createProject(projectId, null, "状态项目", 2001L, "active");
        ProjectStatusActionReqVO reqVO = new ProjectStatusActionReqVO();
        reqVO.setId(projectId);
        reqVO.setActionCode("pause");
        reqVO.setReason("环境受限");
        when(projectMapper.selectById(projectId)).thenReturn(project);
        when(objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction("project", "active", "pause"))
                .thenReturn(createTransition("pause", "paused", true));
        when(projectMapper.updateStatusByIdAndStatus(projectId, "active", "paused", "环境受限")).thenReturn(1);

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(3003L, "操作人")) {
            projectService.changeProjectStatus(reqVO);
        }

        verify(projectMapper).updateStatusByIdAndStatus(projectId, "active", "paused", "环境受限");
        ArgumentCaptor<ProjectStatusLogDO> statusCaptor = ArgumentCaptor.forClass(ProjectStatusLogDO.class);
        verify(projectStatusLogMapper).insert(statusCaptor.capture());
        assertEquals("pause", statusCaptor.getValue().getActionType());
        assertEquals("active", statusCaptor.getValue().getFromStatus());
        assertEquals("paused", statusCaptor.getValue().getToStatus());
    }

    private ProjectSaveReqVO createReqVO(String code, Long productId, String projectName, Long managerUserId) {
        ProjectSaveReqVO reqVO = new ProjectSaveReqVO();
        reqVO.setProjectCode(code);
        reqVO.setProductId(productId);
        reqVO.setProjectName(projectName);
        reqVO.setProjectType("delivery");
        reqVO.setManagerUserId(managerUserId);
        reqVO.setDirectionCode("manual_direction");
        return reqVO;
    }

    private ProjectDO createProject(Long id, Long productId, String projectName, Long managerUserId, String statusCode) {
        ProjectDO project = new ProjectDO();
        project.setId(id);
        project.setProjectCode("CNPJ" + LocalDate.now().getYear() + "001");
        project.setProductId(productId);
        project.setProjectName(projectName);
        project.setProjectType("delivery");
        project.setManagerUserId(managerUserId);
        project.setStatusCode(statusCode);
        return project;
    }

    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 ObjectMenuRespDTO createMenu(Long id, String name, String permission, Integer type, Integer sort,
                                         String path, String icon, Boolean visible) {
        ObjectMenuRespDTO menu = new ObjectMenuRespDTO();
        menu.setId(id);
        menu.setName(name);
        menu.setPermission(permission);
        menu.setType(type);
        menu.setSort(sort);
        menu.setPath(path);
        menu.setIcon(icon);
        menu.setVisible(visible);
        return menu;
    }

    private ObjectStatusTransitionDO createTransition(String actionCode, String toStatus, boolean needReason) {
        ObjectStatusTransitionDO transition = new ObjectStatusTransitionDO();
        transition.setActionCode(actionCode);
        transition.setToStatusCode(toStatus);
        transition.setNeedReason(needReason);
        return transition;
    }

    private ProjectLifecycleActionRespVO createLifecycleAction(String actionCode, String actionName, boolean needReason) {
        ProjectLifecycleActionRespVO action = new ProjectLifecycleActionRespVO();
        action.setActionCode(actionCode);
        action.setActionName(actionName);
        action.setNeedReason(needReason);
        return action;
    }

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

    private ObjectStatusModelDO createStatusModel(String statusCode, Integer sort, Integer status) {
        ObjectStatusModelDO statusModel = new ObjectStatusModelDO();
        statusModel.setObjectType("project");
        statusModel.setStatusCode(statusCode);
        statusModel.setSort(sort);
        statusModel.setStatus(status);
        return statusModel;
    }

    private MockedStatic<SecurityFrameworkUtils> mockLoginUser(Long loginUserId, String nickname) {
        MockedStatic<SecurityFrameworkUtils> mockedStatic = mockStatic(SecurityFrameworkUtils.class);
        mockedStatic.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId);
        mockedStatic.when(SecurityFrameworkUtils::getLoginUserNickname).thenReturn(nickname);
        return mockedStatic;
    }

}
