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;
import static org.assertj.core.api.Assertions.catchThrowable;

@Testcontainers
@DisplayName("Инварианты точечных изменений расписания базовой схемы")
class ScheduleOverrideMigrationIntegrationTest {

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

    @Test
    @DisplayName("V1 принимает допустимые варианты и отклоняет недопустимые данные")
    void migrationEnforcesScheduleOverrideInvariants() throws SQLException {
        Flyway flyway = baselineFlyway();
        flyway.clean();
        flyway.migrate();

        assertThat(flyway.info().current())
                .as("После полной миграции должна существовать текущая версия")
                .isNotNull();
        assertThat(flyway.info().current().getVersion().getVersion())
                .as("Последней успешно применённой миграцией должна быть V1")
                .isEqualTo("1");

        try (Connection connection = POSTGRES.createConnection("")) {
            SeedIds seedIds = loadSeedIds(connection);
            LocalDate validDate = LocalDate.of(2026, 3, 2);

            assertThat(queryLong(connection, """
                    SELECT count(*)
                    FROM information_schema.columns
                    WHERE table_schema = 'public'
                      AND table_name = 'schedule_overrides'
                      AND column_name = 'target_lesson_date'
                    """))
                    .as("V1 должна создавать целевую дату переноса")
                    .isEqualTo(1L);
            assertThat(queryLong(connection, """
                    SELECT count(*)
                    FROM pg_indexes
                    WHERE schemaname = 'public'
                      AND tablename = 'schedule_overrides'
                      AND indexname = 'idx_schedule_overrides_target_date'
                    """))
                    .as("V1 должна создавать индекс целевой даты")
                    .isEqualTo(1L);

            insertOverride(connection, seedIds, validDate, "CANCEL", null, null, null);
            insertOverride(
                    connection,
                    seedIds,
                    validDate.plusDays(1),
                    null,
                    "MOVE",
                    seedIds.timeSlotId(),
                    seedIds.classroomId(),
                    null,
                    null
            );
            insertOverride(connection, seedIds, validDate.plusDays(2), "REPLACE", null, seedIds.teacherId(), null);
            insertOverride(connection, seedIds, validDate.plusDays(3), "REPLACE", null, null, "Онлайн");
            insertOverride(
                    connection,
                    seedIds,
                    validDate.plusDays(4),
                    validDate.plusDays(9),
                    "MOVE",
                    seedIds.timeSlotId(),
                    null,
                    null,
                    null
            );

            assertThat(queryLong(connection, "SELECT count(*) FROM schedule_overrides"))
                    .as("Все допустимые варианты точечного изменения должны сохраниться")
                    .isEqualTo(5L);

            LocalDate invalidDate = LocalDate.of(2026, 4, 6);
            assertCheckViolation(
                    connection,
                    seedIds,
                    invalidDate,
                    "CANCEL",
                    seedIds.classroomId(),
                    null,
                    null,
                    "chk_schedule_overrides_cancel_payload"
            );
            assertCheckViolation(
                    connection,
                    seedIds,
                    invalidDate.plusDays(1),
                    "MOVE",
                    null,
                    null,
                    null,
                    "chk_schedule_overrides_move_payload"
            );
            assertCheckViolation(
                    connection,
                    seedIds,
                    invalidDate.plusDays(2),
                    "REPLACE",
                    null,
                    null,
                    null,
                    "chk_schedule_overrides_replace_payload"
            );
            assertCheckViolation(
                    connection,
                    seedIds,
                    invalidDate.plusDays(3),
                    "REPLACE",
                    null,
                    seedIds.teacherId(),
                    "Смешанный",
                    "chk_schedule_overrides_format"
            );
            assertTargetDateCheckViolation(
                    connection,
                    seedIds,
                    invalidDate.plusDays(4),
                    invalidDate.plusDays(4),
                    "MOVE",
                    seedIds.timeSlotId()
            );
        }
    }

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

    private SeedIds loadSeedIds(Connection connection) throws SQLException {
        return new SeedIds(
                queryLong(connection, "SELECT id FROM schedule_rule_slots ORDER BY id LIMIT 1"),
                queryLong(connection, "SELECT id FROM classrooms ORDER BY id LIMIT 1"),
                queryLong(connection, "SELECT id FROM users WHERE role = 'TEACHER' ORDER BY id LIMIT 1"),
                queryLong(connection, "SELECT id FROM time_slots ORDER BY id LIMIT 1")
        );
    }

    private void insertOverride(
            Connection connection,
            SeedIds seedIds,
            LocalDate lessonDate,
            String action,
            Long classroomId,
            Long teacherId,
            String lessonFormat
    ) throws SQLException {
        insertOverride(
                connection,
                seedIds,
                lessonDate,
                null,
                action,
                null,
                classroomId,
                teacherId,
                lessonFormat
        );
    }

    private void insertOverride(
            Connection connection,
            SeedIds seedIds,
            LocalDate lessonDate,
            LocalDate targetLessonDate,
            String action,
            Long timeSlotId,
            Long classroomId,
            Long teacherId,
            String lessonFormat
    ) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement("""
                INSERT INTO schedule_overrides (
                    base_rule_slot_id,
                    lesson_date,
                    target_lesson_date,
                    action,
                    new_time_slot_id,
                    new_classroom_id,
                    new_teacher_id,
                    new_lesson_format
                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
                """)) {
            statement.setLong(1, seedIds.baseRuleSlotId());
            statement.setDate(2, Date.valueOf(lessonDate));
            statement.setObject(3, targetLessonDate == null ? null : Date.valueOf(targetLessonDate));
            statement.setString(4, action);
            setNullableLong(statement, 5, timeSlotId);
            setNullableLong(statement, 6, classroomId);
            setNullableLong(statement, 7, teacherId);
            statement.setString(8, lessonFormat);
            statement.executeUpdate();
        }
    }

    private void assertTargetDateCheckViolation(Connection connection,
                                                SeedIds seedIds,
                                                LocalDate lessonDate,
                                                LocalDate targetLessonDate,
                                                String action,
                                                Long timeSlotId) {
        Throwable failure = catchThrowable(() -> insertOverride(
                connection,
                seedIds,
                lessonDate,
                targetLessonDate,
                action,
                timeSlotId,
                action.equals("MOVE") && timeSlotId == null ? seedIds.classroomId() : null,
                action.equals("REPLACE") ? seedIds.teacherId() : null,
                null
        ));

        assertThat(failure).isInstanceOf(SQLException.class);
        SQLException sqlFailure = (SQLException) failure;
        assertThat(sqlFailure.getSQLState()).isEqualTo("23514");
        assertThat(sqlFailure.getMessage()).contains("chk_schedule_overrides_target_date");
    }

    private void assertCheckViolation(
            Connection connection,
            SeedIds seedIds,
            LocalDate lessonDate,
            String action,
            Long classroomId,
            Long teacherId,
            String lessonFormat,
            String expectedConstraint
    ) {
        Throwable failure = catchThrowable(() -> insertOverride(
                connection,
                seedIds,
                lessonDate,
                action,
                classroomId,
                teacherId,
                lessonFormat
        ));

        assertThat(failure)
                .as("Недопустимые данные должны быть отклонены ограничением %s", expectedConstraint)
                .isInstanceOf(SQLException.class);
        SQLException sqlFailure = (SQLException) failure;
        assertThat(sqlFailure.getSQLState())
                .as("Ограничение %s должно вернуть SQLSTATE нарушения CHECK", expectedConstraint)
                .isEqualTo("23514");
        assertThat(sqlFailure.getMessage())
                .as("Ошибка должна содержать имя сработавшего ограничения")
                .contains(expectedConstraint);
    }

    private void setNullableLong(PreparedStatement statement, int index, Long value) throws SQLException {
        if (value == null) {
            statement.setObject(index, null);
        } else {
            statement.setLong(index, value);
        }
    }

    private long queryLong(Connection connection, String sql, Object... parameters) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            for (int index = 0; index < parameters.length; index++) {
                statement.setObject(index + 1, parameters[index]);
            }
            try (ResultSet resultSet = statement.executeQuery()) {
                assertThat(resultSet.next())
                        .as("SQL-запрос должен вернуть одну строку: %s", sql)
                        .isTrue();
                return resultSet.getLong(1);
            }
        }
    }

    private record SeedIds(long baseRuleSlotId, long classroomId, long teacherId, long timeSlotId) {
    }
}
