package com.magistr.app.config.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.magistr.app.config.tenant.TenantContext;
import com.magistr.app.model.Role;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

class AuthorizationInterceptorTest {

    private static final String TENANT = "tenant-a";
    private static final String TEACHER_TOKEN = "teacher-token";

    private JwtTokenService jwtTokenService;
    private ContextProbeController controller;
    private MockMvc mockMvc;

    @BeforeEach
    void setUp() {
        AuthContext.clear();
        TenantContext.clear();
        TenantContext.setCurrentTenant(TENANT);

        jwtTokenService = mock(JwtTokenService.class);
        controller = new ContextProbeController();
        mockMvc = MockMvcBuilders
                .standaloneSetup(controller)
                .addInterceptors(new AuthorizationInterceptor(jwtTokenService, new ObjectMapper()))
                .build();
    }

    @AfterEach
    void clearThreadLocalContexts() {
        AuthContext.clear();
        TenantContext.clear();
    }

    @Test
    @DisplayName("Отказ по роли не оставляет пользователя в следующих запросах того же потока")
    void forbiddenRequestDoesNotLeakAuthContextToFollowingRequestsOnSameThread() throws Exception {
        AuthenticatedUser teacher = new AuthenticatedUser(7L, "преподаватель", Role.TEACHER, 3L);
        when(jwtTokenService.authenticate(TEACHER_TOKEN, TENANT)).thenReturn(Optional.of(teacher));
        long junitThreadId = Thread.currentThread().getId();

        mockMvc.perform(get("/api/test/admin")
                        .header("Authorization", "Bearer " + TEACHER_TOKEN))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.message").value("Недостаточно прав для выполнения операции"));

        assertThat(AuthContext.getCurrentUser())
                .as("Контекст должен быть пуст после отказа по роли")
                .isNull();

        mockMvc.perform(post("/api/auth/refresh"))
                .andExpect(status().isOk());

        assertThat(controller.publicHandlerCalled)
                .as("Публичный обработчик должен быть вызван")
                .isTrue();
        assertThat(controller.publicHandlerUser)
                .as("Публичный обработчик не должен видеть пользователя предыдущего запроса")
                .isNull();
        assertThat(controller.publicHandlerThreadId)
                .as("Публичный запрос должен выполняться в потоке JUnit")
                .isEqualTo(junitThreadId);

        mockMvc.perform(get("/api/test/teacher")
                        .header("Authorization", "Bearer " + TEACHER_TOKEN))
                .andExpect(status().isOk());

        assertThat(controller.protectedHandlerUser)
                .as("Разрешённый обработчик должен видеть аутентифицированного пользователя")
                .isEqualTo(teacher);
        assertThat(controller.protectedHandlerThreadId)
                .as("Разрешённый запрос должен выполняться в том же потоке JUnit")
                .isEqualTo(junitThreadId);
        assertThat(controller.protectedHandlerThreadId)
                .as("Последовательные обработчики должны выполняться в одном потоке")
                .isEqualTo(controller.publicHandlerThreadId);
        assertThat(AuthContext.getCurrentUser())
                .as("После завершения разрешённого запроса контекст должен быть очищен")
                .isNull();

        verify(jwtTokenService, times(2)).authenticate(TEACHER_TOKEN, TENANT);
    }

    @RestController
    static class ContextProbeController {

        private boolean publicHandlerCalled;
        private AuthenticatedUser publicHandlerUser;
        private long publicHandlerThreadId;
        private AuthenticatedUser protectedHandlerUser;
        private long protectedHandlerThreadId;

        @PostMapping("/api/auth/refresh")
        String publicRefresh() {
            publicHandlerCalled = true;
            publicHandlerUser = AuthContext.getCurrentUser();
            publicHandlerThreadId = Thread.currentThread().getId();
            return publicHandlerUser == null ? "Контекст пуст" : "Контекст содержит пользователя";
        }

        @GetMapping("/api/test/admin")
        @RequireRoles(Role.ADMIN)
        String adminOnly() {
            return "Администратор";
        }

        @GetMapping("/api/test/teacher")
        @RequireRoles(Role.TEACHER)
        String teacherOnly() {
            protectedHandlerUser = AuthContext.getCurrentUser();
            protectedHandlerThreadId = Thread.currentThread().getId();
            return protectedHandlerUser.username();
        }
    }
}
