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

import com.fasterxml.jackson.databind.JsonNode;
import com.njcn.rdms.framework.common.exception.ServiceException;
import com.njcn.rdms.framework.security.core.util.SecurityFrameworkUtils;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
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.product.vo.product.ProductContextRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductDeleteReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductOverviewSummaryRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductPageReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductRespVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductSaveReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.product.ProductStatusActionReqVO;
import com.njcn.rdms.module.project.service.datascope.ObjectDataScope;
import com.njcn.rdms.module.project.service.datascope.ObjectDataScopeService;
import com.njcn.rdms.module.project.controller.admin.product.vo.setting.ProductSettingBaseInfoUpdateReqVO;
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.product.ProductStatusLogDO;
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.product.ProductStatusLogMapper;
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.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 org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.MockedStatic;
import org.mockito.Mock;

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.assertFalse;
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.anyString;
import static org.mockito.ArgumentMatchers.eq;
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 ProductServiceImplTest extends BaseMockitoUnitTest {

    @InjectMocks
    private ProductServiceImpl productService;
    @Mock
    private ProductMapper productMapper;
    @Mock
    private ProductStatusLogMapper productStatusLogMapper;
    @Mock
    private BizAuditLogMapper bizAuditLogMapper;
    @Mock
    private ObjectStatusTransitionMapper objectStatusTransitionMapper;
    @Mock
    private ObjectStatusModelMapper objectStatusModelMapper;
    @Mock
    private UserObjectRoleMapper userObjectRoleMapper;
    @Mock
    private ObjectPermissionApi objectPermissionApi;
    @Mock
    private AdminUserApi adminUserApi;
    @Mock
    private com.njcn.rdms.module.project.dal.mysql.product.ProductRequirementModuleMapper requirementModuleMapper;
    @Mock
    private com.njcn.rdms.module.project.service.member.ObjectRoleAutoAssignService objectRoleAutoAssignService;
    @Mock
    private com.njcn.rdms.module.project.service.member.CurrentUserRoleResolver currentUserRoleResolver;
    @Mock
    private ObjectDataScopeService objectDataScopeService;
    @Mock
    private com.njcn.rdms.module.project.service.status.StatusActionTextResolver statusActionTextResolver;
    @Mock
    private com.njcn.rdms.module.project.framework.notify.TeamNotifyRecipientResolver teamNotifyRecipientResolver;
    @Mock
    private org.springframework.context.ApplicationEventPublisher applicationEventPublisher;

    @Test
    void createProduct_shouldCreateDefaultRequirementModule() {
        Long loginUserId = 3001L;
        ProductSaveReqVO reqVO = new ProductSaveReqVO();
        reqVO.setDirectionCode("direction_value");
        reqVO.setName("新产品");
        reqVO.setManagerUserId(2001L);
        reqVO.setDescription("产品描述");

        when(productMapper.selectByName("新产品")).thenReturn(null);
        when(adminUserApi.getUser(2001L)).thenReturn(
                success(new com.njcn.rdms.module.system.api.user.dto.AdminUserRespDTO()));

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
            productService.createProduct(reqVO);
        }

        // 验证产品已创建
        ArgumentCaptor<ProductDO> productCaptor = ArgumentCaptor.forClass(ProductDO.class);
        verify(productMapper, times(1)).insert(productCaptor.capture());
        ProductDO createdProduct = productCaptor.getValue();
        assertEquals("新产品", createdProduct.getName());

        // 验证"全部需求"模块已自动创建
        ArgumentCaptor<com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO> moduleCaptor =
                ArgumentCaptor.forClass(com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO.class);
        verify(requirementModuleMapper, times(1)).insert(moduleCaptor.capture());
        com.njcn.rdms.module.project.dal.dataobject.product.ProductRequirementModuleDO defaultModule = moduleCaptor.getValue();
        assertEquals(0L, defaultModule.getParentId());
        assertEquals(createdProduct.getId(), defaultModule.getProductId());
        assertEquals("全部需求", defaultModule.getModuleName());
        assertEquals("自动创建的模块", defaultModule.getRemark());
        assertEquals(0, defaultModule.getSort());
    }

    @Test
    void createProduct_shouldUseConfiguredInitialStatusAndWriteCreateAudit() {
        ProductSaveReqVO reqVO = new ProductSaveReqVO();
        reqVO.setCode("CNPD2026999");
        reqVO.setDirectionCode("direction_value");
        reqVO.setName("新产品");
        reqVO.setManagerUserId(2001L);
        reqVO.setDescription("  产品描述  ");

        when(productMapper.selectByCode("CNPD2026999")).thenReturn(null);
        when(productMapper.selectByName("新产品")).thenReturn(null);
        when(objectStatusModelMapper.selectInitialByObjectTypeEnabled("product"))
                .thenReturn(createStatus("active", true, true));
        when(objectPermissionApi.getObjectRoleByCode("product_manager", "object", "product"))
                .thenReturn(success(createRole(3101L, "product_manager", "产品经理")));

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

        ArgumentCaptor<ProductDO> productCaptor = ArgumentCaptor.forClass(ProductDO.class);
        verify(productMapper).insert(productCaptor.capture());
        assertEquals("active", productCaptor.getValue().getStatusCode());
        assertEquals("产品描述", productCaptor.getValue().getDescription());

        BizAuditLogDO auditLog = captureCreateAuditLog();
        assertEquals("product", auditLog.getBizType());
        assertEquals("create", auditLog.getActionType());
        assertEquals("active", auditLog.getToStatus());
    }

    @Test
    void updateProductBaseInfo_shouldOnlyUpdateBaseInfoAndRecordFieldChanges() {
        Long productId = 1002L;
        ProductDO product = createProduct(productId, "direction_value", "旧产品", 2003L, "旧描述", "active");
        ProductSettingBaseInfoUpdateReqVO reqVO = new ProductSettingBaseInfoUpdateReqVO();
        reqVO.setDirectionCode("direction_value_updated");
        reqVO.setName("新产品");
        reqVO.setDescription("  新描述  ");

        when(productMapper.selectById(productId)).thenReturn(product);
        when(productMapper.selectByName("新产品")).thenReturn(null);
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("product", "active"))
                .thenReturn(createStatus("active", false, true));

        productService.updateProductBaseInfo(productId, reqVO);

        ProductDO updated = captureUpdatedProduct();
        assertProductBaseInfoUpdated(updated, "CNPD2026002", 2003L, "active",
                "direction_value_updated", "新产品", "新描述");

        BizAuditLogDO auditLog = captureAuditLog();
        assertAuditFieldChanges(auditLog, productId, "direction_value", "direction_value_updated",
                "旧产品", "新产品", "旧描述", "新描述");
    }

    @Test
    void updateProduct_shouldReuseBaseInfoUpdateFlow() {
        Long productId = 1003L;
        ProductDO product = createProduct(productId, "direction_value", "旧产品", 2004L, "旧描述", "active");
        ProductSaveReqVO reqVO = new ProductSaveReqVO();
        reqVO.setId(productId);
        reqVO.setCode("CNPD2026003");
        reqVO.setDirectionCode("direction_value_updated");
        reqVO.setName("新产品");
        reqVO.setManagerUserId(2004L);
        reqVO.setDescription("  新描述  ");

        when(productMapper.selectById(productId)).thenReturn(product);
        when(productMapper.selectByName("新产品")).thenReturn(null);
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("product", "active"))
                .thenReturn(createStatus("active", false, true));

        productService.updateProduct(reqVO);

        ProductDO updated = captureUpdatedProduct();
        assertProductBaseInfoUpdated(updated, "CNPD2026003", 2004L, "active",
                "direction_value_updated", "新产品", "新描述");

        BizAuditLogDO auditLog = captureAuditLog();
        assertAuditFieldChanges(auditLog, productId, "direction_value", "direction_value_updated",
                "旧产品", "新产品", "旧描述", "新描述");
    }

    @Test
    void updateProduct_whenManagerUserIdChanged_shouldThrowException() {
        Long productId = 1002L;
        ProductDO product = createProduct(productId, "direction_value", "旧产品", 2005L, "旧描述", "active");
        ProductSaveReqVO reqVO = new ProductSaveReqVO();
        reqVO.setId(productId);
        reqVO.setCode("CNPD2026002");
        reqVO.setDirectionCode("direction_value_updated");
        reqVO.setName("新产品");
        reqVO.setManagerUserId(2006L);
        reqVO.setDescription("新描述");

        when(productMapper.selectById(productId)).thenReturn(product);

        ServiceException ex = assertThrows(ServiceException.class,
                () -> productService.updateProduct(reqVO));
        assertEquals(ErrorCodeConstants.PRODUCT_MANAGER_NOT_MODIFIABLE.getCode(), ex.getCode());
        verify(productMapper, never()).updateById(any(ProductDO.class));
        verify(bizAuditLogMapper, never()).insert(any(BizAuditLogDO.class));
    }

    @Test
    void getProductOverviewSummary_shouldReturnFixedStatusCountsAndIgnoreUnknownStatus() {
        try (MockedStatic<SecurityFrameworkUtils> mocked = mockStatic(SecurityFrameworkUtils.class)) {
            mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(1L);
            when(objectDataScopeService.compute(1L, "product")).thenReturn(ObjectDataScope.all());
            when(objectStatusModelMapper.selectListByObjectType("product")).thenReturn(List.of(
                    createBoardStatusModel("active", "启用", 10, 0, false),
                    createBoardStatusModel("paused", "暂停", 20, 0, false),
                    createBoardStatusModel("abandoned", "废弃", 30, 0, true),
                    createBoardStatusModel("disabled", "已禁用", 40, 1, false)
            ));
            when(productMapper.selectStatusCountList()).thenReturn(List.of(
                    Map.of("statusCode", "active", "countValue", 5L),
                    Map.of("statusCode", "abandoned", "countValue", 1L),
                    Map.of("statusCode", "unknown", "countValue", 99L)
            ));

            ProductOverviewSummaryRespVO respVO = productService.getProductOverviewSummary();

            // 旧字段兼容：仅启用状态进 statusCounts，未知状态忽略
            assertEquals(3, respVO.getStatusCounts().size());
            assertEquals(5L, respVO.getStatusCounts().get("active"));
            assertEquals(0L, respVO.getStatusCounts().get("paused"));
            assertEquals(1L, respVO.getStatusCounts().get("abandoned"));
            // 新状态看板：仅启用状态、按 sort 升序、includeInAll 恒 true
            assertEquals(3, respVO.getItems().size());
            assertEquals("active", respVO.getItems().get(0).getStatusCode());
            assertEquals("启用", respVO.getItems().get(0).getStatusName());
            assertEquals(5L, respVO.getItems().get(0).getCount());
            assertEquals(Boolean.TRUE, respVO.getItems().get(0).getIncludeInAll());
            assertEquals("abandoned", respVO.getItems().get(2).getStatusCode());
            assertEquals(Boolean.TRUE, respVO.getItems().get(2).getTerminal());
            assertEquals(6L, respVO.getTotal()); // 5+0+1
        }
    }

    @Test
    void updateProductBaseInfo_whenPausedAndDirectionOrNameChanged_shouldThrowException() {
        Long productId = 1004L;
        ProductDO product = createProduct(productId, "direction_value", "暂停产品", 2007L, "旧描述", "paused");
        ProductSettingBaseInfoUpdateReqVO reqVO = new ProductSettingBaseInfoUpdateReqVO();
        reqVO.setDirectionCode("direction_value_updated");
        reqVO.setName("暂停产品");
        reqVO.setDescription("新描述");

        when(productMapper.selectById(productId)).thenReturn(product);
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("product", "paused"))
                .thenReturn(createStatus("paused", false, true));

        ServiceException ex = assertThrows(ServiceException.class,
                () -> productService.updateProductBaseInfo(productId, reqVO));
        assertEquals(ErrorCodeConstants.PRODUCT_PAUSED_ONLY_ALLOW_LIMITED_UPDATE.getCode(), ex.getCode());
        verify(productMapper, never()).updateById(any(ProductDO.class));
    }

    @Test
    void changeProductStatus_shouldUpdateStatusByConditionAndWriteLogs() {
        Long productId = 1006L;
        Long loginUserId = 3002L;
        ProductDO product = createProduct(productId, "direction_value", "产品B", 2009L, "旧描述", "active");
        ProductStatusActionReqVO reqVO = new ProductStatusActionReqVO();
        reqVO.setId(productId);
        reqVO.setActionCode("pause");
        reqVO.setReason("环境受限");

        when(productMapper.selectById(productId)).thenReturn(product);
        when(objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction("product", "active", "pause"))
                .thenReturn(createTransition("pause", "paused", true));
        when(productMapper.updateStatusByIdAndStatus(productId, "active", "paused", "环境受限")).thenReturn(1);

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
            productService.changeProductStatus(reqVO);
        }

        verify(productMapper, times(1)).updateStatusByIdAndStatus(productId, "active", "paused", "环境受限");

        ArgumentCaptor<ProductStatusLogDO> statusLogCaptor = ArgumentCaptor.forClass(ProductStatusLogDO.class);
        verify(productStatusLogMapper, times(1)).insert(statusLogCaptor.capture());
        assertEquals("pause", statusLogCaptor.getValue().getActionType());
        assertEquals("active", statusLogCaptor.getValue().getFromStatus());
        assertEquals("paused", statusLogCaptor.getValue().getToStatus());
        assertEquals("环境受限", statusLogCaptor.getValue().getReason());

        BizAuditLogDO auditLog = captureAuditLog();
        assertEquals("pause", auditLog.getActionType());
        assertEquals("active", auditLog.getFromStatus());
        assertEquals("paused", auditLog.getToStatus());
        assertEquals("环境受限", auditLog.getReason());
    }

    @Test
    void changeProductStatus_whenConditionalUpdateReturnsZero_shouldThrowException() {
        Long productId = 1007L;
        Long loginUserId = 3003L;
        ProductDO product = createProduct(productId, "direction_value", "产品C", 2010L, "旧描述", "active");
        ProductStatusActionReqVO reqVO = new ProductStatusActionReqVO();
        reqVO.setId(productId);
        reqVO.setActionCode("pause");
        reqVO.setReason("环境受限");

        when(productMapper.selectById(productId)).thenReturn(product);
        when(objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction("product", "active", "pause"))
                .thenReturn(createTransition("pause", "paused", true));
        when(productMapper.updateStatusByIdAndStatus(productId, "active", "paused", "环境受限")).thenReturn(0);

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
            ServiceException ex = assertThrows(ServiceException.class,
                    () -> productService.changeProductStatus(reqVO));
            assertEquals(ErrorCodeConstants.PRODUCT_STATUS_CONCURRENT_MODIFIED.getCode(), ex.getCode());
            verify(productStatusLogMapper, never()).insert(any(ProductStatusLogDO.class));
            verify(bizAuditLogMapper, never()).insert(any(BizAuditLogDO.class));
        }
    }

    @Test
    void changeProductStatus_whenActionNotAllowed_shouldThrowException() {
        Long productId = 1010L;
        ProductDO product = createProduct(productId, "direction_value", "产品F", 2013L, "旧描述", "archived");
        ProductStatusActionReqVO reqVO = new ProductStatusActionReqVO();
        reqVO.setId(productId);
        reqVO.setActionCode("pause");
        reqVO.setReason("再次暂停");

        when(productMapper.selectById(productId)).thenReturn(product);
        when(objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction("product", "archived", "pause"))
                .thenReturn(null);

        ServiceException ex = assertThrows(ServiceException.class,
                () -> productService.changeProductStatus(reqVO));
        assertEquals(ErrorCodeConstants.PRODUCT_STATUS_ACTION_NOT_ALLOWED.getCode(), ex.getCode());
    }

    @Test
    void changeProductStatus_whenReasonRequiredButMissing_shouldThrowException() {
        Long productId = 1011L;
        ProductDO product = createProduct(productId, "direction_value", "产品G", 2014L, "旧描述", "active");
        ProductStatusActionReqVO reqVO = new ProductStatusActionReqVO();
        reqVO.setId(productId);
        reqVO.setActionCode("pause");
        reqVO.setReason("   ");

        when(productMapper.selectById(productId)).thenReturn(product);
        when(objectStatusTransitionMapper.selectByObjectTypeAndFromStatusAndAction("product", "active", "pause"))
                .thenReturn(createTransition("pause", "paused", true));

        ServiceException ex = assertThrows(ServiceException.class,
                () -> productService.changeProductStatus(reqVO));
        assertEquals(ErrorCodeConstants.PRODUCT_STATUS_ACTION_REASON_REQUIRED.getCode(), ex.getCode());
    }

    @Test
    void deleteProduct_whenConfirmTextInvalid_shouldThrowException() {
        Long productId = 1008L;
        Long loginUserId = 3004L;
        ProductDO product = createProduct(productId, "direction_value", "产品D", 2011L, "旧描述", "active");
        ProductDeleteReqVO reqVO = new ProductDeleteReqVO();
        reqVO.setId(productId);
        reqVO.setProductName("产品D");
        reqVO.setReason("录入错误");
        reqVO.setConfirmText("WRONG");

        when(productMapper.selectById(productId)).thenReturn(product);

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
            ServiceException ex = assertThrows(ServiceException.class,
                    () -> productService.deleteProduct(reqVO));
            assertEquals(ErrorCodeConstants.PRODUCT_DELETE_CONFIRM_TEXT_INVALID.getCode(), ex.getCode());
            verify(productMapper, never()).deleteByIdAndStatus(any(Long.class), any(String.class));
            verify(productStatusLogMapper, never()).insert(any(ProductStatusLogDO.class));
            verify(bizAuditLogMapper, never()).insert(any(BizAuditLogDO.class));
        }
    }

    @Test
    void deleteProduct_whenProductNameMismatch_shouldThrowException() {
        Long productId = 1012L;
        ProductDO product = createProduct(productId, "direction_value", "产品H", 2015L, "旧描述", "active");
        ProductDeleteReqVO reqVO = new ProductDeleteReqVO();
        reqVO.setId(productId);
        reqVO.setProductName("错误名称");
        reqVO.setReason("录入错误");
        reqVO.setConfirmText("DELETE");

        when(productMapper.selectById(productId)).thenReturn(product);

        ServiceException ex = assertThrows(ServiceException.class,
                () -> productService.deleteProduct(reqVO));
        assertEquals(ErrorCodeConstants.PRODUCT_DELETE_NAME_MISMATCH.getCode(), ex.getCode());
    }

    @Test
    void deleteProduct_shouldDeleteByConditionAndWriteLogs() {
        Long productId = 1009L;
        Long loginUserId = 3005L;
        ProductDO product = createProduct(productId, "direction_value", "产品E", 2012L, "旧描述", "active");
        ProductDeleteReqVO reqVO = new ProductDeleteReqVO();
        reqVO.setId(productId);
        reqVO.setProductName("产品E");
        reqVO.setReason("录入错误");
        reqVO.setConfirmText("DELETE");

        when(productMapper.selectById(productId)).thenReturn(product);
        when(productMapper.deleteByIdAndStatus(productId, "active")).thenReturn(1);

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
            productService.deleteProduct(reqVO);
        }

        verify(productMapper, times(1)).deleteByIdAndStatus(productId, "active");

        ArgumentCaptor<ProductStatusLogDO> statusLogCaptor = ArgumentCaptor.forClass(ProductStatusLogDO.class);
        verify(productStatusLogMapper, times(1)).insert(statusLogCaptor.capture());
        assertEquals("delete", statusLogCaptor.getValue().getActionType());
        assertEquals("active", statusLogCaptor.getValue().getFromStatus());
        assertNull(statusLogCaptor.getValue().getToStatus());
        assertEquals("录入错误", statusLogCaptor.getValue().getReason());

        BizAuditLogDO auditLog = captureAuditLog();
        assertEquals("delete", auditLog.getActionType());
        assertEquals("active", auditLog.getFromStatus());
        assertNull(auditLog.getToStatus());
        assertEquals("录入错误", auditLog.getReason());
    }

    @Test
    void updateProductBaseInfo_whenArchived_shouldThrowException() {
        Long productId = 1013L;
        ProductDO product = createProduct(productId, "direction_value", "归档产品", 2016L, "旧描述", "archived");
        ProductSettingBaseInfoUpdateReqVO reqVO = new ProductSettingBaseInfoUpdateReqVO();
        reqVO.setDirectionCode("direction_value");
        reqVO.setName("归档产品");
        reqVO.setDescription("新描述");

        when(productMapper.selectById(productId)).thenReturn(product);
        when(objectStatusModelMapper.selectByObjectTypeAndStatusCodeEnabled("product", "archived"))
                .thenReturn(createStatus("archived", false, false));

        ServiceException ex = assertThrows(ServiceException.class,
                () -> productService.updateProductBaseInfo(productId, reqVO));
        assertEquals(ErrorCodeConstants.PRODUCT_STATUS_NOT_ALLOW_EDIT.getCode(), ex.getCode());
    }

    @Test
    void getProductContext_shouldAssembleCurrentRoleNavsAndButtonsFromObjectPermissionApi() {
        Long productId = 1014L;
        Long loginUserId = 3014L;
        Long roleId = 9101L;
        ProductDO product = createProduct(productId, "direction_value", "产品上下文", 2017L, "旧描述", "active");
        UserObjectRoleDO currentMember = createMember(productId, loginUserId, roleId);
        ObjectRolePermissionRespDTO detail = new ObjectRolePermissionRespDTO();
        detail.setCurrentRole(createRole(roleId, "product_manager", "产品经理"));
        detail.setMenus(List.of(
                createMenu(9303L, "保存", "project:product:update", 3, 30, null, null, true),
                createMenu(9302L, "隐藏导航", null, 2, 20, "/product/hidden", "mdi:hidden", false),
                createMenu(9301L, "概览", null, 2, 10, "/product/overview", "mdi:view-dashboard-outline", true)
        ));
        detail.setPermissions(Set.of("project:product:update"));

        when(productMapper.selectById(productId)).thenReturn(product);
        when(userObjectRoleMapper.selectActiveListByObjectAndUserId("product", productId, loginUserId))
                .thenReturn(List.of(currentMember));
        when(objectPermissionApi.getObjectRolePermissionDetailMerged(List.of(roleId), "object", "product"))
                .thenReturn(success(detail));

        ProductContextRespVO respVO;
        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockLoginUser(loginUserId, "测试人")) {
            respVO = productService.getProductContext(productId);
        }

        assertNotNull(respVO.getCurrentProduct());
        assertEquals(roleId, respVO.getCurrentRole().getRoleId());
        assertEquals("product_manager", respVO.getCurrentRole().getRoleCode());
        assertEquals("产品经理", respVO.getCurrentRole().getRoleName());
        assertEquals(1, respVO.getNavs().size());
        assertEquals(9301L, respVO.getNavs().get(0).getId());
        assertEquals(List.of("project:product:update"), respVO.getButtons());
    }

    @Test
    void getProductContext_returnsImplicitObserver_whenNoExplicitRoleButScopeContains() {
        Long productId = 1015L;
        Long loginUserId = 3015L;
        Long observerRoleId = 9201L;
        ProductDO product = createProduct(productId, "direction_value", "隐式观察产品", 2018L, "描述", "active");

        ObjectRoleRespDTO observerRole = createRole(observerRoleId, "implicit_observer_product", "产品隐式观察者");
        ObjectRolePermissionRespDTO detail = new ObjectRolePermissionRespDTO();
        detail.setCurrentRole(observerRole);
        detail.setMenus(List.of(
                createMenu(9401L, "概览", null, 2, 10, "/product/overview", "mdi:view-dashboard-outline", true)
        ));

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockStatic(SecurityFrameworkUtils.class)) {
            mockedStatic.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId);
            mockedStatic.when(SecurityFrameworkUtils::getLoginUserNickname).thenReturn("观察人");

            when(productMapper.selectById(productId)).thenReturn(product);
            when(userObjectRoleMapper.selectActiveListByObjectAndUserId("product", productId, loginUserId))
                    .thenReturn(Collections.emptyList());
            when(objectDataScopeService.compute(loginUserId, "product"))
                    .thenReturn(ObjectDataScope.idList(Set.of(productId), Set.of()));
            when(objectPermissionApi.getObjectRoleByCode("implicit_observer_product", "object", "product"))
                    .thenReturn(success(observerRole));
            when(objectPermissionApi.getObjectRolePermissionDetailMerged(List.of(observerRoleId), "object", "product"))
                    .thenReturn(success(detail));

            ProductContextRespVO respVO = productService.getProductContext(productId);

            assertNotNull(respVO.getCurrentProduct());
            assertEquals(observerRoleId, respVO.getCurrentRole().getRoleId());
            assertEquals("implicit_observer_product", respVO.getCurrentRole().getRoleCode());
            assertEquals(Boolean.TRUE, respVO.getCurrentRole().getGuestFlag());
            assertEquals(1, respVO.getNavs().size());
            assertEquals(9401L, respVO.getNavs().get(0).getId());
        }
    }

    @Test
    void getProductContext_throws_whenNoExplicitRoleAndScopeMisses() {
        Long productId = 1016L;
        Long loginUserId = 3016L;
        ProductDO product = createProduct(productId, "direction_value", "无权限产品", 2019L, "描述", "active");

        try (MockedStatic<SecurityFrameworkUtils> mockedStatic = mockStatic(SecurityFrameworkUtils.class)) {
            mockedStatic.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(loginUserId);

            when(productMapper.selectById(productId)).thenReturn(product);
            when(userObjectRoleMapper.selectActiveListByObjectAndUserId("product", productId, loginUserId))
                    .thenReturn(Collections.emptyList());
            // scope 不包含该产品（空 ID 列表，directionCode 也不匹配）
            when(objectDataScopeService.compute(loginUserId, "product"))
                    .thenReturn(ObjectDataScope.idList(Set.of(9999L), Set.of("other_direction")));

            ServiceException ex = assertThrows(ServiceException.class,
                    () -> productService.getProductContext(productId));
            assertEquals(ErrorCodeConstants.PRODUCT_OBJECT_PERMISSION_DENIED.getCode(), ex.getCode());
        }
    }

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

            PageResult<ProductRespVO> result = productService.getProductPage(new ProductPageReqVO());

            assertThat(result.getList()).isEmpty();
            verify(productMapper, never()).selectPage(any(ProductPageReqVO.class), any());
        }
    }

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

            productService.getProductPage(new ProductPageReqVO());

            verify(productMapper, times(1)).selectPage(any(ProductPageReqVO.class), any());
        }
    }

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

            productService.getProductPage(new ProductPageReqVO());

            verify(productMapper, times(1)).selectPage(any(ProductPageReqVO.class), any());
        }
    }

    @Test
    void publishProductCreatedNotify_perMemberWithRole() {
        try (MockedStatic<SecurityFrameworkUtils> mocked = mockStatic(SecurityFrameworkUtils.class)) {
            mocked.when(SecurityFrameworkUtils::getLoginUserId).thenReturn(9L);
            ProductDO product = new ProductDO();
            product.setId(20L);
            product.setName("智能终端");
            when(teamNotifyRecipientResolver.resolveActiveExcludingCreator(
                    eq(com.njcn.rdms.module.project.constant.ProductObjectConstants.OBJECT_TYPE), eq(20L),
                    anyString(), anyString()))
                    .thenReturn(List.of(new com.njcn.rdms.module.project.framework.notify.TeamRecipient(5L, "产品经理")));

            ArgumentCaptor<com.njcn.rdms.module.project.framework.notify.NotifySendEvent> captor =
                    ArgumentCaptor.forClass(com.njcn.rdms.module.project.framework.notify.NotifySendEvent.class);
            productService.publishProductCreatedNotify(product);
            verify(applicationEventPublisher).publishEvent(captor.capture());
            com.njcn.rdms.module.project.framework.notify.NotifySendEvent ev = captor.getValue();
            assertEquals("product_created", ev.getTemplateCode());
            assertEquals(9L, ev.getOperatorUserId());
            assertEquals("智能终端", ev.getParams().get("productName"));
            assertEquals("产品经理", ev.getParams().get("roleName"));
            assertTrue(ev.getUserIds().contains(5L));
        }
    }

    private ProductDO createProduct(Long id, String directionCode, String name, Long managerUserId,
                                    String description, String statusCode) {
        ProductDO product = new ProductDO();
        product.setId(id);
        product.setCode("CNPD202600" + (id % 10));
        product.setDirectionCode(directionCode);
        product.setStatusCode(statusCode);
        product.setName(name);
        product.setManagerUserId(managerUserId);
        product.setDescription(description);
        return product;
    }

    private UserObjectRoleDO createMember(Long productId, Long userId, Long roleId) {
        UserObjectRoleDO currentMember = new UserObjectRoleDO();
        currentMember.setId(9001L);
        currentMember.setUserId(userId);
        currentMember.setObjectType("product");
        currentMember.setObjectId(productId);
        currentMember.setRoleId(roleId);
        currentMember.setStatus(0);
        return currentMember;
    }

    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("product");
        return role;
    }

    private ObjectMenuRespDTO createMenu(Long menuId, String name, String permission, Integer type, Integer sort,
                                         String path, String icon, Boolean visible) {
        ObjectMenuRespDTO menu = new ObjectMenuRespDTO();
        menu.setId(menuId);
        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 ObjectStatusModelDO createStatus(String statusCode, boolean initialFlag, boolean allowEdit) {
        ObjectStatusModelDO statusModel = new ObjectStatusModelDO();
        statusModel.setObjectType("product");
        statusModel.setStatusCode(statusCode);
        statusModel.setStatusName(statusCode);
        statusModel.setInitialFlag(initialFlag);
        statusModel.setAllowEdit(allowEdit);
        return statusModel;
    }

    /** 状态看板测试用状态机行：带展示名 / 终态标识。 */
    private ObjectStatusModelDO createBoardStatusModel(String statusCode, String statusName,
                                                       int sort, int status, boolean terminalFlag) {
        ObjectStatusModelDO statusModel = new ObjectStatusModelDO();
        statusModel.setObjectType("product");
        statusModel.setStatusCode(statusCode);
        statusModel.setStatusName(statusName);
        statusModel.setSort(sort);
        statusModel.setStatus(status);
        statusModel.setTerminalFlag(terminalFlag);
        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;
    }

    private ProductDO captureUpdatedProduct() {
        ArgumentCaptor<ProductDO> productCaptor = ArgumentCaptor.forClass(ProductDO.class);
        verify(productMapper, times(1)).updateById(productCaptor.capture());
        return productCaptor.getValue();
    }

    private BizAuditLogDO captureAuditLog() {
        ArgumentCaptor<BizAuditLogDO> auditCaptor = ArgumentCaptor.forClass(BizAuditLogDO.class);
        verify(bizAuditLogMapper, times(1)).insert(auditCaptor.capture());
        return auditCaptor.getValue();
    }

    private BizAuditLogDO captureCreateAuditLog() {
        ArgumentCaptor<BizAuditLogDO> auditCaptor = ArgumentCaptor.forClass(BizAuditLogDO.class);
        verify(bizAuditLogMapper, times(3)).insert(auditCaptor.capture());
        return auditCaptor.getAllValues().stream()
                .filter(log -> "product".equals(log.getBizType()) && "create".equals(log.getActionType()))
                .findFirst()
                .orElseThrow();
    }

    private void assertProductBaseInfoUpdated(ProductDO updated, String code, Long managerUserId,
                                              String statusCode, String directionCode,
                                              String name, String description) {
        assertEquals(code, updated.getCode());
        assertEquals(managerUserId, updated.getManagerUserId());
        assertEquals(statusCode, updated.getStatusCode());
        assertEquals(directionCode, updated.getDirectionCode());
        assertEquals(name, updated.getName());
        assertEquals(description, updated.getDescription());
        assertNull(updated.getLastStatusReason());
    }

    private void assertAuditFieldChanges(BizAuditLogDO auditLog, Long productId, String beforeDirectionCode,
                                         String afterDirectionCode, String beforeName, String afterName,
                                         String beforeDescription, String afterDescription) {
        assertEquals("product", auditLog.getBizType());
        assertEquals(productId, auditLog.getBizId());
        assertEquals("update", auditLog.getActionType());
        assertEquals("active", auditLog.getFromStatus());
        assertEquals("active", auditLog.getToStatus());

        JsonNode fieldChanges = JsonUtils.parseTree(auditLog.getFieldChanges());
        assertEquals(beforeDirectionCode, fieldChanges.path("directionCode").path("before").asText());
        assertEquals(afterDirectionCode, fieldChanges.path("directionCode").path("after").asText());
        assertEquals(beforeName, fieldChanges.path("name").path("before").asText());
        assertEquals(afterName, fieldChanges.path("name").path("after").asText());
        assertEquals(beforeDescription, fieldChanges.path("description").path("before").asText());
        assertEquals(afterDescription, fieldChanges.path("description").path("after").asText());
        assertFalse(fieldChanges.has("managerUserId"));
    }

}
