package com.magistr.app.service;

import com.magistr.app.config.tenant.TenantConfig;
import com.magistr.app.config.tenant.TenantConfigPersistenceException;
import com.magistr.app.config.tenant.TenantConfigStore;
import com.magistr.app.config.tenant.TenantContext;
import com.magistr.app.config.tenant.TenantRoutingDataSource;
import com.magistr.app.config.tenant.TenantSecretUpdateReceipt;
import com.magistr.app.config.tenant.health.TenantReadinessRegistry;
import com.zaxxer.hikari.HikariDataSource;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@Testcontainers
class TenantLifecyclePostgreSqlIntegrationTest {

    private static final String DOMAIN = "alpha";
    private static final String OLD_DATABASE = "tenant_old";
    private static final String BAD_CHECKSUM_DATABASE = "tenant_bad_checksum";
    private static final String VALID_DATABASE = "tenant_valid";

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

    private TenantRoutingDataSource routingDataSource;
    private TenantDatabaseMigrationService migrationService;
    private TenantConfigStore tenantConfigStore;
    private TenantReadinessRegistry readinessRegistry;
    private TenantLifecycleService lifecycleService;
    private RetiredTenantPoolService retiredPoolService;
    private HikariDataSource oldPool;
    private TenantConfig oldConfig;

    @BeforeEach
    void setUp() throws Exception {
        createDatabase(OLD_DATABASE);
        createDatabase(BAD_CHECKSUM_DATABASE);
        createDatabase(VALID_DATABASE);

        routingDataSource = new TenantRoutingDataSource();
        migrationService = new TenantDatabaseMigrationService();
        tenantConfigStore = mock(TenantConfigStore.class);
        readinessRegistry = new TenantReadinessRegistry();
        retiredPoolService = new RetiredTenantPoolService(Duration.ofSeconds(2), Duration.ofMillis(25));
        lifecycleService = new TenantLifecycleService(
                routingDataSource,
                tenantConfigStore,
                migrationService,
                retiredPoolService,
                readinessRegistry
        );

        oldConfig = config("Старый tenant", jdbcUrl(OLD_DATABASE), POSTGRES.getPassword());
        migrateDatabase(oldConfig);
        TenantConfig badChecksumConfig = config(
                "Tenant с повреждённой историей",
                jdbcUrl(BAD_CHECKSUM_DATABASE),
                POSTGRES.getPassword()
        );
        migrateDatabase(badChecksumConfig);
        corruptFlywayChecksum(jdbcUrl(BAD_CHECKSUM_DATABASE));

        oldPool = routingDataSource.prepareTenantDataSource(oldConfig);
        assertThat(routingDataSource.swapTenant(oldConfig, oldPool)).isNull();
        readinessRegistry.replaceDesired(List.of(oldConfig));
        readinessRegistry.markMigrationSucceeded(oldConfig);
        readinessRegistry.markConnectivity(DOMAIN, true);
        assertOldState();
    }

    @AfterEach
    void closePools() {
        TenantContext.clear();
        if (routingDataSource != null) {
            routingDataSource.getTenantDataSource(DOMAIN).ifPresent(this::closeIfHikari);
        }
        if (oldPool != null && !oldPool.isClosed()) {
            oldPool.close();
        }
        if (retiredPoolService != null) {
            retiredPoolService.shutdown();
        }
    }

    @Test
    void preservesOldPoolForCredentialsFlywayAndPersistenceFailuresThenSwapsSuccessfully() throws Exception {
        TenantConfig wrongCredentials = config(
                "Неверные credentials",
                jdbcUrl(OLD_DATABASE),
                POSTGRES.getPassword() + "-wrong"
        );

        assertThatThrownBy(() -> lifecycleService.addOrUpdateTenant(wrongCredentials))
                .isInstanceOf(TenantLifecycleException.class)
                .hasMessage("Не удалось подключиться к базе данных тенанта");
        verify(tenantConfigStore, never()).upsertTenant(any());
        assertOldState();

        reset(tenantConfigStore);
        TenantConfig badChecksum = config(
                "Повреждённый Flyway",
                jdbcUrl(BAD_CHECKSUM_DATABASE),
                POSTGRES.getPassword()
        );
        assertThatThrownBy(() -> lifecycleService.addOrUpdateTenant(badChecksum))
                .isInstanceOf(TenantLifecycleException.class)
                .hasMessage("Не удалось выполнить миграции базы данных тенанта");
        verify(tenantConfigStore, never()).upsertTenant(any());
        assertOldState();

        reset(tenantConfigStore);
        when(tenantConfigStore.upsertTenant(any())).thenThrow(
                new TenantConfigPersistenceException("Не удалось сохранить конфигурацию тенантов")
        );
        TenantConfig validCandidate = config(
                "Новый tenant",
                jdbcUrl(VALID_DATABASE),
                POSTGRES.getPassword()
        );
        assertThatThrownBy(() -> lifecycleService.addOrUpdateTenant(validCandidate))
                .isInstanceOf(TenantLifecycleException.class)
                .hasMessage("Не удалось сохранить конфигурацию тенантов");
        verify(tenantConfigStore).upsertTenant(any());
        verify(tenantConfigStore, never()).compensate(any());
        assertOldState();

        reset(tenantConfigStore);
        when(tenantConfigStore.upsertTenant(any())).thenReturn(new TenantSecretUpdateReceipt(
                true,
                true,
                "2",
                List.of(oldConfig),
                List.of(validCandidate)
        ));
        TenantConfig saved;
        try (Connection inFlightOldConnection = oldPool.getConnection()) {
            saved = lifecycleService.addOrUpdateTenant(validCandidate);

            assertThat(oldPool.isClosed()).isFalse();
            try (Statement statement = inFlightOldConnection.createStatement();
                 ResultSet result = statement.executeQuery("SELECT current_database()")) {
                assertThat(result.next()).isTrue();
                assertThat(result.getString(1)).isEqualTo(OLD_DATABASE);
            }
        }

        assertThat(saved.getDomain()).isEqualTo(DOMAIN);
        assertThat(saved.getUrl()).isEqualTo(jdbcUrl(VALID_DATABASE));
        awaitPoolClosed(oldPool);
        DataSource activePool = routingDataSource.getTenantDataSource(DOMAIN).orElseThrow();
        assertThat(activePool).isNotSameAs(oldPool);
        assertThat(activePool).isInstanceOfSatisfying(
                HikariDataSource.class,
                pool -> assertThat(pool.isClosed()).isFalse()
        );
        assertThat(currentDatabaseThroughRouter()).isEqualTo(VALID_DATABASE);
    }

    @Test
    void persistedConfigReplacementKeepsOldPoolOnFailureAndSwapsWithoutSecretWrite() throws Exception {
        TenantConfig wrongCredentials = config(
                "Неверные credentials из проекции",
                jdbcUrl(OLD_DATABASE),
                POSTGRES.getPassword() + "-wrong"
        );

        assertThatThrownBy(() -> lifecycleService.synchronizeFromPersistedConfig(List.of(wrongCredentials)))
                .isInstanceOf(TenantLifecycleException.class)
                .hasMessage("Не удалось подключиться к базе данных тенанта");
        assertOldState();

        TenantConfig badChecksum = config(
                "Повреждённый Flyway из проекции",
                jdbcUrl(BAD_CHECKSUM_DATABASE),
                POSTGRES.getPassword()
        );
        assertThatThrownBy(() -> lifecycleService.synchronizeFromPersistedConfig(List.of(badChecksum)))
                .isInstanceOf(TenantLifecycleException.class)
                .hasMessage("Не удалось выполнить миграции базы данных тенанта");
        assertOldState();

        TenantConfig validCandidate = config(
                "Новый tenant из проекции",
                jdbcUrl(VALID_DATABASE),
                POSTGRES.getPassword()
        );
        try (Connection inFlightOldConnection = oldPool.getConnection()) {
            lifecycleService.synchronizeFromPersistedConfig(List.of(validCandidate));

            assertThat(oldPool.isClosed()).isFalse();
            try (Statement statement = inFlightOldConnection.createStatement();
                 ResultSet result = statement.executeQuery("SELECT current_database()")) {
                assertThat(result.next()).isTrue();
                assertThat(result.getString(1)).isEqualTo(OLD_DATABASE);
            }
        }

        awaitPoolClosed(oldPool);
        assertThat(currentDatabaseThroughRouter()).isEqualTo(VALID_DATABASE);
        verify(tenantConfigStore, never()).upsertTenant(any());
        verify(tenantConfigStore, never()).removeTenant(any());
        verify(tenantConfigStore, never()).compensate(any());
    }

    private void awaitPoolClosed(HikariDataSource pool) {
        long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3);
        while (!pool.isClosed() && System.nanoTime() < deadline) {
            try {
                Thread.sleep(25);
            } catch (InterruptedException exception) {
                Thread.currentThread().interrupt();
                throw new IllegalStateException("Ожидание закрытия старого tenant pool прервано", exception);
            }
        }
        assertThat(pool.isClosed()).as("Старый tenant pool должен закрыться после drain").isTrue();
    }

    private void assertOldState() {
        assertThat(routingDataSource.getTenantDataSource(DOMAIN)).containsSame(oldPool);
        TenantConfig storedConfig = routingDataSource.snapshotTenantConfigs().get(DOMAIN);
        assertThat(storedConfig).isNotNull();
        assertThat(storedConfig.getUrl()).isEqualTo(jdbcUrl(OLD_DATABASE));
        assertThat(storedConfig.getUsername()).isEqualTo(POSTGRES.getUsername());
        assertThat(oldPool.isClosed()).isFalse();
        assertThat(currentDatabaseThroughRouter()).isEqualTo(OLD_DATABASE);
    }

    private String currentDatabaseThroughRouter() {
        TenantContext.setCurrentTenant(DOMAIN);
        try (Connection connection = routingDataSource.getConnection();
             Statement statement = connection.createStatement();
             ResultSet result = statement.executeQuery("SELECT current_database()")) {
            assertThat(result.next()).isTrue();
            return result.getString(1);
        } catch (Exception failure) {
            throw new IllegalStateException("Старое tenant-подключение перестало работать", failure);
        } finally {
            TenantContext.clear();
        }
    }

    private void createDatabase(String databaseName) throws Exception {
        try (Connection connection = POSTGRES.createConnection("");
             Statement statement = connection.createStatement()) {
            statement.execute("DROP DATABASE IF EXISTS " + databaseName + " WITH (FORCE)");
            statement.execute("CREATE DATABASE " + databaseName);
        }
    }

    private void migrateDatabase(TenantConfig config) {
        HikariDataSource dataSource = routingDataSource.prepareTenantDataSource(config);
        try {
            migrationService.migrate(dataSource);
        } finally {
            dataSource.close();
        }
    }

    private void corruptFlywayChecksum(String url) throws Exception {
        try (Connection connection = DriverManager.getConnection(
                url,
                POSTGRES.getUsername(),
                POSTGRES.getPassword()
        ); Statement statement = connection.createStatement()) {
            int changed = statement.executeUpdate("""
                    UPDATE flyway_schema_history
                    SET checksum = checksum + 1
                    WHERE version = '1'
                    """);
            assertThat(changed).isEqualTo(1);
        }
    }

    private TenantConfig config(String name, String url, String password) {
        return new TenantConfig(name, DOMAIN, url, POSTGRES.getUsername(), password);
    }

    private String jdbcUrl(String databaseName) {
        return "jdbc:postgresql://" + POSTGRES.getHost() + ":"
                + POSTGRES.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT) + "/" + databaseName;
    }

    private void closeIfHikari(DataSource dataSource) {
        if (dataSource instanceof HikariDataSource hikariDataSource && !hikariDataSource.isClosed()) {
            hikariDataSource.close();
        }
    }
}
