package com.ruoyi.app.pay;

import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.ruoyi.app.pay.dto.LinePayCreateResult;
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.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 org.springframework.dao.DuplicateKeyException;

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.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
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 LinePayServiceCreateTest {

    private IPosOrderService orderService;
    private IPosStoreLinePayService credentialService;
    private IPosOrderLinePaymentService paymentService;
    private LinePayClient linePayClient;
    private LinePayService service;

    @BeforeEach
    void setUp() {
        orderService = mock(IPosOrderService.class);
        credentialService = mock(IPosStoreLinePayService.class);
        paymentService = mock(IPosOrderLinePaymentService.class);
        linePayClient = mock(LinePayClient.class);
        service = new LinePayService(orderService, credentialService, paymentService,
                mock(IPosOrderLineRefundService.class), linePayClient, mock(LinePayFactService.class));
    }

    @Test
    void duplicateCreateReusesActiveAttemptWithoutCallingLineAgain() throws Exception {
        PosOrder order = order();
        PosOrderLinePayment active = payment("WAITING_AUTH");
        active.setPaymentUrlWeb("https://pay.example/existing");
        when(orderService.getOne(any(Wrapper.class))).thenReturn(order);
        when(orderService.count(any(Wrapper.class))).thenReturn(1L);
        when(credentialService.getEnabledCurrent(9L)).thenReturn(credential());
        when(paymentService.getActiveByDdId("DD-100")).thenReturn(active);

        LinePayCreateResult result = service.create(101L, "DD-100");

        assertTrue(result.isReusedAttempt());
        assertEquals(70L, result.getPaymentId());
        assertEquals("https://pay.example/existing", result.getPaymentUrl());
        verify(linePayClient, never()).request(any(), any());
        verify(paymentService, never()).createRequesting(any(), any(), any(), any(), any(), any(), any(), any());
    }

    @Test
    void newCreatePersistsIntentBeforeSavingGatewayIdentifiers() throws Exception {
        PosOrder order = order();
        PosOrderLinePayment requesting = payment("REQUESTING");
        when(orderService.getOne(any(Wrapper.class))).thenReturn(order);
        when(orderService.count(any(Wrapper.class))).thenReturn(1L);
        when(credentialService.getEnabledCurrent(9L)).thenReturn(credential());
        when(paymentService.getActiveByDdId("DD-100")).thenReturn(null);
        when(paymentService.createRequesting(eq("DD-100"), anyString(), eq(5L), eq(9L),
                eq(280), eq("TWD"), eq("ONLINE"), any())).thenReturn(requesting);
        JSONObject paymentUrl = new JSONObject();
        paymentUrl.put("web", "https://pay.example/web");
        JSONObject info = new JSONObject();
        info.put("transactionId", "2026081200000000001");
        info.put("paymentUrl", paymentUrl);
        when(linePayClient.request(any(), any())).thenReturn(new LinePayResponse(
                200, "0000", "Success", "2026081200000000001", info, "{}"));
        when(paymentService.markRequestSucceeded(eq(70L), eq(0L),
                eq("2026081200000000001"), eq("https://pay.example/web"),
                any(), any(), any())).thenReturn(1);

        LinePayCreateResult result = service.create(101L, "DD-100");

        assertEquals(70L, result.getPaymentId());
        assertEquals("WAITING_AUTH", result.getStatus());
        assertEquals("2026081200000000001", result.getTransactionId());
        verify(paymentService).createRequesting(eq("DD-100"), anyString(), eq(5L), eq(9L),
                eq(280), eq("TWD"), eq("ONLINE"), any());
        verify(paymentService).markRequestSucceeded(eq(70L), eq(0L),
                eq("2026081200000000001"), eq("https://pay.example/web"),
                any(), any(), any());
    }

    @Test
    void transportUnknownKeepsSameAttemptBlockedForReadOnlyRecovery() throws Exception {
        when(orderService.getOne(any(Wrapper.class))).thenReturn(order());
        when(orderService.count(any(Wrapper.class))).thenReturn(1L);
        when(credentialService.getEnabledCurrent(9L)).thenReturn(credential());
        when(paymentService.createRequesting(eq("DD-100"), anyString(), eq(5L), eq(9L),
                eq(280), eq("TWD"), eq("ONLINE"), any())).thenReturn(payment("REQUESTING"));
        when(linePayClient.request(any(), any())).thenThrow(new IllegalStateException("timeout"));
        when(paymentService.markRequestUnknown(eq(70L), eq(0L), any(), any())).thenReturn(1);

        LinePayCreateResult result = service.create(101L, "DD-100");

        assertEquals("REQUEST_UNKNOWN", result.getStatus());
        verify(paymentService).markRequestUnknown(eq(70L), eq(0L), any(), any());
        verify(paymentService, never()).markTerminal(any(), any(), any(), any());
    }

    @Test
    void concurrentIntentNeverCallsRequestTwiceForTheExistingRow() throws Exception {
        PosOrderLinePayment concurrent = payment("REQUESTING");
        when(orderService.getOne(any(Wrapper.class))).thenReturn(order());
        when(orderService.count(any(Wrapper.class))).thenReturn(1L);
        when(credentialService.getEnabledCurrent(9L)).thenReturn(credential());
        when(paymentService.getActiveByDdId("DD-100")).thenReturn(null, concurrent);
        when(paymentService.createRequesting(eq("DD-100"), anyString(), eq(5L), eq(9L),
                eq(280), eq("TWD"), eq("ONLINE"), any())).thenThrow(new DuplicateKeyException("active"));

        LinePayCreateResult result = service.create(101L, "DD-100");

        assertTrue(result.isReusedAttempt());
        assertEquals(70L, result.getPaymentId());
        verify(linePayClient, never()).request(any(), any());
    }

    @Test
    void definiteRequestRejectionReleasesAttemptForAnotherPayment() throws Exception {
        when(orderService.getOne(any(Wrapper.class))).thenReturn(order());
        when(orderService.count(any(Wrapper.class))).thenReturn(1L);
        when(credentialService.getEnabledCurrent(9L)).thenReturn(credential());
        when(paymentService.createRequesting(eq("DD-100"), anyString(), eq(5L), eq(9L),
                eq(280), eq("TWD"), eq("ONLINE"), any())).thenReturn(payment("REQUESTING"));
        when(linePayClient.request(any(), any())).thenReturn(new LinePayResponse(
                200, "1104", "Invalid amount", null, null, "{}"));
        when(paymentService.markTerminal(70L, 0L, "REQUESTING", "FAILED")).thenReturn(1);

        LinePayCreateResult result = service.create(101L, "DD-100");

        assertEquals("FAILED", result.getStatus());
        verify(paymentService).markTerminal(70L, 0L, "REQUESTING", "FAILED");
        verify(paymentService, never()).markRequestUnknown(any(), any(), any(), any());
    }

    @Test
    void inProgressOrDuplicateOrderRequestNeverReleasesActiveAttempt() throws Exception {
        when(orderService.getOne(any(Wrapper.class))).thenReturn(order());
        when(orderService.count(any(Wrapper.class))).thenReturn(1L);
        when(credentialService.getEnabledCurrent(9L)).thenReturn(credential());
        when(paymentService.createRequesting(eq("DD-100"), anyString(), eq(5L), eq(9L),
                eq(280), eq("TWD"), eq("ONLINE"), any())).thenReturn(payment("REQUESTING"));
        when(linePayClient.request(any(), any())).thenReturn(new LinePayResponse(
                200, "1172", "Order already exists", null, null, "{}"));
        when(paymentService.markRequestUnknown(eq(70L), eq(0L), any(), any())).thenReturn(1);

        LinePayCreateResult result = service.create(101L, "DD-100");

        assertEquals("REQUEST_UNKNOWN", result.getStatus());
        verify(paymentService, never()).markTerminal(any(), any(), any(), any());
        verify(paymentService).markRequestUnknown(eq(70L), eq(0L), any(), any());
    }

    @Test
    void failedTerminalCasReturnsConcurrentDurableStateWithoutReleasingAttempt() throws Exception {
        when(orderService.getOne(any(Wrapper.class))).thenReturn(order());
        when(orderService.count(any(Wrapper.class))).thenReturn(1L);
        when(credentialService.getEnabledCurrent(9L)).thenReturn(credential());
        when(paymentService.createRequesting(eq("DD-100"), anyString(), eq(5L), eq(9L),
                eq(280), eq("TWD"), eq("ONLINE"), any())).thenReturn(payment("REQUESTING"));
        when(linePayClient.request(any(), any())).thenReturn(new LinePayResponse(
                200, "1104", "Invalid merchant", null, null, "{}"));
        when(paymentService.markTerminal(70L, 0L, "REQUESTING", "FAILED")).thenReturn(0);
        PosOrderLinePayment concurrent = payment("WAITING_AUTH");
        when(paymentService.getById(70L)).thenReturn(concurrent);

        assertEquals("WAITING_AUTH", service.create(101L, "DD-100").getStatus());

        verify(paymentService, never()).markRequestUnknown(any(), any(), any(), any());
    }

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

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

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