package org.raddatz.familienarchiv.document.annotation;

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.mockito.ArgumentCaptor;
import org.raddatz.familienarchiv.audit.AuditKind;
import org.raddatz.familienarchiv.audit.AuditService;
import org.raddatz.familienarchiv.document.transcription.TranscriptionBlockRepository;
import org.raddatz.familienarchiv.document.annotation.CreateAnnotationDTO;
import org.raddatz.familienarchiv.document.annotation.UpdateAnnotationDTO;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.document.annotation.DocumentAnnotation;
import org.raddatz.familienarchiv.document.annotation.AnnotationRepository;
import org.springframework.dao.DataIntegrityViolationException;

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.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.FORBIDDEN;
import static org.springframework.http.HttpStatus.NOT_FOUND;

@ExtendWith(MockitoExtension.class)
class AnnotationServiceTest {

    @Mock AnnotationRepository annotationRepository;
    @Mock TranscriptionBlockRepository transcriptionBlockRepository;
    @Mock AuditService auditService;
    @InjectMocks AnnotationService annotationService;

    // ─── createAnnotation ─────────────────────────────────────────────────────

    @Test
    void createAnnotation_savesAnnotation() {
        UUID docId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.3, 0.3, "#ff0000");
        DocumentAnnotation saved = DocumentAnnotation.builder()
                .id(UUID.randomUUID()).documentId(docId).pageNumber(1)
                .x(0.1).y(0.1).width(0.3).height(0.3).color("#ff0000").createdBy(userId).build();
        when(annotationRepository.save(any())).thenReturn(saved);

        DocumentAnnotation result = annotationService.createAnnotation(docId, dto, userId, null);

        assertThat(result).isEqualTo(saved);
        verify(annotationRepository).save(any());
    }

    @Test
    void createAnnotation_allowsOverlappingAnnotations() {
        UUID docId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.3, 0.3, "#ff0000");
        when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        // Should not throw even when overlapping annotations exist on the same page
        annotationService.createAnnotation(docId, dto, userId, null);

        verify(annotationRepository).save(any());
    }

    @Test
    void createAnnotation_setsFileHash_whenProvided() {
        UUID docId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.0, 0.0, 0.05, 0.05, "#ff0000");
        when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        DocumentAnnotation result = annotationService.createAnnotation(docId, dto, userId, "abc123");

        assertThat(result.getFileHash()).isEqualTo("abc123");
    }

    @Test
    void createAnnotation_setsNullFileHash_whenNoneProvided() {
        UUID docId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.0, 0.0, 0.05, 0.05, "#ff0000");
        when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        DocumentAnnotation result = annotationService.createAnnotation(docId, dto, userId, null);

        assertThat(result.getFileHash()).isNull();
    }

    @Test
    void createAnnotation_logsAnnotationCreatedWithPageNumber() {
        UUID docId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        CreateAnnotationDTO dto = new CreateAnnotationDTO(5, 0.1, 0.1, 0.3, 0.3, "#ff0000");
        DocumentAnnotation saved = DocumentAnnotation.builder()
                .id(UUID.randomUUID()).documentId(docId).pageNumber(5)
                .x(0.1).y(0.1).width(0.3).height(0.3).color("#ff0000").createdBy(userId).build();
        when(annotationRepository.save(any())).thenReturn(saved);

        annotationService.createAnnotation(docId, dto, userId, null);

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

    @Test
    void createOcrAnnotation_doesNotLogAuditEvent() {
        UUID docId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.8, 0.04, "#00C7B1");
        when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        annotationService.createOcrAnnotation(docId, dto, userId, null, null);

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

    // ─── createOcrAnnotation ──────────────────────────────────────────────────

    @Test
    void createOcrAnnotation_savesWithPolygon() {
        UUID docId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.8, 0.04, "#00C7B1");
        List<List<Double>> polygon = List.of(
                List.of(0.1, 0.1), List.of(0.9, 0.11),
                List.of(0.89, 0.14), List.of(0.11, 0.13));
        when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        DocumentAnnotation result = annotationService.createOcrAnnotation(
                docId, dto, userId, "filehash", polygon);

        assertThat(result.getPolygon()).isEqualTo(polygon);
        assertThat(result.getDocumentId()).isEqualTo(docId);
        verify(annotationRepository).save(any());
    }

    @Test
    void createOcrAnnotation_savesWithNullPolygon_whenPolygonNotProvided() {
        UUID docId = UUID.randomUUID();
        UUID userId = UUID.randomUUID();
        CreateAnnotationDTO dto = new CreateAnnotationDTO(1, 0.1, 0.1, 0.8, 0.04, "#00C7B1");
        when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        DocumentAnnotation result = annotationService.createOcrAnnotation(
                docId, dto, userId, "filehash", null);

        assertThat(result.getPolygon()).isNull();
        verify(annotationRepository).save(any());
    }

    // ─── deleteAnnotation ─────────────────────────────────────────────────────

    @Test
    void deleteAnnotation_throwsNotFound_whenMissing() {
        UUID docId = UUID.randomUUID();
        UUID annotId = UUID.randomUUID();
        when(annotationRepository.findByIdAndDocumentId(annotId, docId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> annotationService.deleteAnnotation(docId, annotId, UUID.randomUUID()))
                .isInstanceOf(DomainException.class)
                .satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(NOT_FOUND));
    }

    @Test
    void deleteAnnotation_throwsForbidden_whenNotOwner() {
        UUID docId = UUID.randomUUID();
        UUID annotId = UUID.randomUUID();
        UUID ownerId = UUID.randomUUID();
        UUID otherId = UUID.randomUUID();

        DocumentAnnotation annotation = DocumentAnnotation.builder()
                .id(annotId).documentId(docId).createdBy(ownerId).build();
        when(annotationRepository.findByIdAndDocumentId(annotId, docId))
                .thenReturn(Optional.of(annotation));

        assertThatThrownBy(() -> annotationService.deleteAnnotation(docId, annotId, otherId))
                .isInstanceOf(DomainException.class)
                .satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(FORBIDDEN));

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

    @Test
    void deleteAnnotation_succeeds_whenOwner() {
        UUID docId = UUID.randomUUID();
        UUID annotId = UUID.randomUUID();
        UUID ownerId = UUID.randomUUID();

        DocumentAnnotation annotation = DocumentAnnotation.builder()
                .id(annotId).documentId(docId).createdBy(ownerId).build();
        when(annotationRepository.findByIdAndDocumentId(annotId, docId))
                .thenReturn(Optional.of(annotation));

        annotationService.deleteAnnotation(docId, annotId, ownerId);

        verify(transcriptionBlockRepository).deleteByAnnotationId(annotId);
        verify(annotationRepository).delete(annotation);
    }

    @Test
    void deleteAnnotation_deletesLinkedTranscriptionBlock_beforeAnnotation() {
        UUID docId = UUID.randomUUID();
        UUID annotId = UUID.randomUUID();
        UUID ownerId = UUID.randomUUID();

        DocumentAnnotation annotation = DocumentAnnotation.builder()
                .id(annotId).documentId(docId).createdBy(ownerId).build();
        when(annotationRepository.findByIdAndDocumentId(annotId, docId))
                .thenReturn(Optional.of(annotation));

        annotationService.deleteAnnotation(docId, annotId, ownerId);

        var inOrder = org.mockito.Mockito.inOrder(transcriptionBlockRepository, annotationRepository);
        inOrder.verify(transcriptionBlockRepository).deleteByAnnotationId(annotId);
        inOrder.verify(annotationRepository).delete(annotation);
    }

    @Test
    void deleteAnnotation_cascadesToTranscriptionBlocks() {
        UUID docId = UUID.randomUUID();
        UUID annotId = UUID.randomUUID();
        UUID ownerId = UUID.randomUUID();

        DocumentAnnotation annotation = DocumentAnnotation.builder()
                .id(annotId).documentId(docId).createdBy(ownerId).build();
        when(annotationRepository.findByIdAndDocumentId(annotId, docId))
                .thenReturn(Optional.of(annotation));

        annotationService.deleteAnnotation(docId, annotId, ownerId);

        verify(transcriptionBlockRepository).deleteByAnnotationId(annotId);
    }

    @Test
    void deleteAnnotation_throwsForbidden_whenUserIdIsNull() {
        UUID docId = UUID.randomUUID();
        UUID annotId = UUID.randomUUID();
        UUID ownerId = UUID.randomUUID();

        DocumentAnnotation annotation = DocumentAnnotation.builder()
                .id(annotId).documentId(docId).createdBy(ownerId).build();
        when(annotationRepository.findByIdAndDocumentId(annotId, docId))
                .thenReturn(Optional.of(annotation));

        assertThatThrownBy(() -> annotationService.deleteAnnotation(docId, annotId, null))
                .isInstanceOf(DomainException.class)
                .satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(FORBIDDEN));
    }

    // ─── updateAnnotation ─────────────────────────────────────────────────────

    @Test
    void updateAnnotation_throwsNotFound_whenAnnotationNotInDocument() {
        UUID docId = UUID.randomUUID();
        UUID annotId = UUID.randomUUID();
        when(annotationRepository.findByIdAndDocumentId(annotId, docId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> annotationService.updateAnnotation(docId, annotId, new UpdateAnnotationDTO()))
                .isInstanceOf(DomainException.class)
                .satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(NOT_FOUND));
    }

    @Test
    void updateAnnotation_updatesOnlyPresentFields() {
        UUID docId = UUID.randomUUID();
        UUID annotId = UUID.randomUUID();
        DocumentAnnotation annotation = DocumentAnnotation.builder()
                .id(annotId).documentId(docId)
                .x(0.1).y(0.2).width(0.3).height(0.4).build();
        when(annotationRepository.findByIdAndDocumentId(annotId, docId))
                .thenReturn(Optional.of(annotation));
        when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        UpdateAnnotationDTO dto = new UpdateAnnotationDTO();
        dto.setX(0.5);
        dto.setY(0.6);

        DocumentAnnotation result = annotationService.updateAnnotation(docId, annotId, dto);

        assertThat(result.getX()).isEqualTo(0.5);
        assertThat(result.getY()).isEqualTo(0.6);
        assertThat(result.getWidth()).isEqualTo(0.3);  // unchanged
        assertThat(result.getHeight()).isEqualTo(0.4); // unchanged
        verify(annotationRepository).save(annotation);
    }

    @Test
    void updateAnnotation_throwsAnnotationUpdateFailed_whenDbConstraintViolated() {
        UUID docId = UUID.randomUUID();
        UUID annotId = UUID.randomUUID();
        DocumentAnnotation annotation = DocumentAnnotation.builder()
                .id(annotId).documentId(docId)
                .x(0.1).y(0.2).width(0.3).height(0.4).build();
        when(annotationRepository.findByIdAndDocumentId(annotId, docId))
                .thenReturn(Optional.of(annotation));
        when(annotationRepository.save(any())).thenThrow(new DataIntegrityViolationException("constraint"));

        UpdateAnnotationDTO dto = new UpdateAnnotationDTO();
        dto.setX(1.5);

        assertThatThrownBy(() -> annotationService.updateAnnotation(docId, annotId, dto))
                .isInstanceOf(DomainException.class)
                .satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(BAD_REQUEST));
    }

    // ─── listAnnotations ──────────────────────────────────────────────────────

    @Test
    void listAnnotations_returnsAllForDocument() {
        UUID docId = UUID.randomUUID();
        DocumentAnnotation a = DocumentAnnotation.builder()
                .id(UUID.randomUUID()).documentId(docId).build();
        when(annotationRepository.findByDocumentId(docId)).thenReturn(List.of(a));

        assertThat(annotationService.listAnnotations(docId)).containsExactly(a);
    }

    // ─── backfillAnnotationFileHashForDocument ────────────────────────────────

    @Test
    void backfillAnnotationFileHashForDocument_setsHashOnAnnotationsWithNullHash() {
        UUID docId = UUID.randomUUID();
        String hash = "abc123";
        DocumentAnnotation a = DocumentAnnotation.builder()
                .id(UUID.randomUUID()).documentId(docId).build();
        when(annotationRepository.findByDocumentIdAndFileHashIsNull(docId)).thenReturn(List.of(a));

        annotationService.backfillAnnotationFileHashForDocument(docId, hash);

        assertThat(a.getFileHash()).isEqualTo(hash);
        verify(annotationRepository).save(a);
    }

    @Test
    void backfillAnnotationFileHashForDocument_doesNothingWhenNoAnnotations() {
        UUID docId = UUID.randomUUID();
        when(annotationRepository.findByDocumentIdAndFileHashIsNull(docId)).thenReturn(List.of());

        annotationService.backfillAnnotationFileHashForDocument(docId, "hash");

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