package dev.awesomedev.mirim.web;

import dev.awesomedev.mirim.config.SecurityConfig;
import dev.awesomedev.mirim.domain.User;
import dev.awesomedev.mirim.repository.UserRepository;
import dev.awesomedev.mirim.service.AuditService;
import dev.awesomedev.mirim.service.AuthService;
import dev.awesomedev.mirim.service.CodingService;
import dev.awesomedev.mirim.service.CourseProgressService;
import dev.awesomedev.mirim.service.LoginAttemptService;
import dev.awesomedev.mirim.service.MentorAnalyticsService;
import dev.awesomedev.mirim.service.ProgressService;
import dev.awesomedev.mirim.service.QuizService;
import dev.awesomedev.mirim.service.RateLimiterService;
import dev.awesomedev.mirim.service.StudentProfileService;
import dev.awesomedev.mirim.service.StudentPurgeService;
import dev.awesomedev.mirim.service.SubmissionService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import java.util.List;
import java.util.Map;

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

/**
 * 이 파일이 하는 일:
 * "어떤 URL에 어떤 권한이 필요한가"라는 보안 계약(SecurityConfig)을 실제로 검증하는 통합테스트다.
 *
 * 학습 포인트 ① — 왜 이 테스트가 값어치 있나?
 *   권한 규칙 전체가 SecurityConfig.java 한 곳(URL 패턴 → 권한)에만 있고, 컨트롤러엔
 *   권한 검사 코드가 없다(전적으로 URL 패턴에 의존). 누군가 SecurityConfig를 리팩터링하다
 *   /api/mentor/** 규칙을 실수로 깨면, 학생이 전체 개인정보 CSV를 볼 수 있는 사고가 난다.
 *   이 테스트가 그 계약을 못 박아, 규칙이 깨지면 CI에서 빨간불이 뜨게 한다.
 *
 * 학습 포인트 ② — @WebMvcTest 슬라이스 테스트.
 *   DB·전체 앱을 띄우지 않고 "웹 계층(컨트롤러 + 보안 필터)"만 얇게 로드한다. 서비스는
 *   @MockitoBean으로 가짜를 끼워, 우리가 검증하려는 "보안 규칙"에만 집중한다.
 *
 * 학습 포인트 ③ — 인증 상태를 어떻게 흉내 내나?
 *   실제 앱과 똑같이 "세션에 SecurityContext를 넣는다"(AuthController가 로그인 성공 시 하는 그 일).
 *   그 세션을 요청에 실어 보내면, 보안 필터가 세션에서 권한을 복원해 규칙을 평가한다.
 *   → 가짜 우회가 아니라 실제 흐름을 그대로 태운 검증이다.
 */
@WebMvcTest(controllers = {MentorController.class, CodingController.class, AuthController.class})
@Import(SecurityConfig.class)
class SecurityContractTest {

    @Autowired
    private MockMvc mockMvc;

    // 컨트롤러들이 의존하는 서비스는 전부 가짜로 끼운다(보안 규칙만 보려는 것이므로).
    @MockitoBean private AuthService authService;
    @MockitoBean private LoginAttemptService loginAttemptService;
    @MockitoBean private RateLimiterService rateLimiterService;
    @MockitoBean private AuditService auditService;
    @MockitoBean private CodingService codingService;
    @MockitoBean private SubmissionService submissionService;
    @MockitoBean private CourseProgressService courseProgressService;
    @MockitoBean private ProgressService progressService;
    @MockitoBean private QuizService quizService;
    @MockitoBean private StudentProfileService studentProfileService;
    @MockitoBean private StudentPurgeService studentPurgeService;
    @MockitoBean private MentorAnalyticsService mentorAnalyticsService;
    @MockitoBean private UserRepository userRepository;

    /** 실제 로그인과 똑같이, 주어진 역할의 인증 정보를 담은 세션을 만든다. */
    private MockHttpSession sessionWithRole(String username, String role) {
        var auth = UsernamePasswordAuthenticationToken.authenticated(
                username, null, List.of(new SimpleGrantedAuthority("ROLE_" + role)));
        SecurityContext ctx = SecurityContextHolder.createEmptyContext();
        ctx.setAuthentication(auth);
        MockHttpSession session = new MockHttpSession();
        session.setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, ctx);
        return session;
    }

    // ─────────────────────── /api/mentor/** = 멘토 전용 ───────────────────────

    @Test
    @DisplayName("멘토 API: 비로그인이면 401")
    void mentor_requires_login() throws Exception {
        mockMvc.perform(get("/api/mentor/students"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @DisplayName("멘토 API: 학생(STUDENT)이면 403 — 권한 부족")
    void mentor_forbidden_for_student() throws Exception {
        mockMvc.perform(get("/api/mentor/students").session(sessionWithRole("s1", "STUDENT")))
                .andExpect(status().isForbidden());
    }

    @Test
    @DisplayName("멘토 API: 멘토(MENTOR)면 통과한다(200)")
    void mentor_ok_for_mentor() throws Exception {
        when(studentProfileService.allProfiles()).thenReturn(List.of());
        mockMvc.perform(get("/api/mentor/students").session(sessionWithRole("m1", "MENTOR")))
                .andExpect(status().isOk());
    }

    // ─────────────────────── /api/** = 로그인 필요 ───────────────────────

    @Test
    @DisplayName("일반 API(코딩 문제): 비로그인이면 401")
    void coding_requires_login() throws Exception {
        mockMvc.perform(get("/api/coding/problems"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @DisplayName("일반 API(코딩 문제): 로그인한 학생이면 통과한다(200)")
    void coding_ok_for_authenticated() throws Exception {
        when(authService.requireUser(any())).thenReturn(new User("s1", "해시", "학생", "STUDENT", "DEV"));
        when(codingService.mySubmissionsByProblemId(any())).thenReturn(Map.of());
        when(codingService.allProblems()).thenReturn(List.of());
        mockMvc.perform(get("/api/coding/problems").session(sessionWithRole("s1", "STUDENT")))
                .andExpect(status().isOk());
    }

    // ─────────────────────── /api/auth/** = 무인증 허용(permitAll) ───────────────────────
    // 학습 포인트: 잘못된 본문을 보내 400이 나오면, "인증에 막히지 않고 컨트롤러까지 도달했다"는
    //   증거다(막혔다면 401이 났을 것이다). 즉 permitAll이 살아 있음을 400으로 증명한다.

    @Test
    @DisplayName("로그인 API: 비로그인도 도달 가능(permitAll) — 잘못된 본문은 400")
    void login_is_public() throws Exception {
        mockMvc.perform(post("/api/auth/login").contentType(MediaType.APPLICATION_JSON).content("{}"))
                .andExpect(status().isBadRequest());
    }

    @Test
    @DisplayName("회원가입 API: 비로그인도 도달 가능(permitAll) — 잘못된 본문은 400")
    void signup_is_public() throws Exception {
        mockMvc.perform(post("/api/auth/signup").contentType(MediaType.APPLICATION_JSON).content("{}"))
                .andExpect(status().isBadRequest());
    }

    @Test
    @DisplayName("아이디 찾기 API: 비로그인도 도달 가능(permitAll) — 401이 아니다")
    void findId_is_public() throws Exception {
        int statusCode = mockMvc.perform(
                        post("/api/auth/find-id").contentType(MediaType.APPLICATION_JSON).content("{}"))
                .andReturn().getResponse().getStatus();
        // permitAll이므로 인증 차단(401)이 아니어야 한다. (검증 실패로 400이 나든, 200이 나든 무방)
        org.junit.jupiter.api.Assertions.assertNotEquals(401, statusCode);
    }

    // ─────────────────────── 공개 쓰기 레이트리밋(SEC-03) 배선 검증 ───────────────────────
    // 학습 포인트: 레이트리밋이 "컨트롤러에 실제로 연결됐는지"를 검증한다.
    //   한도 초과(tryAcquire=false)면 429가 나오고, 여유(true)면 서비스까지 도달해야 한다.

    @Test
    @DisplayName("아이디 찾기: 레이트리밋 한도 초과면 429")
    void findId_rate_limited_returns_429() throws Exception {
        when(rateLimiterService.tryAcquire(anyString(), anyString(), anyInt(), any())).thenReturn(false);
        mockMvc.perform(post("/api/auth/find-id")
                        .contentType(MediaType.APPLICATION_JSON).content("{\"name\":\"홍길동\"}"))
                .andExpect(status().isTooManyRequests());
    }

    @Test
    @DisplayName("아이디 찾기: 레이트리밋 여유가 있으면 통과해 서비스까지 도달(200)")
    void findId_within_limit_reaches_service() throws Exception {
        when(rateLimiterService.tryAcquire(anyString(), anyString(), anyInt(), any())).thenReturn(true);
        when(authService.findUsernamesByName(any())).thenReturn(List.of("ho****1"));
        mockMvc.perform(post("/api/auth/find-id")
                        .contentType(MediaType.APPLICATION_JSON).content("{\"name\":\"홍길동\"}"))
                .andExpect(status().isOk());
    }
}
