package com.magistr.app.service;

import com.magistr.app.model.ScheduleOverride;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.transaction.support.TransactionTemplate;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

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

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

@Testcontainers
@SpringBootTest(
        classes = PostgreSqlAdvisoryLockIntegrationTest.TestApplication.class,
        properties = {
                "spring.flyway.enabled=false",
                "spring.jpa.hibernate.ddl-auto=none",
                "management.endpoint.health.validate-group-membership=false"
        }
)
class PostgreSqlAdvisoryLockIntegrationTest {

    private static final LocalDate TEST_DATE = LocalDate.of(2026, 9, 1);

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

    @DynamicPropertySource
    static void databaseProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
        registry.add("spring.datasource.username", POSTGRES::getUsername);
        registry.add("spring.datasource.password", POSTGRES::getPassword);
    }

    @Autowired
    private PostgreSqlAdvisoryLock advisoryLock;

    @Autowired
    private TransactionTemplate transactionTemplate;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    private ExecutorService executor;

    @AfterEach
    void stopExecutor() throws InterruptedException {
        if (executor == null) {
            return;
        }
        executor.shutdownNow();
        assertThat(executor.awaitTermination(5, TimeUnit.SECONDS)).isTrue();
    }

    @Test
    void serializesTwoSpringTransactionsForSameDate() throws Exception {
        executor = Executors.newFixedThreadPool(2);
        CountDownLatch firstHasLock = new CountDownLatch(1);
        CountDownLatch releaseFirst = new CountDownLatch(1);
        CountDownLatch secondReachedLock = new CountDownLatch(1);
        AtomicInteger secondBackendPid = new AtomicInteger();

        Future<?> first = executor.submit(() -> transactionTemplate.executeWithoutResult(status -> {
            advisoryLock.lockScheduleOverrideDate(TEST_DATE);
            firstHasLock.countDown();
            await(releaseFirst, "Ожидание освобождения первой advisory-блокировки прервано");
        }));

        assertThat(firstHasLock.await(5, TimeUnit.SECONDS)).isTrue();

        Future<?> second = executor.submit(() -> transactionTemplate.executeWithoutResult(status -> {
            secondBackendPid.set(jdbcTemplate.queryForObject("select pg_backend_pid()", Integer.class));
            secondReachedLock.countDown();
            advisoryLock.lockScheduleOverrideDate(TEST_DATE);
        }));

        assertThat(secondReachedLock.await(5, TimeUnit.SECONDS)).isTrue();
        awaitAdvisoryLockWait(secondBackendPid.get());
        assertThat(second).as("вторая транзакция должна ждать ту же дату").isNotDone();

        releaseFirst.countDown();
        first.get(5, TimeUnit.SECONDS);
        second.get(5, TimeUnit.SECONDS);
    }

    @Test
    void serializesConcurrentMovesToSameResourcesAndTargetDate() throws Exception {
        jdbcTemplate.execute("""
                CREATE TABLE IF NOT EXISTS schedule_move_concurrency_probe (
                    source_date DATE NOT NULL,
                    target_date DATE NOT NULL,
                    teacher_id BIGINT NOT NULL,
                    classroom_id BIGINT NOT NULL
                )
                """);
        jdbcTemplate.update("DELETE FROM schedule_move_concurrency_probe");

        executor = Executors.newFixedThreadPool(2);
        CountDownLatch ready = new CountDownLatch(2);
        CountDownLatch start = new CountDownLatch(1);
        Future<Boolean> first = executor.submit(() -> tryMove(
                TEST_DATE.minusDays(7), TEST_DATE, 11L, 21L, ready, start
        ));
        Future<Boolean> second = executor.submit(() -> tryMove(
                TEST_DATE.minusDays(14), TEST_DATE, 11L, 21L, ready, start
        ));

        assertThat(ready.await(5, TimeUnit.SECONDS)).isTrue();
        start.countDown();

        assertThat(java.util.List.of(
                first.get(5, TimeUnit.SECONDS),
                second.get(5, TimeUnit.SECONDS)
        )).containsExactlyInAnyOrder(true, false);
        assertThat(jdbcTemplate.queryForObject(
                "SELECT count(*) FROM schedule_move_concurrency_probe",
                Long.class
        )).isEqualTo(1L);
    }

    private boolean tryMove(LocalDate sourceDate,
                            LocalDate targetDate,
                            long teacherId,
                            long classroomId,
                            CountDownLatch ready,
                            CountDownLatch start) {
        ready.countDown();
        await(start, "Конкурентные переносы не получили общий сигнал запуска");
        Boolean saved = transactionTemplate.execute(status -> {
            advisoryLock.lockScheduleOverrideDate(targetDate);
            Long conflicts = jdbcTemplate.queryForObject("""
                    SELECT count(*)
                    FROM schedule_move_concurrency_probe
                    WHERE target_date = ?
                      AND (teacher_id = ? OR classroom_id = ?)
                    """, Long.class, targetDate, teacherId, classroomId);
            if (conflicts != null && conflicts > 0) {
                return false;
            }
            jdbcTemplate.update("""
                    INSERT INTO schedule_move_concurrency_probe (
                        source_date, target_date, teacher_id, classroom_id
                    ) VALUES (?, ?, ?, ?)
                    """, sourceDate, targetDate, teacherId, classroomId);
            return true;
        });
        return Boolean.TRUE.equals(saved);
    }

    private void awaitAdvisoryLockWait(int backendPid) {
        long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
        do {
            Boolean waiting = jdbcTemplate.queryForObject("""
                    select wait_event_type = 'Lock' and wait_event = 'advisory'
                    from pg_stat_activity
                    where pid = ?
                    """, Boolean.class, backendPid);
            if (Boolean.TRUE.equals(waiting)) {
                return;
            }
            try {
                Thread.sleep(25);
            } catch (InterruptedException exception) {
                Thread.currentThread().interrupt();
                throw new IllegalStateException("Ожидание PostgreSQL advisory-блокировки прервано", exception);
            }
        } while (System.nanoTime() < deadline);

        throw new IllegalStateException("Вторая транзакция не перешла в ожидание advisory-блокировки");
    }

    private void await(CountDownLatch latch, String message) {
        try {
            if (!latch.await(5, TimeUnit.SECONDS)) {
                throw new IllegalStateException(message);
            }
        } catch (InterruptedException exception) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException(message, exception);
        }
    }

    @SpringBootConfiguration
    @EnableAutoConfiguration
    @EntityScan(basePackageClasses = ScheduleOverride.class)
    @Import(PostgreSqlAdvisoryLock.class)
    static class TestApplication {
    }
}
