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.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 SubjectOwnershipInvariantIntegrationTest {

    private static final String UNIQUE_INDEX = "uq_subjects_name_ci";

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

    @Test
    @DisplayName("V1 запрещает совпадение названия дисциплины без учёта регистра")
    void baselineEnforcesCaseInsensitiveGlobalSubjectName() throws SQLException {
        migrateBaseline();

        try (Connection connection = POSTGRES.createConnection("")) {
            long anotherDepartmentId = queryLong(connection, "SELECT max(id) FROM departments");
            Throwable failure = catchThrowable(() -> insertSubject(
                    connection,
                    "информатика",
                    anotherDepartmentId
            ));

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

    @Test
    @DisplayName("V1 конкурентно принимает только одну дисциплину с одинаковым названием")
    void baselineProtectsConcurrentSubjectCreation() throws Exception {
        migrateBaseline();
        long firstDepartmentId;
        long secondDepartmentId;
        try (Connection connection = POSTGRES.createConnection("")) {
            firstDepartmentId = queryLong(connection, "SELECT min(id) FROM departments");
            secondDepartmentId = queryLong(connection, "SELECT max(id) FROM departments");
        }

        ExecutorService executor = Executors.newFixedThreadPool(2);
        CountDownLatch ready = new CountDownLatch(2);
        CountDownLatch start = new CountDownLatch(1);
        try {
            List<Future<InsertOutcome>> futures = List.of(
                    executor.submit(() -> concurrentInsert("Теория систем", firstDepartmentId, ready, start)),
                    executor.submit(() -> concurrentInsert("теория систем", secondDepartmentId, ready, start))
            );
            assertThat(ready.await(5, TimeUnit.SECONDS)).isTrue();
            start.countDown();

            List<InsertOutcome> outcomes = List.of(
                    futures.get(0).get(10, TimeUnit.SECONDS),
                    futures.get(1).get(10, TimeUnit.SECONDS)
            );
            assertThat(outcomes).filteredOn(InsertOutcome::success).hasSize(1);
            assertThat(outcomes).filteredOn(outcome -> !outcome.success()).singleElement()
                    .satisfies(outcome -> {
                        assertThat(outcome.sqlState()).isEqualTo("23505");
                        assertThat(outcome.message()).contains(UNIQUE_INDEX);
                    });
        } finally {
            start.countDown();
            executor.shutdownNow();
        }
    }

    private void migrateBaseline() {
        Flyway flyway = Flyway.configure()
                .dataSource(POSTGRES.getJdbcUrl(), POSTGRES.getUsername(), POSTGRES.getPassword())
                .locations("classpath:db/migration")
                .target(MigrationVersion.fromVersion("1"))
                .cleanDisabled(false)
                .load();
        flyway.clean();
        flyway.migrate();
    }

    private InsertOutcome concurrentInsert(String name,
                                           long departmentId,
                                           CountDownLatch ready,
                                           CountDownLatch start) {
        try (Connection connection = POSTGRES.createConnection("")) {
            connection.setAutoCommit(false);
            ready.countDown();
            if (!start.await(5, TimeUnit.SECONDS)) {
                return new InsertOutcome(false, null, "Не получен общий сигнал запуска");
            }
            try {
                insertSubject(connection, name, departmentId);
                connection.commit();
                return new InsertOutcome(true, null, null);
            } catch (SQLException exception) {
                connection.rollback();
                return new InsertOutcome(false, exception.getSQLState(), exception.getMessage());
            }
        } catch (Exception exception) {
            return new InsertOutcome(false, null, exception.getMessage());
        }
    }

    private void insertSubject(Connection connection, String name, long departmentId) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement("""
                INSERT INTO subjects (name, department_id)
                VALUES (?, ?)
                """)) {
            statement.setString(1, name);
            statement.setLong(2, departmentId);
            statement.executeUpdate();
        }
    }

    private long queryLong(Connection connection, String sql) throws SQLException {
        try (PreparedStatement statement = connection.prepareStatement(sql);
             var resultSet = statement.executeQuery()) {
            assertThat(resultSet.next()).isTrue();
            return resultSet.getLong(1);
        }
    }

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