package org.raddatz.familienarchiv.document;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.raddatz.familienarchiv.document.Document;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.Optional;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

class ThumbnailAsyncRunnerTest {

    private DocumentRepository documentRepository;
    private ThumbnailService thumbnailService;
    private ThumbnailAsyncRunner runner;

    @BeforeEach
    void setUp() {
        documentRepository = mock(DocumentRepository.class);
        thumbnailService = mock(ThumbnailService.class);
        runner = new ThumbnailAsyncRunner(documentRepository, thumbnailService);
    }

    @Test
    void dispatchAfterCommit_whenNoTransaction_dispatchesImmediately() {
        UUID id = UUID.randomUUID();
        Document doc = Document.builder().id(id).originalFilename("f.pdf").title("t").build();
        when(documentRepository.findById(id)).thenReturn(Optional.of(doc));

        runner.dispatchAfterCommit(id);

        verify(thumbnailService).generate(doc);
    }

    @Test
    void dispatchAfterCommit_whenTransactionActive_registersAfterCommitSynchronization() {
        UUID id = UUID.randomUUID();
        Document doc = Document.builder().id(id).originalFilename("f.pdf").title("t").build();
        when(documentRepository.findById(id)).thenReturn(Optional.of(doc));

        TransactionSynchronizationManager.initSynchronization();
        try {
            runner.dispatchAfterCommit(id);

            // Nothing fired yet — registered, not executed
            verify(thumbnailService, never()).generate(any());

            // Simulate commit
            ArgumentCaptor<TransactionSynchronization> captor =
                    ArgumentCaptor.forClass(TransactionSynchronization.class);
            assertThat(TransactionSynchronizationManager.getSynchronizations()).hasSize(1);
            TransactionSynchronizationManager.getSynchronizations().get(0).afterCommit();

            verify(thumbnailService).generate(doc);
        } finally {
            TransactionSynchronizationManager.clearSynchronization();
        }
    }

    @Test
    void dispatchAfterCommit_whenRollback_doesNotDispatch() {
        UUID id = UUID.randomUUID();
        Document doc = Document.builder().id(id).originalFilename("f.pdf").title("t").build();
        when(documentRepository.findById(id)).thenReturn(Optional.of(doc));

        TransactionSynchronizationManager.initSynchronization();
        try {
            runner.dispatchAfterCommit(id);

            // Simulate rollback — afterCompletion with STATUS_ROLLED_BACK, no afterCommit fired
            TransactionSynchronizationManager.getSynchronizations().get(0)
                    .afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK);

            verify(thumbnailService, never()).generate(any());
        } finally {
            TransactionSynchronizationManager.clearSynchronization();
        }
    }

    @Test
    void generateAsync_skipsWhenDocumentMissing() {
        UUID id = UUID.randomUUID();
        when(documentRepository.findById(id)).thenReturn(Optional.empty());

        runner.generateAsync(id);

        verifyNoInteractions(thumbnailService);
    }

    @Test
    void generateAsync_timesOutWhenGenerateExceedsLimit() throws Exception {
        UUID id = UUID.randomUUID();
        Document doc = Document.builder().id(id).originalFilename("f.pdf").title("t").build();
        when(documentRepository.findById(id)).thenReturn(Optional.of(doc));
        // generate sleeps longer than the timeout — simulates a hung PDFBox render
        when(thumbnailService.generate(doc)).thenAnswer(inv -> {
            Thread.sleep(5_000);
            return ThumbnailService.Outcome.SUCCESS;
        });
        // Shrink timeout for the test
        ReflectionTestUtils.setField(runner, "generateTimeoutSeconds", 1L);

        long start = System.currentTimeMillis();
        runner.generateAsync(id);
        long elapsed = System.currentTimeMillis() - start;

        // Must return before the 5s sleep — within ~2s with timeout=1s plus overhead
        assertThat(elapsed).isLessThan(3_000);
    }
}
