package com.magistr.app.migration;

import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.MigrationVersion;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import java.sql.Connection;
import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;

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

@Testcontainers
@DisplayName("Миграция нумерации недель календарного графика")
class AcademicCalendarWeekNumberMigrationIntegrationTest {

    @Container
    static final PostgreSQLContainer<?> POSTGRES =
            new PostgreSQLContainer<>(com.magistr.app.testing.TestContainerImages.POSTGRES);

    @Test
    @DisplayName("V2 переносит первый понедельник после неполной недели в неделю 2")
    void migrationAlignsWeeksToMondayFor2026And2027() throws SQLException {
        Flyway baseline = flyway("1");
        baseline.clean();
        baseline.migrate();

        long calendar2026;
        long calendar2027;
        try (Connection connection = POSTGRES.createConnection("")) {
            SeedReferences references = loadSeedReferences(connection);

            long year2026 = insertAcademicYear(
                    connection,
                    "2026-2027",
                    LocalDate.of(2026, 9, 1),
                    LocalDate.of(2027, 6, 30)
            );
            calendar2026 = insertCalendar(connection, year2026, references, "График 2026-2027");
            insertCalendarDay(connection, calendar2026, references.activityTypeId(),
                    LocalDate.of(2026, 9, 1), 1, 2);
            insertCalendarDay(connection, calendar2026, references.activityTypeId(),
                    LocalDate.of(2026, 9, 7), 1, 1);

            long year2027 = insertAcademicYear(
                    connection,
                    "2027-2028",
                    LocalDate.of(2027, 9, 1),
                    LocalDate.of(2028, 6, 30)
            );
            calendar2027 = insertCalendar(connection, year2027, references, "График 2027-2028");
            insertCalendarDay(connection, calendar2027, references.activityTypeId(),
                    LocalDate.of(2027, 9, 1), 1, 3);
            insertCalendarDay(connection, calendar2027, references.activityTypeId(),
                    LocalDate.of(2027, 9, 6), 1, 1);
        }

        Flyway latest = latestFlyway();
        latest.migrate();

        assertThat(latest.info().current()).isNotNull();
        assertThat(latest.info().current().getVersion().getVersion()).isEqualTo("2");
        try (Connection connection = POSTGRES.createConnection("")) {
            assertThat(findWeekNumber(connection, calendar2026, LocalDate.of(2026, 9, 1))).isEqualTo(1);
            assertThat(findWeekNumber(connection, calendar2026, LocalDate.of(2026, 9, 7))).isEqualTo(2);
            assertThat(findWeekNumber(connection, calendar2027, LocalDate.of(2027, 9, 1))).isEqualTo(1);
            assertThat(findWeekNumber(connection, calendar2027, LocalDate.of(2027, 9, 6))).isEqualTo(2);
        }
    }

    private Flyway flyway(String targetVersion) {
        return Flyway.configure()
                .dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
                .locations("classpath:db/migration")
                .target(MigrationVersion.fromVersion(targetVersion))
                .cleanDisabled(false)
                .load();
    }

    private Flyway latestFlyway() {
        return Flyway.configure()
                .dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
                .locations("classpath:db/migration")
                .cleanDisabled(false)
                .load();
    }

    private SeedReferences loadSeedReferences(Connection connection) throws SQLException {
        long specialtyId = queryLong(connection, "SELECT id FROM specialties ORDER BY id LIMIT 1");
        long profileId = queryLong(
                connection,
                "SELECT id FROM specialty_profiles WHERE specialty_id = ? ORDER BY id LIMIT 1",
                specialtyId
        );
        long studyFormId = queryLong(connection, "SELECT id FROM education_forms ORDER BY id LIMIT 1");
        long activityTypeId = queryLong(
                connection,
                "SELECT id FROM academic_calendar_activity_types WHERE code = 'Т'"
        );
        return new SeedReferences(specialtyId, profileId, studyFormId, activityTypeId);
    }

    private long insertAcademicYear(Connection connection,
                                    String title,
                                    LocalDate startDate,
                                    LocalDate endDate) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement("""
                INSERT INTO academic_years (title, start_date, end_date)
                VALUES (?, ?, ?)
                RETURNING id
                """)) {
            statement.setString(1, title);
            statement.setDate(2, Date.valueOf(startDate));
            statement.setDate(3, Date.valueOf(endDate));
            return requiredLong(statement);
        }
    }

    private long insertCalendar(Connection connection,
                                long academicYearId,
                                SeedReferences references,
                                String title) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement("""
                INSERT INTO academic_calendars (
                    title,
                    academic_year_id,
                    specialty_id,
                    specialty_profile_id,
                    study_form_id,
                    course_count
                )
                VALUES (?, ?, ?, ?, ?, 1)
                RETURNING id
                """)) {
            statement.setString(1, title);
            statement.setLong(2, academicYearId);
            statement.setLong(3, references.specialtyId());
            statement.setLong(4, references.profileId());
            statement.setLong(5, references.studyFormId());
            return requiredLong(statement);
        }
    }

    private void insertCalendarDay(Connection connection,
                                   long calendarId,
                                   long activityTypeId,
                                   LocalDate date,
                                   int weekNumber,
                                   int dayOfWeek) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement("""
                INSERT INTO academic_calendar_days (
                    calendar_id,
                    course_number,
                    date,
                    week_number,
                    day_of_week,
                    activity_type_id
                )
                VALUES (?, 1, ?, ?, ?, ?)
                """)) {
            statement.setLong(1, calendarId);
            statement.setDate(2, Date.valueOf(date));
            statement.setInt(3, weekNumber);
            statement.setInt(4, dayOfWeek);
            statement.setLong(5, activityTypeId);
            statement.executeUpdate();
        }
    }

    private int findWeekNumber(Connection connection,
                               long calendarId,
                               LocalDate date) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement("""
                SELECT week_number
                FROM academic_calendar_days
                WHERE calendar_id = ?
                  AND date = ?
                """)) {
            statement.setLong(1, calendarId);
            statement.setDate(2, Date.valueOf(date));
            try (ResultSet resultSet = statement.executeQuery()) {
                assertThat(resultSet.next()).isTrue();
                return resultSet.getInt(1);
            }
        }
    }

    private long queryLong(Connection connection, String sql, Object... arguments) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            for (int index = 0; index < arguments.length; index++) {
                statement.setObject(index + 1, arguments[index]);
            }
            return requiredLong(statement);
        }
    }

    private long requiredLong(PreparedStatement statement) throws SQLException {
        try (ResultSet resultSet = statement.executeQuery()) {
            assertThat(resultSet.next()).isTrue();
            return resultSet.getLong(1);
        }
    }

    private record SeedReferences(
            long specialtyId,
            long profileId,
            long studyFormId,
            long activityTypeId
    ) {
    }
}
