package tech.easyflow.log;

import cn.dev33.satoken.stp.StpUtil;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.junit.After;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import tech.easyflow.log.annotation.LogRecord;
import tech.easyflow.log.entity.WriteLog;
import tech.easyflow.log.mapper.WriteLogMapper;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Collections;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * {@link LogAspect} 操作结果与 Skill 标识审计测试。
 */
public class LogAspectTest {

    /**
     * 清理线程绑定的请求上下文。
     */
    @After
    public void tearDown() {
        RequestContextHolder.resetRequestAttributes();
    }

    /**
     * 验证成功操作记录成功状态，且 Skill body 只保留资源标识。
     *
     * @throws Throwable 切面执行异常
     */
    @Test
    public void successfulSkillActionShouldRecordIdentifiersOnly() throws Throwable {
        WriteLogMapper mapper = mock(WriteLogMapper.class);
        LogAspect aspect = new LogAspect(mapper, properties());
        HttpServletRequest request = request("""
                {"id":101,"resourceId":"202","skillId":303,"name":"secret-name","config":{"token":"secret"}}
                """);
        ProceedingJoinPoint joinPoint = joinPoint();
        Object expected = new Object();
        when(joinPoint.proceed()).thenReturn(expected);

        try (MockedStatic<StpUtil> stp = Mockito.mockStatic(StpUtil.class)) {
            stp.when(StpUtil::isLogin).thenReturn(false);
            assertSame(expected, aspect.doAround(joinPoint));
        }

        ArgumentCaptor<WriteLog> captor = ArgumentCaptor.forClass(WriteLog.class);
        verify(mapper).insert(captor.capture());
        WriteLog log = captor.getValue();
        assertEquals(Integer.valueOf(1), log.getStatus());
        assertEquals("{\"id\":101,\"resourceId\":\"202\",\"skillId\":303}", log.getActionBody());
    }

    /**
     * 验证业务异常原样抛出，同时审计记录失败状态。
     *
     * @throws Throwable 预期业务异常
     */
    @Test
    public void failedActionShouldKeepOriginalThrowableAndRecordFailure() throws Throwable {
        WriteLogMapper mapper = mock(WriteLogMapper.class);
        LogAspect aspect = new LogAspect(mapper, properties());
        request("{\"id\":101}");
        ProceedingJoinPoint joinPoint = joinPoint();
        IllegalStateException expected = new IllegalStateException("business failed");
        doThrow(expected).when(joinPoint).proceed();

        Throwable actual = null;
        try (MockedStatic<StpUtil> stp = Mockito.mockStatic(StpUtil.class)) {
            stp.when(StpUtil::isLogin).thenReturn(false);
            try {
                aspect.doAround(joinPoint);
            } catch (Throwable throwable) {
                actual = throwable;
            }
        }

        assertSame(expected, actual);
        ArgumentCaptor<WriteLog> captor = ArgumentCaptor.forClass(WriteLog.class);
        verify(mapper).insert(captor.capture());
        assertEquals(Integer.valueOf(9), captor.getValue().getStatus());
        assertEquals("{\"id\":101}", captor.getValue().getActionBody());
    }

    /**
     * 验证日志落库异常不会覆盖已发生的业务异常。
     *
     * @throws Throwable 预期业务异常
     */
    @Test
    public void loggingFailureShouldNotMaskBusinessFailure() throws Throwable {
        WriteLogMapper mapper = mock(WriteLogMapper.class);
        RuntimeException loggingFailure = new RuntimeException("log failed");
        when(mapper.insert(any(WriteLog.class))).thenThrow(loggingFailure);
        LogAspect aspect = new LogAspect(mapper, properties());
        request("{\"id\":101}");
        ProceedingJoinPoint joinPoint = joinPoint();
        IllegalArgumentException expected = new IllegalArgumentException("business failed");
        doThrow(expected).when(joinPoint).proceed();

        Throwable actual = null;
        try (MockedStatic<StpUtil> stp = Mockito.mockStatic(StpUtil.class)) {
            stp.when(StpUtil::isLogin).thenReturn(false);
            try {
                aspect.doAround(joinPoint);
            } catch (Throwable throwable) {
                actual = throwable;
            }
        }

        assertSame(expected, actual);
        assertEquals(1, actual.getSuppressed().length);
        assertSame(loggingFailure, actual.getSuppressed()[0]);
    }

    /**
     * 验证畸形 JSON 不会影响业务结果，且不会写入不可信正文。
     *
     * @throws Throwable 切面执行异常
     */
    @Test
    public void malformedSkillBodyShouldNotAffectAction() throws Throwable {
        WriteLogMapper mapper = mock(WriteLogMapper.class);
        LogAspect aspect = new LogAspect(mapper, properties());
        request("{invalid-json");
        ProceedingJoinPoint joinPoint = joinPoint();
        when(joinPoint.proceed()).thenReturn("ok");

        try (MockedStatic<StpUtil> stp = Mockito.mockStatic(StpUtil.class)) {
            stp.when(StpUtil::isLogin).thenReturn(false);
            assertEquals("ok", aspect.doAround(joinPoint));
        }

        ArgumentCaptor<WriteLog> captor = ArgumentCaptor.forClass(WriteLog.class);
        verify(mapper).insert(captor.capture());
        assertEquals(Integer.valueOf(1), captor.getValue().getStatus());
        assertNull(captor.getValue().getActionBody());
    }

    /**
     * 验证未显式标注的 GET 请求不会产生操作日志。
     *
     * @throws Throwable 切面执行异常
     */
    @Test
    public void unannotatedGetShouldNotWriteLog() throws Throwable {
        WriteLogMapper mapper = mock(WriteLogMapper.class);
        LogAspect aspect = new LogAspect(mapper, properties());
        request("{}", "GET");
        ProceedingJoinPoint joinPoint = joinPoint("read");
        when(joinPoint.proceed()).thenReturn("ok");

        assertEquals("ok", aspect.doAround(joinPoint));

        verify(mapper, never()).insert(any(WriteLog.class));
    }

    /**
     * 验证显式标注的 GET 请求仍会产生操作日志。
     *
     * @throws Throwable 切面执行异常
     */
    @Test
    public void annotatedGetShouldWriteLog() throws Throwable {
        WriteLogMapper mapper = mock(WriteLogMapper.class);
        LogAspect aspect = new LogAspect(mapper, properties());
        request("{}", "GET");
        ProceedingJoinPoint joinPoint = joinPoint("auditedRead");
        when(joinPoint.proceed()).thenReturn("ok");

        try (MockedStatic<StpUtil> stp = Mockito.mockStatic(StpUtil.class)) {
            stp.when(StpUtil::isLogin).thenReturn(false);
            assertEquals("ok", aspect.doAround(joinPoint));
        }

        ArgumentCaptor<WriteLog> captor = ArgumentCaptor.forClass(WriteLog.class);
        verify(mapper).insert(captor.capture());
        assertEquals("审计读取", captor.getValue().getActionName());
    }

    /**
     * 创建测试日志配置。
     *
     * @return 日志配置
     */
    private LogRecordProperties properties() {
        LogRecordProperties properties = new LogRecordProperties();
        properties.setRecordActionPrefix("/api/v1");
        return properties;
    }

    /**
     * 创建并绑定测试请求。
     *
     * @param body JSON body
     * @return 测试请求
     * @throws IOException 输入流创建失败
     */
    private HttpServletRequest request(String body) throws IOException {
        return request(body, "POST");
    }

    /**
     * 创建并绑定指定 HTTP 方法的测试请求。
     *
     * @param body JSON body
     * @param httpMethod HTTP 方法
     * @return 测试请求
     * @throws IOException 输入流创建失败
     */
    private HttpServletRequest request(String body, String httpMethod) throws IOException {
        HttpServletRequest request = mock(HttpServletRequest.class);
        when(request.getServletPath()).thenReturn("/api/v1/skill/update");
        when(request.getMethod()).thenReturn(httpMethod);
        when(request.getContentType()).thenReturn("application/json;charset=UTF-8");
        when(request.getCharacterEncoding()).thenReturn(StandardCharsets.UTF_8.name());
        when(request.getParameterNames()).thenReturn(Collections.emptyEnumeration());
        when(request.getRequestURL()).thenReturn(new StringBuffer("http://localhost/api/v1/skill/update"));
        when(request.getRemoteAddr()).thenReturn("127.0.0.1");
        when(request.getInputStream()).thenReturn(inputStream(body));
        RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
        return request;
    }

    /**
     * 创建测试连接点。
     *
     * @return 测试连接点
     * @throws NoSuchMethodException 测试方法不存在
     */
    private ProceedingJoinPoint joinPoint() throws NoSuchMethodException {
        return joinPoint("update");
    }

    /**
     * 创建指定控制器方法的测试连接点。
     *
     * @param methodName 控制器方法名
     * @return 测试连接点
     * @throws NoSuchMethodException 测试方法不存在
     */
    private ProceedingJoinPoint joinPoint(String methodName) throws NoSuchMethodException {
        ProceedingJoinPoint joinPoint = mock(ProceedingJoinPoint.class);
        MethodSignature signature = mock(MethodSignature.class);
        Method method = TestController.class.getMethod(methodName);
        when(joinPoint.getSignature()).thenReturn(signature);
        when(signature.getDeclaringType()).thenReturn(TestController.class);
        when(signature.getMethod()).thenReturn(method);
        return joinPoint;
    }

    /**
     * 创建基于字节数组的 Servlet 输入流。
     *
     * @param body 请求正文
     * @return Servlet 输入流
     */
    private ServletInputStream inputStream(String body) {
        ByteArrayInputStream input = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
        return new ServletInputStream() {
            @Override
            public boolean isFinished() {
                return input.available() == 0;
            }

            @Override
            public boolean isReady() {
                return true;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
                // 同步测试输入流不需要异步读取监听。
            }

            @Override
            public int read() {
                return input.read();
            }
        };
    }

    /**
     * 测试 Controller 签名载体。
     */
    public static class TestController {

        /**
         * 模拟更新入口。
         *
         * @return 空结果
         */
        public Object update() {
            return null;
        }

        /**
         * 模拟普通读取入口。
         *
         * @return 空结果
         */
        public Object read() {
            return null;
        }

        /**
         * 模拟需要审计的读取入口。
         *
         * @return 空结果
         */
        @LogRecord("审计读取")
        public Object auditedRead() {
            return null;
        }
    }
}
