package de.gecheckt.pdf.umbenenner.adapter.out.sqlite;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.nio.file.Path;
import java.time.Instant;
import java.time.temporal.ChronoUnit;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentRecord;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentTerminalFinalFailure;
import de.gecheckt.pdf.umbenenner.application.port.out.DocumentUnknown;
import de.gecheckt.pdf.umbenenner.application.port.out.FailureCounters;
import de.gecheckt.pdf.umbenenner.application.port.out.ProcessingAttempt;
import de.gecheckt.pdf.umbenenner.domain.model.DocumentFingerprint;
import de.gecheckt.pdf.umbenenner.domain.model.ProcessingStatus;
import de.gecheckt.pdf.umbenenner.domain.model.RunId;
import de.gecheckt.pdf.umbenenner.domain.model.SourceDocumentLocator;

/**
 * Unit tests for {@link SqliteUnitOfWorkAdapter}.
 * <p>
 * Tests verify transactional semantics: successful commits, rollback on first-write failure,
 * rollback on second-write failure, and proper handling of DocumentPersistenceException.
 *
 */
class SqliteUnitOfWorkAdapterTest {

    @TempDir
    Path tempDir;

    private String jdbcUrl;
    private SqliteUnitOfWorkAdapter unitOfWorkAdapter;
    private SqliteSchemaInitializationAdapter schemaAdapter;

    @BeforeEach
    void setUp() throws Exception {
        Path dbFile = tempDir.resolve("test.db");
        jdbcUrl = "jdbc:sqlite:" + dbFile.toAbsolutePath().toString().replace('\\', '/');

        // Initialize schema
        schemaAdapter = new SqliteSchemaInitializationAdapter(jdbcUrl);
        schemaAdapter.initializeSchema();

        // Create the unit of work adapter
        unitOfWorkAdapter = new SqliteUnitOfWorkAdapter(jdbcUrl);
    }

    /**
     * Verifies DocumentPersistenceException is properly re-thrown
     * without double-wrapping.
     */
    @Test
    void executeInTransaction_reThrowsDocumentPersistenceExceptionAsIs() {
        DocumentPersistenceException originalException =
                new DocumentPersistenceException("Original error");

        DocumentPersistenceException thrownException = assertThrows(
                DocumentPersistenceException.class,
                () -> {
                    unitOfWorkAdapter.executeInTransaction(txOps -> {
                        throw originalException;
                    });
                }
        );

        assertSame(originalException, thrownException,
                "DocumentPersistenceException should be re-thrown as-is, not wrapped");
    }

    /**
     * Verifies rollback occurs when the first write fails.
     * The transaction should not commit any data.
     */
    @Test
    void executeInTransaction_rollsBackWhenFirstWriteFails() {
        DocumentFingerprint fingerprint = new DocumentFingerprint("2222222222222222222222222222222222222222222222222222222222222222");

        // Create repositories for verification
        SqliteDocumentRecordRepositoryAdapter docRepository =
                new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);

        assertThrows(DocumentPersistenceException.class, () -> {
            unitOfWorkAdapter.executeInTransaction(txOps -> {
                // First write: throw exception directly (simulates write failure)
                throw new DocumentPersistenceException("Simulated first write failure");
            });
        });

        // Verify no records were persisted (rollback occurred)
        var lookupResult = docRepository.findByFingerprint(fingerprint);
        assertTrue(lookupResult instanceof de.gecheckt.pdf.umbenenner.application.port.out.DocumentUnknown,
                "No DocumentRecord should be persisted after rollback");
    }

    /**
     * Verifies rollback occurs when the second write fails after the first write succeeds.
     * The transaction should not commit the first write without the second.
     */
    @Test
    void executeInTransaction_rollsBackWhenSecondWriteFails() {
        DocumentFingerprint fingerprint = new DocumentFingerprint("3333333333333333333333333333333333333333333333333333333333333333");
        Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS);
        DocumentRecord record = new DocumentRecord(
                fingerprint,
                new SourceDocumentLocator("/source/test.pdf"),
                "test.pdf",
                ProcessingStatus.PROCESSING,
                FailureCounters.zero(),
                null,
                null,
                now,
                now,
                null,
                null
        );

        // Create repositories for verification
        SqliteDocumentRecordRepositoryAdapter docRepository =
                new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);

        assertThrows(DocumentPersistenceException.class, () -> {
            unitOfWorkAdapter.executeInTransaction(txOps -> {
                // First write: succeeds
                txOps.createDocumentRecord(record);

                // Second write: fails by throwing DocumentPersistenceException
                throw new DocumentPersistenceException("Simulated write failure");
            });
        });

        // Verify no records were persisted due to rollback
        var lookupResult = docRepository.findByFingerprint(fingerprint);
        assertTrue(lookupResult instanceof de.gecheckt.pdf.umbenenner.application.port.out.DocumentUnknown,
                "DocumentRecord should be rolled back when second write fails");
    }

    /**
     * Verifies rollback occurs when an arbitrary RuntimeException is thrown.
     */
    @Test
    void executeInTransaction_rollsBackOnArbitraryRuntimeException() {
        DocumentFingerprint fingerprint = new DocumentFingerprint("4444444444444444444444444444444444444444444444444444444444444444");
        Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS);
        DocumentRecord record = new DocumentRecord(
                fingerprint,
                new SourceDocumentLocator("/source/test.pdf"),
                "test.pdf",
                ProcessingStatus.PROCESSING,
                FailureCounters.zero(),
                null,
                null,
                now,
                now,
                null,
                null
        );

        RuntimeException customException = new RuntimeException("Custom runtime error");

        // Create repositories for verification
        SqliteDocumentRecordRepositoryAdapter docRepository =
                new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);

        DocumentPersistenceException thrownException = assertThrows(
                DocumentPersistenceException.class,
                () -> {
                    unitOfWorkAdapter.executeInTransaction(txOps -> {
                        txOps.createDocumentRecord(record);
                        throw customException;
                    });
                }
        );

        // Verify the exception was wrapped in DocumentPersistenceException
        assertSame(customException, thrownException.getCause(),
                "RuntimeException should be wrapped in DocumentPersistenceException");

        // Verify rollback occurred
        var lookupResult = docRepository.findByFingerprint(fingerprint);
        assertTrue(lookupResult instanceof de.gecheckt.pdf.umbenenner.application.port.out.DocumentUnknown,
                "DocumentRecord should be rolled back on runtime exception");
    }

    /**
     * Verifies that null operations Consumer throws NullPointerException.
     */
    @Test
    void executeInTransaction_throwsNullPointerExceptionForNullOperations() {
        assertThrows(NullPointerException.class, () -> {
            unitOfWorkAdapter.executeInTransaction(null);
        });
    }

    /**
     * Verifies that a document record written inside a successful transaction is persisted.
     * <p>
     * This confirms that the actual write operation is invoked and the transaction is
     * committed. Without an actual call to the underlying repository, the record would
     * not be retrievable after the transaction completes.
     */
    @Test
    void executeInTransaction_committedRecordIsRetrievable() {
        DocumentFingerprint fingerprint = new DocumentFingerprint(
                "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
        Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS);
        DocumentRecord record = new DocumentRecord(
                fingerprint,
                new SourceDocumentLocator("/source/commit-test.pdf"),
                "commit-test.pdf",
                ProcessingStatus.PROCESSING,
                FailureCounters.zero(),
                null,
                null,
                now,
                now,
                null,
                null
        );

        SqliteDocumentRecordRepositoryAdapter docRepository =
                new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);

        unitOfWorkAdapter.executeInTransaction(txOps -> txOps.createDocumentRecord(record));

        var result = docRepository.findByFingerprint(fingerprint);
        assertFalse(result instanceof DocumentUnknown,
                "Record must be persisted and retrievable after a successfully committed transaction");
    }

    // -------------------------------------------------------------------------
    // resetDocumentByFingerprint
    // -------------------------------------------------------------------------

    @Test
    void resetDocumentByFingerprint_deletesMasterRecordAndAttempts() {
        DocumentFingerprint fingerprint = new DocumentFingerprint(
                "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
        Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS);
        DocumentRecord record = new DocumentRecord(
                fingerprint,
                new SourceDocumentLocator("/source/reset-test.pdf"),
                "reset-test.pdf",
                ProcessingStatus.PROCESSING,
                FailureCounters.zero(),
                null,
                null,
                now,
                now,
                null,
                null
        );

        SqliteDocumentRecordRepositoryAdapter docRepository =
                new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
        SqliteProcessingAttemptRepositoryAdapter attemptRepository =
                new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl);

        // Persist master record and one attempt
        unitOfWorkAdapter.executeInTransaction(txOps -> {
            txOps.createDocumentRecord(record);
            txOps.saveProcessingAttempt(ProcessingAttempt.withoutAiFields(
                    fingerprint, new RunId("run-reset"), 1, now, now.plusSeconds(1),
                    ProcessingStatus.FAILED_RETRYABLE, "Err", "msg", true));
        });
        assertThat(docRepository.findByFingerprint(fingerprint)).isNotInstanceOf(DocumentUnknown.class);
        assertThat(attemptRepository.findAllByFingerprint(fingerprint)).hasSize(1);

        // Reset
        unitOfWorkAdapter.executeInTransaction(txOps ->
                txOps.resetDocumentByFingerprint(fingerprint));

        assertThat(docRepository.findByFingerprint(fingerprint)).isInstanceOf(DocumentUnknown.class);
        assertThat(attemptRepository.findAllByFingerprint(fingerprint)).isEmpty();
    }

    @Test
    void resetDocumentByFingerprint_isIdempotentWhenRecordAbsent() {
        DocumentFingerprint fingerprint = new DocumentFingerprint(
                "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc");

        // Must not throw even if no record exists
        unitOfWorkAdapter.executeInTransaction(txOps ->
                txOps.resetDocumentByFingerprint(fingerprint));

        SqliteDocumentRecordRepositoryAdapter docRepository =
                new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);
        assertThat(docRepository.findByFingerprint(fingerprint)).isInstanceOf(DocumentUnknown.class);
    }

    /**
     * Stellt sicher, dass ein Dokument mit Status FAILED_FINAL nach dem Reset nicht mehr
     * auffindbar ist. Dieser Test deckt den Reprocessing-Pfad ab, bei dem der GUI-Nutzer
     * ein final fehlgeschlagenes Dokument erneut verarbeiten lässt.
     */
    @Test
    void resetDocumentByFingerprint_deletesFailedFinalRecord_resultIsDocumentUnknown() {
        DocumentFingerprint fingerprint = new DocumentFingerprint(
                "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd");
        Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS);
        DocumentRecord record = new DocumentRecord(
                fingerprint,
                new SourceDocumentLocator("/source/failed-final.pdf"),
                "failed-final.pdf",
                ProcessingStatus.FAILED_FINAL,
                new FailureCounters(2, 0),
                now.minusSeconds(10),
                null,
                now.minusSeconds(20),
                now,
                null,
                null
        );

        SqliteDocumentRecordRepositoryAdapter docRepository =
                new SqliteDocumentRecordRepositoryAdapter(jdbcUrl);

        // Datensatz mit FAILED_FINAL anlegen
        unitOfWorkAdapter.executeInTransaction(txOps -> txOps.createDocumentRecord(record));
        assertThat(docRepository.findByFingerprint(fingerprint))
                .isInstanceOf(DocumentTerminalFinalFailure.class);

        // Reset ausführen
        unitOfWorkAdapter.executeInTransaction(txOps ->
                txOps.resetDocumentByFingerprint(fingerprint));

        // Nach dem Reset muss der Datensatz vollständig entfernt sein
        assertThat(docRepository.findByFingerprint(fingerprint))
                .isInstanceOf(DocumentUnknown.class);
    }

}
