package com.ruoyi.app.omgpay;

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import com.ruoyi.app.omgpay.dto.OmgCreatePaymentRequest;
import com.ruoyi.app.omgpay.dto.OmgCreatePaymentResponse;
import com.ruoyi.app.omgpay.dto.OmgPaymentErrorResponse;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.system.omgpay.service.OmgIpnAuditService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class OmgPaymentControllerTest {
    private MockedStatic<MessageUtils> messages;

    @BeforeEach
    void setUpMessages() {
        messages = mockStatic(MessageUtils.class);
        messages.when(() -> MessageUtils.message(anyString())).thenAnswer(invocation -> invocation.getArgument(0));
    }

    @AfterEach
    void closeMessages() {
        messages.close();
    }

    @Test
    void exposesOnlyExplicitCreateContract() throws Exception {
        RequestMapping mapping = OmgPaymentController.class.getAnnotation(RequestMapping.class);
        assertArrayEquals(new String[]{"/pay/omg"}, mapping.value());
        Method create = OmgPaymentController.class.getDeclaredMethod("create", String.class, OmgCreatePaymentRequest.class);
        assertArrayEquals(new String[]{"/create"}, create.getAnnotation(PostMapping.class).value());
        assertEquals("token", create.getParameters()[0].getAnnotation(RequestHeader.class).name());
        assertNotNull(create.getParameters()[1].getAnnotation(RequestBody.class));
        assertEquals(Set.of("orderId", "paymentMethod"), java.util.Arrays.stream(OmgCreatePaymentRequest.class.getDeclaredFields())
                .map(Field::getName).collect(Collectors.toSet()));
        assertTrue(java.util.Arrays.stream(create.getParameterTypes()).noneMatch(Map.class::isAssignableFrom));
    }

    @Test
    void returnsSuccessShapeAndSafeDiagnosticLogs() {
        OmgPaymentTokenUserResolver resolver = mock(OmgPaymentTokenUserResolver.class);
        OmgPaymentCreateService service = mock(OmgPaymentCreateService.class);
        when(resolver.requireUserId("SECRET_TOKEN")).thenReturn(5L);
        OmgCreatePaymentResponse response = new OmgCreatePaymentResponse("stage", Map.of("CheckMacValue", "A".repeat(64)));
        when(service.create(5L, "DD-1\r\nforged=true", OmgPaymentMethod.CREDIT)).thenReturn(new OmgPaymentCreateOutcome(
                response, 9L, "DD-1", 5L, 77L, 100, "OMG12***4567"));
        OmgPaymentController controller = controller(resolver, service);
        OmgCreatePaymentRequest request = new OmgCreatePaymentRequest();
        request.setOrderId("DD-1\r\nforged=true");
        request.setPaymentMethod("CREDIT");
        ListAppender<ILoggingEvent> logs = captureLogs();

        AjaxResult result = controller.create("SECRET_TOKEN", request);

        assertSame(response, result.get("data"));
        verify(service).create(5L, "DD-1\r\nforged=true", OmgPaymentMethod.CREDIT);
        String messages = logs.list.stream().map(ILoggingEvent::getFormattedMessage).collect(Collectors.joining("\n"));
        assertTrue(messages.contains("attemptId=9"));
        assertFalse(messages.contains("SECRET_TOKEN"));
        assertFalse(messages.contains("\r"));
        assertFalse(messages.contains("forged=true"));
        assertFalse(messages.contains("A".repeat(64)));
        assertFalse(messages.contains("CheckMacValue"));
    }

    @Test
    void mapsBusinessAndUnexpectedErrorsToStableStatus() {
        OmgPaymentTokenUserResolver resolver = mock(OmgPaymentTokenUserResolver.class);
        OmgPaymentCreateService service = mock(OmgPaymentCreateService.class);
        when(resolver.requireUserId(anyString())).thenReturn(5L);
        OmgPaymentController controller = controller(resolver, service);
        AjaxResult business = controller.create("token", null);
        assertEquals("PAYMENT_METHOD_INVALID", ((OmgPaymentErrorResponse) business.get("data")).status());

        RuntimeException failure = new RuntimeException("unexpected");
        when(service.create(5L, "DD-2", OmgPaymentMethod.CREDIT)).thenThrow(failure);
        OmgCreatePaymentRequest request = new OmgCreatePaymentRequest();
        request.setOrderId("DD-2");
        request.setPaymentMethod("CREDIT");
        ListAppender<ILoggingEvent> logs = captureLogs();
        AjaxResult unexpected = controller.create("token", request);
        assertEquals("PAYMENT_CREATION_FAILED", ((OmgPaymentErrorResponse) unexpected.get("data")).status());
        assertTrue(logs.list.stream().anyMatch(event -> event.getThrowableProxy() != null
                && event.getThrowableProxy().getMessage().equals("unexpected")));
    }

    @Test
    void mapsEveryBusinessErrorToItsStableStatus() {
        for (OmgPaymentErrorCode code : OmgPaymentErrorCode.values()) {
            OmgPaymentTokenUserResolver resolver = mock(OmgPaymentTokenUserResolver.class);
            OmgPaymentCreateService service = mock(OmgPaymentCreateService.class);
            when(resolver.requireUserId("token")).thenReturn(5L);
            when(service.create(5L, "DD-1", OmgPaymentMethod.CREDIT)).thenThrow(new OmgPaymentBusinessException(code, 77L));
            OmgCreatePaymentRequest request = new OmgCreatePaymentRequest();
            request.setOrderId("DD-1");
            request.setPaymentMethod("CREDIT");

            AjaxResult result = controller(resolver, service).create("token", request);

            assertEquals(code.name(), ((OmgPaymentErrorResponse) result.get("data")).status());
        }
    }

    @Test
    void mapsResolverFailureWithoutLoggingToken() {
        OmgPaymentTokenUserResolver resolver = mock(OmgPaymentTokenUserResolver.class);
        when(resolver.requireUserId("NEVER_LOG_THIS")).thenThrow(
                new OmgPaymentBusinessException(OmgPaymentErrorCode.AUTH_REQUIRED));
        OmgPaymentController controller = controller(resolver, mock(OmgPaymentCreateService.class));
        ListAppender<ILoggingEvent> logs = captureLogs();

        AjaxResult result = controller.create("NEVER_LOG_THIS", null);

        assertEquals("AUTH_REQUIRED", ((OmgPaymentErrorResponse) result.get("data")).status());
        assertTrue(logs.list.stream().noneMatch(event -> event.getFormattedMessage().contains("NEVER_LOG_THIS")));
    }

    @Test
    void rejectsInvalidPaymentMethodsWithoutCallingCreateService() {
        OmgPaymentTokenUserResolver resolver = mock(OmgPaymentTokenUserResolver.class);
        OmgPaymentCreateService service = mock(OmgPaymentCreateService.class);
        when(resolver.requireUserId("token")).thenReturn(5L);
        OmgPaymentController controller = controller(resolver, service);

        for (String paymentMethod : new String[]{null, "ALL", "Credit", " CREDIT "}) {
            OmgCreatePaymentRequest request = new OmgCreatePaymentRequest();
            request.setOrderId("DD-1");
            request.setPaymentMethod(paymentMethod);

            AjaxResult result = controller.create("token", request);

            assertEquals("PAYMENT_METHOD_INVALID", ((OmgPaymentErrorResponse) result.get("data")).status());
        }
        verifyNoInteractions(service);
    }

    private static ListAppender<ILoggingEvent> captureLogs() {
        Logger logger = (Logger) LoggerFactory.getLogger(OmgPaymentController.class);
        ListAppender<ILoggingEvent> appender = new ListAppender<>();
        appender.start();
        logger.addAppender(appender);
        return appender;
    }

    private static OmgPaymentController controller(OmgPaymentTokenUserResolver resolver,
                                                   OmgPaymentCreateService service) {
        return new OmgPaymentController(resolver, service, mock(OmgPaymentNotifyService.class),
                mock(OmgIpnAuditService.class), mock(OmgPaymentQueryService.class),
                mock(OmgPaymentRetryService.class),
                mock(OmgPaymentRefundService.class));
    }
}
