package org.raddatz.familienarchiv.auth;

import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.auth.AuthService.LoginResult;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.security.SecurityConfig;
import org.raddatz.familienarchiv.security.PermissionAspect;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.user.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import java.util.UUID;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@WebMvcTest(AuthSessionController.class)
@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class})
class AuthSessionControllerTest {

    @Autowired MockMvc mockMvc;

    @MockitoBean AuthService authService;
    @MockitoBean CustomUserDetailsService customUserDetailsService;
    @MockitoBean SessionAuthenticationStrategy sessionAuthenticationStrategy;

    // ─── POST /api/auth/login ──────────────────────────────────────────────────

    @Test
    void login_returns_200_with_user_on_valid_credentials() throws Exception {
        UUID userId = UUID.randomUUID();
        AppUser appUser = AppUser.builder().id(userId).email("user@test.de").build();
        Authentication auth = mock(Authentication.class);
        when(authService.login(anyString(), anyString(), anyString(), anyString()))
            .thenReturn(new LoginResult(appUser, auth));

        mockMvc.perform(post("/api/auth/login")
                .with(csrf())
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"email\":\"user@test.de\",\"password\":\"pass123\"}"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.email").value("user@test.de"))
            .andExpect(jsonPath("$.id").value(userId.toString()));
    }

    @Test
    void login_returns_401_with_INVALID_CREDENTIALS_on_bad_credentials() throws Exception {
        when(authService.login(anyString(), anyString(), anyString(), anyString()))
            .thenThrow(DomainException.invalidCredentials());

        mockMvc.perform(post("/api/auth/login")
                .with(csrf())
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"email\":\"user@test.de\",\"password\":\"wrong\"}"))
            .andExpect(status().isUnauthorized())
            .andExpect(jsonPath("$.code").value(ErrorCode.INVALID_CREDENTIALS.name()));
    }

    @Test
    void login_is_public_no_session_required() throws Exception {
        UUID userId = UUID.randomUUID();
        AppUser appUser = AppUser.builder().id(userId).email("pub@test.de").build();
        Authentication auth = mock(Authentication.class);
        when(authService.login(anyString(), anyString(), anyString(), anyString()))
            .thenReturn(new LoginResult(appUser, auth));

        // No WithMockUser — must be reachable without an active session
        mockMvc.perform(post("/api/auth/login")
                .with(csrf())
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"email\":\"pub@test.de\",\"password\":\"pass\"}"))
            .andExpect(status().isOk());
    }

    @Test
    void login_delegates_to_SessionAuthenticationStrategy_for_fixation_protection() throws Exception {
        UUID userId = UUID.randomUUID();
        AppUser appUser = AppUser.builder().id(userId).email("fix@test.de").build();
        Authentication auth = mock(Authentication.class);
        when(authService.login(anyString(), anyString(), anyString(), anyString()))
            .thenReturn(new LoginResult(appUser, auth));

        mockMvc.perform(post("/api/auth/login")
                .with(csrf())
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"email\":\"fix@test.de\",\"password\":\"pass\"}"))
            .andExpect(status().isOk());

        // Session-fixation defense (CWE-384): the controller must hand the new
        // Authentication to Spring Security's strategy, which rotates the session ID.
        verify(sessionAuthenticationStrategy).onAuthentication(eq(auth), any(), any());
    }

    @Test
    void login_response_body_does_not_contain_password_field() throws Exception {
        // Regression guard: AppUser.password is @JsonProperty(WRITE_ONLY). If anyone
        // ever drops that annotation, this assertion catches the credential leak on
        // the very next CI run.
        UUID userId = UUID.randomUUID();
        AppUser appUser = AppUser.builder()
            .id(userId)
            .email("leak@test.de")
            .password("$2a$10$shouldnotappearinresponse")
            .build();
        Authentication auth = mock(Authentication.class);
        when(authService.login(anyString(), anyString(), anyString(), anyString()))
            .thenReturn(new LoginResult(appUser, auth));

        mockMvc.perform(post("/api/auth/login")
                .with(csrf())
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"email\":\"leak@test.de\",\"password\":\"pass\"}"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.password").doesNotExist())
            .andExpect(jsonPath("$.pwd").doesNotExist())
            .andExpect(content().string(org.hamcrest.Matchers.not(
                org.hamcrest.Matchers.containsString("$2a$10$shouldnotappearinresponse"))));
    }

    @Test
    void login_does_not_set_cookie_on_failure() throws Exception {
        when(authService.login(anyString(), anyString(), anyString(), anyString()))
            .thenThrow(DomainException.invalidCredentials());

        mockMvc.perform(post("/api/auth/login")
                .with(csrf())
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"email\":\"user@test.de\",\"password\":\"wrong\"}"))
            .andExpect(status().isUnauthorized())
            .andExpect(header().doesNotExist("Set-Cookie"));
    }

    // ─── CSRF protection ──────────────────────────────────────────────────────

    @Test
    void authenticated_post_without_csrf_token_returns_403_CSRF_TOKEN_MISSING() throws Exception {
        // Red test: CSRF disabled → returns 204; after re-enabling returns 403.
        mockMvc.perform(post("/api/auth/logout")
                .with(user("user@test.de")))   // authenticated but no CSRF token
            .andExpect(status().isForbidden())
            .andExpect(jsonPath("$.code").value(ErrorCode.CSRF_TOKEN_MISSING.name()));
    }

    // ─── POST /api/auth/logout ─────────────────────────────────────────────────

    @Test
    void logout_returns_204_when_authenticated() throws Exception {
        doNothing().when(authService).logout(anyString(), anyString(), anyString());

        mockMvc.perform(post("/api/auth/logout")
                .with(user("user@test.de"))
                .with(csrf()))
            .andExpect(status().isNoContent());
    }

    @Test
    void logout_without_session_returns_403() throws Exception {
        // CsrfFilter runs before AnonymousAuthenticationFilter. When authentication is null,
        // ExceptionTranslationFilter routes CSRF AccessDeniedException to accessDeniedHandler → 403.
        mockMvc.perform(post("/api/auth/logout"))
            .andExpect(status().isForbidden())
            .andExpect(jsonPath("$.code").value(ErrorCode.CSRF_TOKEN_MISSING.name()));
    }

    @Test
    void logout_returns_204_even_when_audit_throws() throws Exception {
        // CWE-613 defense: the session MUST be invalidated even if the audit lookup
        // explodes (e.g. user deleted between login and logout). Audit is best-effort.
        doThrow(new RuntimeException("audit DB down"))
            .when(authService).logout(anyString(), anyString(), anyString());

        mockMvc.perform(post("/api/auth/logout")
                .with(user("ghost@test.de"))
                .with(csrf()))
            .andExpect(status().isNoContent());
    }
}
