package com.ruoyi.app.pay;

import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.ruoyi.app.utils.linepay.LinePayClient;
import com.ruoyi.app.utils.linepay.LinePayResponse;
import com.ruoyi.system.domain.PosOrder;
import com.ruoyi.system.domain.PosOrderLinePayment;
import com.ruoyi.system.domain.PosOrderLineRefund;
import com.ruoyi.system.domain.PosStoreLinePay;
import com.ruoyi.system.service.IPosOrderLinePaymentService;
import com.ruoyi.system.service.IPosOrderLineRefundService;
import com.ruoyi.system.service.IPosOrderService;
import com.ruoyi.system.service.IPosStoreLinePayService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

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;

class LinePayReconcileTest {

    private IPosOrderService orderService;
    private IPosStoreLinePayService credentialService;
    private IPosOrderLinePaymentService paymentService;
    private IPosOrderLineRefundService refundService;
    private LinePayClient client;
    private LinePayFactService factService;
    private LinePayService service;

    @BeforeEach
    void setUp() {
        orderService = mock(IPosOrderService.class);
        credentialService = mock(IPosStoreLinePayService.class);
        paymentService = mock(IPosOrderLinePaymentService.class);
        refundService = mock(IPosOrderLineRefundService.class);
        client = mock(LinePayClient.class);
        factService = mock(LinePayFactService.class);
        service = new LinePayService(orderService, credentialService, paymentService,
                refundService, client, factService);
        when(paymentService.getById(70L)).thenReturn(payment("WAITING_AUTH"));
        when(credentialService.getById(5L)).thenReturn(credential());
    }

    @Test
    void retrieveUniqueCapturedPaymentAppliesPaidFactWithoutCheckOrConfirm() throws Exception {
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(response(
                "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\","
                        + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\","
                        + "\"currency\":\"TWD\",\"payInfo\":[{\"amount\":280}]}]}"));

        String status = service.reconcilePayment(70L, "TASK");

        assertEquals("PAID", status);
        verify(factService).applyPaidFact(eq(70L), eq("TX-100"), any(), any());
        verify(client, never()).check(any(), any());
        verify(client, never()).confirm(any(), any(), any(Integer.class), any());
    }

    @Test
    void rotatedSameChannelCredentialMayRecoverOnlyAfterStrictRetrieveProof() throws Exception {
        PosStoreLinePay current = currentCredential("channel");
        when(credentialService.getCurrent(9L)).thenReturn(current);
        when(client.retrieveByTransactionId(eq(new com.ruoyi.app.utils.linepay.LinePayCredential(
                5L, "channel", "secret")), eq("TX-100"))).thenReturn(
                response("{\"returnCode\":\"1105\"}"));
        when(client.retrieveByTransactionId(eq(new com.ruoyi.app.utils.linepay.LinePayCredential(
                6L, "channel", "new-secret")), eq("TX-100"))).thenReturn(response(
                "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\"," 
                        + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\"," 
                        + "\"currency\":\"TWD\",\"payInfo\":[{\"amount\":280}]}]}"));

        assertEquals("PAID", service.reconcilePayment(70L, "TASK"));

        verify(factService).applyPaidFact(eq(70L), eq("TX-100"), any(), any());
    }

    @Test
    void rotatedDifferentChannelCredentialIsNeverUsedForOldTransaction() throws Exception {
        when(credentialService.getCurrent(9L)).thenReturn(currentCredential("different-channel"));
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
                response("{\"returnCode\":\"1105\"}"));

        assertEquals("WAITING_AUTH", service.reconcilePayment(70L, "TASK"));

        verify(client, org.mockito.Mockito.times(1)).retrieveByTransactionId(any(), eq("TX-100"));
        verify(factService, never()).applyPaidFact(any(), any(), any(), any());
    }

    @Test
    void confirmedLocalRefundIsNeverDowngradedByLaterAmbiguousEvidence() throws Exception {
        PosOrderLineRefund refunded = new PosOrderLineRefund();
        refunded.setPaymentId(70L);
        refunded.setStatus("REFUNDED");
        when(refundService.getByPaymentId(70L)).thenReturn(refunded);
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(response(
                "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\"," 
                        + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\"," 
                        + "\"currency\":\"TWD\",\"payInfo\":[{\"amount\":280}],"
                        + "\"refundList\":[{\"refundTransactionId\":\"RF-PART\"," 
                        + "\"refundAmount\":-100}]}]}"));

        assertEquals("REFUNDED", service.reconcilePayment(70L, "TASK"));

        verify(factService, never()).applyPaidWithRefundReviewFact(any(), any(), any(), any());
    }

    @Test
    void check0110ConfirmsOnlyAfterLocalClaimAndThenAppliesPaidFact() throws Exception {
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
                response("{\"returnCode\":\"1150\"}"));
        when(client.check(any(), eq("TX-100"))).thenReturn(
                response("{\"returnCode\":\"0110\"}"));
        when(orderService.getOne(any(Wrapper.class))).thenReturn(order(0L));
        when(paymentService.claimConfirm(eq(70L), eq(2L), eq("WAITING_AUTH"),
                any(), any())).thenReturn(1);
        when(client.confirm(any(), eq("TX-100"), eq(280), eq("TWD"))).thenReturn(
                response("{\"returnCode\":\"0000\",\"info\":{\"transactionId\":\"TX-100\","
                        + "\"orderId\":\"LP-100\",\"currency\":\"TWD\","
                        + "\"payInfo\":[{\"amount\":280}]}}"));

        String status = service.reconcilePayment(70L, "TASK");

        assertEquals("PAID", status);
        verify(paymentService).claimConfirm(eq(70L), eq(2L), eq("WAITING_AUTH"), any(), any());
        verify(factService).applyPaidFact(eq(70L), eq("TX-100"), any(), any());
    }

    @Test
    void check0121TerminatesOnlyAfterRetrieveFoundNoPayment() throws Exception {
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
                response("{\"returnCode\":\"1150\"}"));
        when(client.check(any(), eq("TX-100"))).thenReturn(
                response("{\"returnCode\":\"0121\"}"));
        when(paymentService.markTerminal(70L, 2L, "WAITING_AUTH", "CANCELLED_OR_EXPIRED"))
                .thenReturn(1);

        String status = service.reconcilePayment(70L, "TASK");

        assertEquals("CANCELLED_OR_EXPIRED", status);
        verify(paymentService).markTerminal(70L, 2L, "WAITING_AUTH", "CANCELLED_OR_EXPIRED");
    }

    @Test
    void cancelledOrderIsNeverConfirmedAfterAuthorization() throws Exception {
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
                response("{\"returnCode\":\"1150\"}"));
        when(client.check(any(), eq("TX-100"))).thenReturn(
                response("{\"returnCode\":\"0110\"}"));
        when(orderService.getOne(any(Wrapper.class))).thenReturn(order(4L));
        when(paymentService.markAuthDoneOrderCancelled(eq(70L), eq(2L), eq("WAITING_AUTH"),
                any(), any()))
                .thenReturn(1);

        String status = service.reconcilePayment(70L, "TASK");

        assertEquals("AUTH_DONE_ORDER_CANCELLED", status);
        verify(client, never()).confirm(any(), any(), any(Integer.class), any());
    }

    @Test
    void requestUnknownCanRecoverCapturedTransactionByOrderId() throws Exception {
        PosOrderLinePayment unknown = payment("REQUEST_UNKNOWN");
        unknown.setTransactionId(null);
        when(paymentService.getById(70L)).thenReturn(unknown);
        when(client.retrieveByOrderId(any(), eq("LP-100"))).thenReturn(response(
                "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-RECOVERED\","
                        + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\","
                        + "\"currency\":\"TWD\",\"payInfo\":[{\"amount\":280}]}]}"));

        String status = service.reconcilePayment(70L, "TASK");

        assertEquals("PAID", status);
        verify(factService).applyPaidFact(eq(70L), eq("TX-RECOVERED"), any(), any());
    }

    @Test
    void check0123ImmediatelyRetrievesAgainBeforeDeciding() throws Exception {
        when(client.retrieveByTransactionId(any(), eq("TX-100")))
                .thenReturn(response("{\"returnCode\":\"1150\"}"))
                .thenReturn(response("{\"returnCode\":\"0000\",\"info\":[{"
                        + "\"transactionId\":\"TX-100\",\"orderId\":\"LP-100\","
                        + "\"transactionType\":\"PAYMENT\",\"currency\":\"TWD\","
                        + "\"payInfo\":[{\"amount\":280}]}]}"));
        when(client.check(any(), eq("TX-100"))).thenReturn(
                response("{\"returnCode\":\"0123\"}"));

        String status = service.reconcilePayment(70L, "TASK");

        assertEquals("PAID", status);
        verify(factService).applyPaidFact(eq(70L), eq("TX-100"), any(), any());
    }

    @Test
    void expiredRecoveryPerformsFinalRetrieveThenStopsForManualReview() throws Exception {
        PosOrderLinePayment expired = payment("CONFIRM_UNKNOWN");
        expired.setReconcileDeadline(new java.util.Date(1L));
        when(paymentService.getById(70L)).thenReturn(expired);
        when(client.retrieveByTransactionId(any(), eq("TX-100")))
                .thenThrow(new IllegalStateException("timeout"));
        when(paymentService.markManualReview(70L, 2L, "CONFIRM_UNKNOWN")).thenReturn(1);

        assertEquals("MANUAL_REVIEW", service.reconcilePayment(70L, "TASK"));
        verify(paymentService).markManualReview(70L, 2L, "CONFIRM_UNKNOWN");
    }

    @Test
    void confirmUnknownNeverChecksOrConfirmsAgain() throws Exception {
        PosOrderLinePayment unknown = payment("CONFIRM_UNKNOWN");
        when(paymentService.getById(70L)).thenReturn(unknown);
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
                response("{\"returnCode\":\"1150\"}"));

        assertEquals("CONFIRM_UNKNOWN", service.reconcilePayment(70L, "TASK"));

        verify(client, never()).check(any(), any());
        verify(client, never()).confirm(any(), any(), any(Integer.class), any());
    }

    @Test
    void retrieveAmountMismatchDoesNotApplyPaidFact() throws Exception {
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(response(
                "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\","
                        + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\","
                        + "\"currency\":\"TWD\",\"payInfo\":[{\"amount\":100}]}]}"));

        assertEquals("WAITING_AUTH", service.reconcilePayment(70L, "TASK"));

        verify(factService, never()).applyPaidFact(any(), any(), any(), any());
        verify(client, never()).check(any(), any());
    }

    @Test
    void retrieveAlreadyFullyRefundedPaymentAppliesBothFacts() throws Exception {
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(response(
                "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\","
                        + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\","
                        + "\"currency\":\"TWD\",\"payInfo\":[{\"amount\":280}],"
                        + "\"refundList\":[{\"refundTransactionId\":\"RF-1\","
                        + "\"transactionType\":\"PARTIAL_REFUND\",\"refundAmount\":-280}]}]}"));

        assertEquals("REFUNDED", service.reconcilePayment(70L, "TASK"));

        verify(factService).applyPaidAndRefundedFact(eq(70L), eq("TX-100"), any(), any(),
                eq("RF-1"), any());
        verify(factService, never()).applyPaidFact(any(), any(), any(), any());
    }

    @Test
    void retrievePartiallyRefundedPaymentStopsForManualReviewWithoutFulfillment() throws Exception {
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(response(
                "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\","
                        + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\","
                        + "\"currency\":\"TWD\",\"payInfo\":[{\"amount\":280}],"
                        + "\"refundList\":[{\"refundTransactionId\":\"RF-PART\","
                        + "\"refundAmount\":-100}]}]}"));
        assertEquals("MANUAL_REVIEW", service.reconcilePayment(70L, "TASK"));

        verify(factService).applyPaidWithRefundReviewFact(eq(70L), eq("TX-100"), any(), any());
        verify(factService, never()).applyPaidFact(any(), any(), any(), any());
    }

    private static LinePayResponse response(String raw) {
        JSONObject root = JSONObject.parseObject(raw);
        return new LinePayResponse(200, root.getString("returnCode"),
                root.getString("returnMessage"), null, root.getJSONObject("info"), raw);
    }

    private static PosOrderLinePayment payment(String status) {
        PosOrderLinePayment payment = new PosOrderLinePayment();
        payment.setId(70L);
        payment.setDdId("DD-100");
        payment.setLineOrderId("LP-100");
        payment.setTransactionId("TX-100");
        payment.setCredentialId(5L);
        payment.setStoreId(9L);
        payment.setAmount(280);
        payment.setCurrency("TWD");
        payment.setStatus(status);
        payment.setActiveDdId("DD-100");
        payment.setVersion(2L);
        return payment;
    }

    private static PosStoreLinePay credential() {
        PosStoreLinePay value = new PosStoreLinePay();
        value.setId(5L);
        value.setEnvironment("SANDBOX");
        value.setChannelId("channel");
        value.setChannelSecret("secret");
        return value;
    }

    private static PosStoreLinePay currentCredential(String channelId) {
        PosStoreLinePay value = new PosStoreLinePay();
        value.setId(6L);
        value.setStoreId(9L);
        value.setEnvironment("SANDBOX");
        value.setChannelId(channelId);
        value.setChannelSecret("new-secret");
        return value;
    }

    private static PosOrder order(Long state) {
        PosOrder order = new PosOrder();
        order.setDdId("DD-100");
        order.setMdId(9L);
        order.setAmount(280);
        order.setPayType("3");
        order.setPayStatus(0L);
        order.setState(state);
        return order;
    }
}
