package com.ruoyi.app.task;

import com.ruoyi.app.pay.LinePayRefundService;
import com.ruoyi.app.pay.LinePayService;
import com.ruoyi.app.pay.LinePayOfflineService;
import com.ruoyi.app.pay.LinePayCancellationCompensationService;
import com.ruoyi.system.domain.PosOrderLinePayment;
import com.ruoyi.system.domain.PosOrderLineRefund;
import com.ruoyi.system.service.IPosOrderLinePaymentService;
import com.ruoyi.system.service.IPosOrderLineRefundService;
import org.junit.jupiter.api.Test;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;

import java.util.Date;
import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.doAnswer;
import org.springframework.test.util.ReflectionTestUtils;

class LinePayReconcileTaskTest {

    @Test
    void watchdogLockRowLeasesAndSingleFailureDoNotStopBatch() {
        IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
        IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
        LinePayService linePay = mock(LinePayService.class);
        LinePayRefundService lineRefund = mock(LinePayRefundService.class);
        RedissonClient redisson = mock(RedissonClient.class);
        RLock lock = mock(RLock.class);
        when(redisson.getLock("lock:line-pay:reconcile")).thenReturn(lock);
        when(lock.tryLock()).thenReturn(true);
        when(lock.isHeldByCurrentThread()).thenReturn(true);

        PosOrderLinePayment first = payment(1L);
        PosOrderLinePayment second = payment(2L);
        when(payments.scanDue(any(), anyInt())).thenReturn(List.of(first, second));
        when(payments.claimReconcileLease(any(), any(), anyString(), any(), any())).thenReturn(1);
        when(linePay.reconcilePayment(org.mockito.ArgumentMatchers.eq(1L),
                org.mockito.ArgumentMatchers.eq("TASK"), anyString()))
                .thenThrow(new IllegalStateException("one failure"));
        PosOrderLineRefund refund = new PosOrderLineRefund();
        refund.setPaymentId(9L);
        when(refunds.scanDue(any(), anyInt())).thenReturn(List.of(refund));

        when(payments.getCancelledPaidWithoutRefund(anyInt())).thenReturn(List.of());
        new LinePayReconcileTask(payments, refunds, linePay, lineRefund, redisson,
                mock(LinePayCancellationCompensationService.class)).reconcile();

        verify(lock).tryLock();
        verify(linePay).reconcilePayment(org.mockito.ArgumentMatchers.eq(2L),
                org.mockito.ArgumentMatchers.eq("TASK"), anyString());
        verify(lineRefund).requestFullRefund(9L, "TASK");
        verify(lock).unlock();
    }

    @Test
    void slowPaymentCannotStarveCancellationRepairOrRefundPhase() {
        IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
        IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
        LinePayService linePay = mock(LinePayService.class);
        LinePayRefundService lineRefund = mock(LinePayRefundService.class);
        LinePayCancellationCompensationService compensation =
                mock(LinePayCancellationCompensationService.class);
        RedissonClient redisson = mock(RedissonClient.class);
        RLock lock = mock(RLock.class);
        when(redisson.getLock("lock:line-pay:reconcile")).thenReturn(lock);
        when(lock.tryLock()).thenReturn(true);
        when(lock.isHeldByCurrentThread()).thenReturn(true);
        PosOrderLinePayment slow = payment(1L);
        when(payments.scanDue(any(), anyInt())).thenReturn(List.of(slow));
        when(payments.claimReconcileLease(any(), any(), anyString(), any(), any())).thenReturn(1);
        doAnswer(invocation -> {
            Thread.sleep(1_050L);
            return "WAITING_AUTH";
        }).when(linePay).reconcilePayment(any(), anyString(), anyString());
        when(payments.getCancelledPaidWithoutRefund(anyInt())).thenReturn(List.of());
        PosOrderLineRefund refund = new PosOrderLineRefund();
        refund.setPaymentId(9L);
        when(refunds.scanDue(any(), anyInt())).thenReturn(List.of(refund));
        LinePayReconcileTask task = new LinePayReconcileTask(payments, refunds, linePay,
                lineRefund, redisson, compensation);
        ReflectionTestUtils.setField(task, "roundBudgetSeconds", 1L);

        task.reconcile();

        verify(compensation).createMissingRefundIntents(any());
        verify(lineRefund).requestFullRefund(9L, "TASK");
    }

    @Test
    void offlineAttemptIsDispatchedToOfflineCheckOnly() {
        IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
        IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
        LinePayService online = mock(LinePayService.class);
        LinePayOfflineService offline = mock(LinePayOfflineService.class);
        LinePayRefundService lineRefund = mock(LinePayRefundService.class);
        RedissonClient redisson = mock(RedissonClient.class);
        RLock lock = mock(RLock.class);
        when(redisson.getLock("lock:line-pay:reconcile")).thenReturn(lock);
        when(lock.tryLock()).thenReturn(true);
        when(lock.isHeldByCurrentThread()).thenReturn(true);
        PosOrderLinePayment payment = payment(3L);
        payment.setPaymentMode("OFFLINE");
        when(payments.scanDue(any(), anyInt())).thenReturn(List.of(payment));
        when(payments.claimReconcileLease(any(), any(), anyString(), any(), any())).thenReturn(1);
        when(payments.getCancelledPaidWithoutRefund(anyInt())).thenReturn(List.of());
        when(refunds.scanDue(any(), anyInt())).thenReturn(List.of());

        new LinePayReconcileTask(payments, refunds, online, offline, lineRefund, redisson,
                mock(LinePayCancellationCompensationService.class)).reconcile();

        verify(offline).reconcilePayment(eq(3L), eq("TASK"));
        verify(online, org.mockito.Mockito.never()).reconcilePayment(any(), anyString(), anyString());
    }

    private static PosOrderLinePayment payment(Long id) {
        PosOrderLinePayment payment = new PosOrderLinePayment();
        payment.setId(id);
        payment.setVersion(0L);
        payment.setNextReconcileAt(new Date());
        return payment;
    }
}
