package org.raddatz.familienarchiv.document.comment;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.raddatz.familienarchiv.audit.AuditKind;
import org.raddatz.familienarchiv.audit.AuditService;
import org.raddatz.familienarchiv.document.transcription.TranscriptionService;
import org.raddatz.familienarchiv.user.UserService;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.document.comment.DocumentComment;
import org.raddatz.familienarchiv.document.transcription.TranscriptionBlock;
import org.raddatz.familienarchiv.user.UserGroup;
import org.raddatz.familienarchiv.document.comment.CommentRepository;
import org.raddatz.familienarchiv.notification.NotificationService;

import java.time.LocalDateTime;
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.anyList;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.HttpStatus.NOT_FOUND;

@ExtendWith(MockitoExtension.class)
class CommentServiceTest {

    @Mock CommentRepository commentRepository;
    @Mock UserService userService;
    @Mock NotificationService notificationService;
    @Mock AuditService auditService;
    @Mock TranscriptionService transcriptionService;
    @InjectMocks CommentService commentService;

    // ─── replyToComment ───────────────────────────────────────────────────────

    @Test
    void replyToComment_throwsNotFound_whenTargetCommentMissing() {
        UUID docId = UUID.randomUUID();
        UUID commentId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();
        when(commentRepository.findById(commentId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> commentService.replyToComment(docId, commentId, "Reply", List.of(), author))
                .isInstanceOf(DomainException.class)
                .satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(NOT_FOUND));

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

    @Test
    void replyToComment_resolvesToRootParent_whenReplyingToAReply() {
        UUID docId = UUID.randomUUID();
        UUID rootId = UUID.randomUUID();
        UUID replyId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();

        DocumentComment root = DocumentComment.builder()
                .id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build();
        DocumentComment existingReply = DocumentComment.builder()
                .id(replyId).documentId(docId).parentId(rootId).content("Reply1").authorName("Anna").build();

        when(commentRepository.findById(replyId)).thenReturn(Optional.of(existingReply));
        when(commentRepository.findById(rootId)).thenReturn(Optional.of(root));
        when(commentRepository.findByParentId(rootId)).thenReturn(List.of(existingReply));
        DocumentComment saved = DocumentComment.builder()
                .id(UUID.randomUUID()).documentId(docId).parentId(rootId).content("Reply2").authorName("anna").build();
        when(commentRepository.save(any())).thenReturn(saved);

        DocumentComment result = commentService.replyToComment(docId, replyId, "Reply2", List.of(), author);

        assertThat(result.getParentId()).isEqualTo(rootId);
    }

    @Test
    void replyToComment_usesDirectComment_whenReplyingToTopLevel() {
        UUID docId = UUID.randomUUID();
        UUID rootId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();

        DocumentComment root = DocumentComment.builder()
                .id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build();

        when(commentRepository.findById(rootId)).thenReturn(Optional.of(root));
        when(commentRepository.findByParentId(rootId)).thenReturn(List.of());
        DocumentComment saved = DocumentComment.builder()
                .id(UUID.randomUUID()).documentId(docId).parentId(rootId).content("Reply").authorName("anna").build();
        when(commentRepository.save(any())).thenReturn(saved);

        DocumentComment result = commentService.replyToComment(docId, rootId, "Reply", List.of(), author);

        assertThat(result.getParentId()).isEqualTo(rootId);
    }

    @Test
    void replyToComment_triggersNotifyReply_afterSave() {
        UUID docId = UUID.randomUUID();
        UUID rootId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();

        DocumentComment root = DocumentComment.builder()
                .id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build();
        DocumentComment saved = DocumentComment.builder()
                .id(UUID.randomUUID()).documentId(docId).parentId(rootId).content("Reply").authorName("anna").build();

        when(commentRepository.findById(rootId)).thenReturn(Optional.of(root));
        when(commentRepository.findByParentId(rootId)).thenReturn(List.of());
        when(commentRepository.save(any())).thenReturn(saved);

        commentService.replyToComment(docId, rootId, "Reply", List.of(), author);

        verify(notificationService).notifyReply(eq(saved), anySet());
    }

    @Test
    void replyToComment_triggersNotifyMentions_whenMentionedUserIdsProvided() {
        UUID docId = UUID.randomUUID();
        UUID rootId = UUID.randomUUID();
        UUID mentionedId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();
        AppUser mentioned = AppUser.builder().id(mentionedId).email("bob@example.com").firstName("Bob").lastName("J").build();

        DocumentComment root = DocumentComment.builder()
                .id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build();
        DocumentComment saved = DocumentComment.builder()
                .id(UUID.randomUUID()).documentId(docId).parentId(rootId).content("Hey @Bob J").authorName("anna").build();

        when(userService.findAllById(List.of(mentionedId))).thenReturn(List.of(mentioned));
        when(commentRepository.findById(rootId)).thenReturn(Optional.of(root));
        when(commentRepository.findByParentId(rootId)).thenReturn(List.of());
        when(commentRepository.save(any())).thenReturn(saved);

        commentService.replyToComment(docId, rootId, "Hey @Bob J", List.of(mentionedId), author);

        verify(notificationService).notifyMentions(eq(List.of(mentionedId)), eq(saved));
    }

    // ─── editComment ──────────────────────────────────────────────────────────

    @Test
    void editComment_throwsForbidden_whenNotAuthor() {
        UUID docId = UUID.randomUUID();
        UUID commentId = UUID.randomUUID();
        UUID ownerId = UUID.randomUUID();
        AppUser other = AppUser.builder().id(UUID.randomUUID()).email("other@example.com").build();

        DocumentComment comment = DocumentComment.builder()
                .id(commentId).documentId(docId).authorId(ownerId).content("Original").authorName("Hans").build();
        when(commentRepository.findById(commentId)).thenReturn(Optional.of(comment));

        assertThatThrownBy(() -> commentService.editComment(docId, commentId, "Changed", other))
                .isInstanceOf(DomainException.class)
                .satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(FORBIDDEN));

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

    @Test
    void editComment_updatesContent_whenAuthor() {
        UUID docId = UUID.randomUUID();
        UUID commentId = UUID.randomUUID();
        UUID authorId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(authorId).email("hans@example.com").build();
        LocalDateTime created = LocalDateTime.now().minusMinutes(5);

        DocumentComment comment = DocumentComment.builder()
                .id(commentId).documentId(docId).authorId(authorId)
                .content("Original").authorName("Hans").createdAt(created).build();
        when(commentRepository.findById(commentId)).thenReturn(Optional.of(comment));
        stubSaveAssigningRandomId();

        DocumentComment result = commentService.editComment(docId, commentId, "Updated", author);

        assertThat(result.getContent()).isEqualTo("Updated");
        assertThat(result.getCreatedAt()).isEqualTo(created);
    }

    // ─── deleteComment ────────────────────────────────────────────────────────

    @Test
    void deleteComment_throwsForbidden_whenNotAuthorAndNotAdmin() {
        UUID docId = UUID.randomUUID();
        UUID commentId = UUID.randomUUID();
        UUID ownerId = UUID.randomUUID();
        AppUser other = AppUser.builder().id(UUID.randomUUID()).email("other@example.com").build();

        DocumentComment comment = DocumentComment.builder()
                .id(commentId).documentId(docId).authorId(ownerId).authorName("Hans").content("X").build();
        when(commentRepository.findById(commentId)).thenReturn(Optional.of(comment));

        assertThatThrownBy(() -> commentService.deleteComment(docId, commentId, other))
                .isInstanceOf(DomainException.class)
                .satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(FORBIDDEN));

        verify(commentRepository, never()).delete(any());
    }

    @Test
    void deleteComment_succeeds_whenAuthor() {
        UUID docId = UUID.randomUUID();
        UUID commentId = UUID.randomUUID();
        UUID authorId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(authorId).email("hans@example.com").build();

        DocumentComment comment = DocumentComment.builder()
                .id(commentId).documentId(docId).authorId(authorId).authorName("Hans").content("X").build();
        when(commentRepository.findById(commentId)).thenReturn(Optional.of(comment));

        commentService.deleteComment(docId, commentId, author);

        verify(commentRepository).delete(comment);
    }

    @Test
    void deleteComment_succeeds_whenAdmin() {
        UUID docId = UUID.randomUUID();
        UUID commentId = UUID.randomUUID();
        UUID ownerId = UUID.randomUUID();
        AppUser admin = buildAdmin();

        DocumentComment comment = DocumentComment.builder()
                .id(commentId).documentId(docId).authorId(ownerId).authorName("Hans").content("X").build();
        when(commentRepository.findById(commentId)).thenReturn(Optional.of(comment));

        commentService.deleteComment(docId, commentId, admin);

        verify(commentRepository).delete(comment);
    }

    // ─── replyToComment — reply with null authorId in thread ─────────────────

    @Test
    void replyToComment_handlesNullAuthorId_inExistingReply() {
        UUID docId = UUID.randomUUID();
        UUID rootId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").firstName("Anna").lastName("S").build();

        DocumentComment root = DocumentComment.builder()
                .id(rootId).documentId(docId).parentId(null).authorId(UUID.randomUUID()).content("Root").authorName("Root").build();
        // Existing reply with null authorId
        DocumentComment nullAuthorReply = DocumentComment.builder()
                .id(UUID.randomUUID()).documentId(docId).parentId(rootId).authorId(null).content("Anon reply").authorName("anon").build();
        DocumentComment saved = DocumentComment.builder()
                .id(UUID.randomUUID()).documentId(docId).parentId(rootId).content("New reply").authorName("Anna S").build();

        when(commentRepository.findById(rootId)).thenReturn(Optional.of(root));
        when(commentRepository.findByParentId(rootId)).thenReturn(List.of(nullAuthorReply));
        when(commentRepository.save(any())).thenReturn(saved);

        commentService.replyToComment(docId, rootId, "New reply", List.of(), author);

        // Must not throw NullPointerException; only non-null authorIds collected
        verify(notificationService).notifyReply(eq(saved), anySet());
    }

    // ─── collectParticipantIds — non-null authorId in reply ──────────────────

    @Test
    void replyToComment_includesNonNullAuthorId_fromExistingReply() {
        UUID docId = UUID.randomUUID();
        UUID rootId = UUID.randomUUID();
        UUID existingReplyAuthorId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();

        DocumentComment root = DocumentComment.builder()
                .id(rootId).documentId(docId).parentId(null).authorId(UUID.randomUUID())
                .content("Root").authorName("root").build();
        // Existing reply WITH a non-null authorId — covers true branch of reply.getAuthorId() != null
        DocumentComment existingReply = DocumentComment.builder()
                .id(UUID.randomUUID()).documentId(docId).parentId(rootId)
                .authorId(existingReplyAuthorId).content("Existing").authorName("someone").build();
        DocumentComment saved = DocumentComment.builder()
                .id(UUID.randomUUID()).documentId(docId).parentId(rootId)
                .content("New reply").authorName("anna").build();

        when(commentRepository.findById(rootId)).thenReturn(Optional.of(root));
        when(commentRepository.findByParentId(rootId)).thenReturn(List.of(existingReply));
        when(commentRepository.save(any())).thenReturn(saved);

        commentService.replyToComment(docId, rootId, "New reply", List.of(), author);

        verify(notificationService).notifyReply(eq(saved), anySet());
    }

    // ─── collectParticipantIds — null authorId ────────────────────────────────

    @Test
    void replyToComment_excludesNullAuthorIds_fromParticipantSet() {
        UUID docId = UUID.randomUUID();
        UUID rootId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").build();

        // Root with null authorId
        DocumentComment root = DocumentComment.builder()
                .id(rootId).documentId(docId).parentId(null).authorId(null).content("Root").authorName("anon").build();
        DocumentComment saved = DocumentComment.builder()
                .id(UUID.randomUUID()).documentId(docId).parentId(rootId).content("Reply").authorName("anna").build();

        when(commentRepository.findById(rootId)).thenReturn(Optional.of(root));
        when(commentRepository.findByParentId(rootId)).thenReturn(List.of());
        when(commentRepository.save(any())).thenReturn(saved);

        // Must not throw NullPointerException
        commentService.replyToComment(docId, rootId, "Reply", List.of(), author);

        verify(notificationService).notifyReply(eq(saved), anySet());
    }

    // ─── helpers ──────────────────────────────────────────────────────────────

    private AppUser buildAdmin() {
        return AppUser.builder()
                .id(UUID.randomUUID())
                .email("admin@example.com")
                .groups(Set.of(UserGroup.builder()
                        .id(UUID.randomUUID())
                        .name("admins")
                        .permissions(Set.of("ADMIN"))
                        .build()))
                .build();
    }

    // ─── audit: COMMENT_ADDED and MENTION_CREATED ─────────────────────────────

    @Test
    void replyToComment_logsCommentAdded() {
        UUID docId = UUID.randomUUID();
        UUID rootId = UUID.randomUUID();
        UUID savedId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").firstName("Anna").lastName("S").build();
        DocumentComment root = DocumentComment.builder()
                .id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build();
        DocumentComment saved = DocumentComment.builder()
                .id(savedId).documentId(docId).parentId(rootId).content("Reply").authorName("Anna S").build();
        when(commentRepository.findById(rootId)).thenReturn(Optional.of(root));
        when(commentRepository.findByParentId(rootId)).thenReturn(List.of());
        when(commentRepository.save(any())).thenReturn(saved);

        commentService.replyToComment(docId, rootId, "Reply", List.of(), author);

        verify(auditService).logAfterCommit(
                eq(AuditKind.COMMENT_ADDED),
                eq(author.getId()),
                eq(docId),
                argThat(p -> savedId.toString().equals(p.get("commentId"))));
    }

    @Test
    void replyToComment_logsMentionCreated_whenMentioned() {
        UUID docId = UUID.randomUUID();
        UUID rootId = UUID.randomUUID();
        UUID savedId = UUID.randomUUID();
        UUID mentionedId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("anna@example.com").firstName("Anna").lastName("S").build();
        AppUser mentioned = AppUser.builder().id(mentionedId).email("bob@example.com").firstName("Bob").lastName("J").build();
        DocumentComment root = DocumentComment.builder()
                .id(rootId).documentId(docId).parentId(null).content("Root").authorName("Hans").build();
        DocumentComment saved = DocumentComment.builder()
                .id(savedId).documentId(docId).parentId(rootId).content("Hey @Bob").authorName("Anna S").build();
        when(userService.findAllById(List.of(mentionedId))).thenReturn(List.of(mentioned));
        when(commentRepository.findById(rootId)).thenReturn(Optional.of(root));
        when(commentRepository.findByParentId(rootId)).thenReturn(List.of());
        when(commentRepository.save(any())).thenReturn(saved);

        commentService.replyToComment(docId, rootId, "Hey @Bob", List.of(mentionedId), author);

        verify(auditService).logAfterCommit(
                eq(AuditKind.MENTION_CREATED),
                eq(author.getId()),
                eq(docId),
                argThat(p -> mentionedId.toString().equals(p.get("mentionedUserId"))));
    }

    @Test
    void postBlockComment_logsCommentAdded() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        UUID savedId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("felix@example.com").firstName("Felix").lastName("B").build();
        DocumentComment saved = DocumentComment.builder()
                .id(savedId).documentId(docId).blockId(blockId).authorName("Felix B").content("Nice").build();
        when(transcriptionService.getBlock(docId, blockId))
                .thenReturn(TranscriptionBlock.builder().id(blockId).documentId(docId).annotationId(UUID.randomUUID()).sortOrder(0).build());
        when(commentRepository.save(any())).thenReturn(saved);

        commentService.postBlockComment(docId, blockId, "Nice", List.of(), author);

        verify(auditService).logAfterCommit(
                eq(AuditKind.COMMENT_ADDED),
                eq(author.getId()),
                eq(docId),
                argThat(p -> savedId.toString().equals(p.get("commentId"))));
    }

    // ─── Block-level comments ────────────────────────────────────────────────

    @Test
    void getCommentsForBlock_returnsRootCommentsFilteredByBlockId() {
        UUID blockId = UUID.randomUUID();
        DocumentComment root = DocumentComment.builder()
                .id(UUID.randomUUID()).blockId(blockId).content("Nice work").authorName("Felix")
                .createdAt(LocalDateTime.now()).updatedAt(LocalDateTime.now()).build();
        when(commentRepository.findByBlockIdAndParentIdIsNull(blockId)).thenReturn(List.of(root));
        when(commentRepository.findByParentId(root.getId())).thenReturn(List.of());

        List<DocumentComment> result = commentService.getCommentsForBlock(blockId);

        assertThat(result).hasSize(1);
        assertThat(result.getFirst().getContent()).isEqualTo("Nice work");
    }

    @Test
    void postBlockComment_setsBlockIdOnComment() {
        UUID documentId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        UUID annotationId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("felix@example.com").firstName("Felix").lastName("Brandt").build();
        when(transcriptionService.getBlock(documentId, blockId))
                .thenReturn(TranscriptionBlock.builder().id(blockId).documentId(documentId).annotationId(annotationId).sortOrder(0).build());
        when(commentRepository.save(any())).thenAnswer(inv -> {
            DocumentComment c = inv.getArgument(0);
            c.setId(UUID.randomUUID());
            return c;
        });

        DocumentComment result = commentService.postBlockComment(
                documentId, blockId, "Looks like Breslau", List.of(), author);

        assertThat(result.getBlockId()).isEqualTo(blockId);
        assertThat(result.getDocumentId()).isEqualTo(documentId);
        assertThat(result.getContent()).isEqualTo("Looks like Breslau");
    }

    @Test
    void postBlockComment_setsAnnotationIdFromBlock() {
        UUID documentId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        UUID annotationId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("felix@example.com").firstName("Felix").lastName("Brandt").build();
        when(transcriptionService.getBlock(documentId, blockId))
                .thenReturn(TranscriptionBlock.builder().id(blockId).documentId(documentId).annotationId(annotationId).sortOrder(0).build());
        when(commentRepository.save(any())).thenAnswer(inv -> {
            DocumentComment c = inv.getArgument(0);
            c.setId(UUID.randomUUID());
            return c;
        });

        DocumentComment result = commentService.postBlockComment(
                documentId, blockId, "Nice work", List.of(), author);

        assertThat(result.getAnnotationId()).isEqualTo(annotationId);
    }

    @Test
    void postBlockComment_propagatesNotFound_whenBlockDoesNotExist() {
        UUID documentId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("felix@example.com").firstName("Felix").lastName("Brandt").build();
        when(transcriptionService.getBlock(documentId, blockId))
                .thenThrow(DomainException.notFound(
                        org.raddatz.familienarchiv.exception.ErrorCode.TRANSCRIPTION_BLOCK_NOT_FOUND,
                        "Transcription block not found: " + blockId));

        assertThatThrownBy(() -> commentService.postBlockComment(documentId, blockId, "Hi", List.of(), author))
                .isInstanceOf(DomainException.class)
                .hasMessageContaining("Transcription block not found");
    }

    // ─── postBlockComment — authorName resolution ────────────────────────────

    @Test
    void postBlockComment_capturesAuthorNameAtWriteTime() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        AppUser author = AppUser.builder()
                .id(UUID.randomUUID()).email("hans@example.com").firstName("Hans").lastName("Müller").build();
        stubBlock(docId, blockId);
        stubSaveAssigningRandomId();

        DocumentComment result = commentService.postBlockComment(docId, blockId, "Test", List.of(), author);

        assertThat(result.getAuthorName()).isEqualTo("Hans Müller");
    }

    @Test
    void postBlockComment_fallsBackToEmail_whenNamesAreBlank() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans42@example.com").build();
        stubBlock(docId, blockId);
        stubSaveAssigningRandomId();

        DocumentComment result = commentService.postBlockComment(docId, blockId, "Test", List.of(), author);

        assertThat(result.getAuthorName()).isEqualTo("hans42@example.com");
    }

    @Test
    void postBlockComment_fallsBackToEmail_whenFirstNameBlankAndLastNameNull() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("user42@example.com")
                .firstName("   ").lastName(null).build();
        stubBlock(docId, blockId);
        stubSaveAssigningRandomId();

        DocumentComment result = commentService.postBlockComment(docId, blockId, "Hi", List.of(), author);

        assertThat(result.getAuthorName()).isEqualTo("user42@example.com");
    }

    @Test
    void postBlockComment_fallsBackToEmail_whenFirstNameNullAndLastNameBlank() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("user42@example.com")
                .firstName(null).lastName("  ").build();
        stubBlock(docId, blockId);
        stubSaveAssigningRandomId();

        DocumentComment result = commentService.postBlockComment(docId, blockId, "Hi", List.of(), author);

        assertThat(result.getAuthorName()).isEqualTo("user42@example.com");
    }

    @Test
    void postBlockComment_usesFirstNameAlone_whenLastNameIsNull() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("user42@example.com")
                .firstName("Hans").lastName(null).build();
        stubBlock(docId, blockId);
        stubSaveAssigningRandomId();

        DocumentComment result = commentService.postBlockComment(docId, blockId, "Hi", List.of(), author);

        assertThat(result.getAuthorName()).isEqualTo("Hans");
    }

    @Test
    void postBlockComment_usesLastNameAlone_whenFirstNameIsNull() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("user42@example.com")
                .firstName(null).lastName("Müller").build();
        stubBlock(docId, blockId);
        stubSaveAssigningRandomId();

        DocumentComment result = commentService.postBlockComment(docId, blockId, "Hi", List.of(), author);

        assertThat(result.getAuthorName()).isEqualTo("Müller");
    }

    // ─── postBlockComment — mentions ─────────────────────────────────────────

    @Test
    void postBlockComment_triggersNotifyMentions_whenMentionedUserIdsProvided() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        UUID mentionedId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans@example.com").firstName("Hans").lastName("M").build();
        AppUser mentioned = AppUser.builder().id(mentionedId).email("anna@example.com").firstName("Anna").lastName("S").build();
        DocumentComment saved = DocumentComment.builder()
                .id(UUID.randomUUID()).documentId(docId).blockId(blockId).authorName("Hans M").content("Hey @Anna S").build();
        stubBlock(docId, blockId);
        when(userService.findAllById(List.of(mentionedId))).thenReturn(List.of(mentioned));
        when(commentRepository.save(any())).thenReturn(saved);

        commentService.postBlockComment(docId, blockId, "Hey @Anna S", List.of(mentionedId), author);

        verify(notificationService).notifyMentions(eq(List.of(mentionedId)), eq(saved));
    }

    @Test
    void postBlockComment_doesNotCallUserService_whenMentionedUserIdsIsNull() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans@example.com")
                .firstName("Hans").lastName("M").build();
        stubBlock(docId, blockId);
        stubSaveAssigningRandomId();

        commentService.postBlockComment(docId, blockId, "Hi", null, author);

        verify(userService, never()).findAllById(anyList());
    }

    @Test
    void postBlockComment_logsMentionCreated_oncePerMentionedUser() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        UUID savedId = UUID.randomUUID();
        UUID mentionedId1 = UUID.randomUUID();
        UUID mentionedId2 = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans@example.com").firstName("Hans").lastName("M").build();
        AppUser mentioned1 = AppUser.builder().id(mentionedId1).email("anna@example.com").firstName("Anna").lastName("S").build();
        AppUser mentioned2 = AppUser.builder().id(mentionedId2).email("bob@example.com").firstName("Bob").lastName("J").build();
        DocumentComment saved = DocumentComment.builder()
                .id(savedId).documentId(docId).blockId(blockId).authorName("Hans M").content("Hey @Anna @Bob").build();
        stubBlock(docId, blockId);
        when(userService.findAllById(List.of(mentionedId1, mentionedId2))).thenReturn(List.of(mentioned1, mentioned2));
        when(commentRepository.save(any())).thenReturn(saved);

        commentService.postBlockComment(docId, blockId, "Hey @Anna @Bob", List.of(mentionedId1, mentionedId2), author);

        verify(auditService).logAfterCommit(
                eq(AuditKind.MENTION_CREATED),
                eq(author.getId()),
                eq(docId),
                argThat(p -> mentionedId1.toString().equals(p.get("mentionedUserId"))));
        verify(auditService).logAfterCommit(
                eq(AuditKind.MENTION_CREATED),
                eq(author.getId()),
                eq(docId),
                argThat(p -> mentionedId2.toString().equals(p.get("mentionedUserId"))));
    }

    @Test
    void postBlockComment_doesNotLogMentionCreated_whenNoMentions() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        AppUser author = AppUser.builder().id(UUID.randomUUID()).email("hans@example.com").firstName("Hans").lastName("M").build();
        DocumentComment saved = DocumentComment.builder()
                .id(UUID.randomUUID()).documentId(docId).blockId(blockId).authorName("Hans M").content("Hello").build();
        stubBlock(docId, blockId);
        when(commentRepository.save(any())).thenReturn(saved);

        commentService.postBlockComment(docId, blockId, "Hello", List.of(), author);

        verify(auditService, never()).logAfterCommit(eq(AuditKind.MENTION_CREATED), any(), any(), any());
    }

    // ─── findDataByIds ────────────────────────────────────────────────────────

    @Test
    void findDataByIds_returns_empty_map_when_input_is_empty() {
        assertThat(commentService.findDataByIds(List.of())).isEmpty();
        verify(commentRepository, never()).findAllById(anyList());
    }

    @Test
    void findDataByIds_strips_html_and_extracts_plain_text() {
        UUID id = UUID.randomUUID();
        when(commentRepository.findAllById(List.of(id)))
                .thenReturn(List.of(DocumentComment.builder().id(id)
                        .content("<p><strong>Hello</strong> world</p>").build()));

        Map<UUID, CommentData> result = commentService.findDataByIds(List.of(id));

        assertThat(result.get(id).preview()).isEqualTo("Hello world");
    }

    @Test
    void findDataByIds_truncates_at_exactly_120_chars() {
        UUID id = UUID.randomUUID();
        String text121 = "a".repeat(121);
        when(commentRepository.findAllById(List.of(id)))
                .thenReturn(List.of(DocumentComment.builder().id(id).content(text121).build()));

        assertThat(commentService.findDataByIds(List.of(id)).get(id).preview()).hasSize(120);
    }

    @Test
    void findDataByIds_preserves_content_at_exactly_120_chars() {
        UUID id = UUID.randomUUID();
        String text120 = "a".repeat(120);
        when(commentRepository.findAllById(List.of(id)))
                .thenReturn(List.of(DocumentComment.builder().id(id).content(text120).build()));

        assertThat(commentService.findDataByIds(List.of(id)).get(id).preview()).hasSize(120);
    }

    @Test
    void findDataByIds_returns_empty_string_for_blank_content() {
        UUID id = UUID.randomUUID();
        when(commentRepository.findAllById(List.of(id)))
                .thenReturn(List.of(DocumentComment.builder().id(id).content("   ").build()));

        assertThat(commentService.findDataByIds(List.of(id)).get(id).preview()).isEmpty();
    }

    @Test
    void findDataByIds_returns_empty_string_for_null_content() {
        UUID id = UUID.randomUUID();
        when(commentRepository.findAllById(List.of(id)))
                .thenReturn(List.of(DocumentComment.builder().id(id).content(null).build()));

        assertThat(commentService.findDataByIds(List.of(id)).get(id).preview()).isEmpty();
    }

    @Test
    void findDataByIds_omits_deleted_comments_from_result_map() {
        UUID present = UUID.randomUUID();
        UUID deleted = UUID.randomUUID();
        when(commentRepository.findAllById(List.of(present, deleted)))
                .thenReturn(List.of(DocumentComment.builder().id(present).content("Hi").build()));

        Map<UUID, CommentData> result = commentService.findDataByIds(List.of(present, deleted));

        assertThat(result).containsKey(present);
        assertThat(result).doesNotContainKey(deleted);
    }

    @Test
    void findDataByIds_preserves_annotationId_alongside_preview() {
        UUID id = UUID.randomUUID();
        UUID annotationId = UUID.randomUUID();
        when(commentRepository.findAllById(List.of(id)))
                .thenReturn(List.of(DocumentComment.builder().id(id)
                        .annotationId(annotationId).content("Text").build()));

        CommentData data = commentService.findDataByIds(List.of(id)).get(id);

        assertThat(data.annotationId()).isEqualTo(annotationId);
        assertThat(data.preview()).isEqualTo("Text");
    }

    @Test
    void findDataByIds_sets_null_annotationId_when_comment_has_no_annotation() {
        UUID id = UUID.randomUUID();
        when(commentRepository.findAllById(List.of(id)))
                .thenReturn(List.of(DocumentComment.builder().id(id)
                        .annotationId(null).content("Text").build()));

        assertThat(commentService.findDataByIds(List.of(id)).get(id).annotationId()).isNull();
    }

    private void stubBlock(UUID docId, UUID blockId) {
        when(transcriptionService.getBlock(docId, blockId))
                .thenReturn(TranscriptionBlock.builder()
                        .id(blockId).documentId(docId).annotationId(UUID.randomUUID()).sortOrder(0).build());
    }

    private void stubSaveAssigningRandomId() {
        when(commentRepository.save(any())).thenAnswer(inv -> {
            DocumentComment c = inv.getArgument(0);
            if (c.getId() == null) c.setId(UUID.randomUUID());
            return c;
        });
    }
}
