package com.ruoyi.app.pay;

import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.app.utils.linepay.LinePayClient;
import com.ruoyi.app.utils.linepay.LinePayResponse;
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.IPosStoreLinePayService;
import org.junit.jupiter.api.BeforeEach;
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.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.never;

class LinePayRefundTest {
    private IPosOrderLinePaymentService paymentService;
    private IPosOrderLineRefundService refundService;
    private IPosStoreLinePayService credentialService;
    private LinePayClient client;
    private LinePayFactService factService;
    private LinePayRefundService service;

    @BeforeEach
    void setUp() {
        paymentService = mock(IPosOrderLinePaymentService.class);
        refundService = mock(IPosOrderLineRefundService.class);
        credentialService = mock(IPosStoreLinePayService.class);
        client = mock(LinePayClient.class);
        factService = mock(LinePayFactService.class);
        service = new LinePayRefundService(paymentService, refundService, credentialService,
                client, factService);
    }

    @Test
    void successfulFullRefundMarksRefundFactWithoutChangingPaymentStatus() throws Exception {
        PosOrderLinePayment payment = payment();
        PosOrderLineRefund refund = refund("CREATED");
        when(paymentService.getById(70L)).thenReturn(payment);
        when(refundService.createIfAbsent(payment, "ADMIN")).thenReturn(refund);
        when(credentialService.getById(5L)).thenReturn(credential());
        when(refundService.claimProcessing(eq(88L), eq(0L), eq("CREATED"), any(), any())).thenReturn(1);
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(noRefundResponse());
        JSONObject info = new JSONObject();
        info.put("refundTransactionId", "RF-100");
        when(client.refundFull(any(), eq("TX-100"))).thenReturn(
                new LinePayResponse(200, "0000", "Success", null, info, "{}"));

        String status = service.requestFullRefund(70L, "ADMIN");

        assertEquals("REFUNDED", status);
        assertEquals("PAID", payment.getStatus());
        verify(client).refundFull(any(), eq("TX-100"));
        verify(factService).applyRefundedFact(eq(88L), eq(70L), eq(1L), eq("RF-100"), any());
    }

    @Test
    void rotatedSameChannelCredentialRequiresStrictRetrieveBeforeRefund() throws Exception {
        PosOrderLinePayment payment = payment();
        PosOrderLineRefund refund = refund("CREATED");
        when(paymentService.getById(70L)).thenReturn(payment);
        when(refundService.createIfAbsent(payment, "ADMIN")).thenReturn(refund);
        when(credentialService.getById(5L)).thenReturn(credential());
        when(credentialService.getCurrent(9L)).thenReturn(currentCredential("channel"));
        when(refundService.claimProcessing(eq(88L), eq(0L), eq("CREATED"), any(), any())).thenReturn(1);
        when(client.retrieveByTransactionId(eq(new com.ruoyi.app.utils.linepay.LinePayCredential(
                5L, "channel", "secret")), eq("TX-100"))).thenReturn(
                new LinePayResponse(200, "1105", "Authentication failed", null, null, "{}"));
        when(client.retrieveByTransactionId(eq(new com.ruoyi.app.utils.linepay.LinePayCredential(
                6L, "channel", "new-secret")), eq("TX-100"))).thenReturn(noRefundResponse());
        JSONObject info = new JSONObject();
        info.put("refundTransactionId", "RF-NEW");
        when(client.refundFull(eq(new com.ruoyi.app.utils.linepay.LinePayCredential(
                6L, "channel", "new-secret")), eq("TX-100"))).thenReturn(
                new LinePayResponse(200, "0000", "Success", null, info, "{}"));

        assertEquals("REFUNDED", service.requestFullRefund(70L, "ADMIN"));

        verify(client).refundFull(eq(new com.ruoyi.app.utils.linepay.LinePayCredential(
                6L, "channel", "new-secret")), eq("TX-100"));
    }

    @Test
    void rotatedCredentialWithMismatchedProofNeverIssuesRefund() throws Exception {
        PosOrderLinePayment payment = payment();
        PosOrderLineRefund refund = refund("CREATED");
        when(paymentService.getById(70L)).thenReturn(payment);
        when(refundService.createIfAbsent(payment, "ADMIN")).thenReturn(refund);
        when(credentialService.getById(5L)).thenReturn(credential());
        when(credentialService.getCurrent(9L)).thenReturn(currentCredential("channel"));
        when(refundService.claimProcessing(eq(88L), eq(0L), eq("CREATED"), any(), any())).thenReturn(1);
        when(client.retrieveByTransactionId(eq(new com.ruoyi.app.utils.linepay.LinePayCredential(
                5L, "channel", "secret")), eq("TX-100"))).thenReturn(
                new LinePayResponse(200, "1105", "Authentication failed", null, null, "{}"));
        String wrongAmount = "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\"," 
                + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\",\"currency\":\"TWD\"," 
                + "\"payInfo\":[{\"amount\":100}],\"refundList\":[]}]}";
        when(client.retrieveByTransactionId(eq(new com.ruoyi.app.utils.linepay.LinePayCredential(
                6L, "channel", "new-secret")), eq("TX-100"))).thenReturn(
                new LinePayResponse(200, "0000", "Success", null, null, wrongAmount));

        assertEquals("UNKNOWN", service.requestFullRefund(70L, "ADMIN"));

        verify(client, never()).refundFull(any(), any());
        verify(refundService).markUnknown(eq(88L), eq(1L), any(), any());
    }

    @Test
    void unknownRefundIsRecoveredByRetrieveAndNeverCallsRefundAgain() throws Exception {
        PosOrderLinePayment payment = payment();
        PosOrderLineRefund refund = refund("UNKNOWN");
        when(paymentService.getById(70L)).thenReturn(payment);
        when(refundService.createIfAbsent(payment, "TASK")).thenReturn(refund);
        when(credentialService.getById(5L)).thenReturn(credential());
        String raw = "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\"," 
                + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\",\"currency\":\"TWD\"," 
                + "\"payInfo\":[{\"amount\":280}],\"refundList\":[{\"refundTransactionId\":\"RF-100\"," 
                + "\"transactionType\":\"PARTIAL_REFUND\",\"refundAmount\":-280}]}]}";
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
                new LinePayResponse(200, "0000", "Success", null, null, raw));

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

        assertEquals("REFUNDED", status);
        verify(client, org.mockito.Mockito.never()).refundFull(any(), any());
        verify(factService).applyRefundedFact(eq(88L), eq(70L), eq(0L), eq("RF-100"), any());
    }

    @Test
    void createdRefundRetrievesBeforeIssuingExternalRefund() throws Exception {
        PosOrderLinePayment payment = payment();
        PosOrderLineRefund refund = refund("CREATED");
        when(paymentService.getById(70L)).thenReturn(payment);
        when(refundService.createIfAbsent(payment, "ADMIN")).thenReturn(refund);
        when(credentialService.getById(5L)).thenReturn(credential());
        when(refundService.claimProcessing(eq(88L), eq(0L), eq("CREATED"), any(), any())).thenReturn(1);
        when(refundService.markManualReview(88L, 1L, "PROCESSING")).thenReturn(1);
        String raw = "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\"," 
                + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\",\"currency\":\"TWD\"," 
                + "\"payInfo\":[{\"amount\":280}],\"refundList\":[{\"refundTransactionId\":\"RF-OLD\"," 
                + "\"transactionType\":\"PARTIAL_REFUND\",\"refundAmount\":-280}]}]}";
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
                new LinePayResponse(200, "0000", "Success", null, null, raw));

        assertEquals("REFUNDED", service.requestFullRefund(70L, "ADMIN"));

        verify(client, org.mockito.Mockito.never()).refundFull(any(), any());
        verify(factService).applyRefundedFact(eq(88L), eq(70L), eq(1L), eq("RF-OLD"), any());
    }

    @Test
    void documentedTemporaryRefundCodeEntersControlledRetryWait() throws Exception {
        PosOrderLinePayment payment = payment();
        PosOrderLineRefund refund = refund("CREATED");
        when(paymentService.getById(70L)).thenReturn(payment);
        when(refundService.createIfAbsent(payment, "TASK")).thenReturn(refund);
        when(credentialService.getById(5L)).thenReturn(credential());
        when(refundService.claimProcessing(eq(88L), eq(0L), eq("CREATED"), any(), any())).thenReturn(1);
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(noRefundResponse());
        when(client.refundFull(any(), eq("TX-100"))).thenReturn(
                new LinePayResponse(200, "1900", "Temporary", null, null, "{}"));

        assertEquals("RETRY_WAIT", service.requestFullRefund(70L, "TASK"));

        verify(refundService).markRetryWait(eq(88L), eq(1L), any());
    }

    @Test
    void existingPartialRefundRequiresManualReviewAndNeverIssuesAnotherRefund() throws Exception {
        PosOrderLinePayment payment = payment();
        PosOrderLineRefund refund = refund("CREATED");
        when(paymentService.getById(70L)).thenReturn(payment);
        when(refundService.createIfAbsent(payment, "TASK")).thenReturn(refund);
        when(credentialService.getById(5L)).thenReturn(credential());
        when(refundService.claimProcessing(eq(88L), eq(0L), eq("CREATED"), any(), any())).thenReturn(1);
        when(refundService.markManualReview(88L, 1L, "PROCESSING")).thenReturn(1);
        String raw = "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\"," 
                + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\",\"currency\":\"TWD\"," 
                + "\"payInfo\":[{\"amount\":280}],\"refundList\":[{\"refundTransactionId\":\"RF-PART\"," 
                + "\"refundAmount\":-100}]}]}";
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
                new LinePayResponse(200, "0000", "Success", null, null, raw));

        assertEquals("MANUAL_REVIEW", service.requestFullRefund(70L, "TASK"));

        verify(client, never()).refundFull(any(), any());
        verify(refundService).markManualReview(88L, 1L, "PROCESSING");
    }

    @Test
    void inconclusiveRetrieveNeverIssuesRefundSideEffect() throws Exception {
        PosOrderLinePayment payment = payment();
        PosOrderLineRefund refund = refund("CREATED");
        when(paymentService.getById(70L)).thenReturn(payment);
        when(refundService.createIfAbsent(payment, "TASK")).thenReturn(refund);
        when(credentialService.getById(5L)).thenReturn(credential());
        when(refundService.claimProcessing(eq(88L), eq(0L), eq("CREATED"), any(), any())).thenReturn(1);
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
                new LinePayResponse(200, "9000", "Temporary", null, null, "{}"));

        assertEquals("UNKNOWN", service.requestFullRefund(70L, "TASK"));

        verify(client, never()).refundFull(any(), any());
        verify(refundService).markUnknown(eq(88L), eq(1L), any(), any());
    }

    @Test
    void mismatchedRetrievedPaymentNeverProvesRefundState() throws Exception {
        PosOrderLinePayment payment = payment();
        PosOrderLineRefund refund = refund("CREATED");
        when(paymentService.getById(70L)).thenReturn(payment);
        when(refundService.createIfAbsent(payment, "TASK")).thenReturn(refund);
        when(credentialService.getById(5L)).thenReturn(credential());
        when(refundService.claimProcessing(eq(88L), eq(0L), eq("CREATED"), any(), any())).thenReturn(1);
        String raw = "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\"," 
                + "\"orderId\":\"WRONG\",\"transactionType\":\"PAYMENT\",\"currency\":\"TWD\"," 
                + "\"payInfo\":[{\"amount\":280}]}]}";
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
                new LinePayResponse(200, "0000", "Success", null, null, raw));

        assertEquals("UNKNOWN", service.requestFullRefund(70L, "TASK"));
        verify(client, never()).refundFull(any(), any());
    }

    @Test
    void alreadyRefundedGatewayCodeRemainsRecoverableByRetrieve() throws Exception {
        PosOrderLinePayment payment = payment();
        PosOrderLineRefund refund = refund("CREATED");
        when(paymentService.getById(70L)).thenReturn(payment);
        when(refundService.createIfAbsent(payment, "TASK")).thenReturn(refund);
        when(credentialService.getById(5L)).thenReturn(credential());
        when(refundService.claimProcessing(eq(88L), eq(0L), eq("CREATED"), any(), any())).thenReturn(1);
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(noRefundResponse());
        when(client.refundFull(any(), eq("TX-100"))).thenReturn(
                new LinePayResponse(200, "1165", "Already refunded", null, null, "{}"));

        assertEquals("UNKNOWN", service.requestFullRefund(70L, "TASK"));

        verify(refundService, never()).markFailed(any(), any());
        verify(refundService).markUnknown(eq(88L), eq(1L), any(), any());
    }

    @Test
    void inconclusiveUnknownRetrieveMovesNextAttemptAndKeepsDeadline() throws Exception {
        PosOrderLinePayment payment = payment();
        PosOrderLineRefund refund = refund("UNKNOWN");
        Date deadline = new Date(System.currentTimeMillis() + 60_000L);
        refund.setReconcileDeadline(deadline);
        when(paymentService.getById(70L)).thenReturn(payment);
        when(refundService.createIfAbsent(payment, "TASK")).thenReturn(refund);
        when(credentialService.getById(5L)).thenReturn(credential());
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
                new LinePayResponse(200, "9000", "Temporary", null, null, "{}"));

        assertEquals("UNKNOWN", service.requestFullRefund(70L, "TASK"));

        verify(refundService).rescheduleUnknown(eq(88L), eq(0L), any(), eq(deadline));
    }

    @Test
    void expiredProcessingLeaseInitializesUnknownDeadlineBeforeRetrieve() throws Exception {
        PosOrderLinePayment payment = payment();
        PosOrderLineRefund processing = refund("PROCESSING");
        processing.setLeaseUntil(new Date(System.currentTimeMillis() - 1_000L));
        PosOrderLineRefund recovered = refund("UNKNOWN");
        recovered.setVersion(1L);
        when(paymentService.getById(70L)).thenReturn(payment);
        when(refundService.createIfAbsent(payment, "TASK")).thenReturn(processing);
        when(refundService.recoverExpiredProcessing(eq(88L), eq(0L), any(), any())).thenReturn(1);
        when(refundService.getByPaymentId(70L)).thenReturn(recovered);
        when(credentialService.getById(5L)).thenReturn(credential());
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
                new LinePayResponse(200, "9000", "Temporary", null, null, "{}"));

        assertEquals("UNKNOWN", service.requestFullRefund(70L, "TASK"));

        verify(refundService).recoverExpiredProcessing(eq(88L), eq(0L), any(), any());
        verify(refundService).rescheduleUnknown(eq(88L), eq(1L), any(), any());
    }

    @Test
    void expiredUnknownWithInconclusiveRetrieveMovesToManualReviewWithoutRescheduling() throws Exception {
        PosOrderLinePayment payment = payment();
        PosOrderLineRefund expired = refund("UNKNOWN");
        expired.setReconcileDeadline(new Date(System.currentTimeMillis() - 1_000L));
        when(paymentService.getById(70L)).thenReturn(payment);
        when(refundService.createIfAbsent(payment, "TASK")).thenReturn(expired);
        when(credentialService.getById(5L)).thenReturn(credential());
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
                new LinePayResponse(200, "9000", "Temporary", null, null, "{}"));
        when(refundService.markManualReview(88L, 0L, "UNKNOWN")).thenReturn(1);

        assertEquals("MANUAL_REVIEW", service.requestFullRefund(70L, "TASK"));

        verify(refundService, never()).rescheduleUnknown(any(), any(), any(), any());
        verify(refundService).markManualReview(88L, 0L, "UNKNOWN");
    }

    @Test
    void offlineRefundUsesPersistedModeAndPermanentLineOrderId() throws Exception {
        PosOrderLinePayment payment = payment();
        payment.setPaymentMode("OFFLINE");
        PosOrderLineRefund refund = refund("CREATED");
        when(paymentService.getById(70L)).thenReturn(payment);
        when(refundService.createIfAbsent(payment, "ADMIN")).thenReturn(refund);
        when(credentialService.getById(5L)).thenReturn(credential());
        when(refundService.claimProcessing(eq(88L), eq(0L), eq("CREATED"), any(), any())).thenReturn(1);
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(noRefundResponse());
        JSONObject info = new JSONObject();
        info.put("refundTransactionId", "RF-OFF-1");
        when(client.refundOfflineFull(any(), eq("LP-100"))).thenReturn(
                new LinePayResponse(200, "0000", "Success", null, info, "{}"));

        assertEquals("REFUNDED", service.requestFullRefund(70L, "ADMIN"));

        verify(client).refundOfflineFull(any(), eq("LP-100"));
        verify(client, never()).refundFull(any(), any());
    }

    @Test
    void amountMismatchRefundUsesCapturedAmountAsStrictEvidence() throws Exception {
        PosOrderLinePayment payment = payment();
        payment.setStatus("AMOUNT_MISMATCH");
        payment.setPaymentMode("OFFLINE");
        payment.setCapturedAmount(100);
        PosOrderLineRefund refund = refund("CREATED");
        refund.setAmount(100);
        when(paymentService.getById(70L)).thenReturn(payment);
        when(refundService.createIfAbsent(payment, "AMOUNT_MISMATCH")).thenReturn(refund);
        when(credentialService.getById(5L)).thenReturn(credential());
        when(refundService.claimProcessing(eq(88L), eq(0L), eq("CREATED"), any(), any())).thenReturn(1);
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
                amountMismatchRetrieve("[]"));
        JSONObject info = new JSONObject();
        info.put("refundTransactionId", "RF-MISMATCH");
        when(client.refundOfflineFull(any(), eq("LP-100"))).thenReturn(
                new LinePayResponse(200, "0000", "Success", null, info, "{}"));

        assertEquals("REFUNDED", service.requestFullRefund(70L, "AMOUNT_MISMATCH"));

        verify(client).refundOfflineFull(any(), eq("LP-100"));
    }

    @Test
    void amountMismatchUnknownRecoversFullRefundByCapturedAmount() throws Exception {
        PosOrderLinePayment payment = payment();
        payment.setStatus("AMOUNT_MISMATCH");
        payment.setPaymentMode("OFFLINE");
        payment.setCapturedAmount(100);
        PosOrderLineRefund refund = refund("UNKNOWN");
        refund.setAmount(100);
        when(paymentService.getById(70L)).thenReturn(payment);
        when(refundService.createIfAbsent(payment, "TASK")).thenReturn(refund);
        when(credentialService.getById(5L)).thenReturn(credential());
        when(client.retrieveByTransactionId(any(), eq("TX-100"))).thenReturn(
                amountMismatchRetrieve("[{\"refundTransactionId\":\"RF-MISMATCH\","
                        + "\"refundAmount\":-100}]"));

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

        verify(client, never()).refundOfflineFull(any(), any());
        verify(factService).applyRefundedFact(eq(88L), eq(70L), eq(0L),
                eq("RF-MISMATCH"), any());
    }

    private static LinePayResponse noRefundResponse() {
        String raw = "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\"," 
                + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\",\"currency\":\"TWD\"," 
                + "\"payInfo\":[{\"amount\":280}],\"refundList\":[]}]}";
        return new LinePayResponse(200, "0000", "Success", null, null, raw);
    }

    private static PosOrderLinePayment payment() {
        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("PAID");
        return payment;
    }

    private static PosOrderLineRefund refund(String status) {
        PosOrderLineRefund refund = new PosOrderLineRefund();
        refund.setId(88L);
        refund.setPaymentId(70L);
        refund.setStatus(status);
        refund.setVersion(0L);
        refund.setAmount(280);
        return refund;
    }

    private static LinePayResponse amountMismatchRetrieve(String refundList) {
        String raw = "{\"returnCode\":\"0000\",\"info\":[{\"transactionId\":\"TX-100\","
                + "\"orderId\":\"LP-100\",\"transactionType\":\"PAYMENT\",\"currency\":\"TWD\","
                + "\"payInfo\":[{\"amount\":100}],\"refundList\":" + refundList + "}]}";
        return new LinePayResponse(200, "0000", "Success", null, null, raw);
    }

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

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