package com.ruoyi.system.omgpay.service;

import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.system.omgpay.domain.OmgPaymentAttempt;
import com.ruoyi.system.omgpay.mapper.OmgPaymentAttemptMapper;
import com.ruoyi.system.omgpay.service.impl.OmgPaymentAttemptServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

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

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.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class OmgPaymentAttemptServiceTest {

    @Mock
    private OmgPaymentAttemptMapper mapper;

    @InjectMocks
    private OmgPaymentAttemptServiceImpl service;

    @Test
    void createCreatedRejectsBlankDdIdUsingMessageUtils() {
        assertLocalizedValidationFailure("omg.payment.ddid.required",
                () -> service.createCreated("   ", "OMG123", 10L, "M1", 100, "KEY", "IV"));
    }

    @Test
    void createCreatedRejectsBlankMerchantTradeNoUsingMessageUtils() {
        assertLocalizedValidationFailure("omg.payment.merchantTradeNo.required",
                () -> service.createCreated("DD-1", "   ", 10L, "M1", 100, "KEY", "IV"));
    }

    @Test
    void createCreatedRejectsInvalidMerchantTradeNoUsingMessageUtils() {
        assertLocalizedValidationFailure("omg.payment.merchantTradeNo.invalid",
                () -> service.createCreated("DD-1", "omg-invalid", 10L, "M1", 100, "KEY", "IV"));
    }

    @Test
    void createCreatedRejectsNullStoreIdUsingMessageUtils() {
        assertLocalizedValidationFailure("omg.payment.storeId.required",
                () -> service.createCreated("DD-1", "OMG123", null, "M1", 100, "KEY", "IV"));
    }

    @Test
    void createCreatedRejectsBlankMerchantIdUsingMessageUtils() {
        assertLocalizedValidationFailure("omg.payment.merchantId.required",
                () -> service.createCreated("DD-1", "OMG123", 10L, "   ", 100, "KEY", "IV"));
    }

    @Test
    void createCreatedRejectsInvalidMerchantIdUsingMessageUtils() {
        assertLocalizedValidationFailure("omg.payment.merchantId.invalid",
                () -> service.createCreated("DD-1", "OMG123", 10L, "merchant-too-long", 100, "KEY", "IV"));
    }

    @Test
    void currentReadQueriesUseTrimmedKeys() {
        service.selectActiveCreatedByDdIdForUpdate(" DD-1 ");
        service.selectByMerchantTradeNoForUpdate(" OMG123 ");

        org.mockito.Mockito.verify(mapper).selectActiveCreatedByDdIdForUpdate("DD-1");
        org.mockito.Mockito.verify(mapper).selectByMerchantTradeNoForUpdate("OMG123");
    }

    @Test
    void createCreatedRejectsNonPositiveAmountUsingMessageUtils() {
        assertLocalizedValidationFailure("omg.payment.amount.invalid",
                () -> service.createCreated("DD-1", "OMG123", 10L, "M1", 0, "KEY", "IV"));
    }

    @Test
    void createCreatedInsertsOnlyCreatedFacts() {
        when(mapper.insertCreated(any())).thenAnswer(invocation -> {
            OmgPaymentAttempt row = invocation.getArgument(0);
            row.setId(7L);
            return 1;
        });

        OmgPaymentAttempt row = service.createCreated("DD-1", "OMG123", 10L, "M1", 100, "KEY", "IV");

        assertEquals(0, row.getAttemptStatus());
        assertEquals(7L, row.getId());
        assertEquals("KEY", row.getHashKeySnapshot());
        assertEquals("IV", row.getHashIvSnapshot());
        assertEquals(0, row.getQueryCount());
        assertEquals(row.getCreateTime(), row.getNextQueryTime());
        assertNull(row.getActiveDdId());
    }

    @Test
    void dueScanIsBoundedAndReservationRejectsInvalidSchedule() {
        Date now = new Date(1000L);
        when(mapper.selectDueCreated(now, 100)).thenReturn(List.of());

        service.scanDueCreated(now, 500);
        assertEquals(0, service.reserveDueQuery(7L, now, now));

        org.mockito.Mockito.verify(mapper).selectDueCreated(now, 100);
        org.mockito.Mockito.verify(mapper, org.mockito.Mockito.never())
                .reserveDueQuery(any(), any(), any());
    }

    @Test
    void createCreatedRejectsMissingCredentialSnapshot() {
        assertLocalizedValidationFailure("omg.payment.credential.snapshot.required",
                () -> service.createCreated("DD-1", "OMG123", 10L, "M1", 100, " ", "IV"));
    }

    @Test
    void supersedeCreatedRequiresIdAndDelegatesGuardedUpdate() {
        assertEquals(0, service.supersedeCreated(null));
        service.supersedeCreated(7L);

        verify(mapper).supersedeCreated(org.mockito.ArgumentMatchers.eq(7L), any(Date.class));
    }

    private void assertLocalizedValidationFailure(String key, ThrowingRunnable runnable) {
        try (var messages = mockStatic(MessageUtils.class)) {
            messages.when(() -> MessageUtils.message(key)).thenReturn("localized-" + key);

            ServiceException exception = assertThrows(ServiceException.class, runnable::run);

            assertEquals("localized-" + key, exception.getMessage());
            verifyNoInteractions(mapper);
        }
    }

    @FunctionalInterface
    private interface ThrowingRunnable {
        void run();
    }
}
