package com.ruoyi.app.pay;

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.ruoyi.system.domain.PosOrder;
import com.ruoyi.system.domain.PosOrderLinePayment;
import com.ruoyi.system.domain.PosOrderLineRefund;
import com.ruoyi.system.service.IPosOrderLinePaymentService;
import com.ruoyi.system.service.IPosOrderLineRefundService;
import com.ruoyi.system.service.IPosOrderService;
import com.ruoyi.system.mapper.PosOrderMapper;
import com.ruoyi.app.order.OrderLifecycleService;
import org.junit.jupiter.api.Test;

import java.util.Date;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.times;

class LinePayCancellationRaceTest {

    @Test
    void offlineCancellationFirstThenLateCaptureCreatesOneDurableRefundIntent() {
        IPosOrderService orders = mock(IPosOrderService.class);
        IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
        IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
        PosOrderLinePayment payment = payment("CONFIRM_UNKNOWN");
        payment.setPaymentMode("OFFLINE");
        payment.setLineOrderId("LPOFF-1");
        when(payments.getById(70L)).thenReturn(payment);
        when(payments.markPaid(eq(70L), eq(2L), eq("CONFIRM_UNKNOWN"), eq("TX-1"),
                any(), any())).thenReturn(1);
        PosOrder cancelled = order(4L);
        when(orders.getOne(any(Wrapper.class))).thenReturn(cancelled);
        LinePayFactService facts = new LinePayFactService(orders, payments, refunds);

        facts.applyPaidFact(70L, "TX-1", "TSP", new Date());

        verify(refunds).createIfAbsent(payment, "TASK");
        assertEquals("PAID", payment.getStatus());
    }

    @Test
    void tableOrderStateOneAcceptsCaptureWithoutCompensationRefund() {
        IPosOrderService orders = mock(IPosOrderService.class);
        IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
        IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
        PosOrderLinePayment payment = payment("WAITING_AUTH");
        when(payments.getById(70L)).thenReturn(payment);
        when(payments.markPaid(eq(70L), eq(2L), eq("WAITING_AUTH"), eq("TX-1"),
                any(), any())).thenReturn(1);
        when(orders.getOne(any(Wrapper.class))).thenReturn(order(1L));
        LinePayFactService facts = new LinePayFactService(orders, payments, refunds);

        facts.applyPaidFact(70L, "TX-1", "TSP", new Date());

        verify(refunds, never()).createIfAbsent(any(), any());
    }

    @Test
    void paidFirstThenCancellationUsesUniqueRefundRow() {
        IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
        IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
        PosOrderLinePayment payment = payment("PAID");
        when(payments.getById(70L)).thenReturn(payment);
        PosOrderLineRefund existing = new PosOrderLineRefund();
        existing.setPaymentId(70L);
        existing.setStatus("CREATED");
        when(refunds.createIfAbsent(payment, "USER_CANCEL")).thenReturn(existing);
        LinePayRefundService service = new LinePayRefundService(payments, refunds,
                mock(com.ruoyi.system.service.IPosStoreLinePayService.class),
                mock(com.ruoyi.app.utils.linepay.LinePayClient.class), mock(LinePayFactService.class));

        assertEquals("CREATED", service.createRefundIntent(70L, "USER_CANCEL"));
        assertEquals("CREATED", service.createRefundIntent(70L, "USER_CANCEL"));

        verify(refunds, org.mockito.Mockito.times(2)).createIfAbsent(payment, "USER_CANCEL");
    }

    @Test
    void lateDuplicateCaptureCreatesCompensationRefundWhenOrderWasAlreadyPaidElsewhere() {
        IPosOrderService orders = mock(IPosOrderService.class);
        IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
        IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
        PosOrderMapper orderMapper = mock(PosOrderMapper.class);
        PosOrderLinePayment payment = payment("FAILED");
        when(payments.getById(70L)).thenReturn(payment);
        when(payments.markPaid(eq(70L), eq(2L), eq("FAILED"), eq("TX-1"),
                any(), any())).thenReturn(1);
        PosOrder order = order(1L);
        order.setPayType("2");
        order.setPayStatus(1L);
        when(orders.getOne(any(Wrapper.class))).thenReturn(order);
        when(orderMapper.markLinePaid("DD-1", 70L)).thenReturn(0);
        LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper);

        facts.applyPaidFact(70L, "TX-1", "TSP", new Date());

        verify(refunds).createIfAbsent(payment, "DUPLICATE_PAYMENT");
    }

    @Test
    void firstSuccessfulOrderCaptureTriggersNotificationOnlyOnce() {
        IPosOrderService orders = mock(IPosOrderService.class);
        IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
        IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
        PosOrderMapper orderMapper = mock(PosOrderMapper.class);
        LinePayOrderNotificationService notifications = mock(LinePayOrderNotificationService.class);
        PosOrderLinePayment payment = payment("WAITING_AUTH");
        when(payments.getById(70L)).thenReturn(payment);
        when(payments.markPaid(eq(70L), eq(2L), eq("WAITING_AUTH"), eq("TX-1"),
                any(), any())).thenReturn(1);
        PosOrder order = order(1L);
        when(orders.getOne(any(Wrapper.class))).thenReturn(order);
        when(orderMapper.markLinePaid("DD-1", 70L)).thenReturn(1, 0);
        LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper,
                notifications, null);

        facts.applyPaidFact(70L, "TX-1", "TSP", new Date());
        payment.setStatus("PAID");
        facts.applyPaidFact(70L, "TX-1", "TSP", new Date());

        verify(notifications, times(1)).paymentCaptured(order);
    }

    @Test
    void alreadyRefundedRetrieveDoesNotNotifyOrderAsNewlyPaid() {
        IPosOrderService orders = mock(IPosOrderService.class);
        IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
        IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
        PosOrderMapper orderMapper = mock(PosOrderMapper.class);
        LinePayOrderNotificationService notifications = mock(LinePayOrderNotificationService.class);
        PosOrderLinePayment payment = payment("WAITING_AUTH");
        when(payments.getById(70L)).thenReturn(payment);
        when(payments.markPaid(eq(70L), eq(2L), eq("WAITING_AUTH"), eq("TX-1"),
                any(), any())).thenReturn(1);
        PosOrder order = order(1L);
        when(orders.getOne(any(Wrapper.class))).thenReturn(order);
        when(orderMapper.markLinePaid("DD-1", 70L)).thenReturn(1);
        when(orderMapper.markLineRefunded("DD-1", 70L)).thenReturn(1);
        PosOrderLineRefund refund = new PosOrderLineRefund();
        refund.setId(80L);
        refund.setPaymentId(70L);
        refund.setStatus("CREATED");
        refund.setVersion(1L);
        when(refunds.createIfAbsent(payment, "RETRIEVE")).thenReturn(refund);
        when(refunds.markRefundedFromEvidence(eq(80L), eq(1L), eq("CREATED"),
                eq("RF-1"), any())).thenReturn(1);
        LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper,
                notifications, null);

        facts.applyPaidAndRefundedFact(70L, "TX-1", "TSP", new Date(), "RF-1", new Date());

        verify(notifications, never()).paymentCaptured(any());
    }

    @Test
    void verifiedFullRefundEvidenceEscalatesPriorFailedRefund() {
        IPosOrderService orders = mock(IPosOrderService.class);
        IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
        IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
        PosOrderMapper orderMapper = mock(PosOrderMapper.class);
        PosOrderLinePayment payment = payment("PAID");
        when(payments.getById(70L)).thenReturn(payment);
        when(payments.markPaid(eq(70L), eq(2L), eq("PAID"), eq("TX-1"),
                any(), any())).thenReturn(1);
        PosOrder order = order(1L);
        order.setPayStatus(1L);
        when(orders.getOne(any(Wrapper.class))).thenReturn(order);
        when(orderMapper.markLineRefunded("DD-1", 70L)).thenReturn(1);
        PosOrderLineRefund failed = new PosOrderLineRefund();
        failed.setId(80L);
        failed.setPaymentId(70L);
        failed.setStatus("FAILED");
        failed.setVersion(3L);
        when(refunds.createIfAbsent(payment, "RETRIEVE")).thenReturn(failed);
        when(refunds.markRefundedFromEvidence(eq(80L), eq(3L), eq("FAILED"),
                eq("RF-EXTERNAL"), any())).thenReturn(1);
        LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper,
                mock(LinePayOrderNotificationService.class), null);

        facts.applyPaidAndRefundedFact(70L, "TX-1", "TSP", new Date(),
                "RF-EXTERNAL", new Date());

        verify(refunds).markRefundedFromEvidence(eq(80L), eq(3L), eq("FAILED"),
                eq("RF-EXTERNAL"), any());
        verify(refunds, never()).markRefunded(any(), any(), any(), any());
    }

    @Test
    void partialRefundEvidenceRecordsPaymentFactWithoutFulfillingOrder() {
        IPosOrderService orders = mock(IPosOrderService.class);
        IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
        IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
        PosOrderMapper orderMapper = mock(PosOrderMapper.class);
        LinePayOrderNotificationService notifications = mock(LinePayOrderNotificationService.class);
        PosOrderLinePayment payment = payment("WAITING_AUTH");
        when(payments.getById(70L)).thenReturn(payment);
        when(payments.markPaid(eq(70L), eq(2L), eq("WAITING_AUTH"), eq("TX-1"),
                any(), any())).thenReturn(1);
        PosOrderLineRefund refund = new PosOrderLineRefund();
        refund.setId(80L);
        refund.setPaymentId(70L);
        refund.setStatus("CREATED");
        refund.setVersion(1L);
        when(refunds.createIfAbsent(payment, "RETRIEVE")).thenReturn(refund);
        when(refunds.markManualReviewFromEvidence(80L, 1L, "CREATED")).thenReturn(1);
        LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper,
                notifications, null);

        facts.applyPaidWithRefundReviewFact(70L, "TX-1", "TSP", new Date());

        verify(orderMapper, never()).markLinePaid(any(), any());
        verify(notifications, never()).paymentCaptured(any());
        verify(refunds).markManualReviewFromEvidence(80L, 1L, "CREATED");
    }

    @Test
    void ambiguousLaterEvidenceNeverDowngradesConfirmedRefund() {
        IPosOrderService orders = mock(IPosOrderService.class);
        IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
        IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
        PosOrderLinePayment payment = payment("PAID");
        when(payments.getById(70L)).thenReturn(payment);
        when(payments.markPaid(eq(70L), eq(2L), eq("PAID"), eq("TX-1"),
                any(), any())).thenReturn(1);
        PosOrderLineRefund refunded = new PosOrderLineRefund();
        refunded.setId(80L);
        refunded.setPaymentId(70L);
        refunded.setStatus("REFUNDED");
        refunded.setVersion(3L);
        when(refunds.createIfAbsent(payment, "RETRIEVE")).thenReturn(refunded);
        LinePayFactService facts = new LinePayFactService(orders, payments, refunds);

        facts.applyPaidWithRefundReviewFact(70L, "TX-1", "TSP", new Date());

        verify(refunds, never()).markManualReview(any(), any(), any());
    }

    @Test
    void verifiedPartialRefundEvidenceEscalatesPriorFailedRefund() {
        IPosOrderService orders = mock(IPosOrderService.class);
        IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
        IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
        PosOrderLinePayment payment = payment("PAID");
        when(payments.getById(70L)).thenReturn(payment);
        when(payments.markPaid(eq(70L), eq(2L), eq("PAID"), eq("TX-1"),
                any(), any())).thenReturn(1);
        PosOrderLineRefund failed = new PosOrderLineRefund();
        failed.setId(80L);
        failed.setPaymentId(70L);
        failed.setStatus("FAILED");
        failed.setVersion(3L);
        when(refunds.createIfAbsent(payment, "RETRIEVE")).thenReturn(failed);
        when(refunds.markManualReviewFromEvidence(80L, 3L, "FAILED")).thenReturn(1);
        LinePayFactService facts = new LinePayFactService(orders, payments, refunds);

        facts.applyPaidWithRefundReviewFact(70L, "TX-1", "TSP", new Date());

        verify(refunds).markManualReviewFromEvidence(80L, 3L, "FAILED");
        verify(refunds, never()).markManualReview(any(), any(), any());
    }

    @Test
    void cancelledLateCaptureCanCommitFullRefundWithoutPaymentNotification() {
        IPosOrderService orders = mock(IPosOrderService.class);
        IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
        IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
        PosOrderMapper orderMapper = mock(PosOrderMapper.class);
        LinePayOrderNotificationService notifications = mock(LinePayOrderNotificationService.class);
        OrderLifecycleService lifecycle = mock(OrderLifecycleService.class);
        PosOrderLinePayment payment = payment("CONFIRM_UNKNOWN");
        when(payments.getById(70L)).thenReturn(payment);
        when(payments.markPaid(eq(70L), eq(2L), eq("CONFIRM_UNKNOWN"), eq("TX-1"),
                any(), any())).thenReturn(1);
        PosOrder cancelled = order(4L);
        when(orders.getOne(any(Wrapper.class))).thenReturn(cancelled);
        when(orderMapper.markLinePaid("DD-1", 70L)).thenAnswer(invocation -> {
            cancelled.setPayStatus(1L);
            return 1;
        });
        PosOrderLineRefund refund = new PosOrderLineRefund();
        refund.setId(80L);
        refund.setPaymentId(70L);
        refund.setStatus("CREATED");
        refund.setVersion(1L);
        when(refunds.createIfAbsent(payment, "TASK")).thenReturn(refund);
        when(refunds.markRefunded(eq(80L), eq(1L), eq("RF-1"), any())).thenReturn(1);
        when(orderMapper.markLineRefunded("DD-1", 70L)).thenAnswer(invocation -> {
            cancelled.setPayStatus(2L);
            return 1;
        });
        LinePayFactService facts = new LinePayFactService(orders, payments, refunds, orderMapper,
                notifications, lifecycle);

        facts.applyPaidFact(70L, "TX-1", "TSP", new Date());
        facts.applyRefundedFact(80L, 70L, 1L, "RF-1", new Date());

        assertEquals(2L, cancelled.getPayStatus());
        verify(notifications, never()).paymentCaptured(any());
        verify(lifecycle).finalizeSystemLineRefund(9L);
    }

    @Test
    void amountMismatchFactCreatesRefundIntentOnlyAfterEvidenceCas() {
        IPosOrderService orders = mock(IPosOrderService.class);
        IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
        IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
        PosOrderLinePayment persisted = payment("AMOUNT_MISMATCH");
        persisted.setCapturedAmount(100);
        when(payments.markOfflineEvidence(eq(70L), eq(2L), eq("REQUESTING"),
                eq("AMOUNT_MISMATCH"), eq("TX-1"), eq("TSP"), eq(100), any(Date.class)))
                .thenReturn(1);
        when(payments.getById(70L)).thenReturn(persisted);
        LinePayFactService facts = new LinePayFactService(orders, payments, refunds);

        assertEquals(true, facts.applyAmountMismatchFact(70L, 2L, "REQUESTING",
                "TX-1", "TSP", 100, new Date()));

        verify(refunds).createIfAbsent(persisted, "AMOUNT_MISMATCH");
    }

    @Test
    void failedAmountMismatchEvidenceCasNeverCreatesRefundIntent() {
        IPosOrderService orders = mock(IPosOrderService.class);
        IPosOrderLinePaymentService payments = mock(IPosOrderLinePaymentService.class);
        IPosOrderLineRefundService refunds = mock(IPosOrderLineRefundService.class);
        when(payments.markOfflineEvidence(eq(70L), eq(2L), eq("REQUESTING"),
                eq("AMOUNT_MISMATCH"), eq("TX-1"), eq("TSP"), eq(100), any(Date.class)))
                .thenReturn(0);
        LinePayFactService facts = new LinePayFactService(orders, payments, refunds);

        assertEquals(false, facts.applyAmountMismatchFact(70L, 2L, "REQUESTING",
                "TX-1", "TSP", 100, new Date()));

        verify(refunds, never()).createIfAbsent(any(), any());
    }

    private static PosOrderLinePayment payment(String status) {
        PosOrderLinePayment payment = new PosOrderLinePayment();
        payment.setId(70L);
        payment.setDdId("DD-1");
        payment.setTransactionId("TX-1");
        payment.setStatus(status);
        payment.setVersion(2L);
        return payment;
    }

    private static PosOrder order(Long state) {
        PosOrder order = new PosOrder();
        order.setId(9L);
        order.setDdId("DD-1");
        order.setState(state);
        order.setPayType("3");
        order.setPayStatus(0L);
        order.setAfterSaleStatus(0L);
        return order;
    }
}
