package com.ruoyi.system.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.MessageUtils;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;

import java.lang.reflect.Method;
import java.util.Date;
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.anyString;
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 AuthAspectTest {

    private RedisCache redisCache;
    private HttpServletRequest request;
    private ProceedingJoinPoint joinPoint;
    private MethodSignature signature;
    private AuthAspect aspect;

    @BeforeEach
    void setUp() {
        redisCache = mock(RedisCache.class);
        request = mock(HttpServletRequest.class);
        joinPoint = mock(ProceedingJoinPoint.class);
        signature = mock(MethodSignature.class);
        aspect = new AuthAspect(redisCache);
        aspect.setHttpServletRequest(request);
        when(joinPoint.getSignature()).thenReturn(signature);
    }

    @Test
    void jwtOnlyAuthDoesNotRequireRedisSession() throws Throwable {
        String token = JwtUtil.token("936", "merchant");
        Object expected = new Object();
        prepareInvocation("jwtOnly", token);
        when(joinPoint.proceed()).thenReturn(expected);

        Object actual = aspect.around(joinPoint);

        assertSame(expected, actual);
        verify(redisCache, never()).hasKey(anyString());
        verify(request).setAttribute(AuthContext.USER_ID_ATTRIBUTE, 936L);
    }

    @Test
    void merchantSessionAuthAllowsExistingRedisSession() throws Throwable {
        String token = JwtUtil.token("936", "merchant");
        String jti = tokenJti(token);
        Object expected = new Object();
        prepareInvocation("merchantSession", token);
        when(redisCache.hasKey(jti)).thenReturn(true);
        when(joinPoint.proceed()).thenReturn(expected);

        Object actual = aspect.around(joinPoint);

        assertSame(expected, actual);
        verify(request).setAttribute(AuthContext.USER_ID_ATTRIBUTE, 936L);
        verify(request).setAttribute(AuthContext.JTI_ATTRIBUTE, jti);
    }

    @Test
    void merchantSessionAuthRejectsMissingRedisSessionBeforeController() throws Throwable {
        String token = JwtUtil.token("936", "merchant");
        String jti = tokenJti(token);
        prepareInvocation("merchantSession", token);
        when(redisCache.hasKey(jti)).thenReturn(false);

        try (MockedStatic<MessageUtils> messages = mockStatic(MessageUtils.class)) {
            messages.when(() -> MessageUtils.message("merchant.session.invalid"))
                    .thenReturn("session invalid");

            ServiceException exception = assertThrows(ServiceException.class,
                    () -> aspect.around(joinPoint));

            assertEquals("session invalid", exception.getMessage());
            verify(joinPoint, never()).proceed();
        }
    }

    @Test
    void merchantSessionAuthRejectsTokenWithoutJti() throws Throwable {
        String token = JWT.create()
                .withClaim("id", "936")
                .withExpiresAt(new Date(System.currentTimeMillis() + 60_000))
                .sign(Algorithm.HMAC256("TEST-AUTH-TOKEN"));
        prepareInvocation("merchantSession", token);

        try (MockedStatic<MessageUtils> messages = mockStatic(MessageUtils.class)) {
            messages.when(() -> MessageUtils.message("merchant.session.invalid"))
                    .thenReturn("session invalid");

            ServiceException exception = assertThrows(ServiceException.class,
                    () -> aspect.around(joinPoint));

            assertEquals("session invalid", exception.getMessage());
            verify(redisCache, never()).hasKey(anyString());
            verify(joinPoint, never()).proceed();
        }
    }

    private void prepareInvocation(String methodName, String token) throws Exception {
        Method method = SecuredMethods.class.getDeclaredMethod(methodName);
        when(signature.getMethod()).thenReturn(method);
        when(request.getHeader("token")).thenReturn(token);
    }

    private String tokenJti(String token) {
        Map<String, Object> claims = JwtUtil.verifyToken(token);
        return (String) claims.get("jti");
    }

    static class SecuredMethods {
        @Auth
        public void jwtOnly() {
        }

        @Auth(session = true)
        public void merchantSession() {
        }
    }
}
