package com.starry.admin.api;

import static org.mockito.ArgumentMatchers.anyString;
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.starry.admin.common.apitest.ApiTestDataSeeder;
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.WxCustomMpService;
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.util.Collections;
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;

class WxCustomSpecifiedOrderApiTest extends WxCustomOrderApiTestSupport {

    @MockBean
    private WxCustomMpService wxCustomMpService;

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

    @org.springframework.beans.factory.annotation.Autowired
    private com.starry.admin.modules.weichat.service.WxTokenService clerkWxTokenService;

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

            ensureTenantContext();
            com.starry.admin.modules.clerk.module.entity.PlayClerkUserInfoEntity clerk =
                    clerkUserInfoService.selectById(ApiTestDataSeeder.DEFAULT_CLERK_ID);
            Assertions.assertThat(clerk.getLevelId())
                    .as("默认店员应绑定基础等级用于价格校验")
                    .isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);

            ensureTenantContext();
            long beforeCount = playOrderInfoService.lambdaQuery()
                    .eq(PlayOrderInfoEntity::getPurchaserBy, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
                    .count();

            String payload = "{" +
                    "\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
                    "\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
                    "\"commodityQuantity\":1," +
                    "\"weiChatCode\":\"apitest-customer-wx\"," +
                    "\"couponIds\":[]," +
                    "\"remark\":\"" + remark + "\"" +
                    "}";

            mockMvc.perform(post("/wx/custom/order/commodity")
                            .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)
                    .count();
            Assertions.assertThat(afterCount).isEqualTo(beforeCount);
        } finally {
            CustomSecurityContextHolder.remove();
        }
    }

    @Test
    // 测试用例：指定单取消后优惠券应恢复为未使用
    void specifiedOrderCancellationReleasesCoupon() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        String remark = "API specified cancel " + IdUtils.getUuid();
        BigDecimal discount = new BigDecimal("10.00");
        try {
            resetCustomerBalance();
            String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
            customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);

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

            String payload = "{" +
                    "\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
                    "\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
                    "\"commodityQuantity\":1," +
                    "\"weiChatCode\":\"apitest-customer-wx\"," +
                    "\"couponIds\":[\"" + couponDetailId + "\"]," +
                    "\"remark\":\"" + remark + "\"" +
                    "}";

            mockMvc.perform(post("/wx/custom/order/commodity")
                            .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 detailBeforeForceCancel = couponDetailsService.getById(couponDetailId);
            Assertions.assertThat(detailBeforeForceCancel).isNotNull();
            Assertions.assertThat(detailBeforeForceCancel.getUseState()).isEqualTo(CouponUseState.USED.getCode());
            Assertions.assertThat(detailBeforeForceCancel.getUseTime()).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 specifiedOrderWithCouponAdjustsAmountAndRevenue() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        String remark = "API specified coupon " + IdUtils.getUuid();
        BigDecimal discount = new BigDecimal("15.00");
        try {
            reset(wxCustomMpService);
            resetCustomerBalance();

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

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

            String payload = "{" +
                    "\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
                    "\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
                    "\"commodityQuantity\":1," +
                    "\"weiChatCode\":\"apitest-customer-wx\"," +
                    "\"couponIds\":[\"" + couponDetailId + "\"]," +
                    "\"remark\":\"" + remark + "\"" +
                    "}";

            mockMvc.perform(post("/wx/custom/order/commodity")
                            .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);

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

            verify(wxCustomMpService).sendCreateOrderMessage(
                    eq(ApiTestDataSeeder.DEFAULT_TENANT_ID),
                    eq(ApiTestDataSeeder.DEFAULT_CLERK_OPEN_ID),
                    anyString(),
                    eq(expectedNet.toString()),
                    eq(order.getCommodityName()),
                    eq(order.getId()),
                    eq(order.getPlaceType()),
                    eq(order.getRewardType()));

            int ratio = order.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(order.getEstimatedRevenue()).isEqualByComparingTo(expectedRevenue);

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

    @Test
    // 测试用例：指定单在接单后由管理员强制取消时，优惠券应恢复为未使用
    void specifiedOrderForceCancelReleasesCoupon() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        String remark = "API specified force cancel " + IdUtils.getUuid();
        BigDecimal discount = new BigDecimal("8.00");
        try {
            reset(wxCustomMpService);
            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-sf-");
            ensureFixedReductionCoupon(
                    couponInfoId,
                    OrderConstant.PlaceType.SPECIFIED,
                    discount,
                    new BigDecimal("60.00"),
                    "0");
            String couponDetailId = claimCouponForOrder(couponInfoId, OrderConstant.PlaceType.SPECIFIED, customerToken);

            String payload = "{" +
                    "\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
                    "\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
                    "\"commodityQuantity\":1," +
                    "\"weiChatCode\":\"apitest-customer-wx\"," +
                    "\"couponIds\":[\"" + couponDetailId + "\"]," +
                    "\"remark\":\"" + remark + "\"" +
                    "}";

            mockMvc.perform(post("/wx/custom/order/commodity")
                            .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();
            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 specifiedOrderCreatesPendingOrder() throws Exception {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        try {
            resetCustomerBalance();
            String customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
            customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);

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

            String payload = "{" +
                    "\"clerkId\":\"" + ApiTestDataSeeder.DEFAULT_CLERK_ID + "\"," +
                    "\"commodityId\":\"" + ApiTestDataSeeder.DEFAULT_COMMODITY_ID + "\"," +
                    "\"commodityQuantity\":1," +
                    "\"weiChatCode\":\"apitest-customer-wx\"," +
                    "\"couponIds\":[]," +
                    "\"remark\":\"" + remark + "\"" +
                    "}";

            reset(wxCustomMpService);

            mockMvc.perform(post("/wx/custom/order/commodity")
                            .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.SPECIFIED.getCode());
            Assertions.assertThat(order.getOrderStatus()).isEqualTo(OrderConstant.OrderStatus.PENDING.getCode());
            Assertions.assertThat(order.getAcceptBy()).isEqualTo(ApiTestDataSeeder.DEFAULT_CLERK_ID);
            Assertions.assertThat(order.getFinalAmount()).isEqualByComparingTo(ApiTestDataSeeder.DEFAULT_COMMODITY_PRICE);

            verify(wxCustomMpService).sendCreateOrderMessage(
                    eq(ApiTestDataSeeder.DEFAULT_TENANT_ID),
                    eq(ApiTestDataSeeder.DEFAULT_CLERK_OPEN_ID),
                    anyString(),
                    eq(order.getFinalAmount().toString()),
                    eq(order.getCommodityName()),
                    eq(order.getId()),
                    eq(order.getPlaceType()),
                    eq(order.getRewardType()));

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