package com.magistr.app.config.auth;

import com.magistr.app.model.AuthRefreshToken;
import com.magistr.app.model.Role;
import com.magistr.app.model.User;
import com.magistr.app.repository.AuthRefreshTokenRepository;
import com.magistr.app.repository.UserRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
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.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
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;

@Testcontainers
@SpringBootTest(
        classes = RefreshTokenServiceConcurrencyIntegrationTest.TestApplication.class,
        properties = {
                "app.jwt.refresh-ttl=7d",
                "management.endpoint.health.validate-group-membership=false"
        }
)
class RefreshTokenServiceConcurrencyIntegrationTest {

    private static final String TENANT = "test-tenant";

    @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 RefreshTokenService service;

    @Autowired
    private AuthRefreshTokenRepository tokenRepository;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private TransactionTemplate transactionTemplate;

    private ExecutorService executor;

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

    @Test
    void onlyOneConcurrentRotationCreatesNextSession() throws Exception {
        assertThat(AopUtils.isAopProxy(service)).isTrue();

        User user = createTestUser();
        String rawToken = service.createSession(user, TENANT, "Исходный браузер", "127.0.0.1");

        executor = Executors.newFixedThreadPool(2);
        CountDownLatch ready = new CountDownLatch(2);
        CountDownLatch start = new CountDownLatch(1);
        List<Future<Optional<RefreshTokenRotation>>> futures = new ArrayList<>();

        transactionTemplate.executeWithoutResult(status -> {
            Long lockedTokenId = jdbcTemplate.queryForObject(
                    "SELECT id FROM auth_refresh_tokens WHERE token_hash = ? FOR UPDATE",
                    Long.class,
                    service.hashToken(rawToken)
            );
            assertThat(lockedTokenId).isNotNull();
            jdbcTemplate.execute("LOCK TABLE auth_refresh_tokens IN ACCESS EXCLUSIVE MODE");

            futures.add(executor.submit(
                    () -> rotateAfterSignal(rawToken, ready, start, "Первый браузер")
            ));
            futures.add(executor.submit(
                    () -> rotateAfterSignal(rawToken, ready, start, "Второй браузер")
            ));

            awaitReady(ready);
            start.countDown();
            awaitBlockedRotations();
        });

        List<Optional<RefreshTokenRotation>> results = List.of(
                futures.get(0).get(10, TimeUnit.SECONDS),
                futures.get(1).get(10, TimeUnit.SECONDS)
        );

        assertThat(results).filteredOn(Optional::isPresent).hasSize(1);
        assertThat(results).filteredOn(Optional::isEmpty).hasSize(1);

        List<AuthRefreshToken> storedTokens = tokenRepository.findAll();
        assertThat(storedTokens).hasSize(2);
        assertThat(storedTokens).filteredOn(token -> token.getRevokedAt() == null).hasSize(1);
        assertThat(storedTokens).filteredOn(token -> token.getRotatedToTokenHash() != null).hasSize(1);
    }

    @Test
    void concurrentCleanupKeepsActiveAndFreshAuditRows() throws Exception {
        User user = createTestUser();
        Instant now = Instant.now();
        Instant cutoff = now.minus(30, ChronoUnit.DAYS);
        AuthRefreshToken oldExpired = token(user, "old-expired", now.minus(31, ChronoUnit.DAYS), null);
        AuthRefreshToken oldRevoked = token(user, "old-revoked", now.plus(7, ChronoUnit.DAYS), now.minus(31, ChronoUnit.DAYS));
        AuthRefreshToken freshExpired = token(user, "fresh-expired", now.minus(1, ChronoUnit.DAYS), null);
        AuthRefreshToken freshRevoked = token(user, "fresh-revoked", now.plus(7, ChronoUnit.DAYS), now.minus(1, ChronoUnit.DAYS));
        AuthRefreshToken active = token(user, "active", now.plus(7, ChronoUnit.DAYS), null);
        tokenRepository.saveAllAndFlush(List.of(
                oldExpired,
                oldRevoked,
                freshExpired,
                freshRevoked,
                active
        ));
        List<String> relevantHashes = List.of(
                oldExpired.getTokenHash(),
                oldRevoked.getTokenHash(),
                freshExpired.getTokenHash(),
                freshRevoked.getTokenHash(),
                active.getTokenHash()
        );

        executor = Executors.newFixedThreadPool(2);
        CountDownLatch ready = new CountDownLatch(2);
        CountDownLatch start = new CountDownLatch(1);
        List<Future<Integer>> futures = List.of(
                executor.submit(() -> cleanupAfterSignal(cutoff, ready, start)),
                executor.submit(() -> cleanupAfterSignal(cutoff, ready, start))
        );
        awaitReady(ready);
        start.countDown();

        int deleted = futures.get(0).get(10, TimeUnit.SECONDS)
                + futures.get(1).get(10, TimeUnit.SECONDS);

        assertThat(deleted).isEqualTo(2);
        List<AuthRefreshToken> remaining = tokenRepository.findAll().stream()
                .filter(token -> relevantHashes.contains(token.getTokenHash()))
                .toList();
        assertThat(remaining)
                .extracting(AuthRefreshToken::getTokenHash)
                .containsExactlyInAnyOrder(
                        freshExpired.getTokenHash(),
                        freshRevoked.getTokenHash(),
                        active.getTokenHash()
                );
        assertThat(tokenRepository.deleteCleanupBatch(cutoff, 100)).isZero();
    }

    private int cleanupAfterSignal(Instant cutoff,
                                   CountDownLatch ready,
                                   CountDownLatch start) throws InterruptedException {
        ready.countDown();
        if (!start.await(5, TimeUnit.SECONDS)) {
            throw new IllegalStateException("Не удалось синхронно запустить очистку refresh-сессий");
        }
        return tokenRepository.deleteCleanupBatch(cutoff, 100);
    }

    private AuthRefreshToken token(User user,
                                   String marker,
                                   Instant expiresAt,
                                   Instant revokedAt) {
        AuthRefreshToken token = new AuthRefreshToken();
        token.setUser(user);
        token.setTenant(TENANT);
        token.setTokenHash(service.hashToken(marker + "-" + UUID.randomUUID()));
        token.setIssuedAt(Instant.now().minus(40, ChronoUnit.DAYS));
        token.setExpiresAt(expiresAt);
        token.setRevokedAt(revokedAt);
        return token;
    }

    private Optional<RefreshTokenRotation> rotateAfterSignal(
            String rawToken,
            CountDownLatch ready,
            CountDownLatch start,
            String userAgent
    ) throws InterruptedException {
        ready.countDown();
        if (!start.await(5, TimeUnit.SECONDS)) {
            throw new IllegalStateException("Не удалось синхронно запустить конкурентную ротацию");
        }
        return service.rotate(rawToken, TENANT, userAgent, "127.0.0.1");
    }

    private void awaitReady(CountDownLatch ready) {
        try {
            if (!ready.await(5, TimeUnit.SECONDS)) {
                throw new IllegalStateException("Конкурентные потоки не успели подготовиться к ротации");
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException("Ожидание конкурентных потоков прервано", e);
        }
    }

    private void awaitBlockedRotations() {
        long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
        do {
            Integer blockedQueries = jdbcTemplate.queryForObject("""
                    SELECT COUNT(*)
                    FROM pg_stat_activity
                    WHERE datname = current_database()
                      AND pid <> pg_backend_pid()
                      AND wait_event_type = 'Lock'
                    """, Integer.class);
            if (blockedQueries != null && blockedQueries >= 2) {
                return;
            }
            try {
                Thread.sleep(25);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new IllegalStateException("Ожидание блокировки ротации прервано", e);
            }
        } while (System.nanoTime() < deadline);

        throw new IllegalStateException("Оба запроса ротации не заблокировались на исходном токене");
    }

    private User createTestUser() {
        Long departmentId = jdbcTemplate.queryForObject(
                "INSERT INTO departments (name, code) VALUES (?, ?) RETURNING id",
                Long.class,
                "Тестовая кафедра ротации " + UUID.randomUUID(),
                System.nanoTime()
        );

        User user = new User();
        user.setUsername("rt-" + UUID.randomUUID());
        user.setPassword("тестовый-хэш");
        user.setRole(Role.ADMIN);
        user.setFullName("Тест конкурентной ротации");
        user.setJobTitle("Тестовый пользователь");
        user.setDepartmentId(departmentId);
        return userRepository.saveAndFlush(user);
    }

    @SpringBootConfiguration
    @EnableAutoConfiguration
    @EntityScan(basePackageClasses = AuthRefreshToken.class)
    @EnableJpaRepositories(basePackageClasses = AuthRefreshTokenRepository.class)
    @EnableConfigurationProperties(JwtProperties.class)
    @Import(RefreshTokenService.class)
    static class TestApplication {
    }
}
