package org.raddatz.familienarchiv.notification;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.raddatz.familienarchiv.notification.NotificationDTO;
import org.raddatz.familienarchiv.document.comment.DocumentComment;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.notification.NotificationRepository;
import org.raddatz.familienarchiv.document.DocumentService;
import org.raddatz.familienarchiv.notification.SseEmitterRegistry;
import org.raddatz.familienarchiv.user.UserService;
import org.springframework.data.domain.PageImpl;
import org.springframework.mail.MailException;
import org.springframework.mail.MailSendException;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class NotificationServiceTest {

    @Mock NotificationRepository notificationRepository;
    @Mock UserService userService;
    @Mock DocumentService documentService;
    @Mock JavaMailSender mailSender;
    @Mock SseEmitterRegistry sseEmitterRegistry;

    NotificationService notificationService;

    private AppUser userA;
    private AppUser userB;
    private AppUser userC;

    @BeforeEach
    void setUp() {
        notificationService = new NotificationService(notificationRepository, userService, documentService, Optional.of(mailSender), sseEmitterRegistry);

        userA = AppUser.builder().id(UUID.randomUUID()).email("userA@example.com")
                .firstName("Anna").lastName("Smith").email("a@test.com")
                .notifyOnReply(false).notifyOnMention(false).build();
        userB = AppUser.builder().id(UUID.randomUUID()).email("userB@example.com")
                .firstName("Bob").lastName("Jones").email("b@test.com")
                .notifyOnReply(false).notifyOnMention(false).build();
        userC = AppUser.builder().id(UUID.randomUUID()).email("userC@example.com")
                .firstName("Clara").lastName("Doe").email("c@test.com")
                .notifyOnReply(false).notifyOnMention(false).build();
    }

    // ─── notifyReply ──────────────────────────────────────────────────────────

    @Test
    void notifyReply_createsNotificationForThreadParticipants() {
        DocumentComment reply = commentWithAuthor(UUID.randomUUID(), null, userC.getId(), "Clara Doe");

        when(userService.findAllById(Set.of(userA.getId(), userB.getId()))).thenReturn(List.of(userA, userB));
        when(notificationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        notificationService.notifyReply(reply, Set.of(userA.getId(), userB.getId()));

        ArgumentCaptor<Notification> captor = ArgumentCaptor.forClass(Notification.class);
        verify(notificationRepository, times(2)).save(captor.capture());

        List<Notification> saved = captor.getAllValues();
        assertThat(saved).extracting(n -> n.getRecipient().getId())
                .containsExactlyInAnyOrder(userA.getId(), userB.getId());
        assertThat(saved).allMatch(n -> n.getType() == NotificationType.REPLY);
        assertThat(saved).allMatch(n -> !n.isRead());
        assertThat(saved).allMatch(n -> "Clara Doe".equals(n.getActorName()));
    }

    @Test
    void notifyReply_doesNothing_whenParticipantSetIsEmpty() {
        DocumentComment reply = commentWithAuthor(UUID.randomUUID(), null, userA.getId(), "Anna Smith");

        notificationService.notifyReply(reply, Set.of());

        verify(notificationRepository, never()).save(any());
    }

    @Test
    void notifyReply_sendsEmailOnlyToUsersWithReplyNotificationsEnabled() {
        userA.setNotifyOnReply(true);
        userB.setNotifyOnReply(false);

        DocumentComment reply = commentWithAuthor(UUID.randomUUID(), null, userC.getId(), "Clara Doe");

        when(userService.findAllById(Set.of(userA.getId(), userB.getId()))).thenReturn(List.of(userA, userB));
        when(notificationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        notificationService.notifyReply(reply, Set.of(userA.getId(), userB.getId()));

        verify(mailSender, times(1)).send(any(SimpleMailMessage.class));
    }

    // ─── notifyMentions ───────────────────────────────────────────────────────

    @Test
    void notifyMentions_createsNotificationPerMentionedUser() {
        DocumentComment comment = commentWithAuthor(UUID.randomUUID(), null, userC.getId(), "Clara Doe");
        when(userService.findAllById(List.of(userA.getId(), userB.getId()))).thenReturn(List.of(userA, userB));
        when(notificationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        notificationService.notifyMentions(List.of(userA.getId(), userB.getId()), comment);

        ArgumentCaptor<Notification> captor = ArgumentCaptor.forClass(Notification.class);
        verify(notificationRepository, times(2)).save(captor.capture());

        List<Notification> saved = captor.getAllValues();
        assertThat(saved).extracting(n -> n.getRecipient().getId())
                .containsExactlyInAnyOrder(userA.getId(), userB.getId());
        assertThat(saved).allMatch(n -> n.getType() == NotificationType.MENTION);
        assertThat(saved).allMatch(n -> "Clara Doe".equals(n.getActorName()));
    }

    @Test
    void notifyMentions_doesNothing_whenListIsEmpty() {
        DocumentComment comment = commentWithAuthor(UUID.randomUUID(), null, userA.getId(), "Anna Smith");

        notificationService.notifyMentions(List.of(), comment);

        verify(notificationRepository, never()).save(any());
    }

    @Test
    void notifyMentions_sendsEmailOnlyToUsersWithMentionNotificationsEnabled() {
        userA.setNotifyOnMention(true);
        userB.setNotifyOnMention(false);

        DocumentComment comment = commentWithAuthor(UUID.randomUUID(), null, userC.getId(), "Clara Doe");
        when(userService.findAllById(List.of(userA.getId(), userB.getId()))).thenReturn(List.of(userA, userB));
        when(notificationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        notificationService.notifyMentions(List.of(userA.getId(), userB.getId()), comment);

        verify(mailSender, times(1)).send(any(SimpleMailMessage.class));
    }

    // ─── SSE push ─────────────────────────────────────────────────────────────

    @Test
    void notifyReply_pushesEventToRegistry_forEachRecipient() {
        DocumentComment reply = commentWithAuthor(UUID.randomUUID(), null, userC.getId(), "Clara Doe");

        when(userService.findAllById(Set.of(userA.getId(), userB.getId()))).thenReturn(List.of(userA, userB));
        when(notificationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        notificationService.notifyReply(reply, Set.of(userA.getId(), userB.getId()));

        verify(sseEmitterRegistry).send(eq(userA.getId()), any(NotificationDTO.class));
        verify(sseEmitterRegistry).send(eq(userB.getId()), any(NotificationDTO.class));
    }

    @Test
    void notifyMentions_pushesEventToRegistry_forEachMentionedUser() {
        DocumentComment comment = commentWithAuthor(UUID.randomUUID(), null, userC.getId(), "Clara Doe");

        when(userService.findAllById(List.of(userA.getId(), userB.getId()))).thenReturn(List.of(userA, userB));
        when(notificationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        notificationService.notifyMentions(List.of(userA.getId(), userB.getId()), comment);

        verify(sseEmitterRegistry).send(eq(userA.getId()), any(NotificationDTO.class));
        verify(sseEmitterRegistry).send(eq(userB.getId()), any(NotificationDTO.class));
    }

    // ─── markRead ─────────────────────────────────────────────────────────────

    @Test
    void markRead_throwsNotFound_whenNotificationDoesNotExist() {
        UUID notifId = UUID.randomUUID();
        when(notificationRepository.findById(notifId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> notificationService.markRead(notifId, userA.getId()))
                .isInstanceOf(DomainException.class)
                .hasMessageContaining("Notification not found");
    }

    @Test
    void markRead_throwsForbidden_whenNotificationBelongsToDifferentUser() {
        Notification notification = Notification.builder()
                .id(UUID.randomUUID())
                .recipient(userA)
                .type(NotificationType.REPLY)
                .read(false)
                .build();

        when(notificationRepository.findById(notification.getId())).thenReturn(Optional.of(notification));

        assertThatThrownBy(() -> notificationService.markRead(notification.getId(), userB.getId()))
                .isInstanceOf(DomainException.class)
                .hasMessageContaining("different user");
    }

    // ─── markAllRead ──────────────────────────────────────────────────────────

    @Test
    void markAllRead_delegatesToRepository() {
        notificationService.markAllRead(userA.getId());

        verify(notificationRepository).markAllReadByRecipientId(userA.getId());
    }

    // ─── markRead — happy path ────────────────────────────────────────────────

    @Test
    void markRead_marksNotificationAsRead_whenRecipientMatches() {
        Notification notification = Notification.builder()
                .id(UUID.randomUUID())
                .recipient(userA)
                .type(NotificationType.REPLY)
                .documentId(UUID.randomUUID())
                .referenceId(UUID.randomUUID())
                .read(false)
                .build();
        when(notificationRepository.findById(notification.getId())).thenReturn(Optional.of(notification));
        when(notificationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        NotificationDTO result = notificationService.markRead(notification.getId(), userA.getId());

        assertThat(result).isNotNull();
        assertThat(notification.isRead()).isTrue();
    }

    // ─── countUnread ──────────────────────────────────────────────────────────

    @Test
    void countUnread_delegatesToRepository() {
        when(notificationRepository.countByRecipientIdAndReadFalse(userA.getId())).thenReturn(3L);

        assertThat(notificationService.countUnread(userA.getId())).isEqualTo(3L);
    }

    // ─── notifyMentions — null list ───────────────────────────────────────────

    @Test
    void notifyMentions_doesNothing_whenMentionedUserIdsIsNull() {
        DocumentComment comment = commentWithAuthor(UUID.randomUUID(), null, userA.getId(), "Anna Smith");

        notificationService.notifyMentions(null, comment);

        verify(notificationRepository, never()).save(any());
    }

    // ─── email — no mailSender ────────────────────────────────────────────────

    @Test
    void notifyReply_skipsEmail_whenMailSenderIsAbsent() {
        NotificationService serviceWithoutMail = new NotificationService(
                notificationRepository, userService, documentService, Optional.empty(), sseEmitterRegistry);

        userA.setNotifyOnReply(true);
        DocumentComment reply = commentWithAuthor(UUID.randomUUID(), null, userC.getId(), "Clara Doe");

        when(userService.findAllById(Set.of(userA.getId()))).thenReturn(List.of(userA));
        when(notificationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        serviceWithoutMail.notifyReply(reply, Set.of(userA.getId()));

        verify(mailSender, never()).send(any(SimpleMailMessage.class));
    }

    @Test
    void notifyMentions_skipsEmail_whenMailSenderIsAbsent() {
        NotificationService serviceWithoutMail = new NotificationService(
                notificationRepository, userService, documentService, Optional.empty(), sseEmitterRegistry);

        userA.setNotifyOnMention(true);
        DocumentComment comment = commentWithAuthor(UUID.randomUUID(), null, userC.getId(), "Clara Doe");

        when(userService.findAllById(List.of(userA.getId()))).thenReturn(List.of(userA));
        when(notificationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        serviceWithoutMail.notifyMentions(List.of(userA.getId()), comment);

        verify(mailSender, never()).send(any(SimpleMailMessage.class));
    }

    // ─── email — recipient email missing ─────────────────────────────────────

    @Test
    void notifyReply_skipsEmail_whenRecipientEmailIsNull() {
        userA.setNotifyOnReply(true);
        userA.setEmail(null);
        DocumentComment reply = commentWithAuthor(UUID.randomUUID(), null, userC.getId(), "Clara Doe");

        when(userService.findAllById(Set.of(userA.getId()))).thenReturn(List.of(userA));
        when(notificationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        notificationService.notifyReply(reply, Set.of(userA.getId()));

        verify(mailSender, never()).send(any(SimpleMailMessage.class));
    }

    @Test
    void notifyReply_skipsEmail_whenRecipientEmailIsBlank() {
        userA.setNotifyOnReply(true);
        userA.setEmail("   ");
        DocumentComment reply = commentWithAuthor(UUID.randomUUID(), null, userC.getId(), "Clara Doe");

        when(userService.findAllById(Set.of(userA.getId()))).thenReturn(List.of(userA));
        when(notificationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        notificationService.notifyReply(reply, Set.of(userA.getId()));

        verify(mailSender, never()).send(any(SimpleMailMessage.class));
    }

    // ─── email — MailException swallowed ─────────────────────────────────────

    @Test
    void notifyReply_doesNotThrow_whenMailExceptionOccurs() {
        userA.setNotifyOnReply(true);
        DocumentComment reply = commentWithAuthor(UUID.randomUUID(), null, userC.getId(), "Clara Doe");

        when(userService.findAllById(Set.of(userA.getId()))).thenReturn(List.of(userA));
        when(notificationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
        doThrow(new MailSendException("SMTP down")).when(mailSender).send(any(SimpleMailMessage.class));

        // Must not throw — MailException is caught and logged
        notificationService.notifyReply(reply, Set.of(userA.getId()));

        verify(mailSender).send(any(SimpleMailMessage.class));
    }

    // ─── email — annotationId included in link ────────────────────────────────

    @Test
    void notifyReply_includesAnnotationIdInEmailLink_whenAnnotationPresent() {
        userA.setNotifyOnReply(true);
        UUID annotationId = UUID.randomUUID();
        DocumentComment reply = DocumentComment.builder()
                .id(UUID.randomUUID())
                .documentId(UUID.randomUUID())
                .annotationId(annotationId)
                .authorId(userC.getId())
                .authorName("Clara Doe")
                .content("reply")
                .build();

        when(userService.findAllById(Set.of(userA.getId()))).thenReturn(List.of(userA));
        when(notificationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        notificationService.notifyReply(reply, Set.of(userA.getId()));

        ArgumentCaptor<SimpleMailMessage> captor = ArgumentCaptor.forClass(SimpleMailMessage.class);
        verify(mailSender).send(captor.capture());
        assertThat(captor.getValue().getText()).contains("annotationId=" + annotationId);
    }

    // ─── getNotifications — filter dispatch ──────────────────────────────────

    @Test
    void getNotifications_withNoFilters_usesUnfilteredRepoMethod() {
        when(notificationRepository.findByRecipientIdOrderByCreatedAtDesc(eq(userA.getId()), any()))
                .thenReturn(Page.empty());

        notificationService.getNotifications(userA.getId(), null, null, Pageable.ofSize(10));

        verify(notificationRepository).findByRecipientIdOrderByCreatedAtDesc(eq(userA.getId()), any());
        verify(notificationRepository, never())
                .findByRecipientIdAndTypeAndReadFalseOrderByCreatedAtDesc(any(), any(), any());
    }

    @Test
    void getNotifications_withTypeAndReadFalse_usesFilteredRepoMethod() {
        when(notificationRepository.findByRecipientIdAndTypeAndReadFalseOrderByCreatedAtDesc(
                eq(userA.getId()), eq(NotificationType.MENTION), any()))
                .thenReturn(Page.empty());

        notificationService.getNotifications(userA.getId(), NotificationType.MENTION, false, Pageable.ofSize(3));

        verify(notificationRepository).findByRecipientIdAndTypeAndReadFalseOrderByCreatedAtDesc(
                eq(userA.getId()), eq(NotificationType.MENTION), any());
        verify(notificationRepository, never()).findByRecipientIdOrderByCreatedAtDesc(any(), any());
    }

    @Test
    void getNotifications_withTypeOnly_usesTypeFilteredRepoMethod() {
        when(notificationRepository.findByRecipientIdAndTypeOrderByCreatedAtDesc(
                eq(userA.getId()), eq(NotificationType.MENTION), any()))
                .thenReturn(Page.empty());

        notificationService.getNotifications(userA.getId(), NotificationType.MENTION, null, Pageable.ofSize(5));

        verify(notificationRepository).findByRecipientIdAndTypeOrderByCreatedAtDesc(
                eq(userA.getId()), eq(NotificationType.MENTION), any());
        verify(notificationRepository, never()).findByRecipientIdOrderByCreatedAtDesc(any(), any());
        verify(notificationRepository, never())
                .findByRecipientIdAndTypeAndReadFalseOrderByCreatedAtDesc(any(), any(), any());
    }

    @Test
    void getNotifications_withReadFalseAndNoType_usesUnreadOnlyRepoMethod() {
        when(notificationRepository.findByRecipientIdAndReadFalseOrderByCreatedAtDesc(
                eq(userA.getId()), any()))
                .thenReturn(Page.empty());

        notificationService.getNotifications(userA.getId(), null, false, Pageable.ofSize(10));

        verify(notificationRepository).findByRecipientIdAndReadFalseOrderByCreatedAtDesc(
                eq(userA.getId()), any());
        verify(notificationRepository, never()).findByRecipientIdOrderByCreatedAtDesc(any(), any());
    }

    @Test
    void getNotifications_mapsDocumentTitleFromDocumentService() {
        UUID docId = UUID.randomUUID();
        Notification notification = Notification.builder()
                .id(UUID.randomUUID())
                .recipient(userA)
                .type(NotificationType.REPLY)
                .documentId(docId)
                .referenceId(UUID.randomUUID())
                .actorName("Clara Doe")
                .build();
        when(notificationRepository.findByRecipientIdOrderByCreatedAtDesc(eq(userA.getId()), any()))
                .thenReturn(new PageImpl<>(List.of(notification)));
        when(documentService.findTitlesByIds(Set.of(docId)))
                .thenReturn(Map.of(docId, "Geburtsurkunde Opa Karl"));

        Page<NotificationDTO> result = notificationService.getNotifications(userA.getId(), null, null, Pageable.ofSize(10));

        assertThat(result.getContent()).hasSize(1);
        assertThat(result.getContent().getFirst().documentTitle()).isEqualTo("Geburtsurkunde Opa Karl");
    }

    @Test
    void getNotifications_mapsDocumentTitleAsNull_whenDocumentDoesNotExist() {
        UUID docId = UUID.randomUUID();
        Notification notification = Notification.builder()
                .id(UUID.randomUUID())
                .recipient(userA)
                .type(NotificationType.MENTION)
                .documentId(docId)
                .referenceId(UUID.randomUUID())
                .actorName("Bob Jones")
                .build();
        when(notificationRepository.findByRecipientIdOrderByCreatedAtDesc(eq(userA.getId()), any()))
                .thenReturn(new PageImpl<>(List.of(notification)));
        when(documentService.findTitlesByIds(Set.of(docId)))
                .thenReturn(Map.of());

        Page<NotificationDTO> result = notificationService.getNotifications(userA.getId(), null, null, Pageable.ofSize(10));

        assertThat(result.getContent()).hasSize(1);
        assertThat(result.getContent().getFirst().documentTitle()).isNull();
    }

    @Test
    void getNotifications_withTypeAndReadTrue_fallsBackToTypeOnlyQuery() {
        // read=true with a type filter falls through to the type-only branch —
        // it returns all notifications of that type (both read and unread).
        // The read=true filter is intentionally not supported on the backend;
        // callers that need only-read results must filter client-side.
        when(notificationRepository.findByRecipientIdAndTypeOrderByCreatedAtDesc(
                eq(userA.getId()), eq(NotificationType.MENTION), any()))
                .thenReturn(Page.empty());

        notificationService.getNotifications(userA.getId(), NotificationType.MENTION, true, Pageable.ofSize(5));

        verify(notificationRepository).findByRecipientIdAndTypeOrderByCreatedAtDesc(
                eq(userA.getId()), eq(NotificationType.MENTION), any());
        verify(notificationRepository, never())
                .findByRecipientIdAndTypeAndReadFalseOrderByCreatedAtDesc(any(), any(), any());
    }

    // ─── private helpers ──────────────────────────────────────────────────────

    private DocumentComment commentWithAuthor(UUID id, UUID parentId, UUID authorId, String authorName) {
        return DocumentComment.builder()
                .id(id)
                .documentId(UUID.randomUUID())
                .parentId(parentId)
                .authorId(authorId)
                .authorName(authorName)
                .content("content")
                .build();
    }
}
