package org.raddatz.familienarchiv.person;

import org.flywaydb.core.Flyway;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

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

/**
 * Verifies V76: persons.birth_year/death_year (integer) become
 * birth_date/death_date (date) + *_date_precision columns, with backfill to
 * YYYY-01-01 at YEAR precision, named CHECK constraints, and a data-quality
 * pre-check that aborts the migration on corrupt year data.
 *
 * <p>Runs Flyway programmatically (no Spring context): each test gets its own
 * database so the staged migrate-to-V75 → seed → migrate-to-latest flow and
 * the abort cases cannot interfere with each other.
 */
class PersonBirthDeathMigrationTest {

    private static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine");
    private static final AtomicInteger DB_COUNTER = new AtomicInteger();

    private String dbUrl;

    @BeforeAll
    static void startContainer() {
        POSTGRES.start();
    }

    @AfterAll
    static void stopContainer() {
        POSTGRES.stop();
    }

    @BeforeEach
    void createFreshDatabase() throws SQLException {
        String dbName = "mig_v76_" + DB_COUNTER.incrementAndGet();
        try (Connection conn = DriverManager.getConnection(
                baseUrl("postgres"), POSTGRES.getUsername(), POSTGRES.getPassword());
             Statement stmt = conn.createStatement()) {
            stmt.execute("CREATE DATABASE " + dbName);
        }
        dbUrl = baseUrl(dbName);
    }

    @Test
    void precheck_abortsWhenBirthYearAfterDeathYear() throws SQLException {
        migrateTo("75");
        seedPerson("Corrupt", 1950, 1940);

        assertThatThrownBy(this::migrateToLatest)
                .hasMessageContaining("V76 aborted")
                .hasMessageContaining("birth_year > death_year");
    }

    @Test
    void precheck_abortsWhenYearZeroPresent() throws SQLException {
        migrateTo("75");
        seedPerson("Zero", 0, null);

        assertThatThrownBy(this::migrateToLatest)
                .hasMessageContaining("V76 aborted")
                .hasMessageContaining("birth_year=0 or death_year=0");
    }

    @Test
    void backfill_birthOnly_becomesYearPrecisionDate_deathStaysUnknown() throws SQLException {
        migrateTo("75");
        seedPerson("BirthOnly", 1901, null);

        migrateToLatest();

        LifeDates row = lifeDates("BirthOnly");
        assertThat(row.birthDate()).hasToString("1901-01-01");
        assertThat(row.birthPrecision()).isEqualTo("YEAR");
        assertThat(row.deathDate()).isNull();
        assertThat(row.deathPrecision()).isEqualTo("UNKNOWN");
    }

    @Test
    void backfill_deathOnly_becomesYearPrecisionDate_birthStaysUnknown() throws SQLException {
        migrateTo("75");
        seedPerson("DeathOnly", null, 1944);

        migrateToLatest();

        LifeDates row = lifeDates("DeathOnly");
        assertThat(row.birthDate()).isNull();
        assertThat(row.birthPrecision()).isEqualTo("UNKNOWN");
        assertThat(row.deathDate()).hasToString("1944-01-01");
        assertThat(row.deathPrecision()).isEqualTo("YEAR");
    }

    @Test
    void backfill_bothNull_leavesDatesNullAndPrecisionsUnknown() throws SQLException {
        migrateTo("75");
        seedPerson("NoDates", null, null);

        migrateToLatest();

        LifeDates row = lifeDates("NoDates");
        assertThat(row.birthDate()).isNull();
        assertThat(row.birthPrecision()).isEqualTo("UNKNOWN");
        assertThat(row.deathDate()).isNull();
        assertThat(row.deathPrecision()).isEqualTo("UNKNOWN");
    }

    @Test
    void backfill_neverProducesBirthDateAfterDeathDate() throws SQLException {
        migrateTo("75");
        seedPerson("SameYear", 1901, 1901);
        seedPerson("Ordered", 1899, 1972);

        migrateToLatest();

        assertThat(countWhere("birth_date IS NOT NULL AND death_date IS NOT NULL AND birth_date > death_date"))
                .isZero();
    }

    @Test
    void yearColumnsDropped_andNamedCheckConstraintsExist() throws SQLException {
        migrateTo("75");
        seedPerson("Schema", 1901, 1944);

        migrateToLatest();

        assertThat(columnExists("birth_year")).isFalse();
        assertThat(columnExists("death_year")).isFalse();
        assertThat(columnExists("birth_date")).isTrue();
        assertThat(columnExists("death_date")).isTrue();
        for (String constraint : new String[]{
                "chk_person_birth_before_death",
                "chk_person_birth_date_precision_coherence",
                "chk_person_birth_date_precision_values",
                "chk_person_death_date_precision_coherence",
                "chk_person_death_date_precision_values"}) {
            assertThat(constraintExists(constraint)).as(constraint).isTrue();
        }
    }

    // --- helpers ---

    private static String baseUrl(String dbName) {
        return "jdbc:postgresql://" + POSTGRES.getHost() + ":" + POSTGRES.getMappedPort(5432) + "/" + dbName;
    }

    private void migrateTo(String targetVersion) {
        flywayBuilder().target(targetVersion).load().migrate();
    }

    private void migrateToLatest() {
        flywayBuilder().load().migrate();
    }

    private org.flywaydb.core.api.configuration.FluentConfiguration flywayBuilder() {
        return Flyway.configure()
                .dataSource(dbUrl, POSTGRES.getUsername(), POSTGRES.getPassword())
                .locations("classpath:db/migration")
                .placeholders(Map.of("grafanaDbPassword", "test-only"));
    }

    private void seedPerson(String lastName, Integer birthYear, Integer deathYear) throws SQLException {
        try (Connection conn = connect();
             PreparedStatement stmt = conn.prepareStatement(
                     "INSERT INTO persons (id, last_name, person_type, family_member, provisional, birth_year, death_year) "
                             + "VALUES (gen_random_uuid(), ?, 'PERSON', false, false, ?, ?)")) {
            stmt.setString(1, lastName);
            stmt.setObject(2, birthYear);
            stmt.setObject(3, deathYear);
            stmt.executeUpdate();
        }
    }

    private record LifeDates(Object birthDate, String birthPrecision, Object deathDate, String deathPrecision) {}

    private LifeDates lifeDates(String lastName) throws SQLException {
        try (Connection conn = connect();
             PreparedStatement stmt = conn.prepareStatement(
                     "SELECT birth_date, birth_date_precision, death_date, death_date_precision "
                             + "FROM persons WHERE last_name = ?")) {
            stmt.setString(1, lastName);
            try (ResultSet rs = stmt.executeQuery()) {
                assertThat(rs.next()).as("person %s exists", lastName).isTrue();
                return new LifeDates(
                        rs.getObject("birth_date"),
                        rs.getString("birth_date_precision"),
                        rs.getObject("death_date"),
                        rs.getString("death_date_precision"));
            }
        }
    }

    private long countWhere(String condition) throws SQLException {
        try (Connection conn = connect();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM persons WHERE " + condition)) {
            rs.next();
            return rs.getLong(1);
        }
    }

    private boolean columnExists(String columnName) throws SQLException {
        try (Connection conn = connect();
             PreparedStatement stmt = conn.prepareStatement(
                     "SELECT COUNT(*) FROM information_schema.columns "
                             + "WHERE table_schema = 'public' AND table_name = 'persons' AND column_name = ?")) {
            stmt.setString(1, columnName);
            try (ResultSet rs = stmt.executeQuery()) {
                rs.next();
                return rs.getInt(1) > 0;
            }
        }
    }

    private boolean constraintExists(String constraintName) throws SQLException {
        try (Connection conn = connect();
             PreparedStatement stmt = conn.prepareStatement(
                     "SELECT COUNT(*) FROM pg_constraint WHERE conname = ?")) {
            stmt.setString(1, constraintName);
            try (ResultSet rs = stmt.executeQuery()) {
                rs.next();
                return rs.getInt(1) > 0;
            }
        }
    }

    private Connection connect() throws SQLException {
        return DriverManager.getConnection(dbUrl, POSTGRES.getUsername(), POSTGRES.getPassword());
    }
}
