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.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
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 {@link SqliteHistoryQueryAdapter#findAttemptsByFingerprint}.
 *
 * <p>Deckt insbesondere die Regression ab, dass der Historien-Leseadapter die
 * Token- und Preis-Snapshot-Spalten mappen kann. Die zugehörige SELECT-Liste muss
 * dieselben Spalten projizieren, die der Mapper liest – andernfalls schlägt jede
 * Detailabfrage mit {@code no such column: 'input_tokens'} fehl.
 */
class SqliteHistoryQueryAdapterTest {

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

    @TempDir
    Path tempDir;

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

    /**
     * Ein Versuch mit gesetzten Token- und Preis-Werten muss ohne Exception geladen und
     * die Werte müssen korrekt zurückgemappt werden.
     */
    @Test
    void findAttemptsByFingerprint_liestTokenUndPreisSpalten() {
        DocumentFingerprint fp = fingerprint("aa");
        insertDocumentRecord(fp);

        Instant now = Instant.now().truncatedTo(ChronoUnit.MICROS);
        ProcessingAttempt attempt = new ProcessingAttempt(
                fp, new RunId("run-tokens"), 1, now, now.plusSeconds(1),
                ProcessingStatus.PROPOSAL_READY,
                null, null, false,
                "claude",
                "claude-haiku-4-5-20251001", "prompt-v1", 3, 4200,
                "{\"title\":\"x\"}", "reasoning",
                null, null, "Titel", "2026-07-01 - Titel.pdf",
                1500L, 800L, 200L, 100L, 250L, 2000L);
        repository.save(attempt);

        List<ProcessingAttempt> loaded = historyQuery.findAttemptsByFingerprint(fp);

        assertThat(loaded).hasSize(1);
        ProcessingAttempt result = loaded.get(0);
        assertThat(result.inputTokens()).isEqualTo(1500L);
        assertThat(result.outputTokens()).isEqualTo(800L);
        assertThat(result.cacheCreationInputTokens()).isEqualTo(200L);
        assertThat(result.cacheReadInputTokens()).isEqualTo(100L);
        assertThat(result.priceInputPerTokenNanoUsd()).isEqualTo(250L);
        assertThat(result.priceOutputPerTokenNanoUsd()).isEqualTo(2000L);
    }

    /**
     * Ein Versuch ohne Token-/Preis-Werte (typisch für Skip-/Pre-Check-Pfade und
     * historische Bestände) muss {@code null} für alle Token-Felder zurückliefern,
     * ohne dass die Abfrage fehlschlägt.
     */
    @Test
    void findAttemptsByFingerprint_nullTokenWerteBleibenNull() {
        DocumentFingerprint fp = fingerprint("bb");
        insertDocumentRecord(fp);

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

        List<ProcessingAttempt> loaded = historyQuery.findAttemptsByFingerprint(fp);

        assertThat(loaded).hasSize(1);
        ProcessingAttempt result = loaded.get(0);
        assertThat(result.inputTokens()).isNull();
        assertThat(result.outputTokens()).isNull();
        assertThat(result.cacheCreationInputTokens()).isNull();
        assertThat(result.cacheReadInputTokens()).isNull();
        assertThat(result.priceInputPerTokenNanoUsd()).isNull();
        assertThat(result.priceOutputPerTokenNanoUsd()).isNull();
    }

    /**
     * Mehrere Versuche müssen aufsteigend nach {@code attempt_number} zurückgegeben werden.
     */
    @Test
    void findAttemptsByFingerprint_sortiertAufsteigend() {
        DocumentFingerprint fp = fingerprint("cc");
        insertDocumentRecord(fp);

        Instant base = Instant.now().truncatedTo(ChronoUnit.MICROS);
        repository.save(ProcessingAttempt.withoutAiFields(
                fp, new RunId("run-1"), 1, base, base.plusSeconds(1),
                ProcessingStatus.FAILED_RETRYABLE, "Err", "msg", true));
        repository.save(new ProcessingAttempt(
                fp, new RunId("run-2"), 2, base.plusSeconds(10), base.plusSeconds(11),
                ProcessingStatus.PROPOSAL_READY,
                null, null, false, "claude",
                "claude-haiku-4-5-20251001", "prompt-v1", 1, 100,
                "{}", "r", null, null, "T", "2026-07-01 - T.pdf",
                10L, 20L, null, null, 250L, 2000L));

        List<ProcessingAttempt> loaded = historyQuery.findAttemptsByFingerprint(fp);

        assertThat(loaded).hasSize(2);
        assertThat(loaded.get(0).attemptNumber()).isEqualTo(1);
        assertThat(loaded.get(1).attemptNumber()).isEqualTo(2);
        assertThat(loaded.get(1).inputTokens()).isEqualTo(10L);
    }

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

    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);
    }
}
