package org.raddatz.familienarchiv.document;

import tools.jackson.databind.ObjectMapper;
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.document.DocumentVersionSummary;
import org.raddatz.familienarchiv.exception.DomainException;
import org.raddatz.familienarchiv.user.AppUser;
import org.raddatz.familienarchiv.document.Document;
import org.raddatz.familienarchiv.document.DocumentVersion;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.tag.Tag;
import org.raddatz.familienarchiv.document.DocumentVersionRepository;
import org.raddatz.familienarchiv.user.UserService;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
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.Mockito.*;

@ExtendWith(MockitoExtension.class)
class DocumentVersionServiceTest {

    @Mock DocumentVersionRepository versionRepository;
    @Mock UserService userService;

    private DocumentVersionService versionService;

    @BeforeEach
    void setUp() {
        versionService = new DocumentVersionService(versionRepository, userService, new ObjectMapper());
    }

    @BeforeEach
    void clearSecurityContext() {
        SecurityContextHolder.clearContext();
    }

    // ─── recordVersion — editor name ─────────────────────────────────────────

    @Test
    void recordVersion_usesFirstAndLastName_whenBothPresent() {
        authenticateAs("emma");
        when(userService.findByEmail("emma")).thenReturn(
                AppUser.builder().id(UUID.randomUUID()).email("emma@example.com")
                        .firstName("Emma").lastName("Müller").build());
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        versionService.recordVersion(minimalDocument());

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getEditorName()).isEqualTo("Emma Müller");
    }

    @Test
    void recordVersion_usesEmail_whenNamesAreBlank() {
        authenticateAs("otto99");
        when(userService.findByEmail("otto99")).thenReturn(
                AppUser.builder().id(UUID.randomUUID()).email("otto99@example.com")
                        .firstName(null).lastName(null).build());
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        versionService.recordVersion(minimalDocument());

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getEditorName()).isEqualTo("otto99@example.com");
    }

    // ─── recordVersion — snapshot ─────────────────────────────────────────────

    @Test
    void recordVersion_savesSnapshotContainingTitle() {
        authenticateAs("user1");
        when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Document doc = Document.builder()
                .id(UUID.randomUUID())
                .title("Wichtiger Brief")
                .originalFilename("brief.pdf")
                .build();

        versionService.recordVersion(doc);

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getSnapshot()).contains("Wichtiger Brief");
        assertThat(captor.getValue().getDocumentId()).isEqualTo(doc.getId());
    }

    // ─── recordVersion — changedFields ────────────────────────────────────────

    @Test
    void recordVersion_changedFieldsIsEmpty_forFirstVersion() {
        authenticateAs("user1");
        when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        versionService.recordVersion(minimalDocument());

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getChangedFields()).isEqualTo("[]");
    }

    @Test
    void recordVersion_includesTitleInChangedFields_whenTitleChanged() throws Exception {
        authenticateAs("user1");
        when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));

        ObjectMapper mapper = new ObjectMapper();
        Document oldDoc = Document.builder().id(UUID.randomUUID()).title("Alt").build();
        String oldSnapshot = mapper.writeValueAsString(oldDoc);

        DocumentVersion previous = DocumentVersion.builder()
                .id(UUID.randomUUID())
                .documentId(oldDoc.getId())
                .snapshot(oldSnapshot)
                .changedFields("[]")
                .savedAt(LocalDateTime.now().minusMinutes(5))
                .editorName("user1")
                .build();

        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(oldDoc.getId()))
                .thenReturn(List.of(previous));
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Document updated = Document.builder().id(oldDoc.getId()).title("Neu").build();
        versionService.recordVersion(updated);

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getChangedFields()).contains("title");
    }

    @Test
    void recordVersion_doesNotIncludeUnchangedFields_inChangedFields() throws Exception {
        authenticateAs("user1");
        when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));

        ObjectMapper mapper = new ObjectMapper();
        UUID docId = UUID.randomUUID();
        Document oldDoc = Document.builder().id(docId).title("Same").location("Berlin").build();
        String oldSnapshot = mapper.writeValueAsString(oldDoc);

        DocumentVersion previous = DocumentVersion.builder()
                .id(UUID.randomUUID()).documentId(docId).snapshot(oldSnapshot)
                .changedFields("[]").savedAt(LocalDateTime.now().minusMinutes(5))
                .editorName("user1").build();

        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(docId)).thenReturn(List.of(previous));
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Document updated = Document.builder().id(docId).title("Same").location("Hamburg").build();
        versionService.recordVersion(updated);

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getChangedFields()).contains("location");
        assertThat(captor.getValue().getChangedFields()).doesNotContain("title");
    }

    @Test
    void recordVersion_tracksSenderChange() throws Exception {
        authenticateAs("user1");
        when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));

        ObjectMapper mapper = new ObjectMapper();
        UUID docId = UUID.randomUUID();
        Person oldSender = Person.builder().id(UUID.randomUUID()).firstName("A").lastName("B").build();
        Document oldDoc = Document.builder().id(docId).title("T").sender(oldSender).build();
        String oldSnapshot = mapper.writeValueAsString(oldDoc);

        DocumentVersion previous = DocumentVersion.builder()
                .id(UUID.randomUUID()).documentId(docId).snapshot(oldSnapshot)
                .changedFields("[]").savedAt(LocalDateTime.now().minusMinutes(5))
                .editorName("user1").build();

        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(docId)).thenReturn(List.of(previous));
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Person newSender = Person.builder().id(UUID.randomUUID()).firstName("C").lastName("D").build();
        Document updated = Document.builder().id(docId).title("T").sender(newSender).build();
        versionService.recordVersion(updated);

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getChangedFields()).contains("sender");
    }

    @Test
    void recordVersion_tracksReceiverChange() throws Exception {
        authenticateAs("user1");
        when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));

        ObjectMapper mapper = new ObjectMapper();
        UUID docId = UUID.randomUUID();
        Person receiver1 = Person.builder().id(UUID.randomUUID()).firstName("A").lastName("B").build();
        Document oldDoc = Document.builder().id(docId).title("T").receivers(Set.of(receiver1)).build();
        String oldSnapshot = mapper.writeValueAsString(oldDoc);

        DocumentVersion previous = DocumentVersion.builder()
                .id(UUID.randomUUID()).documentId(docId).snapshot(oldSnapshot)
                .changedFields("[]").savedAt(LocalDateTime.now().minusMinutes(5))
                .editorName("user1").build();

        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(docId)).thenReturn(List.of(previous));
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Document updated = Document.builder().id(docId).title("T").receivers(Set.of()).build();
        versionService.recordVersion(updated);

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getChangedFields()).contains("receivers");
    }

    @Test
    void recordVersion_tracksTagChange() throws Exception {
        authenticateAs("user1");
        when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));

        ObjectMapper mapper = new ObjectMapper();
        UUID docId = UUID.randomUUID();
        Tag tag = Tag.builder().id(UUID.randomUUID()).name("Familie").build();
        Document oldDoc = Document.builder().id(docId).title("T").tags(Set.of(tag)).build();
        String oldSnapshot = mapper.writeValueAsString(oldDoc);

        DocumentVersion previous = DocumentVersion.builder()
                .id(UUID.randomUUID()).documentId(docId).snapshot(oldSnapshot)
                .changedFields("[]").savedAt(LocalDateTime.now().minusMinutes(5))
                .editorName("user1").build();

        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(docId)).thenReturn(List.of(previous));
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Document updated = Document.builder().id(docId).title("T").tags(Set.of()).build();
        versionService.recordVersion(updated);

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getChangedFields()).contains("tags");
    }

    // ─── getSummaries ─────────────────────────────────────────────────────────

    @Test
    void getSummaries_returnsListWithParsedChangedFields() {
        UUID docId = UUID.randomUUID();
        DocumentVersion v = DocumentVersion.builder()
                .id(UUID.randomUUID()).documentId(docId)
                .savedAt(LocalDateTime.now()).editorName("Emma Müller")
                .snapshot("{}").changedFields("[\"title\",\"location\"]")
                .build();
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(docId)).thenReturn(List.of(v));

        List<DocumentVersionSummary> summaries = versionService.getSummaries(docId);

        assertThat(summaries).hasSize(1);
        assertThat(summaries.get(0).editorName()).isEqualTo("Emma Müller");
        assertThat(summaries.get(0).changedFields()).containsExactlyInAnyOrder("title", "location");
        assertThat(summaries.get(0).id()).isEqualTo(v.getId());
    }

    // ─── getVersion ───────────────────────────────────────────────────────────

    @Test
    void getVersion_returnsVersion_whenFound() {
        UUID docId = UUID.randomUUID();
        UUID versionId = UUID.randomUUID();
        DocumentVersion v = DocumentVersion.builder()
                .id(versionId).documentId(docId).snapshot("{}")
                .changedFields("[]").editorName("x").savedAt(LocalDateTime.now()).build();
        when(versionRepository.findByIdAndDocumentId(versionId, docId)).thenReturn(Optional.of(v));

        assertThat(versionService.getVersion(docId, versionId)).isEqualTo(v);
    }

    @Test
    void getVersion_throwsNotFound_whenVersionBelongsToOtherDocument() {
        UUID docId = UUID.randomUUID();
        UUID versionId = UUID.randomUUID();
        when(versionRepository.findByIdAndDocumentId(versionId, docId)).thenReturn(Optional.empty());

        assertThatThrownBy(() -> versionService.getVersion(docId, versionId))
                .isInstanceOf(DomainException.class);
    }

    // ─── backfillMissingVersions ──────────────────────────────────────────────

    @Test
    void backfill_createsVersion_withEditorNameDatenimport() {
        Document doc = minimalDocument();
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(doc.getId())).thenReturn(List.of());
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        versionService.backfillMissingVersions(List.of(doc));

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getEditorName()).isEqualTo("Datenimport");
    }

    @Test
    void backfill_usesDocumentCreatedAt_asSavedAt() {
        LocalDateTime createdAt = LocalDateTime.of(2020, 3, 15, 10, 0);
        Document doc = Document.builder()
                .id(UUID.randomUUID()).title("T").createdAt(createdAt).build();
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(doc.getId())).thenReturn(List.of());
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        versionService.backfillMissingVersions(List.of(doc));

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getSavedAt()).isEqualTo(createdAt);
    }

    @Test
    void backfill_setsChangedFieldsEmpty() {
        Document doc = minimalDocument();
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(doc.getId())).thenReturn(List.of());
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        versionService.backfillMissingVersions(List.of(doc));

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getChangedFields()).isEqualTo("[]");
    }

    @Test
    void backfill_skipsDocuments_thatAlreadyHaveVersions() {
        Document doc = minimalDocument();
        DocumentVersion existing = DocumentVersion.builder()
                .id(UUID.randomUUID()).documentId(doc.getId()).snapshot("{}")
                .changedFields("[]").editorName("user").savedAt(LocalDateTime.now()).build();
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(doc.getId())).thenReturn(List.of(existing));

        int count = versionService.backfillMissingVersions(List.of(doc));

        verify(versionRepository, never()).save(any());
        assertThat(count).isZero();
    }

    @Test
    void backfill_returnsCountOfCreatedVersions() {
        Document d1 = minimalDocument();
        Document d2 = minimalDocument();
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(d1.getId())).thenReturn(List.of());
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(d2.getId())).thenReturn(List.of());
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        int count = versionService.backfillMissingVersions(List.of(d1, d2));

        assertThat(count).isEqualTo(2);
    }

    // ─── recordVersion — no auth / user not found ─────────────────────────────

    @Test
    void recordVersion_usesUnknown_whenSecurityContextHasNoAuthentication() {
        // No call to authenticateAs — context is cleared
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        versionService.recordVersion(minimalDocument());

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getEditorName()).isEqualTo("Unknown");
        assertThat(captor.getValue().getEditorId()).isNull();
    }

    @Test
    void recordVersion_usesUnknown_whenAuthenticationIsNotAuthenticated() {
        // Auth present but isAuthenticated() = false — use TestingAuthenticationToken
        org.springframework.security.authentication.TestingAuthenticationToken notAuth =
                new org.springframework.security.authentication.TestingAuthenticationToken("user", null);
        notAuth.setAuthenticated(false);
        SecurityContextHolder.getContext().setAuthentication(notAuth);
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        versionService.recordVersion(minimalDocument());

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getEditorName()).isEqualTo("Unknown");
    }

    @Test
    void recordVersion_usesUnknown_whenUserServiceThrows() {
        authenticateAs("missinguser");
        when(userService.findByEmail("missinguser")).thenThrow(new RuntimeException("not found"));
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        versionService.recordVersion(minimalDocument());

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getEditorName()).isEqualTo("Unknown");
    }

    // ─── recordVersion — buildEditorName edge cases ───────────────────────────

    @Test
    void recordVersion_usesEmail_whenFirstNameIsNotBlankButLastNameIsNull() {
        authenticateAs("user42");
        when(userService.findByEmail("user42")).thenReturn(
                AppUser.builder().id(UUID.randomUUID()).email("user42@example.com")
                        .firstName("Hans").lastName(null).build());
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        versionService.recordVersion(minimalDocument());

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getEditorName()).isEqualTo("user42@example.com");
    }

    @Test
    void recordVersion_usesEmail_whenFirstNameIsBlankButLastNameIsPresent() {
        authenticateAs("user42");
        when(userService.findByEmail("user42")).thenReturn(
                AppUser.builder().id(UUID.randomUUID()).email("user42@example.com")
                        .firstName("  ").lastName("Müller").build());
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        versionService.recordVersion(minimalDocument());

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getEditorName()).isEqualTo("user42@example.com");
    }

    @Test
    void recordVersion_usesEmail_whenLastNameIsBlankButFirstNameIsPresent() {
        authenticateAs("user42");
        when(userService.findByEmail("user42")).thenReturn(
                AppUser.builder().id(UUID.randomUUID()).email("user42@example.com")
                        .firstName("Hans").lastName("  ").build());
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(any())).thenReturn(List.of());
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        versionService.recordVersion(minimalDocument());

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getEditorName()).isEqualTo("user42@example.com");
    }

    // ─── recordVersion — computeChangedFields with corrupt snapshot ──────────

    @Test
    void recordVersion_returnsEmptyChangedFields_whenPreviousSnapshotIsInvalidJson() {
        authenticateAs("user1");
        when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));

        UUID docId = UUID.randomUUID();
        DocumentVersion previous = DocumentVersion.builder()
                .id(UUID.randomUUID()).documentId(docId).snapshot("INVALID JSON")
                .changedFields("[]").savedAt(LocalDateTime.now().minusMinutes(5))
                .editorName("user1").build();

        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(docId)).thenReturn(List.of(previous));
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        versionService.recordVersion(Document.builder().id(docId).title("T").build());

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getChangedFields()).isEqualTo("[]");
    }

    // ─── recordVersion — checkSender/checkReceivers/checkTags with no previous ─

    @Test
    void recordVersion_tracksSenderAdded_whenPreviousHadNoSender() throws Exception {
        authenticateAs("user1");
        when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));

        ObjectMapper mapper = new ObjectMapper();
        UUID docId = UUID.randomUUID();
        Document oldDoc = Document.builder().id(docId).title("T").build(); // no sender
        String oldSnapshot = mapper.writeValueAsString(oldDoc);

        DocumentVersion previous = DocumentVersion.builder()
                .id(UUID.randomUUID()).documentId(docId).snapshot(oldSnapshot)
                .changedFields("[]").savedAt(LocalDateTime.now().minusMinutes(5))
                .editorName("user1").build();
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(docId)).thenReturn(List.of(previous));
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Person newSender = Person.builder().id(UUID.randomUUID()).firstName("A").lastName("B").build();
        Document updated = Document.builder().id(docId).title("T").sender(newSender).build();
        versionService.recordVersion(updated);

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getChangedFields()).contains("sender");
    }

    @Test
    void recordVersion_tracksReceiversAdded_whenPreviousHadNone() throws Exception {
        authenticateAs("user1");
        when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));

        ObjectMapper mapper = new ObjectMapper();
        UUID docId = UUID.randomUUID();
        Document oldDoc = Document.builder().id(docId).title("T").build(); // no receivers
        String oldSnapshot = mapper.writeValueAsString(oldDoc);

        DocumentVersion previous = DocumentVersion.builder()
                .id(UUID.randomUUID()).documentId(docId).snapshot(oldSnapshot)
                .changedFields("[]").savedAt(LocalDateTime.now().minusMinutes(5))
                .editorName("user1").build();
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(docId)).thenReturn(List.of(previous));
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Person r = Person.builder().id(UUID.randomUUID()).firstName("C").lastName("D").build();
        Document updated = Document.builder().id(docId).title("T").receivers(Set.of(r)).build();
        versionService.recordVersion(updated);

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getChangedFields()).contains("receivers");
    }

    @Test
    void recordVersion_tracksTagsAdded_whenPreviousHadNone() throws Exception {
        authenticateAs("user1");
        when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));

        ObjectMapper mapper = new ObjectMapper();
        UUID docId = UUID.randomUUID();
        Document oldDoc = Document.builder().id(docId).title("T").build(); // no tags
        String oldSnapshot = mapper.writeValueAsString(oldDoc);

        DocumentVersion previous = DocumentVersion.builder()
                .id(UUID.randomUUID()).documentId(docId).snapshot(oldSnapshot)
                .changedFields("[]").savedAt(LocalDateTime.now().minusMinutes(5))
                .editorName("user1").build();
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(docId)).thenReturn(List.of(previous));
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Tag tag = Tag.builder().id(UUID.randomUUID()).name("Familie").build();
        Document updated = Document.builder().id(docId).title("T").tags(Set.of(tag)).build();
        versionService.recordVersion(updated);

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getChangedFields()).contains("tags");
    }

    // ─── checkSender — sender map with null id ───────────────────────────────

    @Test
    void recordVersion_senderChangedToPresent_whenPreviousSenderHasNullId() throws Exception {
        // Covers: prevSender instanceof Map = true, but id == null → prevId = null
        authenticateAs("user1");
        when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));

        UUID docId = UUID.randomUUID();
        // Manually craft a JSON where sender object exists but id is null
        String oldSnapshot = "{\"id\":\"" + docId + "\",\"title\":\"T\","
                + "\"sender\":{\"id\":null,\"firstName\":\"A\",\"lastName\":\"B\"},"
                + "\"receivers\":[],\"tags\":[]}";

        DocumentVersion previous = DocumentVersion.builder()
                .id(UUID.randomUUID()).documentId(docId).snapshot(oldSnapshot)
                .changedFields("[]").savedAt(LocalDateTime.now().minusMinutes(5))
                .editorName("user1").build();
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(docId)).thenReturn(List.of(previous));
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Person newSender = Person.builder().id(UUID.randomUUID()).firstName("B").lastName("C").build();
        Document updated = Document.builder().id(docId).title("T").sender(newSender).build();
        versionService.recordVersion(updated);

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getChangedFields()).contains("sender");
    }

    // ─── checkSender — sender unchanged → not in changedFields ───────────────

    @Test
    void recordVersion_doesNotTrackSender_whenSenderUnchanged() throws Exception {
        // Covers: !Objects.equals(currentId, prevId) = false → don't add "sender"
        authenticateAs("user1");
        when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));

        ObjectMapper mapper = new ObjectMapper();
        UUID docId = UUID.randomUUID();
        UUID senderId = UUID.randomUUID();
        Person sender = Person.builder().id(senderId).firstName("A").lastName("B").build();
        Document oldDoc = Document.builder().id(docId).title("T").sender(sender).build();
        String oldSnapshot = mapper.writeValueAsString(oldDoc);

        DocumentVersion previous = DocumentVersion.builder()
                .id(UUID.randomUUID()).documentId(docId).snapshot(oldSnapshot)
                .changedFields("[]").savedAt(LocalDateTime.now().minusMinutes(5))
                .editorName("user1").build();
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(docId)).thenReturn(List.of(previous));
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        // Same sender — should NOT be in changedFields
        Document updated = Document.builder().id(docId).title("T").sender(sender).build();
        versionService.recordVersion(updated);

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getChangedFields()).doesNotContain("sender");
    }

    // ─── computeChangedFields — documentDate ternary true branch ─────────────

    @Test
    void recordVersion_tracksDocumentDate_whenCurrentDocHasNonNullDate() throws Exception {
        // current.getDocumentDate() != null = true → ternary true branch in computeChangedFields
        authenticateAs("user1");
        when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));

        ObjectMapper mapper = new ObjectMapper();
        UUID docId = UUID.randomUUID();
        Document oldDoc = Document.builder().id(docId).title("T").build(); // no date in previous
        String oldSnapshot = mapper.writeValueAsString(oldDoc);

        DocumentVersion previous = DocumentVersion.builder()
                .id(UUID.randomUUID()).documentId(docId).snapshot(oldSnapshot)
                .changedFields("[]").savedAt(LocalDateTime.now().minusMinutes(5))
                .editorName("user1").build();
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(docId)).thenReturn(List.of(previous));
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        // Current doc has a non-null documentDate → ternary evaluates its true branch
        Document updated = Document.builder().id(docId).title("T")
                .documentDate(LocalDate.of(1965, 3, 12)).build();
        versionService.recordVersion(updated);

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getChangedFields()).contains("documentDate");
    }

    // ─── checkReceivers / checkTags — when previous snapshot has null values ───

    @Test
    void recordVersion_tracksReceivers_whenPreviousSnapshotHasNullReceivers() throws Exception {
        // prevReceivers NOT instanceof List<?> → prevIds = Set.of() → if currentIds differ → added
        authenticateAs("user1");
        when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));

        UUID docId = UUID.randomUUID();
        // Craft snapshot where "receivers" is JSON null → deserialized as null, NOT a List
        String oldSnapshot = "{\"id\":\"" + docId + "\",\"title\":\"T\",\"receivers\":null,\"tags\":[]}";

        DocumentVersion previous = DocumentVersion.builder()
                .id(UUID.randomUUID()).documentId(docId).snapshot(oldSnapshot)
                .changedFields("[]").savedAt(LocalDateTime.now().minusMinutes(5))
                .editorName("user1").build();
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(docId)).thenReturn(List.of(previous));
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Person r = Person.builder().id(UUID.randomUUID()).firstName("A").lastName("B").build();
        Document updated = Document.builder().id(docId).title("T").receivers(Set.of(r)).build();
        versionService.recordVersion(updated);

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getChangedFields()).contains("receivers");
    }

    @Test
    void recordVersion_tracksTags_whenPreviousSnapshotHasNullTags() throws Exception {
        // prevTags NOT instanceof List<?> → prevNames = Set.of() → if currentNames differ → added
        authenticateAs("user1");
        when(userService.findByEmail("user1")).thenReturn(stubUser("user1"));

        UUID docId = UUID.randomUUID();
        // Craft snapshot where "tags" is JSON null → deserialized as null, NOT a List
        String oldSnapshot = "{\"id\":\"" + docId + "\",\"title\":\"T\",\"receivers\":[],\"tags\":null}";

        DocumentVersion previous = DocumentVersion.builder()
                .id(UUID.randomUUID()).documentId(docId).snapshot(oldSnapshot)
                .changedFields("[]").savedAt(LocalDateTime.now().minusMinutes(5))
                .editorName("user1").build();
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(docId)).thenReturn(List.of(previous));
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Tag tag = Tag.builder().id(UUID.randomUUID()).name("Familie").build();
        Document updated = Document.builder().id(docId).title("T").tags(Set.of(tag)).build();
        versionService.recordVersion(updated);

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getChangedFields()).contains("tags");
    }

    // ─── backfill — uses LocalDateTime.now() when createdAt is null ──────────

    @Test
    void backfill_usesNow_whenDocumentCreatedAtIsNull() {
        Document doc = Document.builder().id(UUID.randomUUID()).title("T").createdAt(null).build();
        when(versionRepository.findByDocumentIdOrderBySavedAtAsc(doc.getId())).thenReturn(List.of());
        when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        versionService.backfillMissingVersions(List.of(doc));

        ArgumentCaptor<DocumentVersion> captor = ArgumentCaptor.forClass(DocumentVersion.class);
        verify(versionRepository).save(captor.capture());
        assertThat(captor.getValue().getSavedAt()).isNotNull();
    }

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

    private void authenticateAs(String username) {
        SecurityContextHolder.getContext().setAuthentication(
                new UsernamePasswordAuthenticationToken(username, null, List.of()));
    }

    private AppUser stubUser(String email) {
        return AppUser.builder().id(UUID.randomUUID()).email(email)
                .firstName(null).lastName(null).build();
    }

    private Document minimalDocument() {
        return Document.builder()
                .id(UUID.randomUUID())
                .title("Test")
                .originalFilename("test.pdf")
                .documentDate(LocalDate.of(1940, 5, 1))
                .build();
    }
}
