package org.raddatz.familienarchiv.user;

import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.audit.AuditService;
import org.raddatz.familienarchiv.auth.AuthService;
import org.raddatz.familienarchiv.security.SecurityConfig;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.security.PermissionAspect;
import org.raddatz.familienarchiv.user.CustomUserDetailsService;
import org.raddatz.familienarchiv.user.UserService;
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.test.context.support.WithMockUser;
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.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
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.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;

@WebMvcTest(UserController.class)
@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class})
class UserControllerTest {

    @Autowired MockMvc mockMvc;

    @MockitoBean UserService userService;
    @MockitoBean AuthService authService;
    @MockitoBean AuditService auditService;
    @MockitoBean CustomUserDetailsService customUserDetailsService;

    // ─── GET /api/users/me ────────────────────────────────────────────────────────

    @Test
    void getCurrentUser_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(get("/api/users/me"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(username = "anna@example.com")
    void getCurrentUser_returns200_whenAuthenticated() throws Exception {
        AppUser user = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();
        when(userService.findByEmail("anna@example.com")).thenReturn(user);

        mockMvc.perform(get("/api/users/me"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.email").value("anna@example.com"));
    }

    // ─── GET /api/users/{id} ──────────────────────────────────────────────────

    @Test
    @WithMockUser(username = "reader@example.com")
    void getUser_returns403_whenCallerLacksAdminUserPermission() throws Exception {
        UUID id = UUID.randomUUID();
        AppUser target = AppUser.builder().id(id).email("target@example.com").build();
        when(userService.getById(id)).thenReturn(target);

        mockMvc.perform(get("/api/users/" + id))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(username = "admin@example.com", authorities = {"ADMIN_USER"})
    void getUser_returns200_whenCallerHasAdminUserPermission() throws Exception {
        UUID id = UUID.randomUUID();
        AppUser user = AppUser.builder().id(id).email("target@example.com").build();
        when(userService.getById(id)).thenReturn(user);

        mockMvc.perform(get("/api/users/" + id))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.email").value("target@example.com"));
    }

    // ─── POST /api/users ──────────────────────────────────────────────────────

    @Test
    @WithMockUser(username = "admin@example.com", authorities = {"ADMIN_USER"})
    void createUser_returns400_whenEmailIsNotValidEmailFormat() throws Exception {
        mockMvc.perform(post("/api/users").with(csrf())
                        .contentType(org.springframework.http.MediaType.APPLICATION_JSON)
                        .content("{\"email\":\"notanemail\",\"initialPassword\":\"secret123\"}"))
                .andExpect(status().isBadRequest());
    }

    @Test
    @WithMockUser(username = "admin@example.com", authorities = {"ADMIN_USER"})
    void createUser_returns400_whenEmailContainsColon() throws Exception {
        mockMvc.perform(post("/api/users").with(csrf())
                        .contentType(org.springframework.http.MediaType.APPLICATION_JSON)
                        .content("{\"email\":\"user:name@example.com\",\"initialPassword\":\"secret123\"}"))
                .andExpect(status().isBadRequest());
    }

    @Test
    @WithMockUser(username = "admin@example.com", authorities = {"ADMIN_USER"})
    void createUser_returns400_whenEmailIsBlank() throws Exception {
        mockMvc.perform(post("/api/users").with(csrf())
                        .contentType(org.springframework.http.MediaType.APPLICATION_JSON)
                        .content("{\"email\":\"\",\"initialPassword\":\"secret123\"}"))
                .andExpect(status().isBadRequest());
    }

    // ─── permission enforcement ───────────────────────────────────────────────

    @Test
    @WithMockUser(username = "reader@example.com")
    void createUser_returns403_whenCallerLacksAdminUserPermission() throws Exception {
        mockMvc.perform(post("/api/users").with(csrf())
                        .contentType(org.springframework.http.MediaType.APPLICATION_JSON)
                        .content("{\"email\":\"x@x.com\",\"initialPassword\":\"secret123\"}"))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(username = "reader@example.com")
    void adminUpdateUser_returns403_whenCallerLacksAdminUserPermission() throws Exception {
        mockMvc.perform(put("/api/users/" + UUID.randomUUID()).with(csrf())
                        .contentType(org.springframework.http.MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(username = "reader@example.com")
    void deleteUser_returns403_whenCallerLacksAdminUserPermission() throws Exception {
        mockMvc.perform(delete("/api/users/" + UUID.randomUUID()).with(csrf()))
                .andExpect(status().isForbidden());
    }

    // ─── unauthenticated access ───────────────────────────────────────────────

    @Test
    void createUser_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(post("/api/users").with(csrf())
                        .contentType(org.springframework.http.MediaType.APPLICATION_JSON)
                        .content("{\"email\":\"x@x.com\",\"initialPassword\":\"secret123\"}"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    void adminUpdateUser_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(put("/api/users/" + UUID.randomUUID()).with(csrf())
                        .contentType(org.springframework.http.MediaType.APPLICATION_JSON)
                        .content("{}"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    void deleteUser_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(delete("/api/users/" + UUID.randomUUID()).with(csrf()))
                .andExpect(status().isUnauthorized());
    }

    // ─── POST /api/users/me/password (changePassword + session revocation) ────

    @Test
    @WithMockUser(username = "user@example.com")
    void changePassword_returns204_and_calls_revokeOtherSessions() throws Exception {
        AppUser user = AppUser.builder().id(UUID.randomUUID()).email("user@example.com").build();
        when(userService.findByEmail("user@example.com")).thenReturn(user);
        when(authService.revokeOtherSessions(any(), any())).thenReturn(1);

        mockMvc.perform(post("/api/users/me/password").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"currentPassword\":\"old\",\"newPassword\":\"new123!\"}"))
                .andExpect(status().isNoContent());

        verify(authService).revokeOtherSessions(any(), eq("user@example.com"));
    }

    @Test
    void changePassword_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(post("/api/users/me/password").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"currentPassword\":\"old\",\"newPassword\":\"new123!\"}"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(username = "user@example.com")
    void changePassword_without_csrf_returns_403_CSRF_TOKEN_MISSING() throws Exception {
        mockMvc.perform(post("/api/users/me/password")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"currentPassword\":\"old\",\"newPassword\":\"new123!\"}"))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.code").value("CSRF_TOKEN_MISSING"));
    }

    // ─── POST /api/users/{id}/force-logout ────────────────────────────────────

    @Test
    @WithMockUser(username = "admin@example.com", authorities = "ADMIN_USER")
    void forceLogout_returns200_and_revokes_target_sessions() throws Exception {
        UUID targetId = UUID.randomUUID();
        AppUser actor = AppUser.builder().id(UUID.randomUUID()).email("admin@example.com").build();
        AppUser target = AppUser.builder().id(targetId).email("target@example.com").build();
        when(userService.findByEmail("admin@example.com")).thenReturn(actor);
        when(userService.getById(targetId)).thenReturn(target);
        when(authService.revokeAllSessions("target@example.com")).thenReturn(2);

        mockMvc.perform(post("/api/users/" + targetId + "/force-logout").with(csrf()))
                .andExpect(status().isOk())
                .andExpect(org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath("$.revokedCount").value(2));
    }

    @Test
    void forceLogout_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(post("/api/users/" + UUID.randomUUID() + "/force-logout").with(csrf()))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser
    void forceLogout_returns403_whenMissingPermission() throws Exception {
        mockMvc.perform(post("/api/users/" + UUID.randomUUID() + "/force-logout").with(csrf()))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(authorities = "ADMIN_USER")
    void forceLogout_returns404_whenUserNotFound() throws Exception {
        UUID targetId = UUID.randomUUID();
        when(userService.getById(targetId)).thenThrow(
                org.raddatz.familienarchiv.exception.DomainException.notFound(
                        org.raddatz.familienarchiv.exception.ErrorCode.USER_NOT_FOUND, "not found"));

        mockMvc.perform(post("/api/users/" + targetId + "/force-logout").with(csrf()))
                .andExpect(status().isNotFound());
    }

    @Test
    @WithMockUser(username = "admin@example.com", authorities = "ADMIN_USER")
    void forceLogout_without_csrf_returns_403_CSRF_TOKEN_MISSING() throws Exception {
        mockMvc.perform(post("/api/users/" + UUID.randomUUID() + "/force-logout"))
                .andExpect(status().isForbidden())
                .andExpect(jsonPath("$.code").value("CSRF_TOKEN_MISSING"));
    }
}
