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

import com.njcn.rdms.framework.common.exception.ServiceException;
import com.njcn.rdms.framework.common.pojo.PageResult;
import com.njcn.rdms.framework.common.util.json.JsonUtils;
import com.njcn.rdms.framework.test.core.ut.BaseMockitoUnitTest;
import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityTimelinePageReqVO;
import com.njcn.rdms.module.project.controller.admin.product.vo.activity.ProductActivityTimelineRespVO;
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.ProductStatusLogDO;
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.ProductStatusLogMapper;
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.springframework.beans.BeanWrapperImpl;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;

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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class ProductActivityTimelineQueryServiceTest extends BaseMockitoUnitTest {

    @InjectMocks
    private ProductActivityTimelineQueryService productActivityTimelineQueryService;
    @Mock
    private ProductStatusLogMapper productStatusLogMapper;
    @Mock
    private BizAuditLogMapper bizAuditLogMapper;
    @Mock
    private UserObjectRoleMapper userObjectRoleMapper;
    @Mock
    private CacheManager cacheManager;
    @Mock
    private Cache userNicknameCache;
    @Mock
    private Cache roleNameCache;
    @Mock
    private AdminUserApi adminUserApi;
    @Mock
    private ObjectPermissionApi objectPermissionApi;

    @Test
    void getProductActivityTimelinePage_whenTimeRangeMissingEnd_shouldThrowInvalidParam() {
        ProductActivityTimelinePageReqVO reqVO = new ProductActivityTimelinePageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(10);
        reqVO.setStartTime(LocalDateTime.of(2026, 4, 1, 0, 0, 0));

        ServiceException ex = assertThrows(ServiceException.class,
                () -> productActivityTimelineQueryService.getProductActivityTimelinePage(1001L, reqVO));

        assertTrue(ex.getMessage().contains("开始时间和结束时间必须同时传入"));
    }

    @Test
    void getProductActivityTimelinePage_shouldUseDefaultRecent30DaysAndActionTypes() {
        Long productId = 1001L;
        ProductActivityTimelinePageReqVO reqVO = new ProductActivityTimelinePageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(10);
        reqVO.setActivityType("status");
        reqVO.setActionTypes(List.of("pause", "archive"));

        when(productStatusLogMapper.selectListByProductIdAndActions(eq(productId), eq(List.of("pause", "archive")),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of());

        PageResult<ProductActivityTimelineRespVO> result =
                productActivityTimelineQueryService.getProductActivityTimelinePage(productId, reqVO);

        ArgumentCaptor<LocalDateTime> startCaptor = ArgumentCaptor.forClass(LocalDateTime.class);
        ArgumentCaptor<LocalDateTime> endCaptor = ArgumentCaptor.forClass(LocalDateTime.class);
        verify(productStatusLogMapper).selectListByProductIdAndActions(eq(productId), eq(List.of("pause", "archive")),
                startCaptor.capture(), endCaptor.capture());

        assertEquals(0L, result.getTotal());
        assertTrue(Duration.between(startCaptor.getValue(), endCaptor.getValue()).toDays() >= 29);
        assertTrue(Duration.between(startCaptor.getValue(), endCaptor.getValue()).toDays() <= 31);
    }

    @Test
    void getProductActivityTimelinePage_shouldKeepSingleCreateAndIgnoreInitNoise() {
        Long productId = 1002L;
        ProductActivityTimelinePageReqVO reqVO = new ProductActivityTimelinePageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(10);

        BizAuditLogDO createLog = buildProductLog(21L, productId, "create",
                "{\"managerUserId\":{\"before\":null,\"after\":2001}}",
                LocalDateTime.of(2026, 4, 23, 10, 0, 0));
        BizAuditLogDO managerInitLog = buildProductLog(22L, productId, "change_manager",
                "{\"managerUserId\":{\"before\":null,\"after\":2001}}",
                LocalDateTime.of(2026, 4, 23, 10, 0, 0));
        BizAuditLogDO memberInitLog = buildMemberLog(31L, 9001L, "add_member",
                "{\"userId\":{\"before\":null,\"after\":2001}}",
                LocalDateTime.of(2026, 4, 23, 10, 0, 0));

        when(productStatusLogMapper.selectListByProductIdAndActions(eq(productId), any(),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of());
        when(bizAuditLogMapper.selectListByBizAndActions(eq("product"), eq(productId), any(),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of(managerInitLog, createLog));
        when(bizAuditLogMapper.selectListByBizTypeAndActions(eq("rdms_user_object_role"), any(),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of(memberInitLog));
        when(userObjectRoleMapper.selectListByIdsAndObject(List.of(9001L), "product", productId))
                .thenReturn(List.of(buildMember(9001L, productId, 2001L)));

        PageResult<ProductActivityTimelineRespVO> result =
                productActivityTimelineQueryService.getProductActivityTimelinePage(productId, reqVO);

        assertEquals(1L, result.getTotal());
        assertEquals("product", result.getList().get(0).getType());
        assertEquals("create", result.getList().get(0).getActionType());
    }

    @Test
    void getProductActivityTimelinePage_shouldPreferStatusLogOverProductStatusAudit() {
        Long productId = 1003L;
        ProductActivityTimelinePageReqVO reqVO = new ProductActivityTimelinePageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(10);

        ProductStatusLogDO statusLog = buildStatusLog(11L, productId, "pause",
                "active", "paused", LocalDateTime.of(2026, 4, 22, 9, 0, 0));
        BizAuditLogDO statusAudit = buildProductLog(12L, productId, "pause", null,
                LocalDateTime.of(2026, 4, 22, 9, 0, 0));

        when(productStatusLogMapper.selectListByProductIdAndActions(eq(productId), any(),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of(statusLog));
        when(bizAuditLogMapper.selectListByBizAndActions(eq("product"), eq(productId), any(),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of(statusAudit));
        when(bizAuditLogMapper.selectListByBizTypeAndActions(eq("rdms_user_object_role"), any(),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of());

        PageResult<ProductActivityTimelineRespVO> result =
                productActivityTimelineQueryService.getProductActivityTimelinePage(productId, reqVO);

        assertEquals(1L, result.getTotal());
        assertEquals("status:11", result.getList().get(0).getId());
    }

    @Test
    void getProductActivityTimelinePage_shouldFilterMemberActionsByIntersection() {
        Long productId = 1004L;
        ProductActivityTimelinePageReqVO reqVO = new ProductActivityTimelinePageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(10);
        reqVO.setActivityType("member");
        reqVO.setActionTypes(List.of("remove_member"));

        when(bizAuditLogMapper.selectListByBizTypeAndActions(eq("rdms_user_object_role"), eq(List.of("remove_member")),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of());

        PageResult<ProductActivityTimelineRespVO> result =
                productActivityTimelineQueryService.getProductActivityTimelinePage(productId, reqVO);

        assertEquals(0L, result.getTotal());
        verify(bizAuditLogMapper).selectListByBizTypeAndActions(eq("rdms_user_object_role"),
                eq(List.of("remove_member")), any(LocalDateTime.class), any(LocalDateTime.class));
    }

    @Test
    void getProductActivityTimelinePage_whenStatusActionIsNewCode_shouldStillQueryStatusLogs() {
        Long productId = 10041L;
        ProductActivityTimelinePageReqVO reqVO = new ProductActivityTimelinePageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(10);
        reqVO.setActivityType("status");
        reqVO.setActionTypes(List.of("freeze"));

        when(productStatusLogMapper.selectListByProductIdAndActions(eq(productId), eq(List.of("freeze")),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of());

        PageResult<ProductActivityTimelineRespVO> result =
                productActivityTimelineQueryService.getProductActivityTimelinePage(productId, reqVO);

        assertEquals(0L, result.getTotal());
        verify(productStatusLogMapper).selectListByProductIdAndActions(eq(productId), eq(List.of("freeze")),
                any(LocalDateTime.class), any(LocalDateTime.class));
    }

    @Test
    void getProductActivityTimelinePage_shouldExcludeUpdateMemberAction() {
        Long productId = 1005L;
        ProductActivityTimelinePageReqVO reqVO = new ProductActivityTimelinePageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(10);

        BizAuditLogDO updateMemberLog = buildMemberLog(41L, 9002L, "update_member",
                "{\"remark\":{\"before\":\"旧\",\"after\":\"新\"}}",
                LocalDateTime.of(2026, 4, 21, 15, 0, 0));

        when(productStatusLogMapper.selectListByProductIdAndActions(eq(productId), any(),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of());
        when(bizAuditLogMapper.selectListByBizAndActions(eq("product"), eq(productId), any(),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of());
        when(bizAuditLogMapper.selectListByBizTypeAndActions(eq("rdms_user_object_role"), any(),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of(updateMemberLog));
        when(userObjectRoleMapper.selectListByIdsAndObject(List.of(9002L), "product", productId))
                .thenReturn(List.of(buildMember(9002L, productId, 2002L)));

        PageResult<ProductActivityTimelineRespVO> result =
                productActivityTimelineQueryService.getProductActivityTimelinePage(productId, reqVO);

        assertEquals(0L, result.getTotal());
    }

    @Test
    void getProductActivityTimelinePage_shouldExposeTargetUserIdForMemberActivity() {
        Long productId = 1006L;
        ProductActivityTimelinePageReqVO reqVO = new ProductActivityTimelinePageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(10);
        reqVO.setActivityType("member");

        BizAuditLogDO addMemberLog = buildMemberLog(51L, 9003L, "add_member",
                "{\"userId\":{\"before\":null,\"after\":2003}}",
                LocalDateTime.of(2026, 4, 21, 16, 0, 0));
        UserObjectRoleDO member = buildMember(9003L, productId, 2003L);

        when(bizAuditLogMapper.selectListByBizTypeAndActions(eq("rdms_user_object_role"), any(),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of(addMemberLog));
        when(userObjectRoleMapper.selectListByIdsAndObject(List.of(9003L), "product", productId))
                .thenReturn(List.of(member));

        PageResult<ProductActivityTimelineRespVO> result =
                productActivityTimelineQueryService.getProductActivityTimelinePage(productId, reqVO);

        assertEquals(1L, result.getTotal());
        assertEquals(2003L, result.getList().get(0).getTargetUserId());
    }

    @Test
    void getProductActivityTimelinePage_shouldReadTargetUserNameFromCacheFirst() {
        Long productId = 1007L;
        ProductActivityTimelinePageReqVO reqVO = new ProductActivityTimelinePageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(10);
        reqVO.setActivityType("member");

        BizAuditLogDO addMemberLog = buildMemberLog(61L, 9004L, "add_member",
                "{\"userId\":{\"before\":null,\"after\":2004}}",
                LocalDateTime.of(2026, 4, 21, 17, 0, 0));
        UserObjectRoleDO member = buildMember(9004L, productId, 2004L);
        when(bizAuditLogMapper.selectListByBizTypeAndActions(eq("rdms_user_object_role"), any(),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of(addMemberLog));
        when(userObjectRoleMapper.selectListByIdsAndObject(List.of(9004L), "product", productId))
                .thenReturn(List.of(member));
        when(cacheManager.getCache("project_timeline_user_nickname#10m")).thenReturn(userNicknameCache);
        when(userNicknameCache.get(2004L, String.class)).thenReturn("成员丁");

        PageResult<ProductActivityTimelineRespVO> result =
                productActivityTimelineQueryService.getProductActivityTimelinePage(productId, reqVO);

        assertEquals("成员丁", new BeanWrapperImpl(result.getList().get(0)).getPropertyValue("targetUserName"));
        verify(adminUserApi, never()).getUserMap(anySet());
    }

    @Test
    void getProductActivityTimelinePage_shouldLoadAndCacheTargetUserNameWhenCacheMiss() {
        Long productId = 1008L;
        ProductActivityTimelinePageReqVO reqVO = new ProductActivityTimelinePageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(10);
        reqVO.setActivityType("member");

        BizAuditLogDO removeMemberLog = buildMemberLog(71L, 9005L, "remove_member",
                "{\"userId\":{\"before\":2005,\"after\":2005}}",
                LocalDateTime.of(2026, 4, 21, 18, 0, 0));
        UserObjectRoleDO member = buildMember(9005L, productId, 2005L);
        when(bizAuditLogMapper.selectListByBizTypeAndActions(eq("rdms_user_object_role"), any(),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of(removeMemberLog));
        when(userObjectRoleMapper.selectListByIdsAndObject(List.of(9005L), "product", productId))
                .thenReturn(List.of(member));
        when(cacheManager.getCache("project_timeline_user_nickname#10m")).thenReturn(userNicknameCache);
        when(userNicknameCache.get(2005L, String.class)).thenReturn(null);
        when(adminUserApi.getUserMap(java.util.Set.of(2005L))).thenReturn(Map.of(2005L, buildUser(2005L, "成员戊")));

        PageResult<ProductActivityTimelineRespVO> result =
                productActivityTimelineQueryService.getProductActivityTimelinePage(productId, reqVO);

        assertEquals("成员戊", new BeanWrapperImpl(result.getList().get(0)).getPropertyValue("targetUserName"));
        verify(userNicknameCache).put(2005L, "成员戊");
    }

    @Test
    void getProductActivityTimelinePage_shouldReadRoleNameFromCacheFirst() {
        Long productId = 1009L;
        ProductActivityTimelinePageReqVO reqVO = new ProductActivityTimelinePageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(10);
        reqVO.setActivityType("member");

        BizAuditLogDO addMemberLog = buildMemberLog(81L, 9006L, "add_member",
                "{\"roleId\":{\"before\":null,\"after\":3101},\"userId\":{\"before\":null,\"after\":2006}}",
                LocalDateTime.of(2026, 4, 21, 19, 0, 0));
        when(bizAuditLogMapper.selectListByBizTypeAndActions(eq("rdms_user_object_role"), any(),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of(addMemberLog));
        when(userObjectRoleMapper.selectListByIdsAndObject(List.of(9006L), "product", productId))
                .thenReturn(List.of(buildMember(9006L, productId, 2006L)));
        when(cacheManager.getCache("project_timeline_user_nickname#10m")).thenReturn(userNicknameCache);
        when(cacheManager.getCache("project_timeline_role_name#10m")).thenReturn(roleNameCache);
        when(roleNameCache.get(3101L, String.class)).thenReturn("产品经理");

        PageResult<ProductActivityTimelineRespVO> result =
                productActivityTimelineQueryService.getProductActivityTimelinePage(productId, reqVO);

        assertEquals("产品经理", JsonUtils.parseTree(result.getList().get(0).getDetails())
                .path("roleName").path("after").asText());
        verify(objectPermissionApi, never()).getObjectRoleMap(anySet(), any(), any());
    }

    @Test
    void getProductActivityTimelinePage_shouldLoadAndCacheRoleNameWhenCacheMiss() {
        Long productId = 1010L;
        ProductActivityTimelinePageReqVO reqVO = new ProductActivityTimelinePageReqVO();
        reqVO.setPageNo(1);
        reqVO.setPageSize(10);
        reqVO.setActivityType("member");

        BizAuditLogDO removeMemberLog = buildMemberLog(91L, 9007L, "remove_member",
                "{\"roleId\":{\"before\":3102,\"after\":3102},\"userId\":{\"before\":2007,\"after\":2007}}",
                LocalDateTime.of(2026, 4, 21, 20, 0, 0));
        when(bizAuditLogMapper.selectListByBizTypeAndActions(eq("rdms_user_object_role"), any(),
                any(LocalDateTime.class), any(LocalDateTime.class)))
                .thenReturn(List.of(removeMemberLog));
        when(userObjectRoleMapper.selectListByIdsAndObject(List.of(9007L), "product", productId))
                .thenReturn(List.of(buildMember(9007L, productId, 2007L)));
        when(cacheManager.getCache("project_timeline_user_nickname#10m")).thenReturn(userNicknameCache);
        when(cacheManager.getCache("project_timeline_role_name#10m")).thenReturn(roleNameCache);
        when(roleNameCache.get(3102L, String.class)).thenReturn(null);
        when(objectPermissionApi.getObjectRoleMap(java.util.Set.of(3102L), "object", "product"))
                .thenReturn(Map.of(3102L, buildRole(3102L, "产品成员")));

        PageResult<ProductActivityTimelineRespVO> result =
                productActivityTimelineQueryService.getProductActivityTimelinePage(productId, reqVO);

        assertEquals("产品成员", JsonUtils.parseTree(result.getList().get(0).getDetails())
                .path("roleName").path("before").asText());
        verify(roleNameCache).put(3102L, "产品成员");
    }

    private BizAuditLogDO buildProductLog(Long id, Long productId, String actionType,
                                          String fieldChanges, LocalDateTime createTime) {
        BizAuditLogDO log = new BizAuditLogDO();
        log.setId(id);
        log.setBizType("product");
        log.setBizId(productId);
        log.setActionType(actionType);
        log.setFieldChanges(fieldChanges);
        log.setOperatorUserId(100L);
        log.setOperatorName("张三");
        log.setCreateTime(createTime);
        return log;
    }

    private BizAuditLogDO buildMemberLog(Long id, Long memberId, String actionType,
                                         String fieldChanges, LocalDateTime createTime) {
        BizAuditLogDO log = new BizAuditLogDO();
        log.setId(id);
        log.setBizType("rdms_user_object_role");
        log.setBizId(memberId);
        log.setActionType(actionType);
        log.setFieldChanges(fieldChanges);
        log.setOperatorUserId(100L);
        log.setOperatorName("张三");
        log.setCreateTime(createTime);
        return log;
    }

    private ProductStatusLogDO buildStatusLog(Long id, Long productId, String actionType,
                                              String fromStatus, String toStatus, LocalDateTime createTime) {
        ProductStatusLogDO log = new ProductStatusLogDO();
        log.setId(id);
        log.setProductId(productId);
        log.setActionType(actionType);
        log.setFromStatus(fromStatus);
        log.setToStatus(toStatus);
        log.setOperatorUserId(101L);
        log.setOperatorName("李四");
        log.setCreateTime(createTime);
        return log;
    }

    private UserObjectRoleDO buildMember(Long id, Long productId, Long userId) {
        UserObjectRoleDO member = new UserObjectRoleDO();
        member.setId(id);
        member.setObjectType("product");
        member.setObjectId(productId);
        member.setUserId(userId);
        return member;
    }

    private AdminUserRespDTO buildUser(Long id, String nickname) {
        AdminUserRespDTO user = new AdminUserRespDTO();
        user.setId(id);
        user.setNickname(nickname);
        return user;
    }

    private ObjectRoleRespDTO buildRole(Long id, String name) {
        ObjectRoleRespDTO role = new ObjectRoleRespDTO();
        role.setId(id);
        role.setName(name);
        role.setScopeType("object");
        role.setObjectType("product");
        return role;
    }

}
