package cloud.nstart.terravault.ordinis.restapi.service.reference;

import cloud.nstart.terravault.ordinis.restapi.service.reference.MaterializedViewRefreshService.MvGateway;
import cloud.nstart.terravault.ordinis.restapi.service.reference.MaterializedViewRefreshService.RefreshMeta;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.test.util.ReflectionTestUtils;

import java.time.OffsetDateTime;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
 * Тесты {@link MaterializedViewRefreshService}: throttle, storm prevention,
 * concurrent guard, fallback REFRESH когда CONCURRENTLY падает.
 *
 * <p>Использует stub {@link MvGateway} — JdbcTemplate не мокается на JDK 25.
 * Stub симулирует in-memory state lineage_mv_meta + REFRESH execution count.
 */
class MaterializedViewRefreshServiceTest {

  private StubGateway gateway;
  private MaterializedViewRefreshService service;

  @BeforeEach
  void setUp() {
    gateway = new StubGateway();
    service = new MaterializedViewRefreshService(gateway);
    ReflectionTestUtils.setField(service, "minIntervalSec", 30L);
  }

  @Test
  void scheduledRefresh_runsRefreshFirstTime() {
    service.scheduledRefresh();
    assertThat(gateway.refreshConcurrentlyCalls.get()).isEqualTo(1);
    assertThat(gateway.refreshPlainCalls.get()).isEqualTo(0);
  }

  @Test
  void scheduledRefresh_skipsIfWithinCooldown() {
    service.scheduledRefresh();
    int afterFirst = gateway.refreshConcurrentlyCalls.get();
    service.scheduledRefresh();
    assertThat(gateway.refreshConcurrentlyCalls.get()).isEqualTo(afterFirst);
  }

  @Test
  void refreshNow_throwsIfThrottled() {
    service.refreshNow();
    assertThatThrownBy(() -> service.refreshNow())
        .isInstanceOf(MaterializedViewRefreshService.RefreshThrottledException.class)
        .hasMessageContaining("throttled");
  }

  @Test
  void refreshNow_succeedsAfterCooldown() {
    gateway.simulatedLastRefresh = OffsetDateTime.now().minusSeconds(31);
    service.refreshNow();
    assertThat(gateway.refreshConcurrentlyCalls.get()).isEqualTo(1);
  }

  @Test
  void refresh_fallsBackToPlainWhenConcurrentlyFails() {
    gateway.failConcurrentlyOnce = true;
    service.scheduledRefresh();
    assertThat(gateway.refreshConcurrentlyCalls.get()).isEqualTo(1);
    assertThat(gateway.refreshPlainCalls.get()).isEqualTo(1);
  }

  @Test
  void stormPrevention_100ConcurrentRefreshAttempts_resultsInOneActualRefresh() throws Exception {
    // R2 eng review: 100 closes за 1 sec не должны вызвать 100 refresh'ей.
    int parallelism = 100;
    ExecutorService exec = Executors.newFixedThreadPool(Math.min(parallelism, 16));
    var latch = new CountDownLatch(parallelism);
    var startGate = new CountDownLatch(1);
    for (int i = 0; i < parallelism; i++) {
      exec.submit(() -> {
        try {
          startGate.await();
          service.scheduledRefresh();
        } catch (InterruptedException ignored) {
          Thread.currentThread().interrupt();
        } finally {
          latch.countDown();
        }
      });
    }
    startGate.countDown();
    latch.await();
    exec.shutdown();

    // Точно 1 refresh выполнен — остальные 99 либо skipped по cooldown'у,
    // либо по in-flight guard.
    assertThat(gateway.refreshConcurrentlyCalls.get()).isEqualTo(1);
  }

  @Test
  void lastRefresh_returnsMetaWhenAvailable() {
    gateway.simulatedLastRefresh = OffsetDateTime.now().minusMinutes(5);
    var meta = service.lastRefresh();
    assertThat(meta).isPresent();
    assertThat(meta.get().lastRefreshedAt()).isEqualTo(gateway.simulatedLastRefresh);
  }

  @Test
  void scheduledRefresh_updatesMetaAfterRefresh() {
    service.scheduledRefresh();
    assertThat(gateway.updateMetaCalls.get()).isEqualTo(1);
    assertThat(gateway.lastUpdateStatus).isEqualTo("ok");
    assertThat(gateway.lastUpdateRowCount).isEqualTo(42L);
  }

  @Test
  void scheduledRefresh_updatesMetaWithErrorIfBothRefreshesFail() {
    gateway.failConcurrentlyOnce = true;
    gateway.failPlain = true;
    service.scheduledRefresh();
    assertThat(gateway.updateMetaCalls.get()).isEqualTo(1);
    assertThat(gateway.lastUpdateStatus).isEqualTo("error");
    assertThat(gateway.lastUpdateError).isNotNull();
  }

  /** Stub gateway — in-memory state machine без real JDBC. */
  private static class StubGateway implements MvGateway {
    final AtomicInteger refreshConcurrentlyCalls = new AtomicInteger(0);
    final AtomicInteger refreshPlainCalls = new AtomicInteger(0);
    final AtomicInteger updateMetaCalls = new AtomicInteger(0);
    OffsetDateTime simulatedLastRefresh;
    boolean failConcurrentlyOnce;
    boolean failPlain;
    String lastUpdateStatus;
    String lastUpdateError;
    Long lastUpdateRowCount;

    @Override
    public void refreshConcurrently() {
      refreshConcurrentlyCalls.incrementAndGet();
      if (failConcurrentlyOnce) {
        failConcurrentlyOnce = false;
        throw new DataIntegrityViolationException("not populated yet");
      }
      simulatedLastRefresh = OffsetDateTime.now();
    }

    @Override
    public void refresh() {
      refreshPlainCalls.incrementAndGet();
      if (failPlain) {
        throw new DataIntegrityViolationException("plain refresh failed");
      }
      simulatedLastRefresh = OffsetDateTime.now();
    }

    @Override
    public Long countRows() {
      return 42L;
    }

    @Override
    public RefreshMeta loadMeta() {
      if (simulatedLastRefresh == null) {
        return new RefreshMeta(null, null, null, "never", null);
      }
      return new RefreshMeta(simulatedLastRefresh, 100L, 42L, "ok", null);
    }

    @Override
    public void updateMeta(long durationMs, Long rowCount, String status, String error) {
      updateMetaCalls.incrementAndGet();
      lastUpdateStatus = status;
      lastUpdateRowCount = rowCount;
      lastUpdateError = error;
      if (status.equals("ok")) {
        simulatedLastRefresh = OffsetDateTime.now();
      }
    }
  }
}
