package tech.easyflow.admin.service.dashboard.impl;

import com.mybatisflex.core.query.QueryWrapper;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.testng.Assert;
import org.testng.annotations.Test;
import tech.easyflow.ai.entity.Bot;
import tech.easyflow.ai.service.BotService;
import tech.easyflow.admin.model.dashboard.DashboardAssistantTrendSeriesVo;
import tech.easyflow.admin.model.dashboard.DashboardDistributionItemVo;
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
import tech.easyflow.admin.model.dashboard.DashboardSummaryVo;
import tech.easyflow.admin.model.dashboard.DashboardTrendItemVo;
import tech.easyflow.admin.model.dashboard.DashboardUserRankItemVo;
import tech.easyflow.admin.model.dashboard.DashboardUserRankQuery;
import tech.easyflow.chatlog.domain.dto.ChatActiveUserRank;
import tech.easyflow.chatlog.domain.dto.ChatAssistantSessionTrend;
import tech.easyflow.chatlog.domain.dto.ChatAssistantUsageRank;
import tech.easyflow.chatlog.domain.dto.ChatDashboardSummary;
import tech.easyflow.chatlog.domain.dto.ChatDashboardTrend;
import tech.easyflow.chatlog.service.ChatDashboardQueryService;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.entity.SysAccount;
import tech.easyflow.system.service.CategoryPermissionService;
import tech.easyflow.system.service.SysAccountService;
import tech.easyflow.system.service.SysAccountRoleService;
import tech.easyflow.system.service.SysRoleService;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List;
import java.util.stream.IntStream;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * {@link DashboardServiceImpl} 测试。
 */
public class DashboardServiceImplTest {

    /**
     * 验证分析库不可用时返回明确不可用状态，且趋势与排行为空。
     *
     * @throws Exception 反射调用失败
     */
    @Test
    public void shouldReturnUnavailableChatPayloadWhenAnalyticalDbIsDisabled() throws Exception {
        DashboardServiceImpl service = new DashboardServiceImpl();
        ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
        when(chatDashboardQueryService.available()).thenReturn(false);
        setField(service, "chatDashboardQueryService", chatDashboardQueryService);

        Object context = newContext("7d", null);
        DashboardSummaryVo summary = new DashboardSummaryVo();
        Object payload = invokeBuildChatPayload(service, context, summary);

        Object chatStatus = readField(payload, "chatStatus");
        List<?> trends = (List<?>) readField(payload, "trends");
        List<?> distribution = (List<?>) readField(payload, "distribution");

        Assert.assertFalse(Boolean.TRUE.equals(invokeGetter(chatStatus, "getAvailable")));
        Assert.assertEquals(invokeGetter(chatStatus, "getMessage"), "聊天数据不可用");
        Assert.assertTrue(trends.isEmpty());
        Assert.assertTrue(distribution.isEmpty());
        Assert.assertEquals(summary.getChatMessageTotal(), Long.valueOf(0L));
        Assert.assertEquals(summary.getChatSessionTotal(), Long.valueOf(0L));
        Assert.assertEquals(summary.getActiveAssistantTotal(), Long.valueOf(0L));
        Assert.assertEquals(summary.getChatActiveUserTotal(), Long.valueOf(0L));
    }

    /**
     * 验证 today 返回 24 个小时点位，且 overview 不再触发用户榜查询。
     *
     * @throws Exception 反射调用失败
     */
    @Test
    @SuppressWarnings("unchecked")
    public void shouldBuildHourlyTrendForToday() throws Exception {
        DashboardServiceImpl service = new DashboardServiceImpl();
        ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
        String currentHourKey = LocalDateTime.of(LocalDate.now(), LocalTime.of(10, 0))
                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00"));
        when(chatDashboardQueryService.available()).thenReturn(true);
        when(chatDashboardQueryService.querySummary(any(), any(), any()))
                .thenReturn(new ChatDashboardSummary(3L, 9L, 1L, 2L));
        when(chatDashboardQueryService.queryHourlyTrends(any(), any(), any()))
                .thenReturn(List.of(new ChatDashboardTrend(currentHourKey, 3L, 9L, 2L)));
        when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), any(Integer.class)))
                .thenReturn(List.of(new ChatAssistantUsageRank(BigInteger.ONE, "", 2L, 3L, 9L)));
        when(chatDashboardQueryService.queryAssistantHourlyTrends(any(), any(), any(), any()))
                .thenReturn(List.of(new ChatAssistantSessionTrend(BigInteger.ONE, "", currentHourKey, 3L)));
        setField(service, "chatDashboardQueryService", chatDashboardQueryService);

        Object context = newContext("today", BigInteger.valueOf(9));
        DashboardSummaryVo summary = new DashboardSummaryVo();
        Object payload = invokeBuildChatPayload(service, context, summary);

        Object chatStatus = readField(payload, "chatStatus");
        List<DashboardTrendItemVo> trends = (List<DashboardTrendItemVo>) readField(payload, "trends");
        List<DashboardAssistantTrendSeriesVo> assistantTrends =
                (List<DashboardAssistantTrendSeriesVo>) readField(payload, "assistantTrends");
        List<DashboardDistributionItemVo> distribution = (List<DashboardDistributionItemVo>) readField(payload, "distribution");

        Assert.assertTrue(Boolean.TRUE.equals(invokeGetter(chatStatus, "getAvailable")));
        Assert.assertEquals(trends.size(), 24);
        Assert.assertEquals(trends.get(0).getLabel(), "00:00");
        Assert.assertEquals(trends.get(10).getKey(), currentHourKey);
        Assert.assertEquals(trends.get(10).getLabel(), "10:00");
        Assert.assertEquals(trends.get(10).getChatMessageTotal(), Long.valueOf(9L));
        Assert.assertEquals(trends.get(10).getChatSessionTotal(), Long.valueOf(3L));
        Assert.assertEquals(trends.get(10).getActiveUserTotal(), Long.valueOf(2L));
        Assert.assertEquals(trends.get(11).getChatMessageTotal(), Long.valueOf(0L));
        Assert.assertEquals(trends.get(11).getChatSessionTotal(), Long.valueOf(0L));
        Assert.assertEquals(summary.getChatMessageTotal(), Long.valueOf(9L));
        Assert.assertEquals(summary.getChatSessionTotal(), Long.valueOf(3L));
        Assert.assertEquals(summary.getActiveAssistantTotal(), Long.valueOf(1L));
        Assert.assertEquals(summary.getChatActiveUserTotal(), Long.valueOf(2L));
        Assert.assertEquals(assistantTrends.size(), 1);
        Assert.assertEquals(assistantTrends.get(0).getLabel(), "智能体-1");
        Assert.assertEquals(assistantTrends.get(0).getTotalSessionCount(), Long.valueOf(3L));
        Assert.assertEquals(assistantTrends.get(0).getPoints().size(), 24);
        Assert.assertEquals(assistantTrends.get(0).getPoints().get(10).getSessionTotal(), Long.valueOf(3L));
        Assert.assertEquals(assistantTrends.get(0).getPoints().get(11).getSessionTotal(), Long.valueOf(0L));
        Assert.assertEquals(distribution.get(0).getLabel(), "智能体-1");
        Assert.assertEquals(distribution.get(0).getAvgMessagePerSession(), Double.valueOf(3D));
        Assert.assertEquals(distribution.get(0).getAvgSessionPerUser(), Double.valueOf(1.5D));
        verify(chatDashboardQueryService, never()).queryActiveUserRanks(any(), any(), any(), any(), any());
    }

    /**
     * 验证当系统账号名称仅回退为纯 ID 时，仍优先继续回退到聊天侧账号。
     */
    @Test
    public void shouldFallbackToUserAccountWhenSystemDisplayNameIsOnlyId() throws Exception {
        DashboardServiceImpl service = new DashboardServiceImpl();
        ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
        when(chatDashboardQueryService.available()).thenReturn(true);
        when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), any(), any()))
                .thenReturn(List.of(new ChatActiveUserRank(BigInteger.valueOf(9), "chat-user", 1L, 1L, 1L)));
        SysAccountService sysAccountService = mock(SysAccountService.class);
        when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(buildSysAccount(9L, "", "仅昵称")));
        setField(service, "chatDashboardQueryService", chatDashboardQueryService);
        setField(service, "sysAccountService", sysAccountService);

        DashboardUserRankQuery query = new DashboardUserRankQuery();
        query.setRange("7d");

        List<DashboardUserRankItemVo> userRanks = service.getUserRanks(new LoginAccount(), query);
        Assert.assertEquals(userRanks.get(0).getLabel(), "chat-user");
        Assert.assertEquals(userRanks.get(0).getLoginName(), "chat-user");
        Assert.assertEquals(userRanks.get(0).getNickname(), "仅昵称");
    }

    /**
     * 验证日趋势会保留 assistantId 为空的排行项，并补齐 7 天点位。
     *
     * @throws Exception 反射调用失败
     */
    @Test
    @SuppressWarnings("unchecked")
    public void shouldBuildDailyAssistantTrendSeriesForRankedAssistants() throws Exception {
        DashboardServiceImpl service = new DashboardServiceImpl();
        ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
        when(chatDashboardQueryService.available()).thenReturn(true);
        when(chatDashboardQueryService.querySummary(any(), any(), any()))
                .thenReturn(new ChatDashboardSummary(4L, 12L, 2L, 3L));
        when(chatDashboardQueryService.queryTrends(any(), any(), any()))
                .thenReturn(List.of());
        when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), any(Integer.class)))
                .thenReturn(List.of(
                        new ChatAssistantUsageRank(BigInteger.ONE, "助手-A", 3L, 4L, 12L),
                        new ChatAssistantUsageRank(null, "未知助手", 1L, 2L, 4L)
                ));
        when(chatDashboardQueryService.queryAssistantTrends(any(), any(), any(), any()))
                .thenReturn(List.of(
                        new ChatAssistantSessionTrend(BigInteger.ONE, "助手-A", LocalDate.now().minusDays(6).toString(), 2L),
                        new ChatAssistantSessionTrend(BigInteger.ONE, "助手-A", LocalDate.now().toString(), 4L),
                        new ChatAssistantSessionTrend(null, "未知助手", LocalDate.now().minusDays(3).toString(), 2L)
                ));
        setField(service, "chatDashboardQueryService", chatDashboardQueryService);

        Object context = newContext("7d", BigInteger.ONE);
        DashboardSummaryVo summary = new DashboardSummaryVo();
        Object payload = invokeBuildChatPayload(service, context, summary);

        List<DashboardAssistantTrendSeriesVo> assistantTrends =
                (List<DashboardAssistantTrendSeriesVo>) readField(payload, "assistantTrends");
        Assert.assertEquals(assistantTrends.size(), 2);
        Assert.assertEquals(assistantTrends.get(0).getLabel(), "助手-A");
        Assert.assertEquals(assistantTrends.get(0).getPoints().size(), 7);
        Assert.assertEquals(assistantTrends.get(0).getPoints().get(0).getSessionTotal(), Long.valueOf(2L));
        Assert.assertEquals(assistantTrends.get(0).getPoints().get(6).getSessionTotal(), Long.valueOf(4L));
        Assert.assertEquals(assistantTrends.get(0).getPoints().get(1).getSessionTotal(), Long.valueOf(0L));
        Assert.assertNull(assistantTrends.get(1).getAssistantId());
        Assert.assertEquals(assistantTrends.get(1).getLabel(), "未知助手");
        Assert.assertEquals(assistantTrends.get(1).getPoints().get(3).getSessionTotal(), Long.valueOf(2L));
    }

    /**
     * 验证自定义单天范围按小时桶构建。
     *
     * @throws Exception 反射调用失败
     */
    @Test
    @SuppressWarnings("unchecked")
    public void shouldBuildHourlyTrendForCustomSingleDayRange() throws Exception {
        DashboardServiceImpl service = new DashboardServiceImpl();
        ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
        LocalDate customDate = LocalDate.now().minusDays(2);
        String currentHourKey = LocalDateTime.of(customDate, LocalTime.of(8, 0))
                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00"));
        when(chatDashboardQueryService.available()).thenReturn(true);
        when(chatDashboardQueryService.querySummary(any(), any(), any()))
                .thenReturn(new ChatDashboardSummary(2L, 6L, 1L, 1L));
        when(chatDashboardQueryService.queryHourlyTrends(any(), any(), any()))
                .thenReturn(List.of(new ChatDashboardTrend(currentHourKey, 2L, 6L, 1L)));
        when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), any(Integer.class)))
                .thenReturn(List.of());
        setField(service, "chatDashboardQueryService", chatDashboardQueryService);

        Object context = newContext(
                "custom",
                BigInteger.ONE,
                LocalDateTime.of(customDate, LocalTime.MIN),
                LocalDateTime.of(customDate.plusDays(1), LocalTime.MIN)
        );
        DashboardSummaryVo summary = new DashboardSummaryVo();
        Object payload = invokeBuildChatPayload(service, context, summary);

        List<DashboardTrendItemVo> trends = (List<DashboardTrendItemVo>) readField(payload, "trends");
        Assert.assertEquals(trends.size(), 24);
        Assert.assertEquals(trends.get(8).getKey(), currentHourKey);
        Assert.assertEquals(trends.get(8).getActiveUserTotal(), Long.valueOf(1L));
        Assert.assertEquals(trends.get(9).getChatSessionTotal(), Long.valueOf(0L));
    }

    /**
     * 验证自定义多天范围按天桶构建，并保留查询日期。
     *
     * @throws Exception 反射调用失败
     */
    @Test
    public void shouldBuildDailyBucketsForCustomMultiDayRangeContext() throws Exception {
        DashboardServiceImpl service = new DashboardServiceImpl();
        DashboardOverviewQuery query = new DashboardOverviewQuery();
        query.setRange("custom");
        query.setStartDate("2026-05-01");
        query.setEndDate("2026-05-03");

        Object context = invokeBuildContext(service, query);
        Assert.assertEquals(readField(context, "range"), "custom");
        Assert.assertEquals(
                readField(context, "startTime"),
                LocalDateTime.of(LocalDate.of(2026, 5, 1), LocalTime.MIN)
        );
        Assert.assertEquals(
                readField(context, "endTime"),
                LocalDateTime.of(LocalDate.of(2026, 5, 4), LocalTime.MIN)
        );
        Assert.assertEquals(readField(context, "queryStartDate"), "2026-05-01");
        Assert.assertEquals(readField(context, "queryEndDate"), "2026-05-03");
    }

    /**
     * 验证近 30 天趋势补齐完整 30 个桶，并按 Top 8 请求智能体活跃排行。
     *
     * @throws Exception 反射调用失败
     */
    @Test
    @SuppressWarnings("unchecked")
    public void shouldBuildThirtyDayBucketsAndRequestTopEightAssistantRanks() throws Exception {
        DashboardServiceImpl service = new DashboardServiceImpl();
        ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
        LocalDate startDate = LocalDate.now().minusDays(29);
        LocalDate endDate = LocalDate.now();
        when(chatDashboardQueryService.available()).thenReturn(true);
        when(chatDashboardQueryService.querySummary(any(), any(), any()))
                .thenReturn(new ChatDashboardSummary(10L, 20L, 8L, 4L));
        when(chatDashboardQueryService.queryTrends(any(), any(), any()))
                .thenReturn(List.of(
                        new ChatDashboardTrend(startDate.toString(), 3L, 6L, 2L),
                        new ChatDashboardTrend(endDate.toString(), 7L, 14L, 4L)
                ));
        when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), eq(8)))
                .thenReturn(IntStream.rangeClosed(1, 8)
                        .mapToObj(index -> new ChatAssistantUsageRank(
                                BigInteger.valueOf(index),
                                "助手-" + index,
                                index,
                                index * 2L,
                                index * 4L
                        ))
                        .toList());
        when(chatDashboardQueryService.queryAssistantTrends(any(), any(), any(), any()))
                .thenReturn(List.of(
                        new ChatAssistantSessionTrend(BigInteger.ONE, "助手-1", startDate.toString(), 2L),
                        new ChatAssistantSessionTrend(BigInteger.ONE, "助手-1", endDate.toString(), 4L)
                ));
        setField(service, "chatDashboardQueryService", chatDashboardQueryService);

        Object context = newContext("30d", BigInteger.ONE);
        DashboardSummaryVo summary = new DashboardSummaryVo();
        Object payload = invokeBuildChatPayload(service, context, summary);

        List<DashboardTrendItemVo> trends = (List<DashboardTrendItemVo>) readField(payload, "trends");
        List<DashboardAssistantTrendSeriesVo> assistantTrends =
                (List<DashboardAssistantTrendSeriesVo>) readField(payload, "assistantTrends");
        Assert.assertEquals(trends.size(), 30);
        Assert.assertEquals(trends.get(0).getKey(), startDate.toString());
        Assert.assertEquals(trends.get(0).getChatSessionTotal(), Long.valueOf(3L));
        Assert.assertEquals(trends.get(29).getKey(), endDate.toString());
        Assert.assertEquals(trends.get(29).getChatMessageTotal(), Long.valueOf(14L));
        Assert.assertEquals(trends.get(1).getChatSessionTotal(), Long.valueOf(0L));
        Assert.assertEquals(assistantTrends.size(), 8);
        Assert.assertEquals(assistantTrends.get(0).getPoints().size(), 30);
        verify(chatDashboardQueryService).queryAssistantUsageRanks(any(), any(), any(), eq(8));
    }

    /**
     * 验证用户榜筛选会透传 assistantId。
     */
    @Test
    public void shouldQueryUserRanksWithAssistantFilter() throws Exception {
        DashboardServiceImpl service = new DashboardServiceImpl();
        ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
        BotService botService = mock(BotService.class);
        CategoryPermissionService categoryPermissionService = mock(CategoryPermissionService.class);
        SysAccountService sysAccountService = mock(SysAccountService.class);
        SysAccountRoleService sysAccountRoleService = mock(SysAccountRoleService.class);
        SysRoleService sysRoleService = mock(SysRoleService.class);

        Bot bot = new Bot();
        bot.setId(BigInteger.TEN);
        bot.setStatus(1);
        bot.setCreatedBy(BigInteger.ONE);
        bot.setCategoryId(BigInteger.valueOf(8));

        when(chatDashboardQueryService.available()).thenReturn(true);
        when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), eq(BigInteger.TEN), eq(5)))
                .thenReturn(List.of(new ChatActiveUserRank(BigInteger.valueOf(2), "demo-user", 2L, 4L, 1L)));
        when(botService.getById(BigInteger.TEN)).thenReturn(bot);
        when(categoryPermissionService.canAccessCategory(any(LoginAccount.class), eq("BOT"), eq(BigInteger.ONE), eq(BigInteger.valueOf(8))))
                .thenReturn(true);
        when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(buildSysAccount(2L, "demo-user", "演示用户")));
        when(sysAccountRoleService.list(any(QueryWrapper.class))).thenReturn(Collections.emptyList());

        setField(service, "chatDashboardQueryService", chatDashboardQueryService);
        setField(service, "botService", botService);
        setField(service, "categoryPermissionService", categoryPermissionService);
        setField(service, "sysAccountService", sysAccountService);
        setField(service, "sysAccountRoleService", sysAccountRoleService);
        setField(service, "sysRoleService", sysRoleService);

        DashboardUserRankQuery query = new DashboardUserRankQuery();
        query.setRange("7d");
        query.setAssistantId(BigInteger.TEN);

        LoginAccount loginAccount = new LoginAccount();
        loginAccount.setId(BigInteger.valueOf(12));
        loginAccount.setTenantId(BigInteger.valueOf(33));

        List<DashboardUserRankItemVo> userRanks = service.getUserRanks(loginAccount, query);
        Assert.assertEquals(userRanks.size(), 1);
        Assert.assertEquals(userRanks.get(0).getLabel(), "demo-user（演示用户）");
        verify(chatDashboardQueryService).queryActiveUserRanks(any(), any(), any(), eq(BigInteger.TEN), eq(5));
    }

    /**
     * 验证未启用智能体会被拒绝。
     */
    @Test(expectedExceptions = BusinessException.class, expectedExceptionsMessageRegExp = "聊天助手不存在或未启用")
    public void shouldRejectDisabledAssistantFilter() {
        DashboardServiceImpl service = new DashboardServiceImpl();
        BotService botService = mock(BotService.class);
        ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);

        Bot bot = new Bot();
        bot.setId(BigInteger.TEN);
        bot.setStatus(0);

        when(botService.getById(BigInteger.TEN)).thenReturn(bot);
        when(chatDashboardQueryService.available()).thenReturn(true);

        setFieldSilently(service, "botService", botService);
        setFieldSilently(service, "chatDashboardQueryService", chatDashboardQueryService);
        setFieldSilently(service, "categoryPermissionService", mock(CategoryPermissionService.class));
        setFieldSilently(service, "sysAccountService", mock(SysAccountService.class));

        DashboardUserRankQuery query = new DashboardUserRankQuery();
        query.setRange("7d");
        query.setAssistantId(BigInteger.TEN);
        service.getUserRanks(new LoginAccount(), query);
    }

    /**
     * 验证当前作用域不可见的智能体会被拒绝。
     */
    @Test(expectedExceptions = BusinessException.class, expectedExceptionsMessageRegExp = "聊天助手不存在或未启用")
    public void shouldRejectInvisibleAssistantFilter() {
        DashboardServiceImpl service = new DashboardServiceImpl();
        BotService botService = mock(BotService.class);
        ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
        CategoryPermissionService categoryPermissionService = mock(CategoryPermissionService.class);

        Bot bot = new Bot();
        bot.setId(BigInteger.TEN);
        bot.setStatus(1);
        bot.setCreatedBy(BigInteger.ONE);
        bot.setCategoryId(BigInteger.valueOf(8));

        when(botService.getById(BigInteger.TEN)).thenReturn(bot);
        when(chatDashboardQueryService.available()).thenReturn(true);
        when(categoryPermissionService.canAccessCategory(any(LoginAccount.class), eq("BOT"), eq(BigInteger.ONE), eq(BigInteger.valueOf(8))))
                .thenReturn(false);

        setFieldSilently(service, "botService", botService);
        setFieldSilently(service, "chatDashboardQueryService", chatDashboardQueryService);
        setFieldSilently(service, "categoryPermissionService", categoryPermissionService);
        setFieldSilently(service, "sysAccountService", mock(SysAccountService.class));

        DashboardUserRankQuery query = new DashboardUserRankQuery();
        query.setRange("7d");
        query.setAssistantId(BigInteger.TEN);
        service.getUserRanks(new LoginAccount(), query);
    }

    /**
     * 验证导出结果包含表头与账号昵称列。
     */
    @Test
    public void shouldExportDashboardUserRanksWithSeparatedIdentityColumns() throws Exception {
        DashboardServiceImpl service = new DashboardServiceImpl();
        ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
        SysAccountService sysAccountService = mock(SysAccountService.class);

        when(chatDashboardQueryService.available()).thenReturn(true);
        when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), isNull(), isNull()))
                .thenReturn(List.of(new ChatActiveUserRank(BigInteger.valueOf(7), "export-user", 3L, 6L, 1L)));
        when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(buildSysAccount(7L, "export-user", "导出演示")));

        setField(service, "chatDashboardQueryService", chatDashboardQueryService);
        setField(service, "sysAccountService", sysAccountService);

        DashboardUserRankQuery query = new DashboardUserRankQuery();
        query.setRange("7d");
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        service.exportUserRanks(new LoginAccount(), query, outputStream);

        try (org.apache.poi.ss.usermodel.Workbook workbook =
                     WorkbookFactory.create(new ByteArrayInputStream(outputStream.toByteArray()))) {
            org.apache.poi.ss.usermodel.Sheet sheet = workbook.getSheetAt(0);
            Assert.assertEquals(sheet.getRow(0).getCell(0).getStringCellValue(), "登录账号");
            Assert.assertEquals(sheet.getRow(0).getCell(1).getStringCellValue(), "昵称");
            Assert.assertEquals(sheet.getRow(0).getCell(2).getStringCellValue(), "会话数");
            Assert.assertEquals(sheet.getRow(0).getCell(3).getStringCellValue(), "消息数");
            Assert.assertEquals(sheet.getRow(1).getCell(0).getStringCellValue(), "export-user");
            Assert.assertEquals(sheet.getRow(1).getCell(1).getStringCellValue(), "导出演示");
            Assert.assertEquals(sheet.getRow(1).getCell(2).getStringCellValue(), "3");
            Assert.assertEquals(sheet.getRow(1).getCell(3).getStringCellValue(), "6");
        }
    }

    /**
     * 构造查询上下文。
     *
     * @param range 时间范围
     * @param tenantId 租户 ID
     * @return 查询上下文实例
     * @throws Exception 反射失败
     */
    private Object newContext(String range, BigInteger tenantId) throws Exception {
        return newContext(
                range,
                tenantId,
                LocalDateTime.of(LocalDate.now(), java.time.LocalTime.MIN),
                LocalDateTime.of(LocalDate.now().plusDays(1), java.time.LocalTime.MIN)
        );
    }

    /**
     * 构造查询上下文。
     *
     * @param range 时间范围
     * @param tenantId 租户 ID
     * @param startTime 开始时间
     * @param endTime 结束时间
     * @return 查询上下文实例
     * @throws Exception 反射失败
     */
    private Object newContext(String range,
                              BigInteger tenantId,
                              LocalDateTime startTime,
                              LocalDateTime endTime) throws Exception {
        Class<?> contextClass = Class.forName(
                "tech.easyflow.admin.service.dashboard.impl.DashboardServiceImpl$DashboardQueryContext"
        );
        Constructor<?> constructor = contextClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        Object context = constructor.newInstance();
        setField(context, "range", range);
        setField(context, "tenantFilterId", tenantId);
        setField(context, "startTime", startTime);
        setField(context, "endTime", endTime);
        return context;
    }

    /**
     * 调用私有上下文构建方法。
     *
     * @param service service
     * @param query 查询参数
     * @return 上下文
     * @throws Exception 反射失败
     */
    private Object invokeBuildContext(DashboardServiceImpl service, DashboardOverviewQuery query) throws Exception {
        Method method = DashboardServiceImpl.class.getDeclaredMethod(
                "buildContext",
                LoginAccount.class,
                DashboardOverviewQuery.class
        );
        method.setAccessible(true);
        return method.invoke(service, null, query);
    }

    /**
     * 调用私有聊天载荷组装方法。
     *
     * @param service service
     * @param context 上下文
     * @param summary 汇总对象
     * @return 载荷
     * @throws Exception 反射失败
     */
    private Object invokeBuildChatPayload(DashboardServiceImpl service, Object context, DashboardSummaryVo summary)
            throws Exception {
        Method method = DashboardServiceImpl.class.getDeclaredMethod(
                "buildChatPayload",
                context.getClass(),
                DashboardSummaryVo.class
        );
        method.setAccessible(true);
        return method.invoke(service, context, summary);
    }

    /**
     * 读取对象字段。
     *
     * @param target 目标对象
     * @param fieldName 字段名
     * @return 字段值
     * @throws Exception 反射失败
     */
    private Object readField(Object target, String fieldName) throws Exception {
        Field field = target.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(target);
    }

    /**
     * 调用 getter。
     *
     * @param target 目标对象
     * @param methodName 方法名
     * @return 返回值
     * @throws Exception 反射失败
     */
    private Object invokeGetter(Object target, String methodName) throws Exception {
        Method method = target.getClass().getMethod(methodName);
        return method.invoke(target);
    }

    /**
     * 通过反射设置字段值。
     *
     * @param target 目标对象
     * @param fieldName 字段名
     * @param value 字段值
     * @throws Exception 反射失败
     */
    private void setField(Object target, String fieldName, Object value) throws Exception {
        Class<?> current = target.getClass();
        while (current != null) {
            try {
                Field field = current.getDeclaredField(fieldName);
                field.setAccessible(true);
                field.set(target, value);
                return;
            } catch (NoSuchFieldException ignored) {
                current = current.getSuperclass();
            }
        }
        throw new IllegalArgumentException("未找到字段: " + fieldName);
    }

    private void setFieldSilently(Object target, String fieldName, Object value) {
        try {
            setField(target, fieldName, value);
        } catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }

    private SysAccount buildSysAccount(long id, String loginName, String nickname) {
        SysAccount account = new SysAccount();
        account.setId(BigInteger.valueOf(id));
        account.setLoginName(loginName);
        account.setNickname(nickname);
        return account;
    }
}
