package org.raddatz.familienarchiv.person.relationship;

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.UUID;
import java.util.concurrent.atomic.AtomicInteger;

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

/**
 * Verifies V78: person_relationships.from_year/to_year (integer) become
 * from_date/to_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. Mirrors
 * {@code PersonBirthDeathMigrationTest} (V76 / ADR-039).
 *
 * <p>Runs Flyway programmatically (no Spring context): each test gets its own
 * database so the staged migrate-to-V77 → seed → migrate-to-latest flow and
 * the abort cases cannot interfere with each other. Uses a real Postgres
 * container — H2 does not honour CHECK constraints.
 */
class RelationshipMigrationTest {

    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_v78_" + 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_abortsWhenFromYearAfterToYear() throws SQLException {
        migrateTo("77");
        UUID a = seedPerson("Alpha");
        UUID b = seedPerson("Beta");
        seedRelationship(a, b, "SPOUSE_OF", 1958, 1923);

        assertThatThrownBy(this::migrateToLatest)
                .hasMessageContaining("V78 aborted")
                .hasMessageContaining("from_year > to_year");
    }

    @Test
    void precheck_abortsWhenYearZeroPresent() throws SQLException {
        migrateTo("77");
        UUID a = seedPerson("Alpha");
        UUID b = seedPerson("Beta");
        seedRelationship(a, b, "FRIEND", 0, null);

        assertThatThrownBy(this::migrateToLatest)
                .hasMessageContaining("V78 aborted")
                .hasMessageContaining("from_year=0 or to_year=0");
    }

    @Test
    void backfill_fromYearAndToYear_becomeYearPrecisionDates() throws SQLException {
        migrateTo("77");
        UUID a = seedPerson("Alpha");
        UUID b = seedPerson("Beta");
        seedRelationship(a, b, "SPOUSE_OF", 1923, 1958);

        migrateToLatest();

        RelationDates row = relationDates(a, b, "SPOUSE_OF");
        assertThat(row.fromDate()).hasToString("1923-01-01");
        assertThat(row.fromPrecision()).isEqualTo("YEAR");
        assertThat(row.toDate()).hasToString("1958-01-01");
        assertThat(row.toPrecision()).isEqualTo("YEAR");
    }

    @Test
    void backfill_bothNull_leavesDatesNullAndPrecisionsUnknown() throws SQLException {
        migrateTo("77");
        UUID a = seedPerson("Alpha");
        UUID b = seedPerson("Beta");
        seedRelationship(a, b, "FRIEND", null, null);

        migrateToLatest();

        RelationDates row = relationDates(a, b, "FRIEND");
        assertThat(row.fromDate()).isNull();
        assertThat(row.fromPrecision()).isEqualTo("UNKNOWN");
        assertThat(row.toDate()).isNull();
        assertThat(row.toPrecision()).isEqualTo("UNKNOWN");
    }

    @Test
    void backfill_preservesRowCount() throws SQLException {
        migrateTo("77");
        UUID a = seedPerson("Alpha");
        UUID b = seedPerson("Beta");
        UUID c = seedPerson("Gamma");
        seedRelationship(a, b, "SPOUSE_OF", 1923, 1958);
        seedRelationship(a, c, "FRIEND", null, null);

        migrateToLatest();

        assertThat(countWhere("1 = 1")).isEqualTo(2);
    }

    @Test
    void orderCheckConstraint_rejectsToDateBeforeFromDate() throws SQLException {
        migrateTo("77");
        UUID a = seedPerson("Alpha");
        UUID b = seedPerson("Beta");
        migrateToLatest();

        assertThatThrownBy(() -> insertDatedRelationship(
                a, b, "FRIEND", "1958-01-01", "YEAR", "1923-01-01", "YEAR"))
                .hasMessageContaining("chk_relationship_date_order");
    }

    @Test
    void coherenceCheckConstraint_rejectsDatePresentWithUnknownPrecision() throws SQLException {
        migrateTo("77");
        UUID a = seedPerson("Alpha");
        UUID b = seedPerson("Beta");
        migrateToLatest();

        assertThatThrownBy(() -> insertDatedRelationship(
                a, b, "FRIEND", "1923-01-01", "UNKNOWN", null, "UNKNOWN"))
                .hasMessageContaining("chk_relationship_from_coherence");
    }

    @Test
    void yearColumnsDropped_andNamedCheckConstraintsExist() throws SQLException {
        migrateTo("77");
        UUID a = seedPerson("Alpha");
        UUID b = seedPerson("Beta");
        seedRelationship(a, b, "SPOUSE_OF", 1923, 1958);

        migrateToLatest();

        assertThat(columnExists("from_year")).isFalse();
        assertThat(columnExists("to_year")).isFalse();
        assertThat(columnExists("from_date")).isTrue();
        assertThat(columnExists("to_date")).isTrue();
        for (String constraint : new String[]{
                "chk_relationship_from_coherence",
                "chk_relationship_to_coherence",
                "chk_relationship_date_order",
                "chk_relationship_from_precision_values",
                "chk_relationship_to_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 UUID seedPerson(String lastName) throws SQLException {
        UUID id = UUID.randomUUID();
        try (Connection conn = connect();
             PreparedStatement stmt = conn.prepareStatement(
                     "INSERT INTO persons (id, last_name, person_type, family_member, provisional) "
                             + "VALUES (?, ?, 'PERSON', false, false)")) {
            stmt.setObject(1, id);
            stmt.setString(2, lastName);
            stmt.executeUpdate();
        }
        return id;
    }

    private void seedRelationship(UUID personId, UUID relatedId, String type, Integer fromYear, Integer toYear)
            throws SQLException {
        try (Connection conn = connect();
             PreparedStatement stmt = conn.prepareStatement(
                     "INSERT INTO person_relationships (id, person_id, related_person_id, relation_type, from_year, to_year) "
                             + "VALUES (gen_random_uuid(), ?, ?, ?, ?, ?)")) {
            stmt.setObject(1, personId);
            stmt.setObject(2, relatedId);
            stmt.setString(3, type);
            stmt.setObject(4, fromYear);
            stmt.setObject(5, toYear);
            stmt.executeUpdate();
        }
    }

    private void insertDatedRelationship(UUID personId, UUID relatedId, String type,
                                         String fromDate, String fromPrecision,
                                         String toDate, String toPrecision) throws SQLException {
        try (Connection conn = connect();
             PreparedStatement stmt = conn.prepareStatement(
                     "INSERT INTO person_relationships "
                             + "(id, person_id, related_person_id, relation_type, from_date, from_date_precision, to_date, to_date_precision) "
                             + "VALUES (gen_random_uuid(), ?, ?, ?, CAST(? AS date), ?, CAST(? AS date), ?)")) {
            stmt.setObject(1, personId);
            stmt.setObject(2, relatedId);
            stmt.setString(3, type);
            stmt.setObject(4, fromDate);
            stmt.setString(5, fromPrecision);
            stmt.setObject(6, toDate);
            stmt.setString(7, toPrecision);
            stmt.executeUpdate();
        }
    }

    private record RelationDates(Object fromDate, String fromPrecision, Object toDate, String toPrecision) {}

    private RelationDates relationDates(UUID personId, UUID relatedId, String type) throws SQLException {
        try (Connection conn = connect();
             PreparedStatement stmt = conn.prepareStatement(
                     "SELECT from_date, from_date_precision, to_date, to_date_precision "
                             + "FROM person_relationships WHERE person_id = ? AND related_person_id = ? AND relation_type = ?")) {
            stmt.setObject(1, personId);
            stmt.setObject(2, relatedId);
            stmt.setString(3, type);
            try (ResultSet rs = stmt.executeQuery()) {
                assertThat(rs.next()).as("relationship exists").isTrue();
                return new RelationDates(
                        rs.getObject("from_date"),
                        rs.getString("from_date_precision"),
                        rs.getObject("to_date"),
                        rs.getString("to_date_precision"));
            }
        }
    }

    private long countWhere(String condition) throws SQLException {
        try (Connection conn = connect();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM person_relationships 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 = 'person_relationships' 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());
    }
}
