package com.ruoyi.app.pay;

import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.ruoyi.app.pay.dto.LinePayOfflineResult;
import com.ruoyi.app.utils.linepay.LinePayClient;
import com.ruoyi.app.utils.linepay.LinePayOfflineRequest;
import com.ruoyi.app.utils.linepay.LinePayResponse;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.system.domain.InfoUser;
import com.ruoyi.system.domain.PosOrder;
import com.ruoyi.system.domain.PosOrderLinePayment;
import com.ruoyi.system.domain.PosStore;
import com.ruoyi.system.domain.PosStoreLinePay;
import com.ruoyi.system.service.IInfoUserService;
import com.ruoyi.system.service.IPosOrderLinePaymentService;
import com.ruoyi.system.service.IPosOrderService;
import com.ruoyi.system.service.IPosStoreService;
import com.ruoyi.system.service.IPosStoreLinePayService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import org.springframework.dao.DuplicateKeyException;

import java.util.Date;

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

class LinePayOfflineServiceTest {
    private IPosOrderService orders;
    private IInfoUserService users;
    private IPosStoreService stores;
    private IPosStoreLinePayService credentials;
    private IPosOrderLinePaymentService payments;
    private LinePayClient client;
    private LinePayFactService facts;
    private LinePayOfflineService service;

    @BeforeEach
    void setUp() {
        orders = mock(IPosOrderService.class);
        users = mock(IInfoUserService.class);
        stores = mock(IPosStoreService.class);
        credentials = mock(IPosStoreLinePayService.class);
        payments = mock(IPosOrderLinePaymentService.class);
        client = mock(LinePayClient.class);
        facts = mock(LinePayFactService.class);
        service = new LinePayOfflineService(orders, users, stores, credentials, payments,
                client, facts);
        when(users.getOne(any(Wrapper.class))).thenReturn(merchant());
        when(orders.getOne(any(Wrapper.class))).thenReturn(order());
        when(orders.count(any(Wrapper.class))).thenReturn(1L);
        when(stores.getById(9L)).thenReturn(store(101L));
        when(credentials.getEnabledCurrent(9L)).thenReturn(credential());
        when(credentials.getById(5L)).thenReturn(credential());
        when(payments.createRequesting(eq("DD-100"), any(), eq(5L), eq(9L), eq(280),
                eq("TWD"), eq("OFFLINE"), any())).thenReturn(intent());
    }

    @Test
    void successfulPayRequiresStrictFactsAndPersistsProvider() throws Exception {
        JSONObject info = new JSONObject();
        info.put("orderId", "LPOFF-100");
        info.put("transactionId", "202608180000000001");
        info.put("paymentProvider", "TSP");
        info.put("payInfo", com.alibaba.fastjson2.JSONArray.parseArray("[{\"amount\":280}]"));
        when(client.payOffline(any(), any(LinePayOfflineRequest.class))).thenReturn(
                new LinePayResponse(200, "0000", "Success", "202608180000000001",
                        info, new JSONObject().fluentPut("returnCode", "0000")
                        .fluentPut("info", info).toJSONString()));

        LinePayOfflineResult result = service.pay(101L, "DD-100", "123456789012345678");

        assertEquals("PAID", result.getStatus());
        verify(facts).applyPaidFact(eq(70L), eq("202608180000000001"), eq("TSP"), any(Date.class));
    }

    @ParameterizedTest
    @ValueSource(strings = {"1145", "1169"})
    void authenticationRequiredIsDurableAndDoesNotApplyPaidFact(String returnCode) throws Exception {
        when(client.payOffline(any(), any(LinePayOfflineRequest.class))).thenReturn(
                new LinePayResponse(200, returnCode, "Authentication required", null, null,
                        "{\"returnCode\":\"" + returnCode + "\"}"));
        when(payments.markOfflineWaitingAuth(eq(70L), eq(0L), any(), any(), any())).thenReturn(1);

        LinePayOfflineResult result = service.pay(101L, "DD-100", "123456789012345678");

        assertEquals("AUTH_REQUIRED", result.getStatus());
        ArgumentCaptor<Date> deadline = ArgumentCaptor.forClass(Date.class);
        verify(payments).markOfflineWaitingAuth(eq(70L), eq(0L), any(), any(), deadline.capture());
        long remaining = deadline.getValue().getTime() - System.currentTimeMillis();
        assertTrue(remaining >= 29L * 60_000L && remaining <= 31L * 60_000L);
        verify(facts, never()).applyPaidFact(any(), any(), any(), any());
    }

    @Test
    void activeOfflineAttemptIsReusedWithoutSendingNewMyCode() throws Exception {
        PosOrderLinePayment active = intent();
        active.setStatus("REQUEST_UNKNOWN");
        when(payments.getActiveByDdId("DD-100")).thenReturn(active);

        LinePayOfflineResult result = service.pay(101L, "DD-100", "999999999999999999");

        assertEquals("PROCESSING", result.getStatus());
        assertEquals(true, result.getReusedAttempt());
        verify(client, never()).payOffline(any(), any());
    }

    @Test
    void directMerchantCannotUseAnotherOwnersStoreCredential() throws Exception {
        when(stores.getById(9L)).thenReturn(store(999L));

        try (MockedStatic<MessageUtils> messages = mockStatic(MessageUtils.class)) {
            messages.when(() -> MessageUtils.message(anyString()))
                    .thenAnswer(invocation -> invocation.getArgument(0));
            org.junit.jupiter.api.Assertions.assertThrows(com.ruoyi.common.exception.ServiceException.class,
                    () -> service.pay(101L, "DD-100", "123456789012345678"));
        }

        verify(client, never()).payOffline(any(), any());
    }

    @Test
    void duplicateIntentRaceReusesConcurrentAttemptWithoutSendingMyCode() throws Exception {
        PosOrderLinePayment concurrent = intent();
        when(payments.getActiveByDdId("DD-100")).thenReturn(null, concurrent);
        when(payments.createRequesting(eq("DD-100"), any(), eq(5L), eq(9L), eq(280),
                eq("TWD"), eq("OFFLINE"), any()))
                .thenThrow(new DuplicateKeyException("active"));

        LinePayOfflineResult result = service.pay(101L, "DD-100", "123456789012345678");

        assertEquals(true, result.getReusedAttempt());
        assertEquals("PROCESSING", result.getStatus());
        verify(client, never()).payOffline(any(), any());
    }

    @Test
    void undocumentedPayCodeDefaultsToUnknownAndKeepsActiveAttempt() throws Exception {
        when(client.payOffline(any(), any(LinePayOfflineRequest.class))).thenReturn(
                new LinePayResponse(200, "1299", "New provider code", null, null,
                        "{\"returnCode\":\"1299\"}"));

        assertEquals("PROCESSING",
                service.pay(101L, "DD-100", "123456789012345678").getStatus());

        verify(payments).markRequestUnknown(eq(70L), eq(0L), any(), any());
        verify(payments, never()).markTerminal(any(), any(), any(), any());
    }

    @Test
    void amountMismatchCreatesOneRefundIntentAndNeverFulfillsOrder() throws Exception {
        JSONObject info = completedInfo(100, "TSP");
        when(client.payOffline(any(), any(LinePayOfflineRequest.class))).thenReturn(
                success(info));
        when(facts.applyAmountMismatchFact(eq(70L), eq(0L), eq("REQUESTING"),
                eq("202608180000000001"), eq("TSP"), eq(100), any())).thenReturn(true);

        LinePayOfflineResult result = service.pay(101L, "DD-100", "123456789012345678");

        assertEquals("MANUAL_REVIEW", result.getStatus());
        verify(facts).applyAmountMismatchFact(eq(70L), eq(0L), eq("REQUESTING"),
                eq("202608180000000001"), eq("TSP"), eq(100), any());
        verify(facts, never()).applyPaidFact(any(), any(), any(), any());
    }

    @Test
    void failedAmountMismatchCasNeverCreatesRefundFromStaleResponse() throws Exception {
        JSONObject info = completedInfo(100, "TSP");
        when(client.payOffline(any(), any(LinePayOfflineRequest.class))).thenReturn(success(info));
        when(facts.applyAmountMismatchFact(eq(70L), eq(0L), eq("REQUESTING"),
                eq("202608180000000001"), eq("TSP"), eq(100), any())).thenReturn(false);
        PosOrderLinePayment current = intent();
        current.setStatus("PAID");
        when(payments.getById(70L)).thenReturn(current);

        assertEquals("PAID",
                service.pay(101L, "DD-100", "123456789012345678").getStatus());

        verify(facts).applyAmountMismatchFact(eq(70L), eq(0L), eq("REQUESTING"),
                eq("202608180000000001"), eq("TSP"), eq(100), any());
    }

    @Test
    void missingPaymentProviderMovesToManualReviewWithoutRefundOrFulfillment() throws Exception {
        JSONObject info = completedInfo(280, null);
        when(client.payOffline(any(), any(LinePayOfflineRequest.class))).thenReturn(success(info));
        when(payments.markOfflineEvidence(eq(70L), eq(0L), eq("REQUESTING"),
                eq("MANUAL_REVIEW"), eq("202608180000000001"), eq(null), eq(280), any()))
                .thenReturn(1);

        LinePayOfflineResult result = service.pay(101L, "DD-100", "123456789012345678");

        assertEquals("MANUAL_REVIEW", result.getStatus());
        verify(payments).markOfflineEvidence(eq(70L), eq(0L), eq("REQUESTING"),
                eq("MANUAL_REVIEW"), eq("202608180000000001"), eq(null), eq(280), any());
        verify(facts, never()).applyAmountMismatchFact(any(), any(), any(), any(), any(), any(), any());
        verify(facts, never()).applyPaidFact(any(), any(), any(), any());
    }

    @Test
    void checkAuthReadyKeepsAttemptBlockingAndReschedules() throws Exception {
        PosOrderLinePayment payment = intent();
        payment.setStatus("REQUEST_UNKNOWN");
        when(payments.getById(70L)).thenReturn(payment);
        JSONObject info = new JSONObject();
        info.put("status", "AUTH_READY");
        when(client.checkOffline(any(), eq("LPOFF-100"))).thenReturn(success(info));
        when(payments.markOfflineWaitingAuth(eq(70L), eq(0L), any(), any(), any()))
                .thenReturn(1);

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

        verify(payments).markOfflineWaitingAuth(eq(70L), eq(0L), any(), any(), any());
        verify(facts, never()).applyPaidFact(any(), any(), any(), any());
    }

    @Test
    void repeatedAuthReadyKeepsOriginalAuthenticationDeadline() throws Exception {
        PosOrderLinePayment payment = intent();
        payment.setStatus("WAITING_AUTH");
        Date originalDeadline = new Date(System.currentTimeMillis() + 10L * 60_000L);
        payment.setReconcileDeadline(originalDeadline);
        when(payments.getById(70L)).thenReturn(payment);
        JSONObject info = new JSONObject();
        info.put("status", "AUTH_READY");
        when(client.checkOffline(any(), eq("LPOFF-100"))).thenReturn(success(info));

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

        verify(payments).markOfflineWaitingAuth(eq(70L), eq(0L), any(), any(),
                eq(originalDeadline));
    }

    @Test
    void checkCompleteAppliesStrictPaidFact() throws Exception {
        PosOrderLinePayment payment = intent();
        payment.setStatus("WAITING_AUTH");
        when(payments.getById(70L)).thenReturn(payment);
        JSONObject info = completedInfo(280, "EPI");
        info.put("status", "COMPLETE");
        when(client.checkOffline(any(), eq("LPOFF-100"))).thenReturn(success(info));

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

        verify(facts).applyPaidFact(eq(70L), eq("202608180000000001"), eq("EPI"), any());
    }

    @Test
    void checkCancelAndFailReleaseOnlyTheirOwnAttempt() throws Exception {
        PosOrderLinePayment payment = intent();
        payment.setStatus("WAITING_AUTH");
        when(payments.getById(70L)).thenReturn(payment);
        JSONObject cancel = new JSONObject();
        cancel.put("status", "CANCEL");
        when(client.checkOffline(any(), eq("LPOFF-100"))).thenReturn(success(cancel));
        when(payments.markTerminal(70L, 0L, "WAITING_AUTH", "CANCELLED_OR_EXPIRED"))
                .thenReturn(1);

        assertEquals("CANCELLED_OR_EXPIRED", service.reconcilePayment(70L, "TASK"));
        verify(payments).markTerminal(70L, 0L, "WAITING_AUTH", "CANCELLED_OR_EXPIRED");
    }

    @Test
    void checkFailReleasesAttemptAsExplicitFailure() throws Exception {
        PosOrderLinePayment payment = intent();
        payment.setStatus("WAITING_AUTH");
        when(payments.getById(70L)).thenReturn(payment);
        JSONObject fail = new JSONObject();
        fail.put("status", "FAIL");
        when(client.checkOffline(any(), eq("LPOFF-100"))).thenReturn(success(fail));
        when(payments.markTerminal(70L, 0L, "WAITING_AUTH", "FAILED")).thenReturn(1);

        assertEquals("FAILED", service.reconcilePayment(70L, "TASK"));
        verify(payments).markTerminal(70L, 0L, "WAITING_AUTH", "FAILED");
    }

    @ParameterizedTest
    @ValueSource(strings = {"1152", "1172", "1198", "1199", "1901", "9000"})
    void uncertainCheckCodeOnlyReschedulesByLineOrderId(String returnCode) throws Exception {
        PosOrderLinePayment payment = intent();
        payment.setStatus("REQUEST_UNKNOWN");
        payment.setReconcileDeadline(new Date(System.currentTimeMillis() + 60_000L));
        when(payments.getById(70L)).thenReturn(payment);
        when(client.checkOffline(any(), eq("LPOFF-100"))).thenReturn(
                new LinePayResponse(200, returnCode, "Pending", null, null,
                        "{\"returnCode\":\"" + returnCode + "\"}"));

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

        verify(payments).rescheduleOffline(eq(70L), eq(0L), eq("REQUEST_UNKNOWN"),
                any(), eq(payment.getReconcileDeadline()));
        verify(client, never()).payOffline(any(), any());
    }

    @Test
    void undocumentedCheckCodeNeverReleasesActiveAttempt() throws Exception {
        PosOrderLinePayment payment = intent();
        payment.setStatus("REQUEST_UNKNOWN");
        payment.setReconcileDeadline(new Date(System.currentTimeMillis() + 60_000L));
        when(payments.getById(70L)).thenReturn(payment);
        when(client.checkOffline(any(), eq("LPOFF-100"))).thenReturn(
                new LinePayResponse(200, "1299", "New provider code", null, null,
                        "{\"returnCode\":\"1299\"}"));

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

        verify(payments).rescheduleOffline(eq(70L), eq(0L), eq("REQUEST_UNKNOWN"),
                any(), eq(payment.getReconcileDeadline()));
        verify(payments, never()).markTerminal(any(), any(), any(), any());
    }

    @Test
    void expiredUnknownGetsOneFinalCheckThenManualReview() throws Exception {
        PosOrderLinePayment payment = intent();
        payment.setStatus("REQUEST_UNKNOWN");
        payment.setReconcileDeadline(new Date(System.currentTimeMillis() - 1_000L));
        when(payments.getById(70L)).thenReturn(payment);
        when(client.checkOffline(any(), eq("LPOFF-100"))).thenReturn(
                new LinePayResponse(200, "9000", "Pending", null, null,
                        "{\"returnCode\":\"9000\"}"));
        when(payments.markManualReview(70L, 0L, "REQUEST_UNKNOWN")).thenReturn(1);

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

        verify(client).checkOffline(any(), eq("LPOFF-100"));
        verify(payments).markManualReview(70L, 0L, "REQUEST_UNKNOWN");
        verify(payments, never()).rescheduleOffline(any(), any(), any(), any(), any());
    }

    private static JSONObject completedInfo(int amount, String provider) {
        JSONObject info = new JSONObject();
        info.put("orderId", "LPOFF-100");
        info.put("transactionId", "202608180000000001");
        if (provider != null) {
            info.put("paymentProvider", provider);
        }
        info.put("payInfo", com.alibaba.fastjson2.JSONArray.parseArray(
                "[{\"amount\":" + amount + "}]"));
        return info;
    }

    private static LinePayResponse success(JSONObject info) {
        JSONObject root = new JSONObject();
        root.put("returnCode", "0000");
        root.put("info", info);
        return new LinePayResponse(200, "0000", "Success",
                info.getString("transactionId"), info, root.toJSONString());
    }

    private static InfoUser merchant() {
        InfoUser user = new InfoUser();
        user.setUserId(101L);
        user.setUserType("1");
        return user;
    }

    private static PosOrder order() {
        PosOrder order = new PosOrder();
        order.setDdId("DD-100");
        order.setParentDdId("DD-100");
        order.setShId(101L);
        order.setMdId(9L);
        order.setAmount(280);
        order.setState(1L);
        order.setPayStatus(0L);
        order.setAfterSaleStatus(0L);
        order.setPayType("3");
        order.setOrderSource("MERCHANT");
        return order;
    }

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

    private static PosStore store(Long ownerId) {
        PosStore value = new PosStore();
        value.setId(9);
        value.setUserId(ownerId);
        return value;
    }

    private static PosOrderLinePayment intent() {
        PosOrderLinePayment value = new PosOrderLinePayment();
        value.setId(70L);
        value.setDdId("DD-100");
        value.setLineOrderId("LPOFF-100");
        value.setCredentialId(5L);
        value.setStoreId(9L);
        value.setAmount(280);
        value.setCurrency("TWD");
        value.setPaymentMode("OFFLINE");
        value.setStatus("REQUESTING");
        value.setActiveDdId("DD-100");
        value.setVersion(0L);
        value.setUpdateTime(new Date());
        return value;
    }
}
