package org.raddatz.familienarchiv.notification;

import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.security.SecurityConfig;
import org.raddatz.familienarchiv.notification.NotificationDTO;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.exception.ErrorCode;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.notification.NotificationType;
import org.raddatz.familienarchiv.security.PermissionAspect;
import org.raddatz.familienarchiv.user.CustomUserDetailsService;
import org.raddatz.familienarchiv.notification.NotificationService;
import org.raddatz.familienarchiv.notification.SseEmitterRegistry;
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.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
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.time.LocalDateTime;
import java.util.List;
import java.util.UUID;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.http.MediaType.TEXT_EVENT_STREAM_VALUE;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;

@WebMvcTest(NotificationController.class)
@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class})
class NotificationControllerTest {

    @Autowired MockMvc mockMvc;

    @MockitoBean NotificationService notificationService;
    @MockitoBean UserService userService;
    @MockitoBean SseEmitterRegistry sseEmitterRegistry;
    @MockitoBean CustomUserDetailsService customUserDetailsService;

    private static final UUID USER_ID = UUID.randomUUID();

    // ─── GET /api/notifications ───────────────────────────────────────────────

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

    @Test
    @WithMockUser(username = "testuser")
    void getNotifications_returns200_whenAuthenticatedWithNoPermissions() throws Exception {
        AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
        when(userService.findByEmail("testuser")).thenReturn(user);
        when(notificationService.getNotifications(eq(USER_ID), any(), any(), any()))
                .thenReturn(new PageImpl<>(List.of()));

        mockMvc.perform(get("/api/notifications"))
                .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void getNotifications_returns200WithList_whenAuthenticated() throws Exception {
        AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
        NotificationDTO dto = new NotificationDTO(
                UUID.randomUUID(), NotificationType.REPLY, UUID.randomUUID(),
                UUID.randomUUID(), null, false, LocalDateTime.now(), "Anna Smith", "Testdokument");

        when(userService.findByEmail("testuser")).thenReturn(user);
        when(notificationService.getNotifications(eq(USER_ID), any(), any(), any()))
                .thenReturn(new PageImpl<>(List.of(dto), PageRequest.of(0, 10), 1));

        mockMvc.perform(get("/api/notifications"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.content").isArray());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void getNotifications_returnsOnlyCurrentUsersNotifications() throws Exception {
        AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
        when(userService.findByEmail("testuser")).thenReturn(user);
        when(notificationService.getNotifications(eq(USER_ID), any(), any(), any()))
                .thenReturn(new PageImpl<>(List.of()));

        mockMvc.perform(get("/api/notifications"))
                .andExpect(status().isOk());

        verify(notificationService).getNotifications(eq(USER_ID), any(), any(), any());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void getNotifications_withTypeAndReadFalse_passesFiltersToService() throws Exception {
        AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
        when(userService.findByEmail("testuser")).thenReturn(user);
        when(notificationService.getNotifications(eq(USER_ID), eq(NotificationType.MENTION), eq(false), any()))
                .thenReturn(new PageImpl<>(List.of()));

        mockMvc.perform(get("/api/notifications")
                        .param("type", "MENTION")
                        .param("read", "false"))
                .andExpect(status().isOk());

        verify(notificationService).getNotifications(eq(USER_ID), eq(NotificationType.MENTION), eq(false), any());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void getNotifications_withInvalidType_returns400() throws Exception {
        mockMvc.perform(get("/api/notifications").param("type", "INVALID_TYPE"))
                .andExpect(status().isBadRequest());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void getNotifications_returns400_whenSizeExceedsMaximum() throws Exception {
        mockMvc.perform(get("/api/notifications").param("size", "200"))
                .andExpect(status().isBadRequest());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void getNotifications_returns400_whenSizeIsZero() throws Exception {
        mockMvc.perform(get("/api/notifications").param("size", "0"))
                .andExpect(status().isBadRequest());
    }

    // ─── POST /api/notifications/read-all ────────────────────────────────────

    @Test
    void markAllRead_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(post("/api/notifications/read-all").with(csrf()))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void markAllRead_returns204_whenAuthenticated() throws Exception {
        AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
        when(userService.findByEmail("testuser")).thenReturn(user);

        mockMvc.perform(post("/api/notifications/read-all").with(csrf()))
                .andExpect(status().isNoContent());

        verify(notificationService).markAllRead(USER_ID);
    }

    // ─── PATCH /api/notifications/{id}/read ──────────────────────────────────

    @Test
    void markOneRead_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(patch("/api/notifications/" + UUID.randomUUID() + "/read").with(csrf()))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void markOneRead_returns403_whenNotificationBelongsToDifferentUser() throws Exception {
        AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
        UUID notifId = UUID.randomUUID();

        when(userService.findByEmail("testuser")).thenReturn(user);
        org.mockito.Mockito.doThrow(
                org.raddatz.familienarchiv.exception.DomainException.forbidden("not yours"))
                .when(notificationService).markRead(notifId, USER_ID);

        mockMvc.perform(patch("/api/notifications/" + notifId + "/read").with(csrf()))
                .andExpect(status().isForbidden());
    }

    // ─── GET /api/users/me/notification-preferences ──────────────────────────

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

    @Test
    @WithMockUser(username = "testuser")
    void getPreferences_returns403_whenUserHasNoPermission() throws Exception {
        mockMvc.perform(get("/api/users/me/notification-preferences"))
                .andExpect(status().isForbidden());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void getPreferences_returns200_whenUserHasReadAll() throws Exception {
        AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com")
                .notifyOnReply(true).notifyOnMention(false).build();
        when(userService.findByEmail("testuser")).thenReturn(user);

        mockMvc.perform(get("/api/users/me/notification-preferences"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.notifyOnReply").value(true))
                .andExpect(jsonPath("$.notifyOnMention").value(false));
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"WRITE_ALL"})
    void getPreferences_returns200_whenUserHasWriteAll() throws Exception {
        AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com")
                .notifyOnReply(false).notifyOnMention(true).build();
        when(userService.findByEmail("testuser")).thenReturn(user);

        mockMvc.perform(get("/api/users/me/notification-preferences"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.notifyOnMention").value(true));
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"ANNOTATE_ALL"})
    void getPreferences_returns200_whenUserHasAnnotateAll() throws Exception {
        AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com")
                .notifyOnReply(false).notifyOnMention(false).build();
        when(userService.findByEmail("testuser")).thenReturn(user);

        mockMvc.perform(get("/api/users/me/notification-preferences"))
                .andExpect(status().isOk());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"WRITE_ALL"})
    void getNotifications_returns200_whenUserHasOnlyWriteAll() throws Exception {
        AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
        when(userService.findByEmail("testuser")).thenReturn(user);
        when(notificationService.getNotifications(eq(USER_ID), any(), any(), any()))
                .thenReturn(new PageImpl<>(List.of()));

        mockMvc.perform(get("/api/notifications"))
                .andExpect(status().isOk());
    }

    // ─── PUT /api/users/me/notification-preferences ──────────────────────────

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void updatePreferences_persistsBothBooleans() throws Exception {
        AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com")
                .notifyOnReply(false).notifyOnMention(false).build();
        when(userService.findByEmail("testuser")).thenReturn(user);

        AppUser updated = AppUser.builder().id(USER_ID).email("testuser@example.com")
                .notifyOnReply(true).notifyOnMention(true).build();
        when(notificationService.updatePreferences(USER_ID, true, true)).thenReturn(updated);

        mockMvc.perform(put("/api/users/me/notification-preferences").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"notifyOnReply\":true,\"notifyOnMention\":true}"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.notifyOnReply").value(true))
                .andExpect(jsonPath("$.notifyOnMention").value(true));
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"WRITE_ALL"})
    void updatePreferences_returns200_whenUserHasWriteAll() throws Exception {
        AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com")
                .notifyOnReply(false).notifyOnMention(false).build();
        when(userService.findByEmail("testuser")).thenReturn(user);

        AppUser updated = AppUser.builder().id(USER_ID).email("testuser@example.com")
                .notifyOnReply(true).notifyOnMention(false).build();
        when(notificationService.updatePreferences(USER_ID, true, false)).thenReturn(updated);

        mockMvc.perform(put("/api/users/me/notification-preferences").with(csrf())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("{\"notifyOnReply\":true,\"notifyOnMention\":false}"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.notifyOnReply").value(true));
    }

    // ─── GET /api/notifications/unread-count ─────────────────────────────────

    @Test
    void countUnread_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(get("/api/notifications/unread-count"))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void countUnread_returns200WithCount_whenAuthenticated() throws Exception {
        AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
        when(userService.findByEmail("testuser")).thenReturn(user);
        when(notificationService.countUnread(USER_ID)).thenReturn(3L);

        mockMvc.perform(get("/api/notifications/unread-count"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.count").value(3));
    }

    // ─── PATCH /api/notifications/{id}/read — additional cases ───────────────

    // ─── GET /api/notifications/stream ───────────────────────────────────────

    @Test
    void stream_returns401_whenUnauthenticated() throws Exception {
        mockMvc.perform(get("/api/notifications/stream")
                        .accept(TEXT_EVENT_STREAM_VALUE))
                .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void stream_returns200_whenAuthenticated() throws Exception {
        AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
        when(userService.findByEmail("testuser")).thenReturn(user);
        when(sseEmitterRegistry.register(USER_ID)).thenReturn(new org.springframework.web.servlet.mvc.method.annotation.SseEmitter());

        mockMvc.perform(get("/api/notifications/stream")
                        .accept(TEXT_EVENT_STREAM_VALUE))
                .andExpect(status().isOk());
    }

    // ─── PATCH /api/notifications/{id}/read — additional cases ───────────────

    @Test
    @WithMockUser(username = "testuser", authorities = {"READ_ALL"})
    void markOneRead_returns404_whenNotificationDoesNotExist() throws Exception {
        AppUser user = AppUser.builder().id(USER_ID).email("testuser@example.com").build();
        UUID notifId = UUID.randomUUID();

        when(userService.findByEmail("testuser")).thenReturn(user);
        doThrow(DomainException.notFound(ErrorCode.NOTIFICATION_NOT_FOUND, "Notification not found: " + notifId))
                .when(notificationService).markRead(notifId, USER_ID);

        mockMvc.perform(patch("/api/notifications/" + notifId + "/read").with(csrf()))
                .andExpect(status().isNotFound());
    }
}
