package org.raddatz.familienarchiv.document;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.PostgresContainerConfig;
import org.raddatz.familienarchiv.audit.AuditLogQueryService;
import org.raddatz.familienarchiv.dashboard.DashboardService;
import org.raddatz.familienarchiv.person.Person;
import org.raddatz.familienarchiv.person.PersonRepository;
import org.raddatz.familienarchiv.tag.Tag;
import org.raddatz.familienarchiv.tag.TagRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.data.domain.PageRequest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import software.amazon.awssdk.services.s3.S3Client;

import java.util.HashSet;
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.raddatz.familienarchiv.document.SearchFiltersFixtures.noFilters;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

/**
 * Verifies that lazy-loaded associations on {@link Document} are accessible after a service
 * method returns — i.e. no {@link org.hibernate.LazyInitializationException} is thrown outside
 * the Hibernate session that loaded the entity.
 *
 * <p><b>Known limitation:</b> calling {@code getDocumentById} (or any other service method) from
 * within an already-open transaction is not covered here. When an outer transaction is active,
 * the service's own {@code @Transactional} merges into it and Hibernate keeps the same session
 * open, so the lazy-init guard behaves differently than in a non-transactional caller. This is a
 * known constraint of the test setup, not a bug in the production code.
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@ActiveProfiles("test")
@Import(PostgresContainerConfig.class)
class DocumentLazyLoadingTest {

    @MockitoBean
    S3Client s3Client;

    @Autowired
    DocumentRepository documentRepository;

    @Autowired
    PersonRepository personRepository;

    @Autowired
    TagRepository tagRepository;

    @Autowired
    DocumentService documentService;

    @Autowired
    DashboardService dashboardService;

    @MockitoBean
    AuditLogQueryService auditLogQueryService;

    @AfterEach
    void cleanup() {
        documentRepository.deleteAll();
        tagRepository.deleteAll();
        personRepository.deleteAll();
    }

    @Test
    void getDocumentById_tagsAndReceiversAccessible_afterReturnFromService() {
        Person sender = savedPerson("Max", "LzSender");
        Person receiver = savedPerson("Anna", "LzReceiver");
        Tag tag = savedTag("LzTag");
        Document doc = savedDocument("LazyTest", "lazy_test.pdf", sender, Set.of(receiver), Set.of(tag));

        Document result = documentService.getDocumentById(doc.getId());

        // Only the collection access itself is in assertThatCode — guards against LazyInitializationException.
        // Value assertions live outside so failures surface as AssertionError, not as unexpected exception.
        assertThatCode(() -> {
            result.getTags().size();
            result.getReceivers().size();
        }).doesNotThrowAnyException();
        assertThat(result.getTags()).isNotEmpty();
        result.getTags().forEach(t -> assertThat(t.getName()).isNotNull());
        assertThat(result.getReceivers()).isNotEmpty();
        result.getReceivers().forEach(r -> assertThat(r.getLastName()).isNotNull());
    }

    @Test
    void getRecentActivity_collectionsAccessibleAfterReturn() {
        Person sender = savedPerson("Hans", "RaSender");
        Tag tag = savedTag("RaTag");
        for (int i = 0; i < 3; i++) {
            savedDocument("RaDoc " + i, "ra_doc" + i + ".pdf", sender, Set.of(), Set.of(tag));
        }

        List<Document> results = documentService.getRecentActivity(3);

        // Access lazy fields inside assertThatCode — guards against LazyInitializationException.
        // Value assertions live outside so failures surface as AssertionError, not as unexpected exception.
        assertThatCode(() -> {
            results.forEach(d -> d.getSender().getLastName());
            results.forEach(d -> d.getTags().size());
        }).doesNotThrowAnyException();
        results.forEach(d -> assertThat(d.getSender()).isNotNull());
        results.forEach(d -> assertThat(d.getSender().getLastName()).isNotNull());
        results.forEach(d -> assertThat(d.getTags()).isNotEmpty());
    }

    @Test
    void searchDocuments_receiverSort_doesNotThrowLazyInitializationException() {
        Person sender = savedPerson("Hans", "SrSender");
        Person receiver = savedPerson("Anna", "SrReceiver");
        Tag tag = savedTag("SrTag");
        savedDocument("SrDoc", "sr_doc.pdf", sender, Set.of(receiver), Set.of(tag));

        DocumentSearchResult result = documentService.searchDocuments(
                noFilters(),
                DocumentSort.RECEIVER, "asc", PageRequest.of(0, 20));
        assertThat(result.totalElements()).isGreaterThan(0);
        assertThatCode(() ->
                result.items().forEach(i -> { if (i.sender() != null) i.sender().getLastName(); }))
                .doesNotThrowAnyException();
    }

    @Test
    void searchDocuments_pureTextRelevance_doesNotThrowLazyInitializationException() {
        // q + default sort + no other filters → the relevance fast path
        // (relevanceSortedPageFromSql), which loads documents by id outside any
        // transaction and must still deliver an initialized tags collection.
        Person sender = savedPerson("Hans", "FtSender");
        Tag tag = savedTag("FtTag");
        savedDocument("Brief von Walter", "ft_doc.pdf", sender, Set.of(), Set.of(tag));

        SearchFilters textOnly = new SearchFilters(
                "Walter", null, null, null, null, null, null, null, null, false);

        DocumentSearchResult result = documentService.searchDocuments(
                textOnly, null, "DESC", PageRequest.of(0, 10));

        assertThat(result.totalElements()).isEqualTo(1);
        assertThatCode(() ->
                result.items().forEach(i -> i.tags().size()))
                .doesNotThrowAnyException();
        assertThat(result.items().getFirst().tags()).extracting(Tag::getName).containsExactly("FtTag");
    }

    @Test
    void searchDocuments_senderSort_doesNotThrowLazyInitializationException() {
        Person sender = savedPerson("Hans", "SsSender");
        Tag tag = savedTag("SsTag");
        savedDocument("SsDoc", "ss_doc.pdf", sender, Set.of(), Set.of(tag));

        assertThatCode(() -> documentService.searchDocuments(
                noFilters(),
                DocumentSort.SENDER, "asc", PageRequest.of(0, 20)))
                .doesNotThrowAnyException();
    }

    @Test
    void dashboardService_getResume_accessesReceiversViaGetDocumentById_withoutException() {
        Person sender = savedPerson("Max", "DsSender");
        Person receiver = savedPerson("Anna", "DsReceiver");
        Document doc = savedDocument("DashboardTest", "dashboard_test.pdf", sender, Set.of(receiver), Set.of());
        UUID fakeUserId = UUID.randomUUID();
        when(auditLogQueryService.findMostRecentDocumentForUser(any())).thenReturn(Optional.of(doc.getId()));
        when(auditLogQueryService.findRecentContributorsPerDocument(any())).thenReturn(java.util.Map.of());

        assertThatCode(() -> dashboardService.getResume(fakeUserId))
                .doesNotThrowAnyException();
    }

    private Person savedPerson(String firstName, String lastName) {
        return personRepository.save(Person.builder().firstName(firstName).lastName(lastName).build());
    }

    private Tag savedTag(String name) {
        return tagRepository.save(Tag.builder().name(name).build());
    }

    private Document savedDocument(String title, String filename, Person sender,
                                   Set<Person> receivers, Set<Tag> tags) {
        return documentRepository.save(Document.builder()
                .title(title).originalFilename(filename)
                .status(DocumentStatus.UPLOADED)
                .sender(sender)
                .receivers(new HashSet<>(receivers))
                .tags(new HashSet<>(tags))
                .build());
    }
}
