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

import static org.assertj.core.api.Assertions.assertThat;

import java.nio.file.Path;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;

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.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;

/**
 * Tests für {@code ai_provider} in {@code processing_attempt}.
 *
 * <p>Prüft Schreib-/Lese-Roundtrips für beide Provider-Identifikatoren,
 * Idempotenz der Initialisierung sowie das Verhalten bei Schemata,
 * die nicht dem Zielschema entsprechen (harter Abbruch per Fall-2-Strategie).
 */
class SqliteAttemptProviderPersistenceTest {

    private String jdbcUrl;
    private SqliteSchemaInitializationAdapter schemaAdapter;
    private SqliteProcessingAttemptRepositoryAdapter repository;

    @TempDir
    Path tempDir;

    @BeforeEach
    void setUp() {
        Path dbFile = tempDir.resolve("provider-test.db");
        jdbcUrl = "jdbc:sqlite:" + dbFile.toAbsolutePath();
        schemaAdapter = new SqliteSchemaInitializationAdapter(jdbcUrl);
        repository = new SqliteProcessingAttemptRepositoryAdapter(jdbcUrl);
    }

    // -------------------------------------------------------------------------
    // Schema migration tests
    // -------------------------------------------------------------------------

    /**
     * A fresh database must contain the {@code ai_provider} column after schema initialisation.
     */
    @Test
    void addsProviderColumnOnFreshDb() throws SQLException {
        schemaAdapter.initializeSchema();

        assertThat(columnExists("processing_attempt", "ai_provider"))
                .as("ai_provider column must exist in processing_attempt after fresh schema init")
                .isTrue();
    }

    /**
     * Eine bestehende Datenbank ohne {@code ai_provider}-Spalte in {@code processing_attempt}
     * entspricht nicht dem vollständigen Zielschema. Die Initialisierung muss mit einem
     * klaren Fehler abbrechen, da kein stilles Heilen stattfindet.
     */
    @Test
    void existingDbOhneAiProviderSpalte_brichtAb() throws SQLException {
        // Schema ohne ai_provider anlegen
        createLegacySchema();

        assertThat(columnExists("processing_attempt", "ai_provider"))
                .as("ai_provider darf im Legacy-Schema noch nicht vorhanden sein")
                .isFalse();

        // Initialisierung muss mit Fehler abbrechen (nicht konformes Schema)
        org.junit.jupiter.api.Assertions.assertThrows(
                de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException.class,
                () -> schemaAdapter.initializeSchema(),
                "Erwarte Fehler bei nicht konformem Schema (fehlende ai_provider-Spalte)");
    }

    /**
     * Running schema initialisation multiple times must not fail and must not change the schema.
     */
    @Test
    void migrationIsIdempotent() throws SQLException {
        schemaAdapter.initializeSchema();
        // Second and third init must not throw or change the schema
        schemaAdapter.initializeSchema();
        schemaAdapter.initializeSchema();

        assertThat(columnExists("processing_attempt", "ai_provider"))
                .as("Column must still be present after repeated init calls")
                .isTrue();
    }

    /**
     * Neue Versuche die ohne Provider-Information gespeichert werden (z. B. über
     * {@code ProcessingAttempt.withoutAiFields}), müssen {@code null} als
     * {@code ai_provider} zurückliefern.
     */
    @Test
    void neuerVersuchOhneProvider_haeltNullProviderNachSchreibenUndLesen() {
        schemaAdapter.initializeSchema();
        DocumentFingerprint fp = fingerprint("aa");
        insertDocumentRecord(fp);

        java.time.Instant now = java.time.Instant.now().truncatedTo(java.time.temporal.ChronoUnit.MICROS);
        ProcessingAttempt attemptOhneProvider = ProcessingAttempt.withoutAiFields(
                fp, new RunId("run-null"), 1,
                now, now.plusSeconds(1),
                ProcessingStatus.FAILED_RETRYABLE,
                "Err", "msg", true);
        repository.save(attemptOhneProvider);

        List<ProcessingAttempt> gelesen = repository.findAllByFingerprint(fp);
        assertThat(gelesen).hasSize(1);
        assertThat(gelesen.get(0).aiProvider())
                .as("Versuche ohne Provider müssen null zurückgeben")
                .isNull();
    }

    // -------------------------------------------------------------------------
    // Write tests
    // -------------------------------------------------------------------------

    /**
     * A new attempt written with an active OpenAI-compatible provider must
     * persist {@code "openai-compatible"} in {@code ai_provider}.
     */
    @Test
    void newAttemptsWriteOpenAiCompatibleProvider() {
        schemaAdapter.initializeSchema();
        DocumentFingerprint fp = fingerprint("bb");
        insertDocumentRecord(fp);

        Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS);
        ProcessingAttempt attempt = new ProcessingAttempt(
                fp, new RunId("run-oai"), 1, now, now.plusSeconds(1),
                ProcessingStatus.READY_FOR_AI,
                null, null, false,
                "openai-compatible",
                null, null, null, null, null, null,
                null, null, null, null);

        repository.save(attempt);

        List<ProcessingAttempt> saved = repository.findAllByFingerprint(fp);
        assertThat(saved).hasSize(1);
        assertThat(saved.get(0).aiProvider()).isEqualTo("openai-compatible");
    }

    /**
     * A new attempt written with an active Claude provider must persist
     * {@code "claude"} in {@code ai_provider}.
     * <p>
     * The provider selection is simulated at the data level here; the actual
     * Claude adapter is wired in a later step.
     */
    @Test
    void newAttemptsWriteClaudeProvider() {
        schemaAdapter.initializeSchema();
        DocumentFingerprint fp = fingerprint("cc");
        insertDocumentRecord(fp);

        Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS);
        ProcessingAttempt attempt = new ProcessingAttempt(
                fp, new RunId("run-claude"), 1, now, now.plusSeconds(1),
                ProcessingStatus.READY_FOR_AI,
                null, null, false,
                "claude",
                null, null, null, null, null, null,
                null, null, null, null);

        repository.save(attempt);

        List<ProcessingAttempt> saved = repository.findAllByFingerprint(fp);
        assertThat(saved).hasSize(1);
        assertThat(saved.get(0).aiProvider()).isEqualTo("claude");
    }

    // -------------------------------------------------------------------------
    // Read tests
    // -------------------------------------------------------------------------

    /**
     * The repository must correctly return the persisted provider identifier
     * when reading an attempt back from the database.
     */
    @Test
    void repositoryReadsProviderColumn() {
        schemaAdapter.initializeSchema();
        DocumentFingerprint fp = fingerprint("dd");
        insertDocumentRecord(fp);

        Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS);
        repository.save(new ProcessingAttempt(
                fp, new RunId("run-read"), 1, now, now.plusSeconds(2),
                ProcessingStatus.FAILED_RETRYABLE,
                "Timeout", "Connection timed out", true,
                "openai-compatible",
                null, null, null, null, null, null,
                null, null, null, null));

        List<ProcessingAttempt> loaded = repository.findAllByFingerprint(fp);
        assertThat(loaded).hasSize(1);
        assertThat(loaded.get(0).aiProvider())
                .as("Repository must return the persisted ai_provider value")
                .isEqualTo("openai-compatible");
    }

    /**
     * Eine Datenbank mit nicht konformem Schema (fehlende Spalten, fehlende Indizes)
     * wird von der Initialisierung mit einem klaren Fehler abgebrochen.
     * Es findet kein stilles Heilen statt.
     */
    @Test
    void nichtKonformesSchema_brichtMitAussagekraeftigemFehlerAb() throws SQLException {
        // Legacy-Schema anlegen (fehlt: ai_provider, last_target_path, last_target_file_name,
        // Indizes fehlen ebenfalls)
        createLegacySchema();
        DocumentFingerprint fp = fingerprint("ee");
        insertLegacyDocumentRecord(fp);
        insertLegacyAttemptRow(fp, "FAILED_RETRYABLE");

        // Initialisierung muss abbrechen
        org.junit.jupiter.api.Assertions.assertThrows(
                de.gecheckt.pdf.umbenenner.application.port.out.DocumentPersistenceException.class,
                () -> schemaAdapter.initializeSchema(),
                "Erwarte Fehler bei nicht konformem Bestands-Schema");
    }

    /**
     * All existing attempt history tests must remain green: the repository
     * handles null {@code ai_provider} values transparently without errors.
     */
    @Test
    void existingHistoryTestsRemainGreen() {
        schemaAdapter.initializeSchema();
        DocumentFingerprint fp = fingerprint("ff");
        insertDocumentRecord(fp);

        Instant base = Instant.now().truncatedTo(ChronoUnit.MICROS);

        // Save attempt with null provider (as in legacy path or non-AI attempt)
        ProcessingAttempt nullProviderAttempt = ProcessingAttempt.withoutAiFields(
                fp, new RunId("run-legacy"), 1,
                base, base.plusSeconds(1),
                ProcessingStatus.FAILED_RETRYABLE,
                "Err", "msg", true);
        repository.save(nullProviderAttempt);

        // Save attempt with explicit provider
        ProcessingAttempt withProvider = new ProcessingAttempt(
                fp, new RunId("run-new"), 2,
                base.plusSeconds(10), base.plusSeconds(11),
                ProcessingStatus.READY_FOR_AI,
                null, null, false,
                "openai-compatible",
                null, null, null, null, null, null,
                null, null, null, null);
        repository.save(withProvider);

        List<ProcessingAttempt> all = repository.findAllByFingerprint(fp);
        assertThat(all).hasSize(2);
        assertThat(all.get(0).aiProvider()).isNull();
        assertThat(all.get(1).aiProvider()).isEqualTo("openai-compatible");
        // Ordering preserved
        assertThat(all.get(0).attemptNumber()).isEqualTo(1);
        assertThat(all.get(1).attemptNumber()).isEqualTo(2);
    }

    // -------------------------------------------------------------------------
    // Helpers
    // -------------------------------------------------------------------------

    private boolean columnExists(String table, String column) throws SQLException {
        try (Connection conn = DriverManager.getConnection(jdbcUrl)) {
            DatabaseMetaData meta = conn.getMetaData();
            try (ResultSet rs = meta.getColumns(null, null, table, column)) {
                return rs.next();
            }
        }
    }

    /**
     * Creates the base tables that existed before the {@code ai_provider} column was added,
     * without running the schema evolution that adds that column.
     */
    private void createLegacySchema() throws SQLException {
        try (Connection conn = DriverManager.getConnection(jdbcUrl);
             Statement stmt = conn.createStatement()) {
            stmt.execute("PRAGMA foreign_keys = ON");
            stmt.execute("""
                    CREATE TABLE IF NOT EXISTS document_record (
                        id                          INTEGER PRIMARY KEY AUTOINCREMENT,
                        fingerprint                 TEXT    NOT NULL,
                        last_known_source_locator   TEXT    NOT NULL,
                        last_known_source_file_name TEXT    NOT NULL,
                        overall_status              TEXT    NOT NULL,
                        content_error_count         INTEGER NOT NULL DEFAULT 0,
                        transient_error_count       INTEGER NOT NULL DEFAULT 0,
                        last_failure_instant        TEXT,
                        last_success_instant        TEXT,
                        created_at                  TEXT    NOT NULL,
                        updated_at                  TEXT    NOT NULL,
                        CONSTRAINT uq_document_record_fingerprint UNIQUE (fingerprint)
                    )""");
            stmt.execute("""
                    CREATE TABLE IF NOT EXISTS processing_attempt (
                        id              INTEGER PRIMARY KEY AUTOINCREMENT,
                        fingerprint     TEXT    NOT NULL,
                        run_id          TEXT    NOT NULL,
                        attempt_number  INTEGER NOT NULL,
                        started_at      TEXT    NOT NULL,
                        ended_at        TEXT    NOT NULL,
                        status          TEXT    NOT NULL,
                        failure_class   TEXT,
                        failure_message TEXT,
                        retryable       INTEGER NOT NULL DEFAULT 0,
                        model_name      TEXT,
                        prompt_identifier TEXT,
                        processed_page_count INTEGER,
                        sent_character_count INTEGER,
                        ai_raw_response TEXT,
                        ai_reasoning    TEXT,
                        resolved_date   TEXT,
                        date_source     TEXT,
                        validated_title TEXT,
                        final_target_file_name TEXT,
                        CONSTRAINT fk_processing_attempt_fingerprint
                            FOREIGN KEY (fingerprint) REFERENCES document_record (fingerprint),
                        CONSTRAINT uq_processing_attempt_fingerprint_number
                            UNIQUE (fingerprint, attempt_number)
                    )""");
        }
    }

    private void insertLegacyDocumentRecord(DocumentFingerprint fp) throws SQLException {
        try (Connection conn = DriverManager.getConnection(jdbcUrl);
             PreparedStatement ps = conn.prepareStatement("""
                     INSERT INTO document_record
                         (fingerprint, last_known_source_locator, last_known_source_file_name,
                          overall_status, created_at, updated_at)
                     VALUES (?, '/tmp/test.pdf', 'test.pdf', 'READY_FOR_AI',
                             strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
                             strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))""")) {
            ps.setString(1, fp.sha256Hex());
            ps.executeUpdate();
        }
    }

    private void insertLegacyAttemptRow(DocumentFingerprint fp, String status) throws SQLException {
        try (Connection conn = DriverManager.getConnection(jdbcUrl);
             PreparedStatement ps = conn.prepareStatement("""
                     INSERT INTO processing_attempt
                         (fingerprint, run_id, attempt_number, started_at, ended_at, status, retryable)
                     VALUES (?, 'run-legacy', 1, strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
                             strftime('%Y-%m-%dT%H:%M:%SZ', 'now'), ?, 1)""")) {
            ps.setString(1, fp.sha256Hex());
            ps.setString(2, status);
            ps.executeUpdate();
        }
    }

    private void insertDocumentRecord(DocumentFingerprint fp) {
        try (Connection conn = DriverManager.getConnection(jdbcUrl);
             PreparedStatement ps = conn.prepareStatement("""
                     INSERT INTO document_record
                         (fingerprint, last_known_source_locator, last_known_source_file_name,
                          overall_status, created_at, updated_at)
                     VALUES (?, '/tmp/test.pdf', 'test.pdf', 'READY_FOR_AI',
                             strftime('%Y-%m-%dT%H:%M:%SZ', 'now'),
                             strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))""")) {
            ps.setString(1, fp.sha256Hex());
            ps.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException("Failed to insert test document record", e);
        }
    }

    private static DocumentFingerprint fingerprint(String suffix) {
        return new DocumentFingerprint(
                ("0".repeat(64 - suffix.length()) + suffix));
    }
}
