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<>("postgres:16.3-alpine");

    @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);
    }

    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 {
    }
}
