package com.starry.admin.modules.statistics.service;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.starry.admin.common.domain.LoginUser;
import com.starry.admin.common.exception.ServiceException;
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
import com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity;
import com.starry.admin.modules.clerk.service.IPlayClerkLevelInfoService;
import com.starry.admin.modules.clerk.service.IPlayClerkUserInfoService;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderInfoService;
import com.starry.admin.modules.personnel.module.entity.PlayPersonnelGroupInfoEntity;
import com.starry.admin.modules.personnel.service.IPlayPersonnelGroupInfoService;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailQueryVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceDetailResponseVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewQueryVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewResponseVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceOverviewSummaryVo;
import com.starry.admin.modules.statistics.module.vo.ClerkPerformanceSnapshotVo;
import com.starry.admin.modules.statistics.module.vo.PlayClerkPerformanceInfoReturnVo;
import com.starry.admin.modules.statistics.service.impl.PlayClerkPerformanceServiceImpl;
import com.starry.admin.modules.withdraw.mapper.EarningsLineMapper;
import com.starry.admin.utils.DateRangeUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.time.Month;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;

/**
 * Unit tests for {@link PlayClerkPerformanceServiceImpl} covering aggregation, ranking and detail logic.
 */
@ExtendWith(MockitoExtension.class)
class PlayClerkPerformanceServiceImplTest {

    @Mock
    private IPlayClerkUserInfoService clerkUserInfoService;

    @Mock
    private IPlayOrderInfoService playOrderInfoService;

    @Mock
    private IPlayClerkLevelInfoService playClerkLevelInfoService;

    @Mock
    private IPlayPersonnelGroupInfoService playPersonnelGroupInfoService;

    @Mock
    private EarningsLineMapper earningsLineMapper;

    @InjectMocks
    private PlayClerkPerformanceServiceImpl service;

    @Test
    @DisplayName("queryOverview should aggregate metrics and sort clerks by GMV")
    void queryOverviewAggregatesAndSorts() {
        ClerkPerformanceOverviewQueryVo vo = new ClerkPerformanceOverviewQueryVo();
        vo.setEndOrderTime(Arrays.asList("2024-08-01 00:00:00", "2024-08-07 23:59:59"));
        vo.setIncludeSummary(true);
        vo.setIncludeRankings(true);

        PlayClerkUserInfoEntity clerkA = buildClerk("c1", "Alice", "g1", "l1");
        PlayClerkUserInfoEntity clerkB = buildClerk("c2", "Bob", "g2", "l2");

        when(playPersonnelGroupInfoService.getValidClerkIdList(any(), any())).thenReturn(Arrays.asList("c1", "c2"));
        when(clerkUserInfoService.list((Wrapper<PlayClerkUserInfoEntity>) any())).thenReturn(Arrays.asList(clerkA, clerkB));
        when(playClerkLevelInfoService.selectAll()).thenReturn(Arrays.asList(level("l1", "白银"), level("l2", "黄金")));
        when(playPersonnelGroupInfoService.selectAll())
                .thenReturn(Arrays.asList(group("g1", "一组"), group("g2", "二组")));

        List<PlayOrderInfoEntity> ordersA = Arrays.asList(
                order("c1", "userA", "1", "0", "0", new BigDecimal("100.00"), new BigDecimal("60.00"),
                        LocalDateTime.of(2024, Month.AUGUST, 1, 10, 0)),
                order("c1", "userA", "0", "2", "0", new BigDecimal("150.00"), new BigDecimal("90.00"),
                        LocalDateTime.of(2024, Month.AUGUST, 2, 14, 0)),
                withRefund(order("c1", "userB", "0", "0", "1", new BigDecimal("50.00"), new BigDecimal("30.00"),
                        LocalDateTime.of(2024, Month.AUGUST, 3, 9, 0)), new BigDecimal("30.00")));

        List<PlayOrderInfoEntity> ordersB = Collections.singletonList(
                order("c2", "userC", "1", "0", "0", new BigDecimal("80.00"), new BigDecimal("50.00"),
                        LocalDateTime.of(2024, Month.AUGUST, 1, 12, 0)));

        when(playOrderInfoService.clerkSelectOrderInfoList(eq("c1"), anyString(), anyString())).thenReturn(ordersA);
        when(playOrderInfoService.clerkSelectOrderInfoList(eq("c2"), anyString(), anyString())).thenReturn(ordersB);
        when(earningsLineMapper.sumAmountByClerkAndOrderIds(eq("c1"), anyCollection(), any(), any()))
                .thenReturn(new BigDecimal("170.00"));
        when(earningsLineMapper.sumAmountByClerkAndOrderIds(eq("c2"), anyCollection(), any(), any()))
                .thenReturn(new BigDecimal("55.00"));

        setAuthentication();
        try {
            ClerkPerformanceOverviewResponseVo response = service.queryOverview(vo);

            assertNotNull(response.getSummary(), "summary should be present when includeSummary=true");
            assertEquals(2, response.getTotal());
            assertEquals(2, response.getRankings().size());

            ClerkPerformanceSnapshotVo top = response.getRankings().get(0);
            assertEquals("c1", top.getClerkId(), "Highest GMV clerk should rank first");
            assertEquals(new BigDecimal("300.00"), top.getGmv());
            assertEquals(new BigDecimal("100.00"), top.getFirstOrderAmount());
            assertEquals(new BigDecimal("200.00"), top.getContinuedOrderAmount());
            assertEquals(new BigDecimal("150.00"), top.getRewardAmount());
            assertEquals(new BigDecimal("30.00"), top.getRefundAmount());
            assertEquals(3, top.getOrderCount());
            assertEquals(2, top.getContinuedOrderCount());
            assertEquals(new BigDecimal("66.67"), top.getContinuedRate());
            assertEquals(new BigDecimal("100.00"), top.getAverageTicketPrice());
            assertEquals(new BigDecimal("170.00"), top.getEstimatedRevenue());
            assertEquals(new BigDecimal("55.00"), response.getRankings().get(1).getEstimatedRevenue());

            ClerkPerformanceOverviewSummaryVo summary = response.getSummary();
            assertEquals(new BigDecimal("380.00"), summary.getTotalGmv());
            assertEquals(4, summary.getTotalOrderCount());
            assertEquals(2, summary.getTotalFirstOrderCount());
            assertEquals(2, summary.getTotalContinuedOrderCount());
            assertEquals(new BigDecimal("50.00"), summary.getContinuedRate());
            assertEquals(new BigDecimal("95.00"), summary.getAverageTicketPrice());
            assertEquals(new BigDecimal("225.00"), summary.getTotalEstimatedRevenue());
        } finally {
            clearAuthentication();
        }
    }

    @Test
    @DisplayName("queryDetail should build snapshot, composition and trend for accessible clerk")
    void queryDetailBuildsSnapshotCompositionAndTrend() {
        ClerkPerformanceDetailQueryVo vo = new ClerkPerformanceDetailQueryVo();
        vo.setClerkId("c1");
        vo.setEndOrderTime(Arrays.asList("2024-08-01 00:00:00", "2024-08-03 23:59:59"));
        vo.setIncludeTrend(true);
        vo.setTrendDays(3);

        PlayClerkUserInfoEntity clerk = buildClerk("c1", "Alice", "g1", "l1");

        when(playPersonnelGroupInfoService.getValidClerkIdList(any(), any())).thenReturn(Collections.singletonList("c1"));
        when(clerkUserInfoService.getById("c1")).thenReturn(clerk);
        when(playClerkLevelInfoService.selectAll()).thenReturn(Collections.singletonList(level("l1", "白银")));
        when(playPersonnelGroupInfoService.selectAll()).thenReturn(Collections.singletonList(group("g1", "一组")));

        List<PlayOrderInfoEntity> orders = Arrays.asList(
                order("c1", "userA", "1", "0", "0", new BigDecimal("120.00"), new BigDecimal("70.00"),
                        LocalDateTime.of(2024, Month.AUGUST, 1, 9, 0)),
                order("c1", "userA", "0", "0", "0", new BigDecimal("80.00"), new BigDecimal("40.00"),
                        LocalDateTime.of(2024, Month.AUGUST, 2, 10, 0)),
                withRefund(order("c1", "userB", "0", "2", "1", new BigDecimal("60.00"), new BigDecimal("30.00"),
                        LocalDateTime.of(2024, Month.AUGUST, 2, 18, 0)), new BigDecimal("20.00")));
        when(playOrderInfoService.clerkSelectOrderInfoList(eq("c1"), anyString(), anyString())).thenReturn(orders);
        when(earningsLineMapper.sumAmountByClerkAndOrderIds(eq("c1"), anyCollection(), any(), any()))
                .thenReturn(new BigDecimal("110.00"));

        setAuthentication();
        try {
            ClerkPerformanceDetailResponseVo response = service.queryDetail(vo);
            assertNotNull(response.getProfile());
            assertEquals("Alice", response.getProfile().getNickname());
            assertEquals("一组", response.getProfile().getGroupName());
            assertNotNull(response.getSnapshot());
            assertEquals(new BigDecimal("260.00"), response.getSnapshot().getGmv());
            assertEquals(3, response.getSnapshot().getOrderCount());
            assertEquals(new BigDecimal("66.67"), response.getSnapshot().getContinuedRate());
            assertEquals(new BigDecimal("86.67"), response.getSnapshot().getAverageTicketPrice());
            assertEquals(new BigDecimal("110.00"), response.getSnapshot().getEstimatedRevenue());

            assertNotNull(response.getComposition());
            assertEquals(4, response.getComposition().getOrderComposition().size());
            assertEquals(new BigDecimal("33.33"), response.getComposition().getOrderComposition().get(0).getCountRatio());

            assertEquals(3, response.getTrend().size());
            assertEquals("DAY", response.getTrendDimension());
            assertEquals(new BigDecimal("120.00"), response.getTrend().get(0).getGmv());
            assertEquals(new BigDecimal("140.00"), response.getTrend().get(1).getGmv());
        } finally {
            clearAuthentication();
        }
    }

    @Test
    @DisplayName("queryDetail should reject access when clerk not under current user")
    void queryDetailShouldEnforcePermissions() {
        ClerkPerformanceDetailQueryVo vo = new ClerkPerformanceDetailQueryVo();
        vo.setClerkId("c99");
        vo.setEndOrderTime(Arrays.asList("2024-08-01 00:00:00", "2024-08-02 23:59:59"));

        when(playPersonnelGroupInfoService.getValidClerkIdList(any(), any())).thenReturn(Collections.singletonList("c1"));
        when(clerkUserInfoService.getById("c99")).thenReturn(buildClerk("c99", "Ghost", "g1", "l1"));

        setAuthentication();
        try {
            ServiceException ex = assertThrows(ServiceException.class, () -> service.queryDetail(vo));
            assertTrue(ex.getMessage().contains("无权"));
        } finally {
            clearAuthentication();
        }
    }

    @Test
    @DisplayName("getClerkPerformanceInfo aligns earnings lookup with purchaser time")
    void getClerkPerformanceInfoAlignsEarningsLookupWithPurchaserTime() {
        PlayClerkUserInfoEntity clerk = buildClerk("c3", "Carol", "g3", "l3");
        List<PlayOrderInfoEntity> orders = Collections.singletonList(
                order("c3", "userX", "1", "0", "0", new BigDecimal("99.00"), new BigDecimal("50.00"),
                        LocalDateTime.of(2024, Month.AUGUST, 1, 15, 30)));
        when(earningsLineMapper.sumAmountByClerkAndOrderIds(eq("c3"), anyCollection(), any(), any()))
                .thenReturn(new BigDecimal("80.00"));

        service.getClerkPerformanceInfo(clerk, orders, Collections.singletonList(level("l3", "钻石")),
                Collections.singletonList(group("g3", "三组")), "2024-08-01", "2024-08-02");

        ArgumentCaptor<Collection<String>> orderIdsCaptor = ArgumentCaptor.forClass(Collection.class);
        ArgumentCaptor<String> startCaptor = ArgumentCaptor.forClass(String.class);
        ArgumentCaptor<String> endCaptor = ArgumentCaptor.forClass(String.class);
        verify(earningsLineMapper).sumAmountByClerkAndOrderIds(eq("c3"), orderIdsCaptor.capture(),
                startCaptor.capture(), endCaptor.capture());

        assertTrue(orderIdsCaptor.getValue().contains(orders.get(0).getId()));
        assertEquals("2024-08-01 00:00:00", startCaptor.getValue());
        assertEquals("2024-08-02 23:59:59", endCaptor.getValue());
    }

    @Test
    @DisplayName("queryOverview ignores non-completed orders when computing summary/GMV")
    void queryOverviewSkipsNonCompletedOrders() {
        ClerkPerformanceOverviewQueryVo vo = new ClerkPerformanceOverviewQueryVo();
        vo.setIncludeSummary(true);
        vo.setIncludeRankings(true);
        vo.setEndOrderTime(Arrays.asList("2024-08-01 00:00:00", "2024-08-10 23:59:59"));

        PlayClerkUserInfoEntity clerk = buildClerk("c10", "Neo", "g10", "l10");
        when(playPersonnelGroupInfoService.getValidClerkIdList(any(), any()))
                .thenReturn(Collections.singletonList("c10"));
        when(clerkUserInfoService.list((Wrapper<PlayClerkUserInfoEntity>) any()))
                .thenReturn(Collections.singletonList(clerk));
        when(playClerkLevelInfoService.selectAll()).thenReturn(Collections.singletonList(level("l10", "至尊")));
        when(playPersonnelGroupInfoService.selectAll()).thenReturn(Collections.singletonList(group("g10", "十组")));

        PlayOrderInfoEntity o1 = order("c10", "user1", "1", "0", "0", new BigDecimal("100.00"),
                new BigDecimal("60.00"), LocalDateTime.of(2024, Month.AUGUST, 1, 10, 0));
        PlayOrderInfoEntity o2 = order("c10", "user2", "0", "0", "0", new BigDecimal("80.00"),
                new BigDecimal("40.00"), LocalDateTime.of(2024, Month.AUGUST, 2, 11, 0));
        PlayOrderInfoEntity o3 = withRefund(
                order("c10", "user3", "0", "0", "1", new BigDecimal("60.00"), new BigDecimal("35.00"),
                        LocalDateTime.of(2024, Month.AUGUST, 3, 9, 30)),
                new BigDecimal("15.00"));
        PlayOrderInfoEntity o4 = order("c10", "user4", "0", "2", "0", new BigDecimal("40.00"),
                new BigDecimal("20.00"), LocalDateTime.of(2024, Month.AUGUST, 4, 16, 45));
        PlayOrderInfoEntity o5 = order("c10", "user5", "1", "0", "0", new BigDecimal("20.00"),
                new BigDecimal("10.00"), LocalDateTime.of(2024, Month.AUGUST, 5, 13, 15));
        PlayOrderInfoEntity o6 = withStatus(
                order("c10", "user6", "0", "0", "0", new BigDecimal("70.00"), new BigDecimal("30.00"),
                        LocalDateTime.of(2024, Month.AUGUST, 6, 14, 0)),
                OrderConstant.OrderStatus.CANCELLED.getCode());
        PlayOrderInfoEntity o7 = withStatus(
                order("c10", "user7", "0", "0", "0", new BigDecimal("50.00"), new BigDecimal("25.00"),
                        LocalDateTime.of(2024, Month.AUGUST, 6, 18, 20)),
                OrderConstant.OrderStatus.IN_PROGRESS.getCode());
        PlayOrderInfoEntity o8 = withStatus(
                order("c10", "user8", "0", "0", "0", new BigDecimal("45.00"), new BigDecimal("22.00"),
                        LocalDateTime.of(2024, Month.AUGUST, 7, 15, 5)),
                OrderConstant.OrderStatus.ACCEPTED.getCode());
        PlayOrderInfoEntity o9 = withStatus(
                order("c10", "user9", "0", "0", "0", new BigDecimal("30.00"), new BigDecimal("15.00"),
                        LocalDateTime.of(2024, Month.AUGUST, 8, 17, 40)),
                OrderConstant.OrderStatus.PENDING.getCode());
        PlayOrderInfoEntity o10 = withStatus(
                order("c10", "user10", "1", "0", "0", new BigDecimal("25.00"), new BigDecimal("12.00"),
                        LocalDateTime.of(2024, Month.AUGUST, 9, 19, 10)),
                OrderConstant.OrderStatus.CANCELLED.getCode());

        List<PlayOrderInfoEntity> mixedOrders =
                Arrays.asList(o1, o2, o3, o4, o5, o6, o7, o8, o9, o10);
        when(playOrderInfoService.clerkSelectOrderInfoList(eq("c10"), anyString(), anyString()))
                .thenReturn(mixedOrders);
        when(earningsLineMapper.sumAmountByClerkAndOrderIds(eq("c10"), anyCollection(), anyString(), anyString()))
                .thenReturn(new BigDecimal("210.00"));

        setAuthentication();
        ClerkPerformanceOverviewResponseVo response;
        try {
            response = service.queryOverview(vo);
        } finally {
            clearAuthentication();
        }

        assertEquals(1, response.getRankings().size());
        ClerkPerformanceSnapshotVo snapshot = response.getRankings().get(0);
        assertEquals(5, snapshot.getOrderCount());
        assertEquals(new BigDecimal("300.00"), snapshot.getGmv());
        assertEquals(new BigDecimal("40.00"), snapshot.getRewardAmount());
        assertEquals(new BigDecimal("15.00"), snapshot.getRefundAmount());
        assertEquals(new BigDecimal("210.00"), snapshot.getEstimatedRevenue());

        ClerkPerformanceOverviewSummaryVo summary = response.getSummary();
        assertEquals(5, summary.getTotalOrderCount());
        assertEquals(new BigDecimal("300.00"), summary.getTotalGmv());
        assertEquals(1, summary.getTotalRefundOrderCount());

        ArgumentCaptor<Collection<String>> idCaptor = ArgumentCaptor.forClass(Collection.class);
        verify(earningsLineMapper).sumAmountByClerkAndOrderIds(eq("c10"), idCaptor.capture(),
                anyString(), anyString());
        assertEquals(5, idCaptor.getValue().size());
        assertTrue(idCaptor.getValue().containsAll(
                Arrays.asList(o1.getId(), o2.getId(), o3.getId(), o4.getId(), o5.getId())));
    }
    @Test
    @DisplayName("queryOverview filters completed orders according to complex date/time ranges")
    void queryOverviewHonorsDateRangesForMultipleClerks() {
        PlayClerkUserInfoEntity alpha = buildClerk("c-alpha", "Alpha", "g-alpha", "l-alpha");
        PlayClerkUserInfoEntity beta = buildClerk("c-beta", "Beta", "g-beta", "l-beta");
        PlayClerkUserInfoEntity gamma = buildClerk("c-gamma", "Gamma", "g-gamma", "l-gamma");
        List<PlayClerkUserInfoEntity> clerks = Arrays.asList(alpha, beta, gamma);

        when(playPersonnelGroupInfoService.getValidClerkIdList(any(), any()))
                .thenReturn(Arrays.asList(alpha.getId(), beta.getId(), gamma.getId()));
        when(clerkUserInfoService.list((Wrapper<PlayClerkUserInfoEntity>) any())).thenReturn(clerks);
        when(playClerkLevelInfoService.selectAll()).thenReturn(Arrays.asList(
                level(alpha.getLevelId(), "铂金"),
                level(beta.getLevelId(), "黄金"),
                level(gamma.getLevelId(), "白银")));
        when(playPersonnelGroupInfoService.selectAll()).thenReturn(Arrays.asList(
                group(alpha.getGroupId(), "一组"),
                group(beta.getGroupId(), "二组"),
                group(gamma.getGroupId(), "三组")));

        Map<String, List<PlayOrderInfoEntity>> ordersByClerk = new HashMap<>();
        ordersByClerk.put(alpha.getId(), Arrays.asList(
                order(alpha.getId(), "userA1", "1", "0", "0", new BigDecimal("100.00"), new BigDecimal("70.00"),
                        LocalDateTime.of(2024, Month.JULY, 1, 10, 0)),
                order(alpha.getId(), "userA2", "0", "0", "0", new BigDecimal("210.00"), new BigDecimal("120.00"),
                        LocalDateTime.of(2024, Month.JULY, 3, 15, 30)),
                order(alpha.getId(), "userA3", "0", "0", "0", new BigDecimal("150.00"), new BigDecimal("80.00"),
                        LocalDateTime.of(2024, Month.JULY, 7, 9, 15)),
                order(alpha.getId(), "userA4", "0", "0", "0", new BigDecimal("130.00"), new BigDecimal("75.00"),
                        LocalDateTime.of(2024, Month.JULY, 10, 18, 45))));
        ordersByClerk.put(beta.getId(), Arrays.asList(
                order(beta.getId(), "userB1", "1", "0", "0", new BigDecimal("95.00"), new BigDecimal("50.00"),
                        LocalDateTime.of(2024, Month.JULY, 2, 11, 30)),
                order(beta.getId(), "userB2", "0", "0", "0", new BigDecimal("120.00"), new BigDecimal("65.00"),
                        LocalDateTime.of(2024, Month.JULY, 4, 13, 0)),
                order(beta.getId(), "userB3", "0", "0", "0", new BigDecimal("85.00"), new BigDecimal("40.00"),
                        LocalDateTime.of(2024, Month.JULY, 8, 16, 0)),
                order(beta.getId(), "userB4", "0", "0", "0", new BigDecimal("150.00"), new BigDecimal("85.00"),
                        LocalDateTime.of(2024, Month.JULY, 10, 19, 0))));
        ordersByClerk.put(gamma.getId(), Arrays.asList(
                order(gamma.getId(), "userC1", "1", "0", "0", new BigDecimal("70.00"), new BigDecimal("35.00"),
                        LocalDateTime.of(2024, Month.JULY, 1, 8, 0)),
                order(gamma.getId(), "userC2", "0", "0", "0", new BigDecimal("135.00"), new BigDecimal("70.00"),
                        LocalDateTime.of(2024, Month.JULY, 5, 17, 20)),
                order(gamma.getId(), "userC3", "0", "0", "0", new BigDecimal("75.00"), new BigDecimal("45.00"),
                        LocalDateTime.of(2024, Month.JULY, 6, 14, 50)),
                order(gamma.getId(), "userC4", "0", "0", "0", new BigDecimal("160.00"), new BigDecimal("90.00"),
                        LocalDateTime.of(2024, Month.JULY, 9, 21, 10))));

        when(playOrderInfoService.clerkSelectOrderInfoList(anyString(), anyString(), anyString()))
                .thenAnswer(invocation -> {
                    String clerkId = invocation.getArgument(0);
                    String startStr = invocation.getArgument(1);
                    String endStr = invocation.getArgument(2);
                    LocalDateTime start = LocalDateTime.parse(startStr, DateRangeUtils.DATE_TIME_FORMATTER);
                    LocalDateTime end = LocalDateTime.parse(endStr, DateRangeUtils.DATE_TIME_FORMATTER);
                    return ordersByClerk.getOrDefault(clerkId, Collections.emptyList()).stream()
                            .filter(order -> !order.getPurchaserTime().isBefore(start)
                                    && !order.getPurchaserTime().isAfter(end))
                            .collect(Collectors.toList());
                });

        setAuthentication();
        try {
            ClerkPerformanceOverviewResponseVo fullRange =
                    service.queryOverview(buildOverviewVo("2024-07-01", "2024-07-10"));
            ClerkPerformanceOverviewSummaryVo fullSummary = fullRange.getSummary();
            assertEquals(new BigDecimal("1480.00"), fullSummary.getTotalGmv());
            assertEquals(12, fullSummary.getTotalOrderCount());
            assertEquals(new BigDecimal("590.00"), snapshotFor(fullRange.getRankings(), alpha.getId()).getGmv());
            assertEquals(new BigDecimal("450.00"), snapshotFor(fullRange.getRankings(), beta.getId()).getGmv());
            assertEquals(new BigDecimal("440.00"), snapshotFor(fullRange.getRankings(), gamma.getId()).getGmv());

            ClerkPerformanceOverviewResponseVo midRange =
                    service.queryOverview(buildOverviewVo("2024-07-04", "2024-07-08"));
            ClerkPerformanceOverviewSummaryVo midSummary = midRange.getSummary();
            assertEquals(5, midSummary.getTotalOrderCount());
            assertEquals(new BigDecimal("565.00"), midSummary.getTotalGmv());
            List<ClerkPerformanceSnapshotVo> midRankings = midRange.getRankings();
            assertEquals("c-gamma", midRankings.get(0).getClerkId());
            assertEquals(new BigDecimal("210.00"), midRankings.get(0).getGmv());
            assertEquals("c-beta", midRankings.get(1).getClerkId());
            assertEquals(new BigDecimal("205.00"), midRankings.get(1).getGmv());
            assertEquals("c-alpha", midRankings.get(2).getClerkId());
            assertEquals(new BigDecimal("150.00"), midRankings.get(2).getGmv());

            ClerkPerformanceOverviewResponseVo shortRange = service.queryOverview(
                    buildOverviewVo("2024-07-10 18:30:00", "2024-07-10 18:59:59"));
            ClerkPerformanceOverviewSummaryVo shortSummary = shortRange.getSummary();
            assertEquals(1, shortSummary.getTotalOrderCount());
            assertEquals(new BigDecimal("130.00"), shortSummary.getTotalGmv());
            ClerkPerformanceSnapshotVo alphaSnapshot = snapshotFor(shortRange.getRankings(), alpha.getId());
            assertEquals(new BigDecimal("130.00"), alphaSnapshot.getGmv());
            assertEquals(1, alphaSnapshot.getOrderCount());
            assertEquals(BigDecimal.ZERO, snapshotFor(shortRange.getRankings(), beta.getId()).getGmv());
            assertEquals(BigDecimal.ZERO, snapshotFor(shortRange.getRankings(), gamma.getId()).getGmv());
        } finally {
            clearAuthentication();
        }
    }
    @Test
    @DisplayName("getClerkPerformanceInfo should ignore non-completed orders when aggregating GMV")
    void getClerkPerformanceInfoSkipsNonCompletedOrders() {
        PlayClerkUserInfoEntity clerk = buildClerk("c9", "Nine", "gX", "lX");
        List<PlayClerkLevelInfoEntity> levels = Collections.singletonList(level("lX", "钻石"));
        List<PlayPersonnelGroupInfoEntity> groups = Collections.singletonList(group("gX", "特战组"));

        PlayOrderInfoEntity completed = order("c9", "userA", "1", "0", "0", new BigDecimal("120.00"),
                new BigDecimal("60.00"), LocalDateTime.of(2024, Month.AUGUST, 1, 10, 0));
        PlayOrderInfoEntity cancelled = withStatus(
                order("c9", "userB", "0", "0", "0", new BigDecimal("200.00"), new BigDecimal("100.00"),
                        LocalDateTime.of(2024, Month.AUGUST, 1, 12, 0)),
                OrderConstant.OrderStatus.CANCELLED.getCode());

        when(earningsLineMapper.sumAmountByClerkAndOrderIds(eq("c9"), anyCollection(), anyString(), anyString()))
                .thenReturn(new BigDecimal("60.00"));

        PlayClerkPerformanceInfoReturnVo result = service.getClerkPerformanceInfo(clerk,
                Arrays.asList(completed, cancelled), levels, groups, "2024-08-01 00:00:00", "2024-08-02 23:59:59");

        assertEquals(new BigDecimal("120.00"), result.getFinalAmount());
        assertEquals(1, result.getOrderNumber());
        assertEquals(1, result.getCustomNumber());

        ArgumentCaptor<Collection<String>> idCaptor = ArgumentCaptor.forClass(Collection.class);
        verify(earningsLineMapper).sumAmountByClerkAndOrderIds(eq("c9"), idCaptor.capture(),
                eq("2024-08-01 00:00:00"), eq("2024-08-02 23:59:59"));
        assertEquals(1, idCaptor.getValue().size());
        assertTrue(idCaptor.getValue().contains(completed.getId()));
    }

    private ClerkPerformanceOverviewQueryVo buildOverviewVo(String start, String end) {
        ClerkPerformanceOverviewQueryVo vo = new ClerkPerformanceOverviewQueryVo();
        vo.setEndOrderTime(Arrays.asList(start, end));
        vo.setIncludeSummary(true);
        vo.setIncludeRankings(true);
        vo.setLimit(10);
        return vo;
    }

    private ClerkPerformanceSnapshotVo snapshotFor(List<ClerkPerformanceSnapshotVo> snapshots, String clerkId) {
        return snapshots.stream()
                .filter(snapshot -> clerkId.equals(snapshot.getClerkId()))
                .findFirst()
                .orElseThrow(() -> new AssertionError("Snapshot not found for clerk " + clerkId));
    }

    private PlayClerkUserInfoEntity buildClerk(String id, String name, String groupId, String levelId) {
        PlayClerkUserInfoEntity entity = new PlayClerkUserInfoEntity();
        entity.setId(id);
        entity.setNickname(name);
        entity.setGroupId(groupId);
        entity.setLevelId(levelId);
        entity.setSex("1");
        entity.setListingState("1");
        entity.setOnlineState("1");
        return entity;
    }

    private PlayClerkLevelInfoEntity level(String id, String name) {
        PlayClerkLevelInfoEntity level = new PlayClerkLevelInfoEntity();
        level.setId(id);
        level.setName(name);
        return level;
    }

    private PlayPersonnelGroupInfoEntity group(String id, String name) {
        PlayPersonnelGroupInfoEntity entity = new PlayPersonnelGroupInfoEntity();
        entity.setId(id);
        entity.setGroupName(name);
        return entity;
    }

    private PlayOrderInfoEntity order(String clerkId, String purchaser, String firstOrder, String placeType,
            String refundType, BigDecimal finalAmount, BigDecimal estimatedRevenue, LocalDateTime purchaserTime) {
        PlayOrderInfoEntity order = new PlayOrderInfoEntity();
        order.setAcceptBy(clerkId);
        order.setPurchaserBy(purchaser);
        order.setFirstOrder(firstOrder);
        order.setPlaceType(placeType);
        order.setRefundType(refundType);
        order.setFinalAmount(finalAmount);
        order.setEstimatedRevenue(estimatedRevenue);
        order.setOrdersExpiredState("1".equals(refundType) ? "1" : "0");
        order.setPurchaserTime(purchaserTime);
        order.setId(clerkId + "-" + purchaser + "-" + purchaserTime.toString());
        order.setOrderStatus(OrderConstant.OrderStatus.COMPLETED.getCode());
        return order;
    }

    private void setAuthentication() {
        LoginUser loginUser = new LoginUser();
        UsernamePasswordAuthenticationToken authentication =
                new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList());
        SecurityContextHolder.getContext().setAuthentication(authentication);
    }

    private void clearAuthentication() {
        SecurityContextHolder.clearContext();
    }

    private PlayOrderInfoEntity withRefund(PlayOrderInfoEntity order, BigDecimal refundAmount) {
        order.setRefundAmount(refundAmount);
        return order;
    }

    private PlayOrderInfoEntity withStatus(PlayOrderInfoEntity order, String status) {
        order.setOrderStatus(status);
        return order;
    }
}
