package com.ruoyi.app.omgpay;

import com.ruoyi.app.order.DeliveryOrderNotificationService;
import com.ruoyi.app.omgpay.dto.OmgNotifyField;
import com.ruoyi.app.omgpay.dto.OmgNotifyRequest;
import com.ruoyi.system.domain.PosOrder;
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.IPosOrderService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.test.util.ReflectionTestUtils;

import java.util.ArrayList;
import java.math.BigDecimal;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

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

    private IOmgPaymentAttemptService attempts;
    private OmgPaymentNotifyService service;
    private OmgPaymentAttempt attempt;

    @BeforeEach
    void setUp() {
        attempts = mock(IOmgPaymentAttemptService.class);
        service = new OmgPaymentNotifyService(attempts, new OmgCheckMacSigner());
        attempt = new OmgPaymentAttempt();
        attempt.setId(7L);
        attempt.setDdId("DD-1");
        attempt.setMerchantTradeNo("OMG123");
        attempt.setMerchantId("1000031");
        attempt.setAmount(100);
        attempt.setHashKeySnapshot(HASH_KEY);
        attempt.setHashIvSnapshot(HASH_IV);
        attempt.setAttemptStatus(0);
        when(attempts.selectByMerchantTradeNo("OMG123")).thenReturn(attempt);
        when(attempts.selectByMerchantTradeNoForUpdate("OMG123")).thenReturn(attempt);
    }

    @Test
    void successfulCallbackUsesSnapshotAndMarksOnlyPaymentFacts() {
        OmgPaymentOrderSnapshot order = order(4L, 0L);
        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order);
        when(attempts.markPaid(any())).thenReturn(1);
        when(attempts.markOrderPaid("DD-1")).thenReturn(1);

        assertTrue(service.process(signedRequest(1, 0, 100)));

        var facts = org.mockito.ArgumentCaptor.forClass(OmgPaymentAttempt.class);
        verify(attempts).markPaid(facts.capture());
        assertEquals("GW123", facts.getValue().getTradeNo());
        assertEquals(1, facts.getValue().getRtnCode());
        assertEquals(0, facts.getValue().getSimulatePaid());
        verify(attempts).supersedeOtherCreated("DD-1", 7L);
        verify(attempts).markOrderPaid("DD-1");
    }

    @Test
    void simulatedSuccessOnCancelledOrderIsStillPaid() {
        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(4L, 0L));
        when(attempts.markPaid(any())).thenReturn(1);
        when(attempts.markOrderPaid("DD-1")).thenReturn(1);
        when(attempts.countOtherPaidAttempts("DD-1", 7L)).thenReturn(1);

        assertTrue(service.process(signedRequest(1, 1, 100)));

        var facts = org.mockito.ArgumentCaptor.forClass(OmgPaymentAttempt.class);
        verify(attempts).markPaid(facts.capture());
        assertEquals(1, facts.getValue().getSimulatePaid());
    }

    @Test
    void failureMarksAttemptFailedWithoutChangingOrder() {
        when(attempts.markFailed(any())).thenReturn(1);

        assertTrue(service.process(signedRequest(10200095, 0, 100)));

        verify(attempts).markFailed(any());
        verify(attempts, never()).markOrderPaid(anyString());
    }

    @Test
    void supersededAttemptAcceptsFailureWithoutLosingSupersededState() {
        attempt.setAttemptStatus(3);

        assertTrue(service.process(signedRequest(10200095, 0, 100)));

        verify(attempts, never()).markFailed(any());
        verify(attempts, never()).markOrderPaid(anyString());
    }

    @Test
    void paidAttemptCannotBeDowngradedByLateFailure() {
        attempt.setAttemptStatus(1);

        assertTrue(service.process(signedRequest(10200095, 0, 100)));

        verify(attempts, never()).markFailed(any());
        verify(attempts, never()).markOrderPaid(anyString());
    }

    @Test
    void failedAttemptMayLaterBecomePaid() {
        attempt.setAttemptStatus(2);
        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(0L, 0L));
        when(attempts.markPaid(any())).thenReturn(1);
        when(attempts.markOrderPaid("DD-1")).thenReturn(1);

        assertTrue(service.process(signedRequest(1, 0, 100)));

        verify(attempts).markPaid(any());
        verify(attempts).markOrderPaid("DD-1");
    }

    @Test
    void paidDeliveryOrderIsOpenedToRidersAfterOmgSettlement() {
        IPosOrderService orders = mock(IPosOrderService.class);
        DeliveryOrderNotificationService notifications = mock(DeliveryOrderNotificationService.class);
        ReflectionTestUtils.setField(service, "posOrderService", orders);
        ReflectionTestUtils.setField(service, "deliveryOrderNotificationService", notifications);
        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(0L, 0L));
        when(attempts.markPaid(any())).thenReturn(1);
        when(attempts.markOrderPaid("DD-1")).thenReturn(1);
        PosOrder paidOrder = new PosOrder();
        paidOrder.setDdId("DD-1");
        paidOrder.setType(0L);
        paidOrder.setState(0L);
        paidOrder.setDeliveryStatus(0L);
        paidOrder.setAfterSaleStatus(0L);
        when(orders.getOne(any())).thenReturn(paidOrder);

        assertTrue(service.process(signedRequest(1, 0, 100)));

        assertEquals(1L, paidOrder.getPayStatus());
        verify(notifications).notifyOrderAvailable(paidOrder);
    }

    @Test
    void rejectsSignatureMerchantAndAmountMismatchWithoutMutation() {
        OmgNotifyRequest badSignature = signedRequest(1, 0, 100);
        List<OmgNotifyField> changed = new ArrayList<>(badSignature.getFields());
        changed.set(changed.size() - 1, new OmgNotifyField("CheckMacValue", "0".repeat(64)));
        assertFalse(service.process(new OmgNotifyRequest("127.0.0.1", "raw", changed, null)));
        assertFalse(service.process(signedRequest(1, 0, 101)));
        assertFalse(service.process(signedRequest(1, 0, 100, "OTHER")));

        verify(attempts, never()).markPaid(any());
        verify(attempts, never()).markFailed(any());
    }

    @Test
    void validExtraFieldIsRequiredBySignatureVerification() {
        OmgNotifyRequest signed = signedRequest(1, 0, 100);
        List<OmgNotifyField> fieldsWithoutExtra = signed.getFields().stream()
                .filter(field -> !"FutureEmptyField".equals(field.name())).toList();

        assertFalse(service.process(new OmgNotifyRequest("127.0.0.1", "raw", fieldsWithoutExtra, null)));

        verify(attempts, never()).markPaid(any());
    }

    @Test
    void verifiedPaidQueryUsesTheSameIrreversibleSettlementAsCallback() {
        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(0L, 0L));
        when(attempts.selectByMerchantTradeNoForUpdate("OMG123")).thenReturn(attempt);
        when(attempts.markPaid(any())).thenReturn(1);
        when(attempts.markOrderPaid("DD-1")).thenReturn(1);
        OmgPaymentGatewayFacts facts = new OmgPaymentGatewayFacts(
                OmgPaymentFactSource.QUERY, "1000031", "OMG123", 100, 1,
                "TradeStatus=1", "GW123", "Credit_CreditCard", null, null,
                new BigDecimal("0.00"), null);

        assertEquals(OmgPaymentSettlementResult.PAID, service.synchronizeVerifiedQuery(facts));

        verify(attempts).markPaid(any());
        verify(attempts).supersedeOtherCreated("DD-1", 7L);
        verify(attempts).markOrderPaid("DD-1");
    }

    @Test
    void failedQueryCannotDowngradeAnAlreadyPaidAttempt() {
        attempt.setAttemptStatus(1);
        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(0L, 1L));
        when(attempts.selectByMerchantTradeNoForUpdate("OMG123")).thenReturn(attempt);
        OmgPaymentGatewayFacts facts = new OmgPaymentGatewayFacts(
                OmgPaymentFactSource.QUERY, "1000031", "OMG123", 100, 10200095,
                "TradeStatus=10200095", null, null, null, null,
                new BigDecimal("0.00"), null);

        assertEquals(OmgPaymentSettlementResult.PAID, service.synchronizeVerifiedQuery(facts));

        verify(attempts, never()).markFailed(any());
    }

    private OmgNotifyRequest signedRequest(int rtnCode, int simulatePaid, int amount) {
        return signedRequest(rtnCode, simulatePaid, amount, "1000031");
    }

    private OmgNotifyRequest signedRequest(int rtnCode, int simulatePaid, int amount, String merchantId) {
        Map<String, String> fields = new LinkedHashMap<>();
        fields.put("MerchantID", merchantId);
        fields.put("MerchantTradeNo", "OMG123");
        fields.put("StoreID", "");
        fields.put("RtnCode", String.valueOf(rtnCode));
        fields.put("RtnMsg", rtnCode == 1 ? "Succeeded" : "Failed");
        fields.put("TradeNo", "GW123");
        fields.put("TradeAmt", String.valueOf(amount));
        fields.put("PaymentDate", rtnCode == 1 ? "2026/08/13 12:00:00" : "");
        fields.put("PaymentType", "Credit_CreditCard");
        fields.put("PaymentTypeChargeFee", "0");
        fields.put("TradeDate", "2026/08/13 11:59:00");
        fields.put("SimulatePaid", String.valueOf(simulatePaid));
        fields.put("CustomField1", "");
        fields.put("CustomField2", "");
        fields.put("CustomField3", "");
        fields.put("CustomField4", "");
        fields.put("FutureEmptyField", "");
        fields.put("CheckMacValue", new OmgCheckMacSigner().sign(fields, HASH_KEY, HASH_IV));
        List<OmgNotifyField> decoded = fields.entrySet().stream()
                .map(entry -> new OmgNotifyField(entry.getKey(), entry.getValue())).toList();
        return new OmgNotifyRequest("127.0.0.1", "raw", decoded, null);
    }

    private static OmgPaymentOrderSnapshot order(long state, long payStatus) {
        OmgPaymentOrderSnapshot order = new OmgPaymentOrderSnapshot();
        order.setDdId("DD-1");
        order.setState(state);
        order.setPayStatus(payStatus);
        return order;
    }
}
