package com.magistr.app.config.tenant.health;

import com.magistr.app.config.tenant.TenantConfig;
import com.magistr.app.config.tenant.TenantRoutingDataSource;
import org.junit.jupiter.api.Test;

import java.nio.charset.StandardCharsets;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class TenantDatabaseHealthMonitorTest {

    private static final byte[] KEY = "0123456789abcdef0123456789abcdef".getBytes(StandardCharsets.UTF_8);
    private static final Instant FAST_COMPLETED_AT = Instant.parse("2026-07-15T10:00:01Z");
    private static final Instant TIMEOUT_RECORDED_AT = Instant.parse("2026-07-15T10:00:02Z");
    private static final Instant SLOW_COMPLETED_AT = Instant.parse("2026-07-15T10:00:03Z");

    @Test
    void проверяетTenantПараллельноНоНеПревышаетЛимит() {
        TenantReadinessRegistry registry = registry();
        List<TenantConfig> tenants = new ArrayList<>();
        for (int index = 0; index < 6; index++) {
            tenants.add(config("tenant-" + index));
        }
        registry.replaceDesired(tenants);
        tenants.forEach(registry::markMigrationSucceeded);

        ControlledRoutingDataSource routing = new ControlledRoutingDataSource(60, true);
        TenantDatabaseHealthMonitor monitor = monitor(registry, routing, 2, Duration.ofSeconds(1));
        try {
            TenantDatabaseHealthMonitor.RefreshSummary summary = monitor.refreshNow();

            assertEquals(6, summary.requested());
            assertEquals(6, summary.available());
            assertEquals(0, summary.unavailable());
            assertTrue(routing.maxActive.get() >= 2);
            assertTrue(routing.maxActive.get() <= 2);
            assertTrue(registry.aggregate(Duration.ofSeconds(30)).ready());
        } finally {
            monitor.close();
        }
    }

    @Test
    void превышениеТаймаутаОтмечаетСоединениеНедоступным() {
        TenantReadinessRegistry registry = registry();
        TenantConfig tenant = config("slow");
        registry.replaceDesired(List.of(tenant));
        registry.markMigrationSucceeded(tenant);

        ControlledRoutingDataSource routing = new ControlledRoutingDataSource(300, true);
        TenantDatabaseHealthMonitor monitor = monitor(registry, routing, 1, Duration.ofMillis(25));
        try {
            TenantDatabaseHealthMonitor.RefreshSummary summary = monitor.refreshNow();

            assertEquals(1, summary.requested());
            assertEquals(0, summary.available());
            assertEquals(1, summary.unavailable());
            assertFalse(registry.aggregate(Duration.ofSeconds(30)).ready());
        } finally {
            monitor.close();
        }
    }

    @Test
    void таймаутПервогоTenantНеОтменяетУжеЗавершившийсяВторой() {
        TenantReadinessRegistry registry = registry();
        TenantConfig slow = config("slow");
        TenantConfig fast = config("fast");
        registry.replaceDesired(List.of(slow, fast));
        registry.markMigrationSucceeded(slow);
        registry.markMigrationSucceeded(fast);

        CompletionAwareClock clock = new CompletionAwareClock();
        SlowFirstRoutingDataSource routing = new SlowFirstRoutingDataSource(clock);
        TenantDatabaseHealthMonitor monitor = new TenantDatabaseHealthMonitor(
                registry,
                routing,
                2,
                Duration.ofMillis(200),
                Duration.ofSeconds(30),
                clock);
        try {
            TenantDatabaseHealthMonitor.RefreshSummary summary = monitor.refreshNow();

            assertEquals(2, summary.requested());
            assertEquals(1, summary.available());
            assertEquals(1, summary.unavailable());

            TenantReadinessRegistry.TenantStatus slowStatus = registry.snapshot().get("slow");
            TenantReadinessRegistry.TenantStatus fastStatus = registry.snapshot().get("fast");
            assertEquals(TenantReadinessRegistry.ConnectivityState.UNAVAILABLE,
                    slowStatus.connectivityState());
            assertEquals(TIMEOUT_RECORDED_AT, slowStatus.connectivityCheckedAt());
            assertEquals(TenantReadinessRegistry.ConnectivityState.AVAILABLE,
                    fastStatus.connectivityState());
            assertEquals(FAST_COMPLETED_AT, fastStatus.connectivityCheckedAt());
        } finally {
            monitor.close();
        }
    }

    @Test
    void неПроверяетTenantДоУспехаМиграцииИХ2() {
        TenantReadinessRegistry registry = registry();
        TenantConfig ready = config("ready");
        TenantConfig pending = config("pending");
        TenantConfig h2 = new TenantConfig("h2", "h2", "jdbc:h2:mem:test", "sa", "");
        registry.replaceDesired(List.of(ready, pending, h2));
        registry.markMigrationSucceeded(ready);
        registry.markMigrationStarted(pending);
        registry.markMigrationSucceeded(h2);

        ControlledRoutingDataSource routing = new ControlledRoutingDataSource(0, true);
        TenantDatabaseHealthMonitor monitor = monitor(registry, routing, 3, Duration.ofSeconds(1));
        try {
            TenantDatabaseHealthMonitor.RefreshSummary summary = monitor.refreshNow();

            assertEquals(1, summary.requested());
            assertEquals(1, routing.total.get());
            assertFalse(registry.aggregate(Duration.ofSeconds(30)).ready());
        } finally {
            monitor.close();
        }
    }

    @Test
    void сообщаетНастроенныйMaxStaleness() {
        TenantReadinessRegistry registry = registry();
        TenantDatabaseHealthMonitor monitor = new TenantDatabaseHealthMonitor(
                registry,
                new ControlledRoutingDataSource(0, true),
                1,
                Duration.ofSeconds(1),
                Duration.ofSeconds(17),
                Clock.systemUTC());
        try {
            assertEquals(Duration.ofSeconds(17), monitor.maxStaleness());
            assertEquals(1, monitor.parallelism());
        } finally {
            monitor.close();
        }
    }

    private TenantReadinessRegistry registry() {
        return new TenantReadinessRegistry(Clock.systemUTC(), KEY);
    }

    private TenantDatabaseHealthMonitor monitor(TenantReadinessRegistry registry,
                                                TenantRoutingDataSource routing,
                                                int parallelism,
                                                Duration timeout) {
        return new TenantDatabaseHealthMonitor(
                registry,
                routing,
                parallelism,
                timeout,
                Duration.ofSeconds(30),
                Clock.systemUTC());
    }

    private TenantConfig config(String domain) {
        return new TenantConfig(domain, domain, "jdbc:postgresql://db/" + domain, "user", "secret");
    }

    private static final class ControlledRoutingDataSource extends TenantRoutingDataSource {
        private final long delayMillis;
        private final boolean result;
        private final AtomicInteger active = new AtomicInteger();
        private final AtomicInteger maxActive = new AtomicInteger();
        private final AtomicInteger total = new AtomicInteger();

        private ControlledRoutingDataSource(long delayMillis, boolean result) {
            this.delayMillis = delayMillis;
            this.result = result;
        }

        @Override
        public boolean testConnection(String domain) {
            total.incrementAndGet();
            int current = active.incrementAndGet();
            maxActive.accumulateAndGet(current, Math::max);
            try {
                if (delayMillis > 0) {
                    Thread.sleep(delayMillis);
                }
                return result;
            } catch (InterruptedException interrupted) {
                Thread.currentThread().interrupt();
                return false;
            } finally {
                active.decrementAndGet();
            }
        }
    }

    private static final class SlowFirstRoutingDataSource extends TenantRoutingDataSource {
        private final CompletionAwareClock clock;
        private final CountDownLatch fastCompleted = new CountDownLatch(1);

        private SlowFirstRoutingDataSource(CompletionAwareClock clock) {
            this.clock = clock;
        }

        @Override
        public boolean testConnection(String domain) {
            clock.bind(domain);
            if ("fast".equals(domain)) {
                fastCompleted.countDown();
                return true;
            }

            try {
                if (!fastCompleted.await(5, TimeUnit.SECONDS)) {
                    return false;
                }
                Thread.sleep(1_000);
                return true;
            } catch (InterruptedException interrupted) {
                Thread.currentThread().interrupt();
                return false;
            }
        }
    }

    private static final class CompletionAwareClock extends Clock {
        private final ThreadLocal<String> currentDomain = new ThreadLocal<>();

        private void bind(String domain) {
            currentDomain.set(domain);
        }

        @Override
        public ZoneId getZone() {
            return ZoneOffset.UTC;
        }

        @Override
        public Clock withZone(ZoneId zone) {
            return this;
        }

        @Override
        public Instant instant() {
            return switch (String.valueOf(currentDomain.get())) {
                case "fast" -> FAST_COMPLETED_AT;
                case "slow" -> SLOW_COMPLETED_AT;
                default -> TIMEOUT_RECORDED_AT;
            };
        }
    }
}
