package com.ruoyi.app.omgpay;

import com.ruoyi.system.domain.PosStoreOmg;
import com.ruoyi.system.omgpay.domain.OmgPaymentAttempt;
import com.ruoyi.system.omgpay.domain.OmgPaymentOrderSnapshot;
import com.ruoyi.system.omgpay.service.IOmgPaymentAttemptService;
import com.ruoyi.system.service.IPosStoreOmgService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.dao.DuplicateKeyException;

import java.util.Map;
import java.util.stream.Stream;

import static com.ruoyi.app.omgpay.OmgPaymentErrorCode.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;

class OmgPaymentCreateServiceTest {
    private IOmgPaymentAttemptService attempts;
    private IPosStoreOmgService credentials;
    private OmgMerchantTradeNoGenerator generator;
    private OmgPaymentFormFactory formFactory;
    private OmgPaymentCreateService service;

    @BeforeEach
    void setUp() {
        attempts = mock(IOmgPaymentAttemptService.class);
        credentials = mock(IPosStoreOmgService.class);
        generator = mock(OmgMerchantTradeNoGenerator.class);
        formFactory = mock(OmgPaymentFormFactory.class);
        service = new OmgPaymentCreateService(attempts, credentials, generator, formFactory);
    }

    @ParameterizedTest(name = "{0}")
    @MethodSource("invalidOrders")
    void rejectsInvalidOrderFactsBeforeGeneration(String name, OmgPaymentOrderSnapshot order,
                                                   OmgPaymentErrorCode expected) {
        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order);

        OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
                () -> service.create(5L, "DD-1", OmgPaymentMethod.CREDIT));

        assertEquals(expected, error.getCode());
        verify(attempts, never()).createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt(),
                anyString(), anyString());
        verifyNoInteractions(generator, formFactory);
    }

    @Test
    void rejectsExistingCreatedAttemptBeforeCredentialOrFormWork() {
        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(payableOrder());
        when(attempts.selectActiveCreatedByDdIdForUpdate("DD-1")).thenReturn(new OmgPaymentAttempt());
        OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
                () -> service.create(5L, "DD-1", OmgPaymentMethod.CREDIT));
        assertEquals(PAYMENT_ATTEMPT_EXISTS, error.getCode());
        verifyNoInteractions(generator, formFactory, credentials);
    }

    @Test
    void replacesOnlyTheExactVerifiedActiveAttemptAndReturnsNewForm() {
        OmgPaymentAttempt current = new OmgPaymentAttempt();
        current.setId(8L);
        current.setMerchantTradeNo("OMGOLD");
        OmgPaymentAttempt created = new OmgPaymentAttempt();
        created.setId(9L);
        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(payableOrder());
        when(attempts.selectActiveCreatedByDdIdForUpdate("DD-1")).thenReturn(current);
        when(credentials.getEnabledCredential(77L)).thenReturn(credential());
        when(attempts.supersedeCreated(8L)).thenReturn(1);
        when(generator.generate()).thenReturn("OMG12345678901234567");
        when(formFactory.create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString(),
                any(OmgPaymentMethod.class)))
                .thenReturn(new OmgPaymentForm("stage", Map.of()));
        when(attempts.createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt(),
                anyString(), anyString())).thenReturn(created);

        OmgPaymentCreateOutcome outcome = service.replaceActiveForRetry(5L, "DD-1", "OMGOLD",
                OmgPaymentMethod.CREDIT);

        assertEquals(9L, outcome.attemptId());
        var orderOfCalls = inOrder(attempts, credentials, generator, formFactory);
        orderOfCalls.verify(attempts).selectOrderForUpdate("DD-1");
        orderOfCalls.verify(attempts).selectActiveCreatedByDdIdForUpdate("DD-1");
        orderOfCalls.verify(credentials).getEnabledCredential(77L);
        orderOfCalls.verify(attempts).supersedeCreated(8L);
        orderOfCalls.verify(generator).generate();
        orderOfCalls.verify(formFactory).create("DD-1", 100, "1000031", "KEY", "IV",
                "OMG12345678901234567", OmgPaymentMethod.CREDIT);
        orderOfCalls.verify(attempts).createCreated("DD-1", "OMG12345678901234567", 77L,
                "1000031", 100, "KEY", "IV");
    }

    @Test
    void rejectsRetryWhenActiveAttemptChangedAfterGatewayQuery() {
        OmgPaymentAttempt current = new OmgPaymentAttempt();
        current.setId(9L);
        current.setMerchantTradeNo("OMGNEW");
        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(payableOrder());
        when(attempts.selectActiveCreatedByDdIdForUpdate("DD-1")).thenReturn(current);

        OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
                () -> service.replaceActiveForRetry(5L, "DD-1", "OMGOLD", OmgPaymentMethod.CREDIT));

        assertEquals(PAYMENT_RETRY_NOT_AVAILABLE, error.getCode());
        verifyNoInteractions(credentials, generator, formFactory);
        verify(attempts, never()).supersedeCreated(anyLong());
    }

    @Test
    void createsAttemptAndReturnsOnlyAuthorizedFormAndSafeMetadata() {
        OmgPaymentOrderSnapshot order = payableOrder();
        PosStoreOmg credential = credential();
        OmgPaymentAttempt attempt = new OmgPaymentAttempt();
        attempt.setId(9L);
        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order);
        when(credentials.getEnabledCredential(77L)).thenReturn(credential);
        when(generator.generate()).thenReturn("OMG12345678901234567");
        when(formFactory.create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString(),
                any(OmgPaymentMethod.class)))
                .thenReturn(new OmgPaymentForm(OmgPaymentFormFactory.STAGE_GATEWAY_URL, Map.of("CheckMacValue", "A".repeat(64))));
        when(attempts.createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt(),
                anyString(), anyString())).thenReturn(attempt);

        OmgPaymentCreateOutcome outcome = service.create(5L, "DD-1", OmgPaymentMethod.CREDIT);

        assertEquals(9L, outcome.attemptId());
        assertEquals("CREATED", outcome.response().status());
        assertEquals("OMG12***4567", outcome.maskedMerchantTradeNo());
        assertTrue(outcome.response().formFields().containsKey("CheckMacValue"));
        var orderOfCalls = inOrder(attempts, credentials, generator, formFactory);
        orderOfCalls.verify(attempts).selectOrderForUpdate("DD-1");
        orderOfCalls.verify(attempts).selectActiveCreatedByDdIdForUpdate("DD-1");
        orderOfCalls.verify(credentials).getEnabledCredential(77L);
        orderOfCalls.verify(generator).generate();
        orderOfCalls.verify(formFactory).create("DD-1", 100, "1000031", "KEY", "IV",
                "OMG12345678901234567", OmgPaymentMethod.CREDIT);
        orderOfCalls.verify(attempts).createCreated("DD-1", "OMG12345678901234567", 77L, "1000031", 100,
                "KEY", "IV");
    }

    @Test
    void duplicateActiveRowWinsOverTradeNumberRetry() {
        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(payableOrder());
        when(credentials.getEnabledCredential(77L)).thenReturn(credential());
        when(generator.generate()).thenReturn("OMG12345678901234567");
        when(formFactory.create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString(),
                any(OmgPaymentMethod.class)))
                .thenReturn(new OmgPaymentForm("stage", Map.of()));
        when(attempts.createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt(),
                anyString(), anyString()))
                .thenThrow(new DuplicateKeyException("collision"));
        when(attempts.selectActiveCreatedByDdIdForUpdate("DD-1")).thenReturn(null, new OmgPaymentAttempt());

        OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
                () -> service.create(5L, "DD-1", OmgPaymentMethod.CREDIT));
        assertEquals(PAYMENT_ATTEMPT_EXISTS, error.getCode());
        verify(generator, times(1)).generate();
    }

    @Test
    void retriesOnlyConfirmedTradeNumberCollisionAndSucceeds() {
        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(payableOrder());
        when(credentials.getEnabledCredential(77L)).thenReturn(credential());
        when(generator.generate()).thenReturn("OMG11111111111111111", "OMG22222222222222222");
        when(formFactory.create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString(),
                any(OmgPaymentMethod.class)))
                .thenReturn(new OmgPaymentForm("stage", Map.of()));
        OmgPaymentAttempt inserted = new OmgPaymentAttempt();
        inserted.setId(11L);
        when(attempts.createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt(),
                anyString(), anyString()))
                .thenThrow(new DuplicateKeyException("trade collision")).thenReturn(inserted);
        when(attempts.selectByMerchantTradeNoForUpdate("OMG11111111111111111"))
                .thenReturn(new OmgPaymentAttempt());

        assertEquals(11L, service.create(5L, "DD-1", OmgPaymentMethod.CREDIT).attemptId());
        verify(generator, times(2)).generate();
        verify(formFactory, times(2)).create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString(),
                eq(OmgPaymentMethod.CREDIT));
    }

    @Test
    void stopsAfterThreeConfirmedTradeNumberCollisions() {
        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(payableOrder());
        when(credentials.getEnabledCredential(77L)).thenReturn(credential());
        when(generator.generate()).thenReturn("OMG11111111111111111", "OMG22222222222222222",
                "OMG33333333333333333");
        when(formFactory.create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString(),
                any(OmgPaymentMethod.class)))
                .thenReturn(new OmgPaymentForm("stage", Map.of()));
        when(attempts.createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt(),
                anyString(), anyString()))
                .thenThrow(new DuplicateKeyException("trade collision"));
        when(attempts.selectByMerchantTradeNoForUpdate(anyString())).thenReturn(new OmgPaymentAttempt());

        OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
                () -> service.create(5L, "DD-1", OmgPaymentMethod.CREDIT));
        assertEquals(PAYMENT_CREATION_FAILED, error.getCode());
        verify(generator, times(3)).generate();
    }

    private static Stream<Arguments> invalidOrders() {
        OmgPaymentOrderSnapshot otherOwner = payableOrder();
        otherOwner.setUserId(6L);
        OmgPaymentOrderSnapshot child = payableOrder();
        child.setParentDdId("PARENT");
        OmgPaymentOrderSnapshot noStore = payableOrder();
        noStore.setStoreId(null);
        OmgPaymentOrderSnapshot stateThree = payableOrder();
        stateThree.setState(3L);
        OmgPaymentOrderSnapshot stateFour = payableOrder();
        stateFour.setState(4L);
        OmgPaymentOrderSnapshot paid = payableOrder();
        paid.setPayStatus(1L);
        OmgPaymentOrderSnapshot wrongType = payableOrder();
        wrongType.setPayType("1");
        OmgPaymentOrderSnapshot zeroAmount = payableOrder();
        zeroAmount.setAmount(0);
        return Stream.of(
                Arguments.of("missing order", null, ORDER_NOT_AVAILABLE),
                Arguments.of("other owner", otherOwner, ORDER_NOT_AVAILABLE),
                Arguments.of("multi-store child", child, MULTI_STORE_ORDER_NOT_SUPPORTED),
                Arguments.of("missing store", noStore, ORDER_NOT_AVAILABLE),
                Arguments.of("state 3", stateThree, ORDER_STATE_NOT_PAYABLE),
                Arguments.of("state 4", stateFour, ORDER_STATE_NOT_PAYABLE),
                Arguments.of("already paid", paid, ORDER_ALREADY_PAID),
                Arguments.of("wrong payment type", wrongType, PAYMENT_TYPE_INVALID),
                Arguments.of("nonpositive amount", zeroAmount, ORDER_AMOUNT_INVALID));
    }

    @Test
    void rejectsMissingCredentialBeforeGenerationOrInsert() {
        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(payableOrder());
        when(credentials.getEnabledCredential(77L)).thenReturn(null);

        OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
                () -> service.create(5L, "DD-1", OmgPaymentMethod.CREDIT));

        assertEquals(STORE_CREDENTIAL_UNAVAILABLE, error.getCode());
        verifyNoInteractions(generator, formFactory);
        verify(attempts, never()).createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt(),
                anyString(), anyString());
    }

    @Test
    void rejectsNullPaymentMethodBeforeAnyDependencyInteraction() {
        OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
                () -> service.create(5L, "DD-1", null));

        assertEquals(PAYMENT_METHOD_INVALID, error.getCode());
        verifyNoInteractions(attempts, credentials, generator, formFactory);
    }

    private static OmgPaymentOrderSnapshot payableOrder() {
        OmgPaymentOrderSnapshot order = new OmgPaymentOrderSnapshot();
        order.setDdId("DD-1");
        order.setParentDdId("DD-1");
        order.setStoreId(77L);
        order.setUserId(5L);
        order.setAmount(100);
        order.setState(0L);
        order.setPayStatus(0L);
        order.setPayType("2");
        return order;
    }

    private static PosStoreOmg credential() {
        PosStoreOmg credential = new PosStoreOmg();
        credential.setMerchantId("1000031");
        credential.setHashKey("KEY");
        credential.setHashIv("IV");
        return credential;
    }
}
