package org.raddatz.familienarchiv.audit;

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.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.task.TaskExecutor;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;

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

@ExtendWith(MockitoExtension.class)
class AuditServiceTest {

    @Mock AuditLogRepository auditLogRepository;
    @Mock TaskExecutor auditExecutor;
    @InjectMocks AuditService auditService;

    @Test
    void log_savesAuditRowWithCorrectFields() {
        UUID actorId = UUID.randomUUID();
        UUID documentId = UUID.randomUUID();
        Map<String, Object> payload = Map.of("pageNumber", 3);

        when(auditLogRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        auditService.log(AuditKind.TEXT_SAVED, actorId, documentId, payload);

        ArgumentCaptor<AuditLog> captor = ArgumentCaptor.forClass(AuditLog.class);
        verify(auditLogRepository).save(captor.capture());
        AuditLog saved = captor.getValue();

        assertThat(saved.getKind()).isEqualTo(AuditKind.TEXT_SAVED);
        assertThat(saved.getActorId()).isEqualTo(actorId);
        assertThat(saved.getDocumentId()).isEqualTo(documentId);
        assertThat(saved.getPayload()).isEqualTo(payload);
    }

    @Test
    void log_doesNotPropagateException_whenRepoThrows() {
        when(auditLogRepository.save(any())).thenThrow(new RuntimeException("DB down"));

        assertThatCode(() ->
            auditService.log(AuditKind.METADATA_UPDATED, UUID.randomUUID(), UUID.randomUUID(), null)
        ).doesNotThrowAnyException();
    }

    @Test
    void log_acceptsNullPayload() {
        when(auditLogRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        auditService.log(AuditKind.FILE_UPLOADED, UUID.randomUUID(), UUID.randomUUID(), null);

        ArgumentCaptor<AuditLog> captor = ArgumentCaptor.forClass(AuditLog.class);
        verify(auditLogRepository).save(captor.capture());
        assertThat(captor.getValue().getPayload()).isNull();
    }

    @Test
    void log_acceptsNullActorId() {
        when(auditLogRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        auditService.log(AuditKind.BLOCK_REVIEWED, null, UUID.randomUUID(), null);

        ArgumentCaptor<AuditLog> captor = ArgumentCaptor.forClass(AuditLog.class);
        verify(auditLogRepository).save(captor.capture());
        assertThat(captor.getValue().getActorId()).isNull();
    }

    // ─── logAfterCommit ───────────────────────────────────────────────────────

    @Test
    void logAfterCommit_savesDirectly_whenNoTransactionIsActive() {
        when(auditLogRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        try (MockedStatic<TransactionSynchronizationManager> mocked =
                     mockStatic(TransactionSynchronizationManager.class)) {
            mocked.when(TransactionSynchronizationManager::isActualTransactionActive).thenReturn(false);

            auditService.logAfterCommit(AuditKind.METADATA_UPDATED, null, null, null);

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

    @Test
    void logAfterCommit_registersCallback_andSubmitsToExecutor_afterCommit() {
        try (MockedStatic<TransactionSynchronizationManager> mocked =
                     mockStatic(TransactionSynchronizationManager.class)) {
            mocked.when(TransactionSynchronizationManager::isActualTransactionActive).thenReturn(true);
            List<TransactionSynchronization> captured = new ArrayList<>();
            mocked.when(() -> TransactionSynchronizationManager.registerSynchronization(any()))
                    .thenAnswer(inv -> { captured.add(inv.getArgument(0)); return null; });

            auditService.logAfterCommit(AuditKind.TEXT_SAVED, null, null, null);

            // Callback registered but executor not yet invoked
            assertThat(captured).hasSize(1);
            verify(auditExecutor, never()).execute(any());

            // Simulate transaction commit
            captured.get(0).afterCommit();

            // Write submitted to executor — not called inline
            verify(auditExecutor).execute(any());
            verify(auditLogRepository, never()).save(any());
        }
    }
}
