package com.ycwl.basic.pricing.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.ycwl.basic.pricing.dto.CouponClaimRequest;
import com.ycwl.basic.pricing.dto.CouponClaimResult;
import com.ycwl.basic.pricing.dto.CouponUseRequest;
import com.ycwl.basic.pricing.entity.PriceCouponClaimRecord;
import com.ycwl.basic.pricing.entity.PriceCouponConfig;
import com.ycwl.basic.pricing.enums.CouponStatus;
import com.ycwl.basic.pricing.exception.CouponInvalidException;
import com.ycwl.basic.pricing.mapper.PriceCouponClaimRecordMapper;
import com.ycwl.basic.pricing.mapper.PriceCouponConfigMapper;
import com.ycwl.basic.pricing.service.impl.CouponServiceImpl;
import org.junit.jupiter.api.BeforeEach;
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 java.time.Duration;
import java.util.Date;

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

@ExtendWith(MockitoExtension.class)
class CouponServiceImplTest {

    @Mock
    private PriceCouponConfigMapper couponConfigMapper;

    @Mock
    private PriceCouponClaimRecordMapper couponClaimRecordMapper;

    private ObjectMapper objectMapper;

    @InjectMocks
    private CouponServiceImpl couponService;

    @BeforeEach
    void setUp() {
        objectMapper = new ObjectMapper();
        couponService = new CouponServiceImpl(couponConfigMapper, couponClaimRecordMapper, objectMapper);
    }

    @Test
    void shouldFailWhenUserClaimLimitReached() {
        PriceCouponConfig coupon = baseCoupon();
        coupon.setUserClaimLimit(1);
        when(couponConfigMapper.selectById(1L)).thenReturn(coupon);
        when(couponClaimRecordMapper.countUserCouponClaims(10L, 1L)).thenReturn(1);

        CouponClaimResult result = couponService.claimCoupon(buildRequest());

        assertFalse(result.isSuccess());
        assertEquals(CouponClaimResult.ERROR_CLAIM_LIMIT_REACHED, result.getErrorCode());
        verify(couponClaimRecordMapper, never()).insert(any());
        verify(couponConfigMapper, never()).incrementClaimedQuantityIfAvailable(anyLong());
    }

    @Test
    void shouldSucceedWhenClaimWithinLimitAndStockAvailable() {
        PriceCouponConfig coupon = baseCoupon();
        coupon.setUserClaimLimit(3);
        coupon.setClaimedQuantity(1);
        when(couponConfigMapper.selectById(1L)).thenReturn(coupon);
        when(couponClaimRecordMapper.countUserCouponClaims(10L, 1L)).thenReturn(0);
        when(couponClaimRecordMapper.insert(any())).thenAnswer(invocation -> {
            PriceCouponClaimRecord record = invocation.getArgument(0);
            record.setId(99L);
            return 1;
        });
        when(couponConfigMapper.incrementClaimedQuantityIfAvailable(1L)).thenReturn(1);

        CouponClaimResult result = couponService.claimCoupon(buildRequest());

        assertTrue(result.isSuccess());
        assertEquals(99L, result.getClaimRecordId());
        assertEquals(2, coupon.getClaimedQuantity());
        ArgumentCaptor<PriceCouponClaimRecord> captor = ArgumentCaptor.forClass(PriceCouponClaimRecord.class);
        verify(couponClaimRecordMapper).insert(captor.capture());
        assertEquals(10L, captor.getValue().getUserId());
        verify(couponConfigMapper).incrementClaimedQuantityIfAvailable(1L);
    }

    @Test
    void shouldReturnOutOfStockWhenInventoryUpdateFails() {
        PriceCouponConfig coupon = baseCoupon();
        coupon.setClaimedQuantity(9);
        when(couponConfigMapper.selectById(1L)).thenReturn(coupon);
        when(couponClaimRecordMapper.insert(any())).thenReturn(1);
        when(couponConfigMapper.incrementClaimedQuantityIfAvailable(1L)).thenReturn(0);

        CouponClaimResult result = couponService.claimCoupon(buildRequest());

        assertFalse(result.isSuccess());
        assertEquals(CouponClaimResult.ERROR_COUPON_OUT_OF_STOCK, result.getErrorCode());
    }

    @Test
    void shouldFailToUseCouponWhenClaimRecordExpiredByClaimWindow() {
        PriceCouponConfig coupon = baseCoupon();
        when(couponConfigMapper.selectById(1L)).thenReturn(coupon);

        PriceCouponClaimRecord expiredRecord = new PriceCouponClaimRecord();
        expiredRecord.setId(99L);
        expiredRecord.setStatus(CouponStatus.CLAIMED);
        expiredRecord.setExpireTime(new Date(System.currentTimeMillis() - 1_000L));
        when(couponClaimRecordMapper.selectUserCouponRecords(10L, 1L)).thenReturn(java.util.List.of(expiredRecord));

        CouponUseRequest request = buildUseRequest();
        CouponInvalidException exception = assertThrows(CouponInvalidException.class, () -> couponService.useCoupon(request));

        assertEquals("优惠券已过期", exception.getMessage());
        verify(couponConfigMapper, never()).incrementUsedQuantity(anyLong());
    }

    @Test
    void shouldFailToUseCouponWhenCouponGlobalValidityExpired() {
        PriceCouponConfig coupon = baseCoupon();
        coupon.setValidUntil(new Date(System.currentTimeMillis() - 1_000L));
        when(couponConfigMapper.selectById(1L)).thenReturn(coupon);

        CouponUseRequest request = buildUseRequest();
        CouponInvalidException exception = assertThrows(CouponInvalidException.class, () -> couponService.useCoupon(request));

        assertEquals("优惠券已过期", exception.getMessage());
        verify(couponConfigMapper, never()).incrementUsedQuantity(anyLong());
    }

    @Test
    void shouldUseCouponWhenClaimRecordAndGlobalWindowAreValid() {
        PriceCouponConfig coupon = baseCoupon();
        when(couponConfigMapper.selectById(1L)).thenReturn(coupon);
        when(couponConfigMapper.incrementUsedQuantity(1L)).thenReturn(1);

        PriceCouponClaimRecord validClaimRecord = new PriceCouponClaimRecord();
        validClaimRecord.setId(99L);
        validClaimRecord.setStatus(CouponStatus.CLAIMED);
        validClaimRecord.setExpireTime(new Date(System.currentTimeMillis() + 10_000L));
        when(couponClaimRecordMapper.selectUserCouponRecords(10L, 1L)).thenReturn(java.util.List.of(validClaimRecord));

        CouponUseRequest request = buildUseRequest();
        couponService.useCoupon(request);

        verify(couponConfigMapper).incrementUsedQuantity(1L);
        verify(couponClaimRecordMapper).updateCouponStatus(
                eq(99L),
                eq(CouponStatus.USED),
                any(Date.class),
                eq("ORDER-1"),
                eq("SCENIC-1"));
    }

    private CouponClaimRequest buildRequest() {
        CouponClaimRequest request = new CouponClaimRequest();
        request.setUserId(10L);
        request.setCouponId(1L);
        request.setScenicId("SCENIC-1");
        return request;
    }

    private CouponUseRequest buildUseRequest() {
        CouponUseRequest request = new CouponUseRequest();
        request.setUserId(10L);
        request.setCouponId(1L);
        request.setOrderId("ORDER-1");
        request.setScenicId("SCENIC-1");
        return request;
    }

    private PriceCouponConfig baseCoupon() {
        PriceCouponConfig coupon = new PriceCouponConfig();
        coupon.setId(1L);
        coupon.setCouponName("新客券");
        coupon.setIsActive(true);
        Date now = new Date();
        long oneDayMillis = Duration.ofDays(1).toMillis();
        coupon.setValidFrom(new Date(now.getTime() - oneDayMillis));
        coupon.setValidUntil(new Date(now.getTime() + oneDayMillis));
        coupon.setTotalQuantity(100);
        coupon.setClaimedQuantity(0);
        coupon.setDeleted(0);
        return coupon;
    }
}
