package com.starry.admin.api;

import static org.hamcrest.Matchers.hasSize;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.fasterxml.jackson.databind.JsonNode;
import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.common.task.OverdueOrderHandlerTask;
import com.starry.admin.modules.clerk.module.entity.PlayClerkLevelInfoEntity;
import com.starry.admin.modules.order.module.constant.OrderConstant;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.shop.module.constant.CouponUseState;
import com.starry.admin.modules.shop.module.entity.PlayCouponDetailsEntity;
import com.starry.admin.modules.shop.module.vo.PlayCouponDetailsReturnVo;
import com.starry.admin.modules.weichat.service.NotificationSender;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.enums.EarningsType;
import com.starry.admin.utils.SecurityUtils;
import com.starry.common.constant.Constants;
import com.starry.common.context.CustomSecurityContextHolder;
import com.starry.common.utils.IdUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;

class WxCustomRandomOrderApiTest extends WxCustomOrderApiTestSupport {

    @MockBean
    private NotificationSender notificationSender;

    @MockBean
    private WxCustomMpService wxCustomMpService;

    @MockBean
    private OverdueOrderHandlerTask overdueOrderHandlerTask;

    @org.springframework.beans.factory.annotation.Autowired
    private com.starry.admin.modules.order.service.IPlayOrderInfoService orderInfoService;

    @Test
    void randomOrderFailsWhenBalanceInsufficient() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        String remark = "API random insufficient " + IdUtils.getUuid();
        try {
            setCustomerBalance(BigDecimal.ZERO);
            String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
            customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);

            ensureTenantContext();
            long beforeCount = playOrderInfoService.lambdaQuery()
                    .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
                    .eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
                    .count();

            String payload = "{" +
                    "\"sex\":\"2\"," +
                    "\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
                    "\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
                    "\"commodityQuantity\":1," +
                    "\"weiChatCode\":\"apitest-customer-wx\"," +
                    "\"excludeHistory\":\"0\"," +
                    "\"couponIds\":[]," +
                    "\"remark\":\"" + remark + "\"" +
                    "}";

            mockMvc.perform(post("/wx/custom/order/random")
                            .header(USER_HEADER, DEFAULT_USER)
                            .header(TENANT_HEADER, DEFAULT_TENANT)
                            .header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(payload))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(998));

            ensureTenantContext();
            long afterCount = playOrderInfoService.lambdaQuery()
                    .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
                    .eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
                    .count();
            Assertions.assertThat(afterCount).isEqualTo(beforeCount);
        } finally {
            CustomSecurityContextHolder.remove();
        }
    }

    @Test
    // 测试用例：客户带随机下单请求命中默认等级与服务，接口应返回成功文案，
    // 并新增一条处于待接单状态的随机订单，金额、商品信息与 remark 与提交参数保持一致。
    void randomOrderCreatesPendingOrder() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        try {
            resetCustomerBalance();
            String rawToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
            customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, rawToken);

            String remark = "API test random order " + IdUtils.getUuid();
            ensureTenantContext();
            long beforeCount = playOrderInfoService.lambdaQuery()
                    .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
                    .eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
                    .count();

            String payload = "{" +
                    "\"sex\":\"2\"," +
                    "\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
                    "\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
                    "\"commodityQuantity\":1," +
                    "\"weiChatCode\":\"apitest-customer-wx\"," +
                    "\"excludeHistory\":\"0\"," +
                    "\"couponIds\":[]," +
                    "\"remark\":\"" + remark + "\"" +
                    "}";

            mockMvc.perform(post("/wx/custom/order/random")
                            .header(USER_HEADER, DEFAULT_USER)
                            .header(TENANT_HEADER, DEFAULT_TENANT)
                            .header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + rawToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(payload))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.data").value("下单成功"));

            ensureTenantContext();
            long afterCount = playOrderInfoService.lambdaQuery()
                    .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
                    .eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
                    .count();

            Assertions.assertThat(afterCount).isEqualTo(beforeCount + 1);

            PlayOrderInfoEntity latest = playOrderInfoService.lambdaQuery()
                    .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
                    .eq(PlayOrderInfoEntity::getPlaceType, OrderConstant.PlaceType.RANDOM.getCode())
                    .eq(PlayOrderInfoEntity::getRemark, remark)
                    .orderByDesc(PlayOrderInfoEntity::getCreatedTime)
                    .last("limit 1")
                    .one();

            Assertions.assertThat(latest).isNotNull();
            Assertions.assertThat(latest.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode());
            Assertions.assertThat(latest.getCommodityId()).isEqualTo(ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
            Assertions.assertThat(latest.getOrderMoney()).isNotNull();
        } finally {
            CustomSecurityContextHolder.remove();
        }
    }

    @Test
    void randomOrderCancellationReleasesCoupon() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        String remark = "API random cancel coupon " + IdUtils.getUuid();
        BigDecimal discount = new BigDecimal("15.00");
        try {
            resetCustomerBalance();
            String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
            customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);

            String couponInfoId = createCouponId("cpn-rc-");
            ensureFixedReductionCoupon(
                    couponInfoId,
                    OrderConstant.PlaceType.RANDOM,
                    discount,
                    new BigDecimal("60.00"),
                    "0");
            String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.RANDOM, customerToken);

            String payload = "{" +
                    "\"sex\":\"2\"," +
                    "\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
                    "\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
                    "\"commodityQuantity\":1," +
                    "\"weiChatCode\":\"apitest-customer-wx\"," +
                    "\"excludeHistory\":\"0\"," +
                    "\"couponIds\":[\"" + couponDetailId + "\"]," +
                    "\"remark\":\"" + remark + "\"" +
                    "}";

            mockMvc.perform(post("/wx/custom/order/random")
                            .header(USER_HEADER, DEFAULT_USER)
                            .header(TENANT_HEADER, DEFAULT_TENANT)
                            .header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(payload))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200));

            ensureTenantContext();
            PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
                    .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
                    .eq(PlayOrderInfoEntity::getRemark, remark)
                    .orderByDesc(PlayOrderInfoEntity::getCreatedTime)
                    .last("limit 1")
                    .one();
            Assertions.assertThat(order).isNotNull();

            PlayCouponDetailsEntity detailBeforeCancel = couponDetailsService.getById(couponDetailId);
            Assertions.assertThat(detailBeforeCancel).isNotNull();
            Assertions.assertThat(detailBeforeCancel.getUseState()).isEqualTo(CouponUseState.USED.getCode());
            Assertions.assertThat(detailBeforeCancel.getUseTime()).isNotNull();

            String cancelPayload = "{" +
                    "\"orderId\":\"" + order.getId() + "\"," +
                    "\"refundReason\":\"测试取消\"," +
                    "\"images\":[]" +
                    "}";
            mockMvc.perform(post("/wx/custom/order/cancellation")
                            .header(USER_HEADER, DEFAULT_USER)
                            .header(TENANT_HEADER, DEFAULT_TENANT)
                            .header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(cancelPayload))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.data").value("取消成功"));

            ensureTenantContext();
            PlayOrderInfoEntity cancelled = playOrderInfoService.selectOrderInfoById(order.getId());
            Assertions.assertThat(cancelled.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode());

            PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId);
            Assertions.assertThat(detail).isNotNull();
            Assertions.assertThat(detail.getUseState())
                    .as("取消订单后优惠券应恢复为未使用")
                    .isEqualTo(CouponUseState.UNUSED.getCode());
            Assertions.assertThat(detail.getUseTime()).isNull();
        } finally {
            CustomSecurityContextHolder.remove();
        }
    }

    @Test
    // 测试用例：订单已接单后由管理员强制取消，也应释放所使用的优惠券
    void randomOrderForceCancelReleasesCoupon() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        String remark = "API random force cancel " + IdUtils.getUuid();
        BigDecimal discount = new BigDecimal("12.00");
        try {
            reset(notificationSender);
            resetCustomerBalance();

            String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
            customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
            String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
            clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);

            String couponInfoId = createCouponId("cpn-rf-");
            ensureFixedReductionCoupon(
                    couponInfoId,
                    OrderConstant.PlaceType.RANDOM,
                    discount,
                    new BigDecimal("60.00"),
                    "0");
            String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.RANDOM, customerToken);

            String payload = "{" +
                    "\"sex\":\"2\"," +
                    "\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
                    "\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
                    "\"commodityQuantity\":1," +
                    "\"weiChatCode\":\"apitest-customer-wx\"," +
                    "\"excludeHistory\":\"0\"," +
                    "\"couponIds\":[\"" + couponDetailId + "\"]," +
                    "\"remark\":\"" + remark + "\"" +
                    "}";

            mockMvc.perform(post("/wx/custom/order/random")
                            .header(USER_HEADER, DEFAULT_USER)
                            .header(TENANT_HEADER, DEFAULT_TENANT)
                            .header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(payload))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200));

            ensureTenantContext();
            PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
                    .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
                    .eq(PlayOrderInfoEntity::getRemark, remark)
                    .orderByDesc(PlayOrderInfoEntity::getCreatedTime)
                    .last("limit 1")
                    .one();
            Assertions.assertThat(order).isNotNull();

            mockMvc.perform(get("/wx/clerk/order/accept")
                            .param("id", order.getId())
                            .header(USER_HEADER, DEFAULT_USER)
                            .header(TENANT_HEADER, DEFAULT_TENANT)
                            .header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.data").value("成功"));

            ensureTenantContext();
            PlayCouponDetailsEntity detailBeforeForceCancel = couponDetailsService.getById(couponDetailId);
            Assertions.assertThat(detailBeforeForceCancel).isNotNull();
            Assertions.assertThat(detailBeforeForceCancel.getUseState()).isEqualTo(CouponUseState.USED.getCode());
            Assertions.assertThat(detailBeforeForceCancel.getUseTime()).isNotNull();

            ensureTenantContext();
            orderInfoService.forceCancelOngoingOrder(
                    "2",
                    ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID,
                    order.getId(),
                    order.getFinalAmount(),
                    "管理员强制取消",
                    java.util.Collections.emptyList());

            ensureTenantContext();
            PlayOrderInfoEntity cancelled = playOrderInfoService.selectOrderInfoById(order.getId());
            Assertions.assertThat(cancelled.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.CANCELLED.getCode());

            PlayCouponDetailsEntity detail = couponDetailsService.getById(couponDetailId);
            Assertions.assertThat(detail).isNotNull();
            Assertions.assertThat(detail.getUseState())
                    .as("强制取消订单后优惠券应恢复为未使用")
                    .isEqualTo(CouponUseState.UNUSED.getCode());
            Assertions.assertThat(detail.getUseTime()).isNull();
        } finally {
            CustomSecurityContextHolder.remove();
        }
    }

    @Test
    // 测试用例：随机单携带店铺承担的满减券下单，需依据折后金额推送通知，
    // 完整履约后优惠券应置为已使用且收益与预计工资保持一致。
    void randomOrderLifecycleWithCouponAdjustsRevenueAndNotifications() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        String remark = "API random coupon " + IdUtils.getUuid();
        BigDecimal discount = new BigDecimal("20.00");
        try {
            reset(notificationSender);
            resetCustomerBalance();

            String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
            customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
            String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
            clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);

            BigDecimal grossAmount = ApiTestDataSeeder.DEFAULT_COMMODITY_PRICE;
            String couponInfoId = createCouponId("cpn-r-");
            ensureFixedReductionCoupon(
                    couponInfoId,
                    OrderConstant.PlaceType.RANDOM,
                    discount,
                    new BigDecimal("60.00"),
                    "0");
            String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.RANDOM, customerToken);

            String payload = "{" +
                    "\"sex\":\"2\"," +
                    "\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
                    "\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
                    "\"commodityQuantity\":1," +
                    "\"weiChatCode\":\"apitest-customer-wx\"," +
                    "\"excludeHistory\":\"0\"," +
                    "\"couponIds\":[\"" + couponDetailId + "\"]," +
                    "\"remark\":\"" + remark + "\"" +
                    "}";

            mockMvc.perform(post("/wx/custom/order/random")
                            .header(USER_HEADER, DEFAULT_USER)
                            .header(TENANT_HEADER, DEFAULT_TENANT)
                            .header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
                            .contentType(MediaType.APPLICATION_JSON)
                            .content(payload))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.data").value("下单成功"));

            ensureTenantContext();
            PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
                    .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
                    .eq(PlayOrderInfoEntity::getRemark, remark)
                    .orderByDesc(PlayOrderInfoEntity::getCreatedTime)
                    .last("limit 1")
                    .one();

            Assertions.assertThat(order).isNotNull();
            Assertions.assertThat(order.getCouponIds()).contains(couponDetailId);

            BigDecimal expectedNet = grossAmount.subtract(discount).setScale(2, RoundingMode.HALF_UP);
            Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(expectedNet);
            Assertions.assertThat(order.getDiscountAmount()).isEqualByComparingTo(discount);

            verify(wxCustomMpService).sendCreateOrderMessageBatch(
                    anyList(),
                    eq(order.getOrderNo()),
                    eq(expectedNet.toString()),
                    eq(order.getCommodityName()),
                    eq(order.getId()),
                    eq(order.getPlaceType()),
                    eq(order.getRewardType()));
            String orderId = order.getId();

            PlayCouponDetailsEntity detailAfterOrderPlaced = couponDetailsService.getById(couponDetailId);
            Assertions.assertThat(detailAfterOrderPlaced).isNotNull();
            Assertions.assertThat(detailAfterOrderPlaced.getUseState()).isEqualTo(CouponUseState.USED.getCode());
            Assertions.assertThat(detailAfterOrderPlaced.getUseTime()).isNotNull();

            mockMvc.perform(get("/wx/clerk/order/accept")
                            .param("id", orderId)
                            .header(USER_HEADER, DEFAULT_USER)
                            .header(TENANT_HEADER, DEFAULT_TENANT)
                            .header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.data").value("成功"));

            verify(notificationSender).sendOrderMessageAsync(argThat(o -> o.getId().equals(orderId)));


            mockMvc.perform(get("/wx/clerk/order/start")
                            .param("id", orderId)
                            .header(USER_HEADER, DEFAULT_USER)
                            .header(TENANT_HEADER, DEFAULT_TENANT)
                            .header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.data").value("成功"));

            mockMvc.perform(get("/wx/custom/order/end")
                            .param("id", orderId)
                            .header(USER_HEADER, DEFAULT_USER)
                            .header(TENANT_HEADER, DEFAULT_TENANT)
                            .header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.data").value("成功"));

            verify(notificationSender).sendOrderFinishMessageAsync(argThat(o -> orderId.equals(o.getId())));

            ensureTenantContext();
            PlayOrderInfoEntity completedOrder = playOrderInfoService.selectOrderInfoById(orderId);
            int ratio = completedOrder.getEstimatedRevenueRatio();
            BigDecimal baseRevenue = grossAmount
                    .multiply(BigDecimal.valueOf(ratio))
                    .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
            PlayCouponDetailsReturnVo detail = couponDetailsService.selectPlayCouponDetailsById(couponDetailId);
            BigDecimal clerkDiscount = BigDecimal.ZERO;
            if (detail != null && "0".equals(detail.getAttributionDiscounts())) {
                BigDecimal discountAmount = detail.getDiscountAmount() == null ? BigDecimal.ZERO : detail.getDiscountAmount();
                clerkDiscount = discountAmount;
            }
            BigDecimal expectedRevenue = baseRevenue.subtract(clerkDiscount).max(BigDecimal.ZERO).setScale(2, RoundingMode.HALF_UP);
            Assertions.assertThat(completedOrder.getEstimatedRevenue()).isEqualByComparingTo(expectedRevenue);

            EarningsLineEntity earningsLine = earningsService.lambdaQuery()
                    .eq(EarningsLineEntity::getOrderId, orderId)
                    .last("limit 1")
                    .one();
            Assertions.assertThat(earningsLine).isNotNull();
            Assertions.assertThat(earningsLine.getAmount()).isEqualByComparingTo(expectedRevenue);

            assertCouponUsed(couponDetailId);

            PlayCouponDetailsEntity detailAfterLifecycle = couponDetailsService.getById(couponDetailId);
            Assertions.assertThat(detailAfterLifecycle).isNotNull();
            Assertions.assertThat(detailAfterLifecycle.getUseState()).isEqualTo(CouponUseState.USED.getCode());
            Assertions.assertThat(detailAfterLifecycle.getUseTime()).isNotNull();
        } finally {
            CustomSecurityContextHolder.remove();
        }
    }

    @Test
    // 测试用例：模拟随机订单完整生命周期——客户下单、陪玩师接单/开局、客户完结，
    // 期间验证微信通知被触发、收益记录生成、冻结解冻时间正确，并校准日终统计接口返回的订单数、GMV 与预计收益。
    void randomOrderLifecycleGeneratesEarningsAndNotifications() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        String remark = "API random lifecycle " + IdUtils.getUuid();
        try {
            resetCustomerBalance();
            String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
            customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
            String clerkToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CLERK_ID);
            clerkUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CLERK_ID, clerkToken);

            LocalDateTime overviewWindowStart = LocalDateTime.now().minusMinutes(5);
            LocalDateTime overviewWindowEnd = LocalDateTime.now().plusMinutes(5);
            OverviewSnapshot overviewBefore = fetchOverview(overviewWindowStart, overviewWindowEnd);

            String orderId = placeRandomOrder(remark, customerToken);

            reset(notificationSender);

            ensureTenantContext();
            mockMvc.perform(get("/wx/clerk/order/accept")
                            .param("id", orderId)
                            .header(USER_HEADER, DEFAULT_USER)
                            .header(TENANT_HEADER, DEFAULT_TENANT)
                            .header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.data").value("成功"));

            verify(notificationSender).sendOrderMessageAsync(argThat(order -> order.getId().equals(orderId)));

            reset(notificationSender);

            ensureTenantContext();
            mockMvc.perform(get("/wx/clerk/order/start")
                            .param("id", orderId)
                            .header(USER_HEADER, DEFAULT_USER)
                            .header(TENANT_HEADER, DEFAULT_TENANT)
                            .header(Constants.CLERK_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + clerkToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.data").value("成功"));

            ensureTenantContext();
            long earningsBefore = earningsService.lambdaQuery()
                    .eq(EarningsLineEntity::getOrderId, orderId)
                    .count();
            Assertions.assertThat(earningsBefore).isZero();

            ensureTenantContext();
            mockMvc.perform(get("/wx/custom/order/end")
                            .param("id", orderId)
                            .header(USER_HEADER, DEFAULT_USER)
                            .header(TENANT_HEADER, DEFAULT_TENANT)
                            .header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken))
                    .andExpect(status().isOk())
                    .andExpect(jsonPath("$.code").value(200))
                    .andExpect(jsonPath("$.data").value("成功"));

            verify(notificationSender).sendOrderFinishMessageAsync(argThat(order -> order.getId().equals(orderId)));

            ensureTenantContext();
            long earningsAfter = earningsService.lambdaQuery()
                    .eq(EarningsLineEntity::getOrderId, orderId)
                    .count();
            Assertions.assertThat(earningsAfter).isEqualTo(1);

            ensureTenantContext();
            PlayOrderInfoEntity completedOrder = playOrderInfoService.selectOrderInfoById(orderId);
            Assertions.assertThat(completedOrder.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.COMPLETED.getCode());
            Assertions.assertThat(completedOrder.getEstimatedRevenue()).isNotNull();

            EarningsLineEntity earningsLine = earningsService.lambdaQuery()
                    .eq(EarningsLineEntity::getOrderId, orderId)
                    .one();
            Assertions.assertThat(earningsLine).isNotNull();
            Assertions.assertThat(earningsLine.getAmount()).isEqualByComparingTo(completedOrder.getEstimatedRevenue());
            int freezeHours = freezePolicyService.resolveFreezeHours(
                    ApiTestDataSeeder.DEFAULT_TENANT_ID,
                    ApiTestDataSeeder.DEFAULT_CLERK_ID);
            LocalDateTime expectedUnlock = completedOrder.getOrderEndTime().plusHours(freezeHours);
            Assertions.assertThat(earningsLine.getUnlockTime()).isEqualTo(expectedUnlock);
            Assertions.assertThat(earningsLine.getUnlockTime()).isAfter(LocalDateTime.now().minusMinutes(5));
            Assertions.assertThat(earningsLine.getEarningType())
                    .isIn(EarningsType.ORDER, EarningsType.ADJUSTMENT);

            OverviewSnapshot overviewAfter = fetchOverview(overviewWindowStart, overviewWindowEnd);
            Assertions.assertThat(overviewAfter.totalOrderCount - overviewBefore.totalOrderCount)
                    .isEqualTo(1);
            Assertions.assertThat(overviewAfter.totalGmv.subtract(overviewBefore.totalGmv).setScale(2, RoundingMode.HALF_UP))
                    .isEqualByComparingTo(completedOrder.getFinalAmount());
            Assertions.assertThat(overviewAfter.totalEstimatedRevenue.subtract(overviewBefore.totalEstimatedRevenue).setScale(2, RoundingMode.HALF_UP))
                    .isEqualByComparingTo(completedOrder.getEstimatedRevenue());
            Assertions.assertThat(overviewAfter.clerkOrderCount - overviewBefore.clerkOrderCount)
                    .isEqualTo(1);
            Assertions.assertThat(overviewAfter.clerkGmv.subtract(overviewBefore.clerkGmv).setScale(2, RoundingMode.HALF_UP))
                    .isEqualByComparingTo(completedOrder.getFinalAmount());
        } finally {
            CustomSecurityContextHolder.remove();
        }
    }

    private OverviewSnapshot fetchOverview(LocalDateTime start, LocalDateTime end) throws Exception {
        String payload = "{" +
                "\"includeSummary\":true," +
                "\"includeRankings\":true," +
                "\"limit\":5," +
                "\"endOrderTime\":[\"" + start.format(DATE_TIME_FORMATTER) + "\",\""
                + end.format(DATE_TIME_FORMATTER) + "\"]" +
                "}";

        MvcResult result = mockMvc.perform(post("/statistics/performance/overview")
                        .header(USER_HEADER, DEFAULT_USER)
                        .header(TENANT_HEADER, DEFAULT_TENANT)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(payload))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andReturn();

        JsonNode data = objectMapper.readTree(result.getResponse().getContentAsString()).path("data");
        OverviewSnapshot snapshot = new OverviewSnapshot();
        JsonNode summary = data.path("summary");
        snapshot.totalOrderCount = summary.path("totalOrderCount").asInt();
        snapshot.totalGmv = new BigDecimal(summary.path("totalGmv").asText("0"));
        snapshot.totalEstimatedRevenue = new BigDecimal(summary.path("totalEstimatedRevenue").asText("0"));

        JsonNode rankings = data.path("rankings");
        if (rankings.isArray()) {
            for (JsonNode node : rankings) {
                if (ApiTestDataSeeder.DEFAULT_CLERK_ID.equals(node.path("clerkId").asText())) {
                    snapshot.clerkOrderCount = node.path("orderCount").asInt();
                    snapshot.clerkGmv = new BigDecimal(node.path("gmv").asText("0"));
                    break;
                }
            }
        }
        return snapshot;
    }

    private static class OverviewSnapshot {
        private int totalOrderCount;
        private BigDecimal totalGmv = BigDecimal.ZERO;
        private BigDecimal totalEstimatedRevenue = BigDecimal.ZERO;
        private int clerkOrderCount;
        private BigDecimal clerkGmv = BigDecimal.ZERO;
    }

    private String placeRandomOrder(String remark, String customerToken) throws Exception {
        ensureTenantContext();
        long beforeCount = playOrderInfoService.lambdaQuery()
                .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
                .count();

        String payload = "{" +
                "\"sex\":\"2\"," +
                "\"levelId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID + "\"," +
                "\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
                "\"commodityQuantity\":1," +
                "\"weiChatCode\":\"apitest-customer-wx\"," +
                "\"excludeHistory\":\"0\"," +
                "\"couponIds\":[]," +
                "\"remark\":\"" + remark + "\"" +
                "}";

        mockMvc.perform(post("/wx/custom/order/random")
                        .header(USER_HEADER, DEFAULT_USER)
                        .header(TENANT_HEADER, DEFAULT_TENANT)
                        .header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(payload))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andExpect(jsonPath("$.data").value("下单成功"));

        ensureTenantContext();
        PlayOrderInfoEntity order = playOrderInfoService.lambdaQuery()
                .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
                .eq(PlayOrderInfoEntity::getRemark, remark)
                .orderByDesc(PlayOrderInfoEntity::getCreatedTime)
                .last("limit 1")
                .one();

        Assertions.assertThat(order).isNotNull();
        Assertions.assertThat(order.getPlaceType()).isEqualTo(OrderConstant.PlaceType.RANDOM.getCode());

        ensureTenantContext();
        long afterCount = playOrderInfoService.lambdaQuery()
                .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
                .count();
        Assertions.assertThat(afterCount).isEqualTo(beforeCount + 1);

        verify(wxCustomMpService).sendCreateOrderMessageBatch(
                anyList(),
                eq(order.getOrderNo()),
                eq(order.getFinalAmount().toString()),
                eq(order.getCommodityName()),
                eq(order.getId()),
                eq(order.getPlaceType()),
                eq(order.getRewardType()));
        verify(overdueOrderHandlerTask).enqueue(order.getId() + "_" + ApiTestDataSeeder.DEFAULT_TENANT_ID);

        reset(wxCustomMpService);
        reset(overdueOrderHandlerTask);

        return order.getId();
    }

}
