package com.ruoyi.app.omgpay;

import com.ruoyi.app.omgpay.dto.OmgQueryPaymentResponse;
import com.ruoyi.system.omgpay.domain.OmgPaymentAttempt;
import com.ruoyi.system.omgpay.service.IOmgPaymentAttemptService;
import org.junit.jupiter.api.BeforeEach;
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.eq;
import static org.mockito.Mockito.*;

class OmgPaymentAutoCompensationTaskTest {
    private IOmgPaymentAttemptService attempts;
    private OmgPaymentQueryService queryService;
    private RedissonClient redisson;
    private RLock lock;
    private OmgPaymentAutoCompensationTask task;

    @BeforeEach
    void setUp() {
        attempts = mock(IOmgPaymentAttemptService.class);
        queryService = mock(OmgPaymentQueryService.class);
        redisson = mock(RedissonClient.class);
        lock = mock(RLock.class);
        task = new OmgPaymentAutoCompensationTask(attempts, queryService, redisson);
        task.batchSize = 20;
        task.retryDelayMs = 300000L;
        task.roundBudgetSeconds = 45L;
    }

    @Test
    void skipsRoundWhenAnotherNodeOwnsTheLock() {
        when(redisson.getLock("lock:omgpay:automatic-compensation")).thenReturn(lock);
        when(lock.tryLock()).thenReturn(false);

        task.reconcile();

        verifyNoInteractions(attempts, queryService);
        verify(lock, never()).unlock();
    }

    @Test
    void reservesDueAttemptsAndContinuesAfterOneGatewayFailure() {
        OmgPaymentAttempt first = attempt(7L, "DD-1", "OMG123");
        OmgPaymentAttempt second = attempt(8L, "DD-2", "OMG456");
        when(redisson.getLock("lock:omgpay:automatic-compensation")).thenReturn(lock);
        when(lock.tryLock()).thenReturn(true);
        when(lock.isHeldByCurrentThread()).thenReturn(true);
        when(attempts.scanDueCreated(any(Date.class), eq(20))).thenReturn(List.of(first, second));
        when(attempts.reserveDueQuery(eq(7L), any(Date.class), any(Date.class))).thenReturn(1);
        when(attempts.reserveDueQuery(eq(8L), any(Date.class), any(Date.class))).thenReturn(1);
        when(queryService.reconcile(first)).thenThrow(new RuntimeException("temporary"));
        when(queryService.reconcile(second)).thenReturn(response("UNPAID", "0"));

        task.reconcile();

        verify(queryService).reconcile(first);
        verify(queryService).reconcile(second);
        verify(lock).unlock();
    }

    @Test
    void doesNotQueryAttemptThatLostTheReservationRace() {
        OmgPaymentAttempt attempt = attempt(7L, "DD-1", "OMG123");
        when(attempts.scanDueCreated(any(Date.class), anyInt())).thenReturn(List.of(attempt));
        when(attempts.reserveDueQuery(eq(7L), any(Date.class), any(Date.class))).thenReturn(0);

        task.reconcileRound();

        verifyNoInteractions(queryService);
    }

    private static OmgPaymentAttempt attempt(Long id, String ddId, String merchantTradeNo) {
        OmgPaymentAttempt result = new OmgPaymentAttempt();
        result.setId(id);
        result.setDdId(ddId);
        result.setMerchantTradeNo(merchantTradeNo);
        result.setStoreId(77L);
        return result;
    }

    private static OmgQueryPaymentResponse response(String status, String tradeStatus) {
        return new OmgQueryPaymentResponse(status, tradeStatus, "OMG456", null,
                100, null, null, null, "0", "0.00");
    }
}
