package com.starry.admin.api;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.anyOf;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
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.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.starry.admin.common.apitest.ApiTestDataSeeder;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.module.constant.OrderConstant;
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.entity.PlayCouponInfoEntity;
import com.starry.admin.modules.shop.module.enums.CouponClaimConditionType;
import com.starry.admin.modules.shop.module.enums.CouponDiscountType;
import com.starry.admin.modules.shop.module.enums.CouponObtainChannel;
import com.starry.admin.modules.shop.module.enums.CouponOnlineState;
import com.starry.admin.modules.shop.module.enums.CouponValidityPeriodType;
import com.starry.admin.modules.shop.service.IPlayCouponDetailsService;
import com.starry.admin.modules.shop.service.IPlayCouponInfoService;
import com.starry.admin.modules.weichat.service.WxTokenService;
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.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MvcResult;

class WxCouponControllerApiTest extends AbstractApiTest {

    private static final String COUPON_ID_PREFIX = "apitest-cpn-";
    private static final BigDecimal DEFAULT_DISCOUNT_AMOUNT = new BigDecimal("5");
    private static final BigDecimal MINIMUM_USAGE_AMOUNT = new BigDecimal("60");
    private static final int DEFAULT_MAX_PER_USER = 3;
    private static final int DEFAULT_STOCK = 100;
    private static final String DISCOUNT_BEARER_SHOP = "0";
    private static final String GLOBAL_CLERK_SCOPE = "0";
    private static final String CHECK_CONDITION_DISABLED = "0";
    private static final String NEW_USER_FLAG_DISABLED = "0";

    @Autowired
    private IPlayCouponInfoService couponInfoService;

    @Autowired
    private IPlayCouponDetailsService couponDetailsService;

    @Autowired
    private WxTokenService wxTokenService;

    @Autowired
    private IPlayCustomUserInfoService customUserInfoService;

    private final ObjectMapper mapper = new ObjectMapper();
    private final List<String> couponIds = new ArrayList<>();
    private final List<String> couponDetailIds = new ArrayList<>();
    private String customerToken;

    @BeforeEach
    void setUp() {
        ensureTenantContext();
        couponInfoService.lambdaUpdate()
                .eq(PlayCouponInfoEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID)
                .like(PlayCouponInfoEntity::getId, COUPON_ID_PREFIX)
                .remove();
        couponDetailsService.lambdaUpdate()
                .eq(PlayCouponDetailsEntity::getTenantId, ApiTestDataSeeder.DEFAULT_TENANT_ID)
                .like(PlayCouponDetailsEntity::getCouponId, COUPON_ID_PREFIX)
                .remove();
        customerToken = wxTokenService.createWxUserToken(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
        customUserInfoService.updateTokenById(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID, customerToken);
    }

    @AfterEach
    void tearDown() {
        ensureTenantContext();
        if (!couponDetailIds.isEmpty()) {
            couponDetailsService.removeByIds(couponDetailIds);
            couponDetailIds.clear();
        }
        if (!couponIds.isEmpty()) {
            couponInfoService.removeByIds(couponIds);
            couponIds.clear();
        }
        CustomSecurityContextHolder.remove();
    }

    @Test
    void obtainCouponRejectsNonWhitelistCustomer() throws Exception {
        ensureTenantContext();
        String couponId = newCouponId("whitelist");
        PlayCouponInfoEntity coupon = createBaseCoupon(couponId);
        coupon.setClaimConditionType(CouponClaimConditionType.WHITELIST.code());
        coupon.setCustomWhitelist(List.of("other-customer"));
        couponInfoService.save(coupon);
        couponIds.add(coupon.getId());

        mockMvc.perform(get("/wx/coupon/custom/obtainCoupon")
                        .param("id", coupon.getId())
                        .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.success").value(false))
                .andExpect(jsonPath("$.data.msg").value("非指定用户"));
    }

    @Test
    void queryAllSkipsOfflineCouponsAndMarksObtainedOnes() throws Exception {
        ensureTenantContext();
        PlayCouponInfoEntity onlineCoupon = createBaseCoupon(newCouponId("online"));
        onlineCoupon.setCustomWhitelist(List.of(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID));
        couponInfoService.save(onlineCoupon);
        couponIds.add(onlineCoupon.getId());

        PlayCouponDetailsEntity obtained = createCouponDetail(onlineCoupon.getId(), CouponUseState.UNUSED);
        couponDetailsService.save(obtained);
        couponDetailIds.add(obtained.getId());

        PlayCouponInfoEntity offlineCoupon = createBaseCoupon(newCouponId("offline"));
        offlineCoupon.setCouponOnLineState(CouponOnlineState.OFFLINE.getCode());
        couponInfoService.save(offlineCoupon);
        couponIds.add(offlineCoupon.getId());

        MvcResult result = mockMvc.perform(post("/wx/coupon/custom/queryAll")
                        .header(USER_HEADER, DEFAULT_USER)
                        .header(TENANT_HEADER, DEFAULT_TENANT)
                        .header(Constants.CUSTOM_USER_LOGIN_TOKEN, Constants.TOKEN_PREFIX + customerToken)
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andReturn();

        JsonNode data = mapper.readTree(result.getResponse().getContentAsString()).path("data");
        assertThat(data).isNotNull();
        assertThat(data.isArray()).isTrue();
        assertThat(data).hasSizeGreaterThanOrEqualTo(1);
        boolean containsOnline = false;
        for (JsonNode node : data) {
            assertThat(node.path("id").asText()).isNotEqualTo(offlineCoupon.getId());
            if (onlineCoupon.getId().equals(node.path("id").asText())) {
                containsOnline = true;
                assertThat(node.path("obtained").asText()).isEqualTo("1");
            }
        }
        assertThat(containsOnline).isTrue();
    }

    @Test
    void queryByOrderFlagsOnlyEligibleCouponsAsAvailable() throws Exception {
        ensureTenantContext();
        PlayCouponInfoEntity eligible = createBaseCoupon(newCouponId("eligible"));
        eligible.setUseMinAmount(MINIMUM_USAGE_AMOUNT);
        couponInfoService.save(eligible);
        couponIds.add(eligible.getId());

        PlayCouponInfoEntity ineligible = createBaseCoupon(newCouponId("ineligible"));
        ineligible.setPlaceType(List.of(OrderConstant.PlaceType.SPECIFIED.getCode()));
        couponInfoService.save(ineligible);
        couponIds.add(ineligible.getId());

        PlayCouponDetailsEntity eligibleDetail = createCouponDetail(eligible.getId(), CouponUseState.UNUSED);
        PlayCouponDetailsEntity ineligibleDetail = createCouponDetail(ineligible.getId(), CouponUseState.UNUSED);
        couponDetailsService.save(eligibleDetail);
        couponDetailsService.save(ineligibleDetail);
        couponDetailIds.add(eligibleDetail.getId());
        couponDetailIds.add(ineligibleDetail.getId());

        ObjectNode payload = mapper.createObjectNode();
        payload.put("commodityId", ApiTestDataSeeder.DEFAULT_COMMODITY_ID);
        payload.put("levelId", ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
        payload.put("clerkId", "");
        payload.put("placeType", OrderConstant.PlaceType.RANDOM.getCode());
        payload.put("commodityQuantity", 1);

        MvcResult result = mockMvc.perform(post("/wx/coupon/custom/queryByOrder")
                        .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.toString()))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.code").value(200))
                .andReturn();

        ArrayNode data = (ArrayNode) mapper.readTree(result.getResponse().getContentAsString()).path("data");
        assertThat(data).isNotNull();
        assertThat(data.size()).isGreaterThanOrEqualTo(2);
        for (JsonNode node : data) {
            if (eligible.getId().equals(node.path("couponId").asText())) {
                assertThat(node.path("available").asText()).isEqualTo("1");
                assertThat(node.path("reasonForUnavailableUse").asText("")).isEmpty();
            }
            if (ineligible.getId().equals(node.path("couponId").asText())) {
                assertThat(node.path("available").asText()).isEqualTo("0");
                assertThat(node.path("reasonForUnavailableUse").asText()).isEqualTo("订单类型不符合");
            }
        }
    }

    @Test
    void obtainCouponSucceedsWhenEligible() throws Exception {
        ensureTenantContext();
        PlayCouponInfoEntity coupon = createBaseCoupon(newCouponId("success"));
        couponInfoService.save(coupon);
        couponIds.add(coupon.getId());

        mockMvc.perform(get("/wx/coupon/custom/obtainCoupon")
                        .param("id", coupon.getId())
                        .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.success").value(true))
                .andExpect(jsonPath("$.data.msg").value(anyOf(is(""), nullValue())));

        ensureTenantContext();
        PlayCouponDetailsEntity detail = couponDetailsService.lambdaQuery()
                .eq(PlayCouponDetailsEntity::getCouponId, coupon.getId())
                .eq(PlayCouponDetailsEntity::getCustomId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
                .orderByDesc(PlayCouponDetailsEntity::getObtainingTime)
                .last("limit 1")
                .one();
        assertThat(detail).isNotNull();
        couponDetailIds.add(detail.getId());
    }

    @Test
    void obtainCouponHonorsPerUserLimit() throws Exception {
        ensureTenantContext();
        PlayCouponInfoEntity coupon = createBaseCoupon(newCouponId("limit"));
        coupon.setClerkObtainedMaxQuantity(1);
        couponInfoService.save(coupon);
        couponIds.add(coupon.getId());

        mockMvc.perform(get("/wx/coupon/custom/obtainCoupon")
                        .param("id", coupon.getId())
                        .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.success").value(true));

        ensureTenantContext();
        PlayCouponDetailsEntity detail = couponDetailsService.lambdaQuery()
                .eq(PlayCouponDetailsEntity::getCouponId, coupon.getId())
                .eq(PlayCouponDetailsEntity::getCustomId, ApiTestDataSeeder.DEFAULT_CUSTOMER_ID)
                .one();
        assertThat(detail).isNotNull();
        couponDetailIds.add(detail.getId());

        mockMvc.perform(get("/wx/coupon/custom/obtainCoupon")
                        .param("id", coupon.getId())
                        .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.success").value(false))
                .andExpect(jsonPath("$.data.msg").value("优惠券已达到领取上限"));
    }

    private PlayCouponInfoEntity createBaseCoupon(String id) {
        PlayCouponInfoEntity coupon = new PlayCouponInfoEntity();
        coupon.setId(id);
        coupon.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        coupon.setCouponName("测试券-" + id);
        coupon.setValidityPeriodType(CouponValidityPeriodType.PERMANENT.getCode());
        coupon.setUseMinAmount(BigDecimal.ZERO);
        coupon.setDiscountType(CouponDiscountType.FULL_REDUCTION.getCode());
        coupon.setDiscountContent("测试优惠");
        coupon.setDiscountAmount(DEFAULT_DISCOUNT_AMOUNT);
        coupon.setAttributionDiscounts(DISCOUNT_BEARER_SHOP);
        coupon.setPlaceType(List.of(
                OrderConstant.PlaceType.RANDOM.getCode(),
                OrderConstant.PlaceType.SPECIFIED.getCode()));
        coupon.setClerkType(GLOBAL_CLERK_SCOPE);
        coupon.setCouponQuantity(DEFAULT_STOCK);
        coupon.setIssuedQuantity(0);
        coupon.setRemainingQuantity(DEFAULT_STOCK);
        coupon.setClerkObtainedMaxQuantity(DEFAULT_MAX_PER_USER);
        coupon.setClaimConditionType(CouponClaimConditionType.ALL.code());
        coupon.setCustomWhitelist(new ArrayList<>());
        coupon.setCustomLevelCheckType(CHECK_CONDITION_DISABLED);
        coupon.setCustomSexCheckType(CHECK_CONDITION_DISABLED);
        coupon.setNewUser(NEW_USER_FLAG_DISABLED);
        coupon.setCouponOnLineState(CouponOnlineState.ONLINE.getCode());
        LocalDateTime now = LocalDateTime.now();
        coupon.setProductiveTime(now.minusDays(1));
        coupon.setExpirationTime(now.plusDays(30));
        coupon.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
        coupon.setCreatedTime(toDate(now));
        coupon.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
        coupon.setUpdatedTime(toDate(now));
        return coupon;
    }

    private PlayCouponDetailsEntity createCouponDetail(String couponId, CouponUseState status) {
        PlayCouponDetailsEntity entity = new PlayCouponDetailsEntity();
        entity.setId("detail-" + IdUtils.getUuid());
        entity.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
        entity.setCustomId(ApiTestDataSeeder.DEFAULT_CUSTOMER_ID);
        entity.setCouponId(couponId);
        entity.setCustomNickname("APITester");
        entity.setCustomLevelId(ApiTestDataSeeder.DEFAULT_CLERK_LEVEL_ID);
        entity.setObtainingChannels(CouponObtainChannel.SELF_SERVICE.getCode());
        entity.setUseState(status.getCode());
        LocalDateTime now = LocalDateTime.now();
        entity.setObtainingTime(now);
        entity.setCreatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
        entity.setCreatedTime(toDate(now));
        entity.setUpdatedBy(ApiTestDataSeeder.DEFAULT_ADMIN_USER_ID);
        entity.setUpdatedTime(toDate(now));
        return entity;
    }

    private void ensureTenantContext() {
        SecurityUtils.setTenantId(ApiTestDataSeeder.DEFAULT_TENANT_ID);
    }

    private String newCouponId(String suffix) {
        return COUPON_ID_PREFIX + suffix + "-" + IdUtils.getUuid();
    }

    private java.util.Date toDate(LocalDateTime value) {
        return java.util.Date.from(value.atZone(ZoneId.systemDefault()).toInstant());
    }
}
