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.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

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

@Testcontainers
@DisplayName("Инварианты зависимостей календарных графиков базовой схемы")
class AcademicStructureInvariantIntegrationTest {

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

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

        assertThat(flyway.info().current()).isNotNull();
        assertThat(flyway.info().current().getVersion().getVersion()).isEqualTo("1");

        try (Connection connection = POSTGRES.createConnection("")) {
            long groupId = queryLong(
                    connection,
                    "SELECT id FROM student_groups WHERE name = 'ИВТ-21-1'"
            );
            long assignmentId = queryLong(
                    connection,
                    "SELECT id FROM student_group_calendar_assignments WHERE group_id = ?",
                    groupId
            );
            long calendarId = queryLong(
                    connection,
                    "SELECT calendar_id FROM student_group_calendar_assignments WHERE id = ?",
                    assignmentId
            );
            long otherFormId = queryLong(
                    connection,
                    "SELECT id FROM education_forms WHERE name = 'Специалитет'"
            );

            assertThat(incompatibleAssignmentCount(connection)).isZero();
            assertSqlState(
                    () -> updateLong(
                            connection,
                            "UPDATE academic_calendars SET study_form_id = ? WHERE id = ?",
                            otherFormId,
                            calendarId
                    ),
                    "23514",
                    "сохранённые назначения групп станут несовместимыми"
            );
            assertSqlState(
                    () -> updateLong(
                            connection,
                            "UPDATE student_groups SET education_form_id = ? WHERE id = ?",
                            otherFormId,
                            groupId
                    ),
                    "23514",
                    "сохранённые назначения календарных графиков станут несовместимыми"
            );
            assertSqlState(
                    () -> updateInt(
                            connection,
                            "UPDATE academic_calendars SET course_count = ? WHERE id = ?",
                            3,
                            calendarId
                    ),
                    "23514",
                    "сохранённая сетка выходит за новые измерения"
            );
            assertSqlState(
                    () -> insertOutOfRangeCalendarDay(connection, calendarId),
                    "23514",
                    "Строка сетки выходит за измерения календарного графика"
            );
            assertSqlState(
                    () -> insertOutOfRangeCalendarSubject(connection, calendarId),
                    "23514",
                    "Номер семестра дисциплины выходит за измерения календарного графика"
            );

            assertThat(queryLong(
                    connection,
                    "SELECT count(*) FROM student_group_calendar_assignments WHERE id = ?",
                    assignmentId
            )).isOne();
            assertThat(queryLong(
                    connection,
                    "SELECT course_count FROM academic_calendars WHERE id = ?",
                    calendarId
            )).isEqualTo(4);
            assertThat(incompatibleAssignmentCount(connection)).isZero();
        }
    }

    @Test
    @DisplayName("V1 не допускает гонку между назначением графика и изменением параметров группы")
    void baselineProtectsConcurrentGroupAndAssignmentWrites() throws Exception {
        Flyway flyway = baselineFlyway();
        flyway.clean();
        flyway.migrate();

        long groupId;
        long calendarId;
        long academicYearId;
        long otherFormId;
        try (Connection connection = POSTGRES.createConnection("")) {
            groupId = insertCompatibleGroup(connection);
            calendarId = queryLong(connection, """
                    SELECT calendar.id
                    FROM academic_calendars calendar
                    JOIN academic_years academic_year ON academic_year.id = calendar.academic_year_id
                    JOIN specialties specialty ON specialty.id = calendar.specialty_id
                    JOIN education_forms study_form ON study_form.id = calendar.study_form_id
                    WHERE academic_year.title = '2025-2026'
                      AND specialty.specialty_code = '09.03.01'
                      AND study_form.name = 'Бакалавриат'
                    ORDER BY calendar.id
                    LIMIT 1
                    """);
            academicYearId = queryLong(
                    connection,
                    "SELECT id FROM academic_years WHERE title = '2025-2026'"
            );
            otherFormId = queryLong(
                    connection,
                    "SELECT id FROM education_forms WHERE name = 'Специалитет'"
            );
        }

        ExecutorService executor = Executors.newFixedThreadPool(2);
        CountDownLatch ready = new CountDownLatch(2);
        CountDownLatch start = new CountDownLatch(1);
        try {
            long finalGroupId = groupId;
            long finalCalendarId = calendarId;
            long finalAcademicYearId = academicYearId;
            long finalOtherFormId = otherFormId;
            List<Future<WriteOutcome>> futures = List.of(
                    executor.submit(() -> concurrentGroupUpdate(
                            finalGroupId,
                            finalOtherFormId,
                            ready,
                            start
                    )),
                    executor.submit(() -> concurrentAssignmentInsert(
                            finalGroupId,
                            finalAcademicYearId,
                            finalCalendarId,
                            ready,
                            start
                    ))
            );

            assertThat(ready.await(5, TimeUnit.SECONDS)).isTrue();
            start.countDown();
            List<WriteOutcome> outcomes = List.of(
                    futures.get(0).get(10, TimeUnit.SECONDS),
                    futures.get(1).get(10, TimeUnit.SECONDS)
            );

            assertThat(outcomes).filteredOn(WriteOutcome::success).hasSize(1);
            assertThat(outcomes).filteredOn(outcome -> !outcome.success()).singleElement()
                    .satisfies(outcome -> {
                        assertThat(outcome.sqlState()).isEqualTo("23514");
                        assertThat(outcome.message()).containsAnyOf(
                                "Календарный график не соответствует",
                                "сохранённые назначения календарных графиков станут несовместимыми"
                        );
                    });

            try (Connection connection = POSTGRES.createConnection("")) {
                assertThat(incompatibleAssignmentCount(connection)).isZero();
                assertThat(queryLong(
                        connection,
                        "SELECT count(*) FROM student_group_calendar_assignments WHERE group_id = ?",
                        groupId
                )).isLessThanOrEqualTo(1);
            }
        } finally {
            start.countDown();
            executor.shutdownNow();
        }
    }

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

        try (Connection connection = POSTGRES.createConnection("")) {
            long groupId = queryLong(
                    connection,
                    "SELECT id FROM student_groups WHERE name = 'ИВТ-21-1'"
            );

            assertSqlState(
                    () -> insertCapacityGroup(connection, "НУЛЕВАЯ-ГРУППА", 0, 2025),
                    "23514",
                    "chk_student_groups_group_size_positive"
            );
            assertSqlState(
                    () -> insertCapacityGroup(connection, "НУЛЕВОЙ-ГОД", 10, 0),
                    "23514",
                    "chk_student_groups_year_start_positive"
            );
            assertSqlState(
                    () -> insertSubgroup(connection, groupId, "Нулевая подгруппа", 0),
                    "23514",
                    "chk_subgroups_student_capacity_positive"
            );
            assertSqlState(
                    () -> updateLong(
                            connection,
                            "UPDATE student_groups SET group_size = ? WHERE id = ?",
                            24,
                            groupId
                    ),
                    "23514",
                    "Сумма численностей активных подгрупп"
            );

            assertThat(queryLong(
                    connection,
                    "SELECT group_size FROM student_groups WHERE id = ?",
                    groupId
            )).isEqualTo(25);
        }
    }

    @Test
    @DisplayName("V1 сериализует конкурентное уменьшение группы и добавление подгруппы")
    void baselineProtectsConcurrentGroupAndSubgroupCapacityWrites() throws Exception {
        Flyway flyway = baselineFlyway();
        flyway.clean();
        flyway.migrate();

        long groupId;
        try (Connection connection = POSTGRES.createConnection("")) {
            groupId = insertCapacityGroup(connection, "ГРУППА-ГОНКА-ВМЕСТИМОСТИ", 30, 2025);
            insertSubgroup(connection, groupId, "Исходная подгруппа", 20);
        }

        ExecutorService executor = Executors.newFixedThreadPool(2);
        CountDownLatch ready = new CountDownLatch(2);
        CountDownLatch start = new CountDownLatch(1);
        try {
            long finalGroupId = groupId;
            List<Future<WriteOutcome>> futures = List.of(
                    executor.submit(() -> concurrentWrite(
                            "UPDATE student_groups SET group_size = ? WHERE id = ?",
                            List.of(20L, finalGroupId),
                            ready,
                            start
                    )),
                    executor.submit(() -> concurrentWrite(
                            "INSERT INTO subgroups (group_id, name, student_capacity) VALUES (?, 'Новая подгруппа', 10)",
                            List.of(finalGroupId),
                            ready,
                            start
                    ))
            );

            assertThat(ready.await(5, TimeUnit.SECONDS)).isTrue();
            start.countDown();
            List<WriteOutcome> outcomes = List.of(
                    futures.get(0).get(10, TimeUnit.SECONDS),
                    futures.get(1).get(10, TimeUnit.SECONDS)
            );

            assertThat(outcomes).filteredOn(WriteOutcome::success).hasSize(1);
            assertThat(outcomes).filteredOn(outcome -> !outcome.success()).singleElement()
                    .satisfies(outcome -> {
                        assertThat(outcome.sqlState()).isEqualTo("23514");
                        assertThat(outcome.message()).contains("Сумма численностей активных подгрупп");
                    });

            try (Connection connection = POSTGRES.createConnection("")) {
                long groupSize = queryLong(
                        connection,
                        "SELECT group_size FROM student_groups WHERE id = ?",
                        groupId
                );
                long subgroupCapacity = queryLong(
                        connection,
                        """
                        SELECT COALESCE(SUM(student_capacity), 0)
                        FROM subgroups
                        WHERE group_id = ? AND status <> 'ARCHIVED'
                        """,
                        groupId
                );
                assertThat(subgroupCapacity).isLessThanOrEqualTo(groupSize);
            }
        } finally {
            start.countDown();
            executor.shutdownNow();
        }
    }

    private WriteOutcome concurrentGroupUpdate(long groupId,
                                                long educationFormId,
                                                CountDownLatch ready,
                                                CountDownLatch start) {
        return concurrentWrite(
                "UPDATE student_groups SET education_form_id = ? WHERE id = ?",
                List.of(educationFormId, groupId),
                ready,
                start
        );
    }

    private WriteOutcome concurrentAssignmentInsert(long groupId,
                                                     long academicYearId,
                                                     long calendarId,
                                                     CountDownLatch ready,
                                                     CountDownLatch start) {
        return concurrentWrite(
                """
                INSERT INTO student_group_calendar_assignments (group_id, academic_year_id, calendar_id)
                VALUES (?, ?, ?)
                """,
                List.of(groupId, academicYearId, calendarId),
                ready,
                start
        );
    }

    private WriteOutcome concurrentWrite(String sql,
                                         List<Long> parameters,
                                         CountDownLatch ready,
                                         CountDownLatch start) {
        try (Connection connection = POSTGRES.createConnection("")) {
            connection.setAutoCommit(false);
            ready.countDown();
            if (!start.await(5, TimeUnit.SECONDS)) {
                return new WriteOutcome(false, null, "Не получен общий сигнал запуска");
            }
            try (PreparedStatement statement = connection.prepareStatement(sql)) {
                for (int index = 0; index < parameters.size(); index++) {
                    statement.setLong(index + 1, parameters.get(index));
                }
                statement.executeUpdate();
                connection.commit();
                return new WriteOutcome(true, null, null);
            } catch (SQLException exception) {
                connection.rollback();
                return new WriteOutcome(false, exception.getSQLState(), exception.getMessage());
            }
        } catch (Exception exception) {
            return new WriteOutcome(false, null, exception.getMessage());
        }
    }

    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 long insertCompatibleGroup(Connection connection) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement("""
                INSERT INTO student_groups (
                    name,
                    group_size,
                    education_form_id,
                    department_id,
                    specialty_id,
                    specialty_profile_id,
                    year_start_study
                )
                SELECT
                    'КОНКУРЕНТНАЯ-ГРУППА',
                    20,
                    study_form.id,
                    department.id,
                    specialty.id,
                    profile.id,
                    2025
                FROM education_forms study_form
                CROSS JOIN departments department
                CROSS JOIN specialties specialty
                JOIN specialty_profiles profile ON profile.specialty_id = specialty.id
                WHERE study_form.name = 'Бакалавриат'
                  AND department.code = 1
                  AND specialty.specialty_code = '09.03.01'
                  AND profile.name = 'Без профиля'
                RETURNING id
                """)) {
            try (ResultSet resultSet = statement.executeQuery()) {
                assertThat(resultSet.next()).isTrue();
                return resultSet.getLong(1);
            }
        }
    }

    private long insertCapacityGroup(Connection connection,
                                     String name,
                                     long groupSize,
                                     int yearStartStudy) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement("""
                INSERT INTO student_groups (
                    name,
                    group_size,
                    education_form_id,
                    department_id,
                    specialty_id,
                    specialty_profile_id,
                    year_start_study
                )
                SELECT
                    ?,
                    ?,
                    study_form.id,
                    department.id,
                    specialty.id,
                    profile.id,
                    ?
                FROM education_forms study_form
                CROSS JOIN departments department
                CROSS JOIN specialties specialty
                JOIN specialty_profiles profile ON profile.specialty_id = specialty.id
                WHERE study_form.name = 'Бакалавриат'
                  AND department.code = 1
                  AND specialty.specialty_code = '09.03.01'
                  AND profile.name = 'Без профиля'
                RETURNING id
                """)) {
            statement.setString(1, name);
            statement.setLong(2, groupSize);
            statement.setInt(3, yearStartStudy);
            try (ResultSet resultSet = statement.executeQuery()) {
                assertThat(resultSet.next()).isTrue();
                return resultSet.getLong(1);
            }
        }
    }

    private void insertSubgroup(Connection connection,
                                long groupId,
                                String name,
                                int studentCapacity) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement("""
                INSERT INTO subgroups (group_id, name, student_capacity)
                VALUES (?, ?, ?)
                """)) {
            statement.setLong(1, groupId);
            statement.setString(2, name);
            statement.setInt(3, studentCapacity);
            statement.executeUpdate();
        }
    }

    private void insertOutOfRangeCalendarDay(Connection connection, long calendarId) 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 (?, 5, DATE '2025-09-01', 1, 1, (
                    SELECT id FROM academic_calendar_activity_types WHERE code = 'Т'
                ))
                """)) {
            statement.setLong(1, calendarId);
            statement.executeUpdate();
        }
    }

    private void insertOutOfRangeCalendarSubject(Connection connection, long calendarId) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement("""
                INSERT INTO academic_calendar_subjects (calendar_id, semester_number, subject_id)
                VALUES (?, 9, (SELECT min(id) FROM subjects))
                """)) {
            statement.setLong(1, calendarId);
            statement.executeUpdate();
        }
    }

    private long incompatibleAssignmentCount(Connection connection) throws SQLException {
        return queryLong(connection, """
                SELECT count(*)
                FROM student_group_calendar_assignments assignment
                JOIN student_groups student_group ON student_group.id = assignment.group_id
                JOIN academic_calendars calendar ON calendar.id = assignment.calendar_id
                WHERE assignment.academic_year_id <> calendar.academic_year_id
                   OR student_group.specialty_id <> calendar.specialty_id
                   OR student_group.specialty_profile_id <> calendar.specialty_profile_id
                   OR student_group.education_form_id <> calendar.study_form_id
                """);
    }

    private void updateLong(Connection connection, String sql, long value, long id) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            statement.setLong(1, value);
            statement.setLong(2, id);
            statement.executeUpdate();
        }
    }

    private void updateInt(Connection connection, String sql, int value, long id) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement(sql)) {
            statement.setInt(1, value);
            statement.setLong(2, id);
            statement.executeUpdate();
        }
    }

    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 void assertSqlState(ThrowingSqlRunnable operation,
                                String expectedSqlState,
                                String expectedMessagePart) {
        Throwable failure = catchThrowable(operation::run);
        assertThat(failure).isInstanceOf(SQLException.class);
        SQLException sqlFailure = (SQLException) failure;
        assertThat(sqlFailure.getSQLState()).isEqualTo(expectedSqlState);
        assertThat(sqlFailure.getMessage()).contains(expectedMessagePart);
    }

    @FunctionalInterface
    private interface ThrowingSqlRunnable {
        void run() throws SQLException;
    }

    private record WriteOutcome(boolean success, String sqlState, String message) {
    }
}
