package com.magistr.app.config.auth;

import com.magistr.app.model.AuthLoginRateLimit;
import com.magistr.app.repository.AuthLoginAttemptAuditRepository;
import com.magistr.app.repository.AuthLoginRateLimitRepository;
import com.magistr.app.repository.UserRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
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.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
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.sql.Timestamp;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.List;
import java.util.UUID;
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 = LoginRateLimitConcurrencyIntegrationTest.TestApplication.class,
        properties = "management.endpoint.health.validate-group-membership=false"
)
class LoginRateLimitConcurrencyIntegrationTest {

    private static final Instant NOW = Instant.parse("2026-07-19T09:00:00Z");

    @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 AuthLoginRateLimitRepository rateLimitRepository;

    @Autowired
    private AuthLoginAttemptAuditRepository auditRepository;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TransactionTemplate transactionTemplate;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    private ExecutorService executor;

    @BeforeEach
    void clearLoginSecurityTables() {
        auditRepository.deleteAll();
        rateLimitRepository.deleteAll();
    }

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

    @Test
    void twoServiceInstancesShareAtomicPostgreSqlCounter() throws Exception {
        LoginSecurityProperties properties = new LoginSecurityProperties();
        properties.setMaxFailures(2);
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(4);
        LoginRateLimitService firstPod = service(properties, encoder);
        LoginRateLimitService secondPod = service(properties, encoder);
        String username = "missing-" + UUID.randomUUID();
        String clientIp = "198.51.100.42";

        executor = Executors.newFixedThreadPool(2);
        CountDownLatch ready = new CountDownLatch(2);
        CountDownLatch start = new CountDownLatch(1);
        List<Future<LoginRateLimitService.AuthenticationDecision>> futures = List.of(
                executor.submit(() -> authenticateAfterSignal(firstPod, username, clientIp, ready, start)),
                executor.submit(() -> authenticateAfterSignal(secondPod, username, clientIp, ready, start))
        );
        assertThat(ready.await(5, TimeUnit.SECONDS)).isTrue();
        start.countDown();

        List<LoginRateLimitService.AuthenticationDecision.Status> statuses = List.of(
                futures.get(0).get(10, TimeUnit.SECONDS).status(),
                futures.get(1).get(10, TimeUnit.SECONDS).status()
        );
        assertThat(statuses).containsExactlyInAnyOrder(
                LoginRateLimitService.AuthenticationDecision.Status.REJECTED,
                LoginRateLimitService.AuthenticationDecision.Status.BLOCKED
        );

        Integer failureCount = jdbcTemplate.queryForObject("""
                SELECT failure_count
                FROM auth_login_rate_limits
                WHERE tenant = ? AND username_normalized = ? AND client_ip = ?
                """, Integer.class, "tenant", username, clientIp);
        assertThat(failureCount).isEqualTo(2);
        assertThat(auditRepository.count()).isEqualTo(2);
    }

    @Test
    void cleanupDeletesOnlyExpiredAuditAndInactiveCounters() {
        Instant now = NOW;
        Instant cutoff = now.minus(90, java.time.temporal.ChronoUnit.DAYS);
        jdbcTemplate.update("""
                INSERT INTO auth_login_attempt_audit
                    (tenant, username_normalized, client_ip, outcome, occurred_at)
                VALUES
                    ('tenant', 'old', '198.51.100.1', 'FAILURE', ?),
                    ('tenant', 'fresh', '198.51.100.2', 'FAILURE', ?)
                """, timestamp(now.minus(91, java.time.temporal.ChronoUnit.DAYS)),
                timestamp(now.minus(1, java.time.temporal.ChronoUnit.DAYS)));
        jdbcTemplate.update("""
                INSERT INTO auth_login_rate_limits
                    (tenant, username_normalized, client_ip, failure_count,
                     window_started_at, blocked_until, updated_at)
                VALUES
                    ('tenant', 'old', '198.51.100.1', 1, ?, NULL, ?),
                    ('tenant', 'blocked', '198.51.100.2', 5, ?, ?, ?),
                    ('tenant', 'fresh', '198.51.100.3', 1, ?, NULL, ?)
                """,
                timestamp(now.minus(91, java.time.temporal.ChronoUnit.DAYS)),
                timestamp(now.minus(91, java.time.temporal.ChronoUnit.DAYS)),
                timestamp(now.minus(91, java.time.temporal.ChronoUnit.DAYS)),
                timestamp(now.plusSeconds(300)),
                timestamp(now.minus(91, java.time.temporal.ChronoUnit.DAYS)),
                timestamp(now.minus(1, java.time.temporal.ChronoUnit.DAYS)),
                timestamp(now.minus(1, java.time.temporal.ChronoUnit.DAYS)));

        assertThat(auditRepository.deleteCleanupBatch(cutoff, 100)).isEqualTo(1);
        assertThat(rateLimitRepository.deleteStaleBatch(cutoff, now, 100)).isEqualTo(1);
        assertThat(auditRepository.count()).isEqualTo(1);
        assertThat(rateLimitRepository.count()).isEqualTo(2);
    }

    private Timestamp timestamp(Instant value) {
        return Timestamp.from(value);
    }

    private LoginRateLimitService.AuthenticationDecision authenticateAfterSignal(
            LoginRateLimitService service,
            String username,
            String clientIp,
            CountDownLatch ready,
            CountDownLatch start
    ) throws InterruptedException {
        ready.countDown();
        if (!start.await(5, TimeUnit.SECONDS)) {
            throw new IllegalStateException("Не удалось синхронно запустить попытки входа");
        }
        return transactionTemplate.execute(status ->
                service.authenticate("tenant", username, "неверный-пароль", clientIp)
        );
    }

    private LoginRateLimitService service(LoginSecurityProperties properties,
                                          BCryptPasswordEncoder encoder) {
        return new LoginRateLimitService(
                rateLimitRepository,
                auditRepository,
                userRepository,
                encoder,
                properties,
                Clock.fixed(NOW, ZoneOffset.UTC)
        );
    }

    @SpringBootConfiguration
    @EnableAutoConfiguration
    @EntityScan(basePackageClasses = AuthLoginRateLimit.class)
    @EnableJpaRepositories(basePackageClasses = AuthLoginRateLimitRepository.class)
    static class TestApplication {
    }
}
