package org.raddatz.familienarchiv.document.transcription;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
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.annotation.AnnotationService;
import org.raddatz.familienarchiv.document.annotation.CreateAnnotationDTO;
import org.raddatz.familienarchiv.document.DocumentService;
import org.raddatz.familienarchiv.document.transcription.CreateTranscriptionBlockDTO;
import org.raddatz.familienarchiv.document.transcription.ReorderTranscriptionBlocksDTO;
import org.raddatz.familienarchiv.document.transcription.UpdateTranscriptionBlockDTO;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.document.BlockSource;
import org.raddatz.familienarchiv.document.Document;
import org.raddatz.familienarchiv.document.annotation.DocumentAnnotation;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.document.transcription.PersonMention;
import org.raddatz.familienarchiv.ocr.ScriptType;
import org.raddatz.familienarchiv.document.transcription.TranscriptionBlock;
import org.raddatz.familienarchiv.document.transcription.TranscriptionBlockVersion;
import org.raddatz.familienarchiv.document.transcription.TranscriptionBlockRepository;
import org.raddatz.familienarchiv.document.transcription.TranscriptionBlockVersionRepository;
import org.raddatz.familienarchiv.ocr.SenderModelService;

import java.util.Map;

import java.util.List;
import java.util.Optional;
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.*;

import static org.springframework.http.HttpStatus.NOT_FOUND;

@ExtendWith(MockitoExtension.class)
class TranscriptionServiceTest {

    @Mock TranscriptionBlockRepository blockRepository;
    @Mock TranscriptionBlockVersionRepository versionRepository;
    @Mock AnnotationService annotationService;
    @Mock DocumentService documentService;
    @Mock SenderModelService senderModelService;
    @Mock AuditService auditService;
    @InjectMocks TranscriptionService transcriptionService;

    // ─── getBlock ────────────────────────────────────────────────────────────────

    @Test
    void getBlock_throwsNotFound_whenBlockDoesNotExist() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> transcriptionService.getBlock(docId, blockId))
                .isInstanceOf(DomainException.class)
                .satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(NOT_FOUND));
    }

    @Test
    void getBlock_returnsBlock_whenExists() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(blockId).documentId(docId).text("hello").build();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));

        TranscriptionBlock result = transcriptionService.getBlock(docId, blockId);

        assertThat(result).isEqualTo(block);
    }

    // ─── createBlock ─────────────────────────────────────────────────────────────

    @Test
    void createBlock_createsAnnotationAndBlockAndVersion() {
        UUID docId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        UUID annotId = UUID.randomUUID();

        Document doc = Document.builder().id(docId).fileHash("hash123").build();
        when(documentService.getDocumentById(docId)).thenReturn(doc);

        DocumentAnnotation annotation = DocumentAnnotation.builder().id(annotId).build();
        when(annotationService.createAnnotation(eq(docId), any(CreateAnnotationDTO.class), eq(userId), eq("hash123")))
                .thenReturn(annotation);

        when(blockRepository.countByDocumentId(docId)).thenReturn(0);
        when(blockRepository.save(any())).thenAnswer(inv -> {
            TranscriptionBlock b = inv.getArgument(0);
            b.setId(UUID.randomUUID());
            return b;
        });

        CreateTranscriptionBlockDTO dto = CreateTranscriptionBlockDTO.builder()
                .pageNumber(1).x(0.1).y(0.2).width(0.3).height(0.4)
                .text("hello").build();

        TranscriptionBlock result = transcriptionService.createBlock(docId, dto, userId);

        assertThat(result.getAnnotationId()).isEqualTo(annotId);
        assertThat(result.getText()).isEqualTo("hello");
        assertThat(result.getSortOrder()).isZero();
        assertThat(result.getCreatedBy()).isEqualTo(userId);
        verify(versionRepository).save(any(TranscriptionBlockVersion.class));
    }

    // ─── createOcrBlock ──────────────────────────────────────────────────────────

    @Test
    void createOcrBlock_createsBlockWithOcrSourceAndSavesVersion() {
        UUID docId = UUID.randomUUID();
        UUID annotId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();

        when(blockRepository.save(any())).thenAnswer(inv -> {
            TranscriptionBlock b = inv.getArgument(0);
            b.setId(UUID.randomUUID());
            return b;
        });

        TranscriptionBlock result = transcriptionService.createOcrBlock(
                docId, annotId, "OCR text", 3, userId);

        assertThat(result.getAnnotationId()).isEqualTo(annotId);
        assertThat(result.getDocumentId()).isEqualTo(docId);
        assertThat(result.getText()).isEqualTo("OCR text");
        assertThat(result.getSortOrder()).isEqualTo(3);
        assertThat(result.getSource()).isEqualTo(BlockSource.OCR);
        assertThat(result.getCreatedBy()).isEqualTo(userId);
        verify(versionRepository).save(any(TranscriptionBlockVersion.class));
    }

    @Test
    void createOcrBlock_sanitizesNullText() {
        UUID docId = UUID.randomUUID();
        UUID annotId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();

        when(blockRepository.save(any())).thenAnswer(inv -> {
            TranscriptionBlock b = inv.getArgument(0);
            b.setId(UUID.randomUUID());
            return b;
        });

        TranscriptionBlock result = transcriptionService.createOcrBlock(
                docId, annotId, null, 0, userId);

        assertThat(result.getText()).isEmpty();
    }

    // ─── updateBlock ─────────────────────────────────────────────────────────────

    @Test
    void updateBlock_updatesTextAndSavesVersion() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();

        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(blockId).documentId(docId).text("old").build();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
        when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
        when(documentService.getDocumentById(any())).thenReturn(
                Document.builder().scriptType(ScriptType.TYPEWRITER).build());

        UpdateTranscriptionBlockDTO dto = UpdateTranscriptionBlockDTO.builder().text("new text").build();

        TranscriptionBlock result = transcriptionService.updateBlock(docId, blockId, dto, userId);

        assertThat(result.getText()).isEqualTo("new text");
        assertThat(result.getUpdatedBy()).isEqualTo(userId);
        verify(versionRepository).save(any(TranscriptionBlockVersion.class));
    }

    @Test
    void updateBlock_updatesLabel_whenProvided() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();

        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(blockId).documentId(docId).text("text").label("old label").build();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
        when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
        when(documentService.getDocumentById(any())).thenReturn(
                Document.builder().scriptType(ScriptType.TYPEWRITER).build());

        UpdateTranscriptionBlockDTO dto = UpdateTranscriptionBlockDTO.builder().text("text").label("Anrede").build();

        TranscriptionBlock result = transcriptionService.updateBlock(docId, blockId, dto, UUID.randomUUID());

        assertThat(result.getLabel()).isEqualTo("Anrede");
    }

    @Test
    void updateBlock_setsSourceToManual() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(blockId).documentId(docId).text("old").source(BlockSource.OCR).build();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
        when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
        when(documentService.getDocumentById(any())).thenReturn(
                Document.builder().scriptType(ScriptType.TYPEWRITER).build());

        TranscriptionBlock result = transcriptionService.updateBlock(
                docId, blockId, UpdateTranscriptionBlockDTO.builder().text("new").build(), UUID.randomUUID());

        assertThat(result.getSource()).isEqualTo(BlockSource.MANUAL);
    }

    @Test
    void updateBlock_replacesMentionedPersonsFromDto() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        UUID personId = UUID.randomUUID();

        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(blockId).documentId(docId).text("old").build();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
        when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
        when(documentService.getDocumentById(any())).thenReturn(
                Document.builder().scriptType(ScriptType.TYPEWRITER).build());

        PersonMention mention = new PersonMention(personId, "Auguste");
        UpdateTranscriptionBlockDTO dto = UpdateTranscriptionBlockDTO.builder()
                .text("@Auguste text")
                .mentionedPersons(List.of(mention))
                .build();

        TranscriptionBlock result = transcriptionService.updateBlock(docId, blockId, dto, UUID.randomUUID());

        assertThat(result.getMentionedPersons())
                .containsExactly(mention);
    }

    @Test
    void updateBlock_clearsPriorMentions_beforeApplyingDto() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();

        PersonMention prior = new PersonMention(UUID.randomUUID(), "Heinrich");
        PersonMention incoming = new PersonMention(UUID.randomUUID(), "Auguste");

        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(blockId).documentId(docId).text("old").build();
        block.getMentionedPersons().add(prior);

        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
        when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
        when(documentService.getDocumentById(any())).thenReturn(
                Document.builder().scriptType(ScriptType.TYPEWRITER).build());

        UpdateTranscriptionBlockDTO dto = UpdateTranscriptionBlockDTO.builder()
                .text("@Auguste text")
                .mentionedPersons(List.of(incoming))
                .build();

        TranscriptionBlock result = transcriptionService.updateBlock(docId, blockId, dto, UUID.randomUUID());

        assertThat(result.getMentionedPersons())
                .containsExactly(incoming)
                .doesNotContain(prior);
    }

    @Test
    void updateBlock_triggersTraining_whenKurrentSenderPresent() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        UUID senderId = UUID.randomUUID();
        Person sender = Person.builder().id(senderId).build();
        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(blockId).documentId(docId).text("old").build();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
        when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
        when(documentService.getDocumentById(any())).thenReturn(
                Document.builder().scriptType(ScriptType.HANDWRITING_KURRENT).sender(sender).build());

        transcriptionService.updateBlock(docId, blockId, UpdateTranscriptionBlockDTO.builder().text("text").build(), UUID.randomUUID());

        verify(senderModelService).checkAndTriggerTraining(senderId);
    }

    @Test
    void updateBlock_doesNotTriggerTraining_whenNoSender() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(blockId).documentId(docId).text("old").build();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
        when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
        when(documentService.getDocumentById(any())).thenReturn(
                Document.builder().scriptType(ScriptType.HANDWRITING_KURRENT).build());

        transcriptionService.updateBlock(docId, blockId, UpdateTranscriptionBlockDTO.builder().text("text").build(), UUID.randomUUID());

        verify(senderModelService, never()).checkAndTriggerTraining(any());
    }

    // ─── deleteBlock ─────────────────────────────────────────────────────────────

    @Test
    void deleteBlock_deletesBlockAndAnnotation() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        UUID annotId = UUID.randomUUID();

        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(blockId).documentId(docId).annotationId(annotId).build();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));

        transcriptionService.deleteBlock(docId, blockId);

        verify(blockRepository).delete(block);
        verify(blockRepository).flush();
        verify(annotationService).deleteById(annotId);
    }

    @Test
    void deleteBlock_throwsNotFound_whenBlockMissing() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> transcriptionService.deleteBlock(docId, blockId))
                .isInstanceOf(DomainException.class)
                .satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(NOT_FOUND));
    }

    // ─── deleteAllBlocksByDocument ─────────────────────────────────────────────

    @Test
    void deleteAllBlocksByDocument_deletesAllBlocksAndAnnotations() {
        UUID docId = UUID.randomUUID();
        UUID annId1 = UUID.randomUUID();
        UUID annId2 = UUID.randomUUID();

        TranscriptionBlock block1 = TranscriptionBlock.builder()
                .id(UUID.randomUUID()).documentId(docId).annotationId(annId1).sortOrder(0).build();
        TranscriptionBlock block2 = TranscriptionBlock.builder()
                .id(UUID.randomUUID()).documentId(docId).annotationId(annId2).sortOrder(1).build();

        when(blockRepository.findByDocumentIdOrderBySortOrderAsc(docId))
                .thenReturn(List.of(block1, block2));

        transcriptionService.deleteAllBlocksByDocument(docId);

        verify(blockRepository).deleteAll(List.of(block1, block2));
        verify(blockRepository).flush();
        verify(annotationService).deleteAllById(List.of(annId1, annId2));
    }

    @Test
    void deleteAllBlocksByDocument_doesNothing_whenNoBlocksExist() {
        UUID docId = UUID.randomUUID();
        when(blockRepository.findByDocumentIdOrderBySortOrderAsc(docId)).thenReturn(List.of());

        transcriptionService.deleteAllBlocksByDocument(docId);

        verify(blockRepository, never()).deleteAll(any());
    }

    // ─── reorderBlocks ───────────────────────────────────────────────────────────

    @Test
    void reorderBlocks_updatesSortOrder() {
        UUID docId = UUID.randomUUID();
        UUID id1 = UUID.randomUUID();
        UUID id2 = UUID.randomUUID();

        TranscriptionBlock block1 = TranscriptionBlock.builder()
                .id(id1).documentId(docId).sortOrder(0).build();
        TranscriptionBlock block2 = TranscriptionBlock.builder()
                .id(id2).documentId(docId).sortOrder(1).build();

        when(blockRepository.findByIdAndDocumentId(id2, docId)).thenReturn(Optional.of(block2));
        when(blockRepository.findByIdAndDocumentId(id1, docId)).thenReturn(Optional.of(block1));
        when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        ReorderTranscriptionBlocksDTO dto = new ReorderTranscriptionBlocksDTO(List.of(id2, id1));

        transcriptionService.reorderBlocks(docId, dto);

        assertThat(block2.getSortOrder()).isZero();
        assertThat(block1.getSortOrder()).isEqualTo(1);
    }

    // ─── getBlockHistory ─────────────────────────────────────────────────────────

    @Test
    void getBlockHistory_returnsVersionsForBlock() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();

        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(blockId).documentId(docId).build();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));

        TranscriptionBlockVersion v = TranscriptionBlockVersion.builder()
                .id(UUID.randomUUID()).blockId(blockId).text("ver1").build();
        when(versionRepository.findByBlockIdOrderByChangedAtDesc(blockId)).thenReturn(List.of(v));

        List<TranscriptionBlockVersion> result = transcriptionService.getBlockHistory(docId, blockId);

        assertThat(result).containsExactly(v);
    }

    // ─── sanitizeText ────────────────────────────────────────────────────────────

    @Test
    void sanitizeText_returnsEmptyString_forNull() {
        assertThat(transcriptionService.sanitizeText(null)).isEmpty();
    }

    @Test
    void sanitizeText_truncatesAtMaxLength() {
        String longText = "a".repeat(15_000);
        String result = transcriptionService.sanitizeText(longText);
        assertThat(result).hasSize(10_000);
    }

    @Test
    void sanitizeText_preservesPlainText() {
        assertThat(transcriptionService.sanitizeText("Liebe Mutter,")).isEqualTo("Liebe Mutter,");
    }

    // ─── listBlocks ──────────────────────────────────────────────────────────────

    @Test
    void listBlocks_returnsBlocksOrderedBySortOrder() {
        UUID docId = UUID.randomUUID();
        TranscriptionBlock b = TranscriptionBlock.builder()
                .id(UUID.randomUUID()).documentId(docId).sortOrder(0).build();
        when(blockRepository.findByDocumentIdOrderBySortOrderAsc(docId)).thenReturn(List.of(b));

        assertThat(transcriptionService.listBlocks(docId)).containsExactly(b);
    }

    // ─── reviewBlock ─────────────────────────────────────────────────────────

    @Test
    void reviewBlock_setsReviewedTrue() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(blockId).documentId(docId).annotationId(UUID.randomUUID())
                .text("corrected text").sortOrder(0).reviewed(false).build();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
        when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        TranscriptionBlock result = transcriptionService.reviewBlock(docId, blockId, userId);

        assertThat(result.isReviewed()).isTrue();
        verify(blockRepository).save(block);
    }

    @Test
    void reviewBlock_togglesReviewedFalse_whenAlreadyReviewed() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(blockId).documentId(docId).annotationId(UUID.randomUUID())
                .text("corrected text").sortOrder(0).reviewed(true).build();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
        when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        TranscriptionBlock result = transcriptionService.reviewBlock(docId, blockId, userId);

        assertThat(result.isReviewed()).isFalse();
    }

    @Test
    void reviewBlock_throwsNotFound_whenBlockMissing() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> transcriptionService.reviewBlock(docId, blockId, UUID.randomUUID()))
                .isInstanceOf(DomainException.class);
    }

    @Test
    void reviewBlock_logsBlockReviewed_whenFlippingFalseToTrue() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(blockId).documentId(docId).reviewed(false).build();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
        when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        transcriptionService.reviewBlock(docId, blockId, userId);

        verify(auditService).logAfterCommit(AuditKind.BLOCK_REVIEWED, userId, docId, null);
    }

    @Test
    void reviewBlock_doesNotLogEvent_whenFlippingTrueToFalse() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(blockId).documentId(docId).reviewed(true).build();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
        when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        transcriptionService.reviewBlock(docId, blockId, userId);

        verify(auditService, never()).logAfterCommit(any(), any(), any(), any());
    }

    @Test
    void updateBlock_logsTextSaved_whenTextChanges() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        UUID annotId = UUID.randomUUID();
        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(blockId).documentId(docId).annotationId(annotId).text("old text").build();
        DocumentAnnotation annotation = DocumentAnnotation.builder()
                .id(annotId).pageNumber(3).build();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
        when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
        when(documentService.getDocumentById(any())).thenReturn(
                Document.builder().scriptType(ScriptType.TYPEWRITER).build());
        when(annotationService.findById(annotId)).thenReturn(Optional.of(annotation));

        transcriptionService.updateBlock(docId, blockId, UpdateTranscriptionBlockDTO.builder().text("new text").build(), userId);

        @SuppressWarnings("unchecked")
        ArgumentCaptor<Map<String, Object>> payloadCaptor = ArgumentCaptor.forClass(Map.class);
        verify(auditService).logAfterCommit(
                org.mockito.ArgumentMatchers.eq(AuditKind.TEXT_SAVED),
                org.mockito.ArgumentMatchers.eq(userId),
                org.mockito.ArgumentMatchers.eq(docId),
                payloadCaptor.capture());
        assertThat(payloadCaptor.getValue()).containsEntry("pageNumber", 3);
        assertThat(payloadCaptor.getValue()).containsEntry("blockId", blockId.toString());
    }

    @Test
    void updateBlock_doesNotLogEvent_whenTextUnchanged() {
        UUID docId = UUID.randomUUID();
        UUID blockId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(blockId).documentId(docId).annotationId(UUID.randomUUID()).text("same text").build();
        when(blockRepository.findByIdAndDocumentId(blockId, docId)).thenReturn(Optional.of(block));
        when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
        when(documentService.getDocumentById(any())).thenReturn(
                Document.builder().scriptType(ScriptType.TYPEWRITER).build());

        transcriptionService.updateBlock(docId, blockId, UpdateTranscriptionBlockDTO.builder().text("same text").build(), userId);

        verify(auditService, never()).logAfterCommit(any(), any(), any(), any());
    }

    // ─── markAllBlocksReviewed ───────────────────────────────────────────────────

    @Test
    void markAllBlocksReviewed_setsAllUnreviewedBlocksToReviewed() {
        UUID docId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        TranscriptionBlock block1 = TranscriptionBlock.builder()
                .id(UUID.randomUUID()).documentId(docId).reviewed(false).build();
        TranscriptionBlock block2 = TranscriptionBlock.builder()
                .id(UUID.randomUUID()).documentId(docId).reviewed(false).build();
        when(blockRepository.findByDocumentIdOrderBySortOrderAsc(docId))
                .thenReturn(List.of(block1, block2));
        when(blockRepository.saveAll(any())).thenAnswer(inv -> inv.getArgument(0));

        List<TranscriptionBlock> result = transcriptionService.markAllBlocksReviewed(docId, userId);

        assertThat(result).allMatch(TranscriptionBlock::isReviewed);
        verify(blockRepository).saveAll(List.of(block1, block2));
    }

    @Test
    void markAllBlocksReviewed_isIdempotent_whenAllBlocksAlreadyReviewed() {
        UUID docId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        TranscriptionBlock block = TranscriptionBlock.builder()
                .id(UUID.randomUUID()).documentId(docId).reviewed(true).build();
        when(blockRepository.findByDocumentIdOrderBySortOrderAsc(docId))
                .thenReturn(List.of(block));
        when(blockRepository.saveAll(any())).thenAnswer(inv -> inv.getArgument(0));

        List<TranscriptionBlock> result = transcriptionService.markAllBlocksReviewed(docId, userId);

        assertThat(result).allMatch(TranscriptionBlock::isReviewed);
        verify(blockRepository).saveAll(any());
    }

    @Test
    void markAllBlocksReviewed_emitsBlockReviewedAuditEvent_forEachUnreviewedBlock() {
        UUID docId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        TranscriptionBlock block1 = TranscriptionBlock.builder()
                .id(UUID.randomUUID()).documentId(docId).reviewed(false).build();
        TranscriptionBlock block2 = TranscriptionBlock.builder()
                .id(UUID.randomUUID()).documentId(docId).reviewed(false).build();
        when(blockRepository.findByDocumentIdOrderBySortOrderAsc(docId))
                .thenReturn(List.of(block1, block2));
        when(blockRepository.saveAll(any())).thenAnswer(inv -> inv.getArgument(0));

        transcriptionService.markAllBlocksReviewed(docId, userId);

        verify(auditService, times(2)).logAfterCommit(AuditKind.BLOCK_REVIEWED, userId, docId, null);
    }

    @Test
    void markAllBlocksReviewed_doesNotEmitAuditEvent_forAlreadyReviewedBlocks() {
        UUID docId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        TranscriptionBlock alreadyReviewed = TranscriptionBlock.builder()
                .id(UUID.randomUUID()).documentId(docId).reviewed(true).build();
        TranscriptionBlock unreviewed = TranscriptionBlock.builder()
                .id(UUID.randomUUID()).documentId(docId).reviewed(false).build();
        when(blockRepository.findByDocumentIdOrderBySortOrderAsc(docId))
                .thenReturn(List.of(alreadyReviewed, unreviewed));
        when(blockRepository.saveAll(any())).thenAnswer(inv -> inv.getArgument(0));

        transcriptionService.markAllBlocksReviewed(docId, userId);

        verify(auditService, times(1)).logAfterCommit(AuditKind.BLOCK_REVIEWED, userId, docId, null);
    }

    @Test
    void markAllBlocksReviewed_returnsEmptyList_whenNoBlocksExist() {
        UUID docId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        when(blockRepository.findByDocumentIdOrderBySortOrderAsc(docId)).thenReturn(List.of());
        when(blockRepository.saveAll(any())).thenAnswer(inv -> inv.getArgument(0));

        List<TranscriptionBlock> result = transcriptionService.markAllBlocksReviewed(docId, userId);

        assertThat(result).isEmpty();
    }
}
