package com.ruoyi.app.omgpay;

import com.ruoyi.app.omgpay.dto.OmgQueryPaymentResponse;
import com.ruoyi.system.omgpay.domain.OmgPaymentAttempt;
import com.ruoyi.system.omgpay.domain.OmgPaymentOrderSnapshot;
import com.ruoyi.system.omgpay.service.IOmgPaymentAttemptService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.LinkedHashMap;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.Mockito.*;

class OmgPaymentQueryServiceTest {
    private static final String HASH_KEY = "5294y06JbISpM5x9";
    private static final String HASH_IV = "v77hoKGq4kWxNNIS";

    private IOmgPaymentAttemptService attempts;
    private OmgPaymentQueryGateway gateway;
    private OmgPaymentNotifyService settlement;
    private OmgPaymentQueryService service;
    private OmgPaymentAttempt attempt;

    @BeforeEach
    void setUp() {
        attempts = mock(IOmgPaymentAttemptService.class);
        gateway = mock(OmgPaymentQueryGateway.class);
        settlement = mock(OmgPaymentNotifyService.class);
        service = new OmgPaymentQueryService(attempts, gateway, new OmgQueryResponseParser(),
                new OmgCheckMacSigner(), settlement,
                Clock.fixed(Instant.ofEpochSecond(1_786_598_400L), ZoneOffset.UTC));
        attempt = attempt(0);
        when(attempts.selectOrder("DD-1")).thenReturn(order(0L));
        when(attempts.selectActiveCreatedByDdId("DD-1")).thenReturn(attempt);
    }

    @Test
    void paidGatewayResultCompensatesAQuitePossiblyLostCallback() {
        when(gateway.query(anyMap())).thenReturn(signedResponse("1", Map.of("FutureEmptyField", "")));
        when(settlement.synchronizeVerifiedQuery(any())).thenReturn(OmgPaymentSettlementResult.PAID);

        OmgQueryPaymentResponse response = service.query(5L, "DD-1");

        assertEquals("PAID", response.status());
        assertEquals("1", response.tradeStatus());
        verify(settlement).synchronizeVerifiedQuery(argThat(facts ->
                facts.source() == OmgPaymentFactSource.QUERY
                        && facts.merchantTradeNo().equals("OMG123")
                        && facts.resultCode() == 1));
        @SuppressWarnings("unchecked")
        var request = org.mockito.ArgumentCaptor.forClass(Map.class);
        verify(gateway).query(request.capture());
        assertEquals("1000031", request.getValue().get("MerchantID"));
        assertEquals("OMG123", request.getValue().get("MerchantTradeNo"));
        assertEquals("1786598400", request.getValue().get("TimeStamp"));
        assertEquals(4, request.getValue().size());
    }

    @Test
    void schedulerReconciliationUsesTheSameVerifiedSettlementPath() {
        when(gateway.query(anyMap())).thenReturn(signedResponse("1", Map.of()));
        when(settlement.synchronizeVerifiedQuery(any())).thenReturn(OmgPaymentSettlementResult.PAID);

        assertEquals("PAID", service.reconcile(attempt).status());

        verify(settlement).synchronizeVerifiedQuery(argThat(facts ->
                facts.source() == OmgPaymentFactSource.QUERY
                        && facts.merchantTradeNo().equals("OMG123")));
        verify(attempts, never()).selectOrder(anyString());
    }

    @Test
    void unpaidResultIsReadOnlyAndExplicitFailureIsSynchronized() {
        when(gateway.query(anyMap())).thenReturn(signedResponse("0", Map.of()));
        assertEquals("UNPAID", service.query(5L, "DD-1").status());
        verify(settlement, never()).synchronizeVerifiedQuery(any());

        reset(gateway, settlement);
        when(gateway.query(anyMap())).thenReturn(signedResponse("10200095", Map.of()));
        when(settlement.synchronizeVerifiedQuery(any())).thenReturn(OmgPaymentSettlementResult.FAILED);
        assertEquals("FAILED", service.query(5L, "DD-1").status());
        verify(settlement).synchronizeVerifiedQuery(argThat(facts -> facts.resultCode() == 10200095));
    }

    @Test
    void acceptsSignedSparseUnpaidResponseWithoutOptionalFields() {
        when(gateway.query(anyMap())).thenReturn(signedSparseResponse("0"));

        OmgQueryPaymentResponse response = service.query(5L, "DD-1");

        assertEquals("UNPAID", response.status());
        assertEquals("0", response.tradeStatus());
        assertEquals(100, response.amount());
        verify(settlement, never()).synchronizeVerifiedQuery(any());
    }

    @Test
    void acceptsSignedSparseFailureResponseWithoutSettlementFields() {
        when(gateway.query(anyMap())).thenReturn(signedSparseResponse("10200095"));
        when(settlement.synchronizeVerifiedQuery(any())).thenReturn(OmgPaymentSettlementResult.FAILED);

        OmgQueryPaymentResponse response = service.query(5L, "DD-1");

        assertEquals("FAILED", response.status());
        verify(settlement).synchronizeVerifiedQuery(argThat(facts ->
                facts.resultCode() == 10200095
                        && facts.tradeNo() == null
                        && facts.paymentType() == null
                        && facts.tradeDate() == null
                        && facts.paymentTypeChargeFee() == null));
    }

    @Test
    void treatsSignedGatewayMissingTradeAsFailedAttempt() {
        when(gateway.query(anyMap())).thenReturn(signedFields(gatewayMissingTradeFields()));
        when(settlement.synchronizeVerifiedQuery(any())).thenReturn(OmgPaymentSettlementResult.FAILED);

        OmgQueryPaymentResponse response = service.query(5L, "DD-1");

        assertEquals("FAILED", response.status());
        assertEquals("10200047", response.tradeStatus());
        assertEquals(0, response.amount());
        verify(settlement).synchronizeVerifiedQuery(argThat(facts ->
                facts.resultCode() == 10200047
                        && facts.amount() == 100
                        && facts.tradeNo() == null
                        && facts.paymentType() == null));
    }

    @Test
    void rejectsGatewayMissingTradeStatusWithNonzeroAmount() {
        Map<String, String> fields = gatewayMissingTradeFields();
        fields.put("TradeAmt", "100");
        when(gateway.query(anyMap())).thenReturn(signedFields(fields));

        OmgPaymentBusinessException exception = assertThrows(OmgPaymentBusinessException.class,
                () -> service.query(5L, "DD-1"));

        assertEquals(OmgPaymentErrorCode.PAYMENT_QUERY_FAILED, exception.getCode());
        verifyNoInteractions(settlement);
    }

    @Test
    void paidOrderQueriesItsPaidAttemptInsteadOfAnOldOrClientSelectedAttempt() {
        OmgPaymentAttempt paid = attempt(1);
        when(attempts.selectOrder("DD-1")).thenReturn(order(1L));
        when(attempts.selectPaidByDdId("DD-1")).thenReturn(paid);
        when(gateway.query(anyMap())).thenReturn(signedResponse("1", Map.of()));
        when(settlement.synchronizeVerifiedQuery(any())).thenReturn(OmgPaymentSettlementResult.PAID);

        assertEquals("PAID", service.query(5L, "DD-1").status());

        verify(attempts).selectPaidByDdId("DD-1");
        verify(attempts, never()).selectActiveCreatedByDdId(anyString());
    }

    @Test
    void paidAttemptIsNeverReportedAsUnpaidByALaterQuery() {
        OmgPaymentAttempt paid = attempt(1);
        when(attempts.selectOrder("DD-1")).thenReturn(order(1L));
        when(attempts.selectPaidByDdId("DD-1")).thenReturn(paid);
        when(gateway.query(anyMap())).thenReturn(signedResponse("0", Map.of()));

        assertEquals("PAID", service.query(5L, "DD-1").status());

        verify(settlement, never()).synchronizeVerifiedQuery(any());
    }

    @Test
    void rejectsUnauthorizedMissingAttemptAndAnyTamperedExtraField() {
        assertThrows(OmgPaymentBusinessException.class, () -> service.query(6L, "DD-1"));
        when(attempts.selectActiveCreatedByDdId("DD-1")).thenReturn(null);
        OmgPaymentBusinessException missing = assertThrows(OmgPaymentBusinessException.class,
                () -> service.query(5L, "DD-1"));
        assertEquals(OmgPaymentErrorCode.PAYMENT_QUERY_NOT_AVAILABLE, missing.getCode());

        when(attempts.selectActiveCreatedByDdId("DD-1")).thenReturn(attempt);
        String signed = signedResponse("1", Map.of("FutureEmptyField", ""));
        when(gateway.query(anyMap())).thenReturn(signed.replace("FutureEmptyField=", "FutureEmptyField=tampered"));
        OmgPaymentBusinessException tampered = assertThrows(OmgPaymentBusinessException.class,
                () -> service.query(5L, "DD-1"));
        assertEquals(OmgPaymentErrorCode.PAYMENT_QUERY_FAILED, tampered.getCode());
        verify(settlement, never()).synchronizeVerifiedQuery(any());
    }

    @Test
    void rejectsSparseResponseWithoutCoreIdentityAndPaidSettlementFields() {
        Map<String, String> missingAmount = sparseFields("0");
        missingAmount.remove("TradeAmt");
        when(gateway.query(anyMap())).thenReturn(signedFields(missingAmount));

        OmgPaymentBusinessException missingIdentity = assertThrows(OmgPaymentBusinessException.class,
                () -> service.query(5L, "DD-1"));
        assertEquals(OmgPaymentErrorCode.PAYMENT_QUERY_FAILED, missingIdentity.getCode());

        reset(gateway, settlement);
        when(gateway.query(anyMap())).thenReturn(signedSparseResponse("1"));

        OmgPaymentBusinessException missingSettlement = assertThrows(OmgPaymentBusinessException.class,
                () -> service.query(5L, "DD-1"));
        assertEquals(OmgPaymentErrorCode.PAYMENT_QUERY_FAILED, missingSettlement.getCode());
        verifyNoInteractions(settlement);
    }

    private String signedResponse(String tradeStatus, Map<String, String> extras) {
        Map<String, String> fields = new LinkedHashMap<>();
        fields.put("MerchantID", "1000031");
        fields.put("MerchantTradeNo", "OMG123");
        fields.put("StoreID", "");
        fields.put("TradeNo", "1".equals(tradeStatus) ? "GW123" : "");
        fields.put("TradeAmt", "100");
        fields.put("PaymentDate", "1".equals(tradeStatus) ? "2026/08/13 12:00:00" : "");
        fields.put("PaymentType", "1".equals(tradeStatus) ? "Credit_CreditCard" : "");
        fields.put("HandlingCharge", "0");
        fields.put("PaymentTypeChargeFee", "0.00");
        fields.put("TradeDate", "2026/08/13 11:59:00");
        fields.put("TradeStatus", tradeStatus);
        fields.put("ItemName", "Order DD-1");
        fields.put("CustomField1", "");
        fields.put("CustomField2", "");
        fields.put("CustomField3", "");
        fields.put("CustomField4", "");
        fields.putAll(extras);
        return signedFields(fields);
    }

    private String signedSparseResponse(String tradeStatus) {
        return signedFields(sparseFields(tradeStatus));
    }

    private Map<String, String> sparseFields(String tradeStatus) {
        Map<String, String> fields = new LinkedHashMap<>();
        fields.put("MerchantID", "1000031");
        fields.put("MerchantTradeNo", "OMG123");
        fields.put("TradeAmt", "100");
        fields.put("TradeStatus", tradeStatus);
        return fields;
    }

    private Map<String, String> gatewayMissingTradeFields() {
        Map<String, String> fields = new LinkedHashMap<>();
        fields.put("MerchantID", "1000031");
        fields.put("MerchantTradeNo", "OMG123");
        fields.put("TradeNo", "");
        fields.put("TradeAmt", "0");
        fields.put("PaymentDate", "");
        fields.put("PaymentType", "");
        fields.put("HandlingCharge", "0");
        fields.put("PaymentTypeChargeFee", "0");
        fields.put("TradeDate", "");
        fields.put("TradeStatus", "10200047");
        fields.put("ItemName", "");
        return fields;
    }

    private String signedFields(Map<String, String> source) {
        Map<String, String> fields = new LinkedHashMap<>(source);
        fields.put("CheckMacValue", new OmgCheckMacSigner().sign(fields, HASH_KEY, HASH_IV));
        return fields.entrySet().stream()
                .map(entry -> encode(entry.getKey()) + "=" + encode(entry.getValue()))
                .collect(java.util.stream.Collectors.joining("&"));
    }

    private static String encode(String value) {
        return URLEncoder.encode(value, StandardCharsets.UTF_8);
    }

    private static OmgPaymentAttempt attempt(int status) {
        OmgPaymentAttempt result = new OmgPaymentAttempt();
        result.setId(7L);
        result.setDdId("DD-1");
        result.setMerchantTradeNo("OMG123");
        result.setMerchantId("1000031");
        result.setAmount(100);
        result.setHashKeySnapshot(HASH_KEY);
        result.setHashIvSnapshot(HASH_IV);
        result.setAttemptStatus(status);
        return result;
    }

    private static OmgPaymentOrderSnapshot order(long payStatus) {
        OmgPaymentOrderSnapshot result = new OmgPaymentOrderSnapshot();
        result.setDdId("DD-1");
        result.setParentDdId("DD-1");
        result.setStoreId(77L);
        result.setUserId(5L);
        result.setPayStatus(payStatus);
        result.setPayType("2");
        return result;
    }
}
