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

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.blindbox.mapper.BlindBoxPoolMapper;
import com.starry.admin.modules.blindbox.mapper.BlindBoxRewardMapper;
import com.starry.admin.modules.blindbox.module.constant.BlindBoxRewardStatus;
import com.starry.admin.modules.blindbox.module.dto.BlindBoxCandidate;
import com.starry.admin.modules.blindbox.module.entity.BlindBoxConfigEntity;
import com.starry.admin.modules.blindbox.module.entity.BlindBoxRewardEntity;
import com.starry.admin.modules.order.module.dto.OrderPlacementResult;
import java.math.BigDecimal;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
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.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class BlindBoxServiceTest {

    @Mock
    private BlindBoxPoolMapper poolMapper;

    @Mock
    private BlindBoxRewardMapper rewardMapper;

    @Mock
    private BlindBoxInventoryService inventoryService;

    @Mock
    private BlindBoxDispatchService dispatchService;

    @Mock
    private BlindBoxConfigService configService;

    private Clock clock;
    private TestRandomAdapter randomAdapter;
    private BlindBoxService blindBoxService;

    @BeforeEach
    void setUp() {
        clock = Clock.fixed(Instant.parse("2024-08-01T10:15:30Z"), ZoneOffset.UTC);
        randomAdapter = new TestRandomAdapter();
        blindBoxService = new BlindBoxService(poolMapper, rewardMapper, inventoryService, dispatchService, configService,
                clock, Duration.ofDays(7), randomAdapter);

        lenient().when(rewardMapper.insert(any())).thenAnswer(invocation -> {
            BlindBoxRewardEntity entity = invocation.getArgument(0);
            if (entity.getId() == null) {
                entity.setId(UUID.randomUUID().toString());
            }
            return 1;
        });
    }

    @Test
    void shouldCreateRewardRecordWhenOrderCompleted() {
        randomAdapter.nextDoubleToReturn = 0.95;
        BlindBoxConfigEntity config = new BlindBoxConfigEntity();
        config.setId("blind-1");
        config.setTenantId("tenant-1");
        config.setPrice(BigDecimal.valueOf(49));
        when(configService.requireById("blind-1")).thenReturn(config);
        List<BlindBoxCandidate> candidates = Arrays.asList(
                BlindBoxCandidate.of(1L, "tenant-1", "blind-1", "gift-low", BigDecimal.valueOf(10), 10, 5),
                BlindBoxCandidate.of(2L, "tenant-1", "blind-1", "gift-high", BigDecimal.valueOf(99), 90, 3)
        );
        when(poolMapper.listActiveEntries(eq("tenant-1"), eq("blind-1"), any(LocalDateTime.class))).thenReturn(candidates);

        BlindBoxRewardEntity entity = blindBoxService.drawReward("tenant-1", "order-1", "customer-1", "blind-1",
                "seed-123");

        ArgumentCaptor<BlindBoxRewardEntity> captor = ArgumentCaptor.forClass(BlindBoxRewardEntity.class);
        verify(rewardMapper).insert(captor.capture());
        BlindBoxRewardEntity persisted = captor.getValue();

        assertEquals("gift-high", persisted.getRewardGiftId());
        assertEquals(BigDecimal.valueOf(99).setScale(2), persisted.getRewardPrice());
        assertEquals(BigDecimal.valueOf(49).setScale(2), persisted.getBoxPrice());
        assertEquals(BlindBoxRewardStatus.UNUSED.getCode(), persisted.getStatus());
        assertEquals(LocalDateTime.ofInstant(clock.instant(), clock.getZone()).plusDays(7), persisted.getExpiresAt());
        verify(inventoryService).reserveRewardStock("tenant-1", 2L, "gift-high");
        assertEquals(entity.getRewardGiftId(), persisted.getRewardGiftId());
    }

    @Test
    void shouldHandleUnlimitedStock() {
        BlindBoxConfigEntity config = new BlindBoxConfigEntity();
        config.setId("blind-1");
        config.setTenantId("tenant-1");
        config.setPrice(BigDecimal.valueOf(29));
        when(configService.requireById("blind-1")).thenReturn(config);
        List<BlindBoxCandidate> candidates = Collections.singletonList(
                BlindBoxCandidate.of(7L, "tenant-1", "blind-1", "gift-unlimited", BigDecimal.valueOf(59), 100, null)
        );
        when(poolMapper.listActiveEntries(eq("tenant-1"), eq("blind-1"), any(LocalDateTime.class))).thenReturn(candidates);

        blindBoxService.drawReward("tenant-1", "order-2", "customer-9", "blind-1", "seed-unlimited");

        ArgumentCaptor<BlindBoxRewardEntity> captor = ArgumentCaptor.forClass(BlindBoxRewardEntity.class);
        verify(rewardMapper).insert(captor.capture());
        BlindBoxRewardEntity persisted = captor.getValue();
        assertEquals("gift-unlimited", persisted.getRewardGiftId());
        assertEquals(BigDecimal.valueOf(29).setScale(2), persisted.getBoxPrice());
        assertEquals(BigDecimal.valueOf(59).setScale(2), persisted.getRewardPrice());
        assertEquals(BlindBoxRewardStatus.UNUSED.getCode(), persisted.getStatus());
        assertNull(persisted.getRewardStockSnapshot());
        verify(inventoryService).reserveRewardStock("tenant-1", 7L, "gift-unlimited");
    }

    @Test
    void shouldPreventDoubleDispatch() {
        BlindBoxRewardEntity reward = buildRewardEntity();
        when(rewardMapper.lockByIdForUpdate("reward-1")).thenReturn(reward);
        when(dispatchService.dispatchRewardOrder(eq(reward), eq("clerk-1"))).thenReturn(mock(OrderPlacementResult.class));
        when(rewardMapper.markUsed(eq("reward-1"), eq("clerk-1"), any(), any())).thenReturn(1);

        blindBoxService.dispatchReward("reward-1", "clerk-1", "customer-1");

        reward.setStatus(BlindBoxRewardStatus.USED.getCode());
        CustomException ex = assertThrows(CustomException.class, () ->
                blindBoxService.dispatchReward("reward-1", "clerk-1", "customer-1"));
        assertTrue(ex.getMessage().contains("已使用"));

        verify(rewardMapper, times(1)).markUsed(eq("reward-1"), eq("clerk-1"), any(), any());
    }

    @Test
    void shouldRejectExpiredReward() {
        BlindBoxRewardEntity reward = buildRewardEntity();
        reward.setExpiresAt(LocalDateTime.ofInstant(clock.instant(), clock.getZone()).minusHours(1));
        when(rewardMapper.lockByIdForUpdate("reward-1")).thenReturn(reward);

        CustomException ex = assertThrows(CustomException.class, () ->
                blindBoxService.dispatchReward("reward-1", "clerk-1", "customer-1"));
        assertTrue(ex.getMessage().contains("已过期"));
        verify(dispatchService, times(0)).dispatchRewardOrder(any(), any());
    }

    @Test
    void shouldRejectDispatchWhenCustomerMismatch() {
        BlindBoxRewardEntity reward = buildRewardEntity();
        when(rewardMapper.lockByIdForUpdate("reward-1")).thenReturn(reward);

        CustomException ex = assertThrows(CustomException.class, () ->
                blindBoxService.dispatchReward("reward-1", "clerk-1", "someone-else"));
        assertTrue(ex.getMessage().contains("无权"));
        verify(dispatchService, times(0)).dispatchRewardOrder(any(), any());
        verify(rewardMapper, times(0)).markUsed(any(), any(), any(), any());
    }

    @Test
    void shouldRejectWhenNoEligibleRewardExists() {
        BlindBoxConfigEntity config = new BlindBoxConfigEntity();
        config.setId("blind-1");
        config.setTenantId("tenant-1");
        when(configService.requireById("blind-1")).thenReturn(config);
        when(poolMapper.listActiveEntries(eq("tenant-1"), eq("blind-1"), any(LocalDateTime.class)))
                .thenReturn(Collections.emptyList());

        CustomException ex = assertThrows(CustomException.class,
                () -> blindBoxService.drawReward("tenant-1", "order-404", "customer-9", "blind-1", "seed-out"));
        assertTrue(ex.getMessage().contains("奖池暂无可用奖励"));
        verify(inventoryService, times(0)).reserveRewardStock(any(), any(), any());
    }

    private BlindBoxRewardEntity buildRewardEntity() {
        BlindBoxRewardEntity reward = new BlindBoxRewardEntity();
        reward.setId("reward-1");
        reward.setTenantId("tenant-1");
        reward.setCustomerId("customer-1");
        reward.setBlindBoxId("blind-1");
        reward.setRewardGiftId("gift-high");
        reward.setRewardPrice(BigDecimal.valueOf(99));
        reward.setBoxPrice(BigDecimal.valueOf(49));
        reward.setRewardStockSnapshot(3);
        reward.setSeed("seed-123");
        reward.setStatus(BlindBoxRewardStatus.UNUSED.getCode());
        reward.setCreatedByOrder("order-1");
        reward.setCreatedTime(java.sql.Timestamp.from(clock.instant()));
        reward.setUpdatedTime(java.sql.Timestamp.from(clock.instant()));
        reward.setExpiresAt(LocalDateTime.ofInstant(clock.instant(), clock.getZone()).plusHours(1));
        return reward;
    }

    private static class TestRandomAdapter implements BlindBoxService.RandomAdapter {

        double nextDoubleToReturn = 0.5;

        @Override
        public double nextDouble() {
            return nextDoubleToReturn;
        }
    }
}
