package com.starry.admin.modules.order.service.impl;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.starry.admin.common.exception.CustomException;
import com.starry.admin.modules.custom.module.entity.PlayCustomUserInfoEntity;
import com.starry.admin.modules.custom.service.IPlayCustomUserInfoService;
import com.starry.admin.modules.order.mapper.PlayOrderInfoMapper;
import com.starry.admin.modules.order.module.constant.OrderConstant.BalanceOperationType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OperatorType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundFlag;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundRecordType;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderRefundState;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderStatus;
import com.starry.admin.modules.order.module.constant.OrderConstant.OrderTriggerSource;
import com.starry.admin.modules.order.module.constant.OrderConstant.ReviewRequirement;
import com.starry.admin.modules.order.module.dto.OrderCompletionContext;
import com.starry.admin.modules.order.module.dto.OrderRefundContext;
import com.starry.admin.modules.order.module.entity.PlayOrderInfoEntity;
import com.starry.admin.modules.order.service.IPlayOrderRefundInfoService;
import com.starry.admin.modules.weichat.service.WxCustomMpService;
import com.starry.admin.modules.withdraw.entity.EarningsLineEntity;
import com.starry.admin.modules.withdraw.service.IEarningsService;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
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.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class OrderLifecycleServiceImplTest {

    @InjectMocks
    private OrderLifecycleServiceImpl lifecycleService;

    @Mock
    private PlayOrderInfoMapper orderInfoMapper;

    @Mock
    private IEarningsService earningsService;

    @Mock
    private WxCustomMpService wxCustomMpService;

    @Mock
    private IPlayOrderRefundInfoService orderRefundInfoService;

    @Mock
    private IPlayCustomUserInfoService customUserInfoService;

    @Test
    void completeOrder_inProgress_createsEarningsAndNotifies() {
        String orderId = UUID.randomUUID().toString();
        PlayOrderInfoEntity inProgress = buildOrder(orderId, OrderStatus.IN_PROGRESS.getCode());
        inProgress.setOrderEndTime(null);

        PlayOrderInfoEntity completed = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
        completed.setOrderEndTime(LocalDateTime.now());

        when(orderInfoMapper.selectById(orderId)).thenReturn(inProgress, completed);
        when(orderInfoMapper.updateById(any(PlayOrderInfoEntity.class))).thenReturn(1);
        mockEarningsCounts(0L, 1L);

        OrderCompletionContext context = OrderCompletionContext.of(
                OperatorType.CLERK.getCode(),
                inProgress.getAcceptBy(),
                OrderTriggerSource.WX_CLERK);

        lifecycleService.completeOrder(orderId, context);

        verify(orderInfoMapper).updateById(argThat(entity ->
                orderId.equals(entity.getId())
                        && OrderStatus.COMPLETED.getCode().equals(entity.getOrderStatus())
                        && entity.getOrderEndTime() != null));
        verify(earningsService).createFromOrder(completed);
        verify(wxCustomMpService).sendOrderFinishMessageAsync(completed);
    }

    @Test
    void completeOrder_alreadyCompleted_skipsEarningsCreation() {
        String orderId = UUID.randomUUID().toString();
        PlayOrderInfoEntity alreadyCompleted = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
        alreadyCompleted.setOrderEndTime(null);

        PlayOrderInfoEntity completedWithEnd = buildOrder(orderId, OrderStatus.COMPLETED.getCode());
        completedWithEnd.setOrderEndTime(LocalDateTime.now());

        when(orderInfoMapper.selectById(orderId)).thenReturn(alreadyCompleted, completedWithEnd);
        when(orderInfoMapper.updateById(any(PlayOrderInfoEntity.class))).thenReturn(1);
        mockEarningsCounts(1L);

        lifecycleService.completeOrder(orderId, OrderCompletionContext.of(
                OperatorType.ADMIN.getCode(),
                "admin-1",
                OrderTriggerSource.ADMIN_CONSOLE));

        verify(earningsService, never()).createFromOrder(any());
        verify(wxCustomMpService).sendOrderFinishMessageAsync(completedWithEnd);
    }

    @Test
    void refundOrder_partialAmount_updatesLedgerAndRecords() {
        String orderId = UUID.randomUUID().toString();
        BigDecimal finalAmount = BigDecimal.valueOf(100);
        BigDecimal refundAmount = BigDecimal.valueOf(40);

        PlayOrderInfoEntity order = buildOrder(orderId, OrderStatus.ACCEPTED.getCode());
        order.setFinalAmount(finalAmount);
        order.setOrderMoney(finalAmount);
        order.setRefundType(OrderRefundFlag.NOT_REFUNDED.getCode());
        order.setPayMethod("1");

        when(orderInfoMapper.selectById(orderId)).thenReturn(order);
        when(orderInfoMapper.updateById(any(PlayOrderInfoEntity.class))).thenReturn(1);

        PlayCustomUserInfoEntity customer = new PlayCustomUserInfoEntity();
        customer.setId(order.getPurchaserBy());
        customer.setAccountBalance(BigDecimal.valueOf(10));
        when(customUserInfoService.getById(order.getPurchaserBy())).thenReturn(customer);

        OrderRefundContext context = new OrderRefundContext();
        context.setOrderId(orderId);
        context.setRefundAmount(refundAmount);
        context.setRefundReason("部分退款测试");
        context.setOperatorType(OperatorType.ADMIN.getCode());
        context.setOperatorId("admin-1");
        context.withTriggerSource(OrderTriggerSource.ADMIN_API);

        lifecycleService.refundOrder(context);

        ArgumentCaptor<PlayOrderInfoEntity> updateCaptor = ArgumentCaptor.forClass(PlayOrderInfoEntity.class);
        verify(orderInfoMapper).updateById(updateCaptor.capture());
        PlayOrderInfoEntity updated = updateCaptor.getValue();
        assertEquals(OrderStatus.CANCELLED.getCode(), updated.getOrderStatus());
        assertEquals(OrderRefundFlag.REFUNDED.getCode(), updated.getRefundType());
        assertEquals(refundAmount, updated.getRefundAmount());

        verify(customUserInfoService).updateAccountBalanceById(
                eq(order.getPurchaserBy()),
                eq(customer.getAccountBalance()),
                eq(customer.getAccountBalance().add(refundAmount)),
                eq(BalanceOperationType.REFUND.getCode()),
                eq("订单退款"),
                eq(refundAmount),
                eq(BigDecimal.ZERO),
                eq(orderId));

        verify(orderRefundInfoService).add(
                eq(orderId),
                eq(order.getPurchaserBy()),
                eq(order.getAcceptBy()),
                eq(order.getPayMethod()),
                eq(OrderRefundRecordType.PARTIAL.getCode()),
                eq(refundAmount),
                eq("部分退款测试"),
                eq(OperatorType.ADMIN.getCode()),
                eq("admin-1"),
                eq(OrderRefundState.PROCESSING.getCode()),
                eq(ReviewRequirement.NOT_REQUIRED.getCode()));
    }

    @Test
    void refundOrder_throwsWhenAlreadyRefunded() {
        String orderId = UUID.randomUUID().toString();
        PlayOrderInfoEntity order = buildOrder(orderId, OrderStatus.ACCEPTED.getCode());
        order.setFinalAmount(BigDecimal.TEN);
        order.setOrderMoney(BigDecimal.TEN);
        order.setRefundType(OrderRefundFlag.REFUNDED.getCode());

        when(orderInfoMapper.selectById(orderId)).thenReturn(order);

        OrderRefundContext context = new OrderRefundContext();
        context.setOrderId(orderId);
        context.setRefundAmount(BigDecimal.ONE);
        context.withTriggerSource(OrderTriggerSource.ADMIN_API);

        assertThrows(CustomException.class, () -> lifecycleService.refundOrder(context));
        verify(orderInfoMapper, never()).updateById(any());
        verify(orderRefundInfoService, never()).add(anyString(), anyString(), anyString(), anyString(), anyString(), any(), anyString(), anyString(), anyString(), anyString(), anyString());
    }

    private PlayOrderInfoEntity buildOrder(String orderId, String status) {
        PlayOrderInfoEntity entity = new PlayOrderInfoEntity();
        entity.setId(orderId);
        entity.setOrderStatus(status);
        entity.setAcceptBy("clerk-1");
        entity.setPurchaserBy("customer-1");
        entity.setTenantId("tenant-1");
        return entity;
    }

    private void mockEarningsCounts(long... counts) {
        LambdaQueryChainWrapper<EarningsLineEntity> chain = Mockito.mock(LambdaQueryChainWrapper.class);
        when(chain.eq(any(), any())).thenReturn(chain);
        if (counts.length == 0) {
            when(chain.count()).thenReturn(0L);
        } else {
            org.mockito.stubbing.OngoingStubbing<Long> stubbing = when(chain.count()).thenReturn(counts[0]);
            for (int i = 1; i < counts.length; i++) {
                stubbing = stubbing.thenReturn(counts[i]);
            }
        }
        when(earningsService.lambdaQuery()).thenReturn(chain);
    }
}
