package com.magistr.app.config.tenant;

import com.magistr.app.config.tenant.health.TenantReadinessRegistry;
import com.magistr.app.service.RetiredTenantPoolService;
import com.magistr.app.service.TenantDatabaseMigrationService;
import com.magistr.app.service.TenantLifecycleService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.test.util.ReflectionTestUtils;

import java.nio.file.Files;
import java.nio.file.Path;
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.AtomicReference;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.never;

class TenantConfigWatcherTest {

    @TempDir
    Path tempDirectory;

    @Test
    void configHashUsesSha256Hex() {
        String hash = TenantConfigWatcher.configHash("{\"domain\":\"localhost\"}");

        assertThat(hash).hasSize(64);
        assertThat(hash).matches("[0-9a-f]{64}");
        assertThat(hash).isNotEqualTo(Integer.toHexString("{\"domain\":\"localhost\"}".hashCode()));
    }

    @Test
    void configHashChangesWhenContentChanges() {
        assertThat(TenantConfigWatcher.configHash("tenant-a"))
                .isNotEqualTo(TenantConfigWatcher.configHash("tenant-b"));
    }

    @Test
    void advancesHashOnlyAfterSuccessfulLifecycleSynchronization() throws Exception {
        Path configPath = tempDirectory.resolve("tenants.json");
        String content = """
                [{
                  "name": "Тестовый tenant",
                  "domain": "alpha",
                  "url": "jdbc:postgresql://db/alpha",
                  "username": "alpha",
                  "password": "тестовый-пароль"
                }]
                """;
        Files.writeString(configPath, content);
        TenantLifecycleService lifecycleService = mock(TenantLifecycleService.class);
        doThrow(new IllegalStateException("тестовая ошибка синхронизации"))
                .doNothing()
                .when(lifecycleService).synchronizeFromPersistedConfig(anyList());
        TenantConfigWatcher watcher = watcher(lifecycleService, configPath);

        watcher.watchForChanges();
        assertThat(ReflectionTestUtils.getField(watcher, "lastConfigHash")).isEqualTo("");

        watcher.watchForChanges();
        assertThat(ReflectionTestUtils.getField(watcher, "lastConfigHash"))
                .isEqualTo(TenantConfigWatcher.configHash(content));
        verify(lifecycleService, times(2)).synchronizeFromPersistedConfig(anyList());

        watcher.watchForChanges();
        verify(lifecycleService, times(2)).synchronizeFromPersistedConfig(anyList());
    }

    @Test
    void successfulHashPreventsRepeatedSynchronizationOfSameContent() throws Exception {
        Path configPath = tempDirectory.resolve("tenants.json");
        String content = "[]";
        Files.writeString(configPath, content);
        TenantLifecycleService lifecycleService = mock(TenantLifecycleService.class);
        doNothing().when(lifecycleService).synchronizeFromPersistedConfig(anyList());
        TenantConfigWatcher watcher = watcher(lifecycleService, configPath);

        watcher.watchForChanges();
        watcher.watchForChanges();

        verify(lifecycleService).synchronizeFromPersistedConfig(anyList());
        assertThat(ReflectionTestUtils.getField(watcher, "lastConfigHash"))
                .isEqualTo(TenantConfigWatcher.configHash(content));
    }

    @Test
    void watcherCannotReadBetweenSerializedApiMutationAndRefreshHash() throws Exception {
        Path configPath = tempDirectory.resolve("tenants.json");
        String content = "[]";
        Files.writeString(configPath, content);
        TenantRoutingDataSource routingDataSource = mock(TenantRoutingDataSource.class);
        TenantLifecycleService lifecycleService = new TenantLifecycleService(
                routingDataSource,
                mock(TenantConfigStore.class),
                mock(TenantDatabaseMigrationService.class),
                mock(RetiredTenantPoolService.class),
                mock(TenantReadinessRegistry.class)
        );
        TenantConfigWatcher watcher = new TenantConfigWatcher(
                lifecycleService,
                mock(TenantReadinessRegistry.class)
        );
        ReflectionTestUtils.setField(watcher, "tenantsConfigPath", configPath.toString());
        CountDownLatch apiMutationReachedRefreshWindow = new CountDownLatch(1);
        CountDownLatch allowApiRefresh = new CountDownLatch(1);
        AtomicReference<Thread> watcherThread = new AtomicReference<>();
        ExecutorService executor = Executors.newFixedThreadPool(2);

        try {
            Future<?> api = executor.submit(() -> lifecycleService.executeSerialized(() -> {
                apiMutationReachedRefreshWindow.countDown();
                await(allowApiRefresh, "Ожидание разрешения refreshHash прервано");
                watcher.refreshHash();
            }));
            assertThat(apiMutationReachedRefreshWindow.await(5, TimeUnit.SECONDS)).isTrue();

            Future<?> concurrentWatcher = executor.submit(() -> {
                watcherThread.set(Thread.currentThread());
                watcher.watchForChanges();
            });
            awaitBlocked(watcherThread);
            assertThat(concurrentWatcher).isNotDone();

            allowApiRefresh.countDown();
            api.get(5, TimeUnit.SECONDS);
            concurrentWatcher.get(5, TimeUnit.SECONDS);

            assertThat(ReflectionTestUtils.getField(watcher, "lastConfigHash"))
                    .isEqualTo(TenantConfigWatcher.configHash(content));
            verify(routingDataSource, never()).snapshotTenantConfigs();
        } finally {
            allowApiRefresh.countDown();
            executor.shutdownNow();
            assertThat(executor.awaitTermination(5, TimeUnit.SECONDS)).isTrue();
        }
    }

    private TenantConfigWatcher watcher(TenantLifecycleService lifecycleService, Path configPath) {
        doAnswer(invocation -> {
            invocation.<Runnable>getArgument(0).run();
            return null;
        }).when(lifecycleService).executeSerialized(any(Runnable.class));
        TenantConfigWatcher watcher = new TenantConfigWatcher(
                lifecycleService,
                mock(TenantReadinessRegistry.class)
        );
        ReflectionTestUtils.setField(watcher, "tenantsConfigPath", configPath.toString());
        return watcher;
    }

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

    private void awaitBlocked(AtomicReference<Thread> threadReference) {
        long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(5);
        while (System.nanoTime() < deadline) {
            Thread thread = threadReference.get();
            if (thread != null && thread.getState() == Thread.State.BLOCKED) {
                return;
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException exception) {
                Thread.currentThread().interrupt();
                throw new IllegalStateException("Ожидание блокировки watcher прервано", exception);
            }
        }
        throw new IllegalStateException("Watcher не заблокировался на общем lifecycle coordinator");
    }
}
