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

import cloud.nstart.terravault.ordinis.auth.ScopeContext;
import cloud.nstart.terravault.ordinis.domain.DataScope;
import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinition;
import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinitionRepository;
import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecordRepository;
import cloud.nstart.terravault.ordinis.restapi.error.OrdinisException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.sql.Timestamp;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

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.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class SnapshotsServiceTest {

  private static final ObjectMapper OM = new ObjectMapper();

  private DictionaryDefinitionRepository defRepo;
  private DictionaryRecordRepository recRepo;
  private DictionaryDefinitionService defService;
  private TestScopeContext scopeCtx;
  private SnapshotsService service;

  @BeforeEach
  void setUp() {
    defRepo = mock(DictionaryDefinitionRepository.class);
    recRepo = mock(DictionaryRecordRepository.class);
    defService = new DictionaryDefinitionService(defRepo, null);
    scopeCtx = new TestScopeContext();
    service = new SnapshotsService(defService, recRepo, scopeCtx);
  }

  static final class TestScopeContext extends ScopeContext {
    private DataScope blockedScope;
    TestScopeContext() { super(null); }
    @Override public boolean canAccess(DataScope dataScope) {
      return dataScope != blockedScope;
    }
    @Override public java.util.Set<DataScope> currentScopes() {
      return java.util.Set.of(DataScope.PUBLIC, DataScope.INTERNAL, DataScope.RESTRICTED);
    }
    void block(DataScope s) { this.blockedScope = s; }
  }

  @Test
  void snapshots_dictNotFound_404() {
    when(defRepo.findByName("missing")).thenReturn(Optional.empty());
    assertThatThrownBy(() -> service.findSnapshots("missing", null, null, "day"))
        .isInstanceOf(OrdinisException.class);
  }

  @Test
  void snapshots_scopeHidden_404() {
    var d = def("private", DataScope.RESTRICTED);
    when(defRepo.findByName("private")).thenReturn(Optional.of(d));
    scopeCtx.block(DataScope.RESTRICTED);
    assertThatThrownBy(() -> service.findSnapshots("private", null, null, "day"))
        .isInstanceOf(OrdinisException.class);
  }

  @Test
  void snapshots_invalidGranularity_400() {
    var d = def("dict", DataScope.PUBLIC);
    when(defRepo.findByName("dict")).thenReturn(Optional.of(d));
    assertThatThrownBy(() -> service.findSnapshots("dict", null, null, "century"))
        .isInstanceOf(OrdinisException.class)
        .hasMessageContaining("granularity");
  }

  @Test
  void snapshots_invalidWindow_400() {
    var d = def("dict", DataScope.PUBLIC);
    when(defRepo.findByName("dict")).thenReturn(Optional.of(d));
    OffsetDateTime from = OffsetDateTime.parse("2026-05-11T00:00:00Z");
    OffsetDateTime to = OffsetDateTime.parse("2026-04-11T00:00:00Z");
    assertThatThrownBy(() -> service.findSnapshots("dict", from, to, "day"))
        .isInstanceOf(OrdinisException.class)
        .hasMessageContaining("from must be before to");
  }

  @Test
  void snapshots_basicBuckets_mapToSnapshots() {
    var d = def("dict", DataScope.PUBLIC);
    when(defRepo.findByName("dict")).thenReturn(Optional.of(d));
    Timestamp april = Timestamp.from(OffsetDateTime.parse("2026-04-01T00:00:00Z").toInstant());
    Timestamp may = Timestamp.from(OffsetDateTime.parse("2026-05-11T00:00:00Z").toInstant());
    when(recRepo.findSnapshotBuckets(eq(d.getId()), anyString(), any(), any(), eq("day")))
        .thenReturn(List.<Object[]>of(
            new Object[]{may, 5L},
            new Object[]{april, 3L}));

    var result = service.findSnapshots("dict", null, null, "day");

    assertThat(result.dictionary()).isEqualTo("dict");
    assertThat(result.snapshots()).hasSize(2);
    // Reversed → oldest first.
    assertThat(result.snapshots().get(0).changedSinceLast()).isEqualTo(3L);
    assertThat(result.snapshots().get(1).changedSinceLast()).isEqualTo(5L);
  }

  @Test
  void snapshots_currentMarkedOnce_evenWithFutureBuckets() {
    var d = def("dict", DataScope.PUBLIC);
    when(defRepo.findByName("dict")).thenReturn(Optional.of(d));
    OffsetDateTime now = OffsetDateTime.now();
    Timestamp pastA = Timestamp.from(now.minusDays(10).toInstant());
    Timestamp pastB = Timestamp.from(now.minusDays(5).toInstant());
    // Repository returns DESC, так order — newest first.
    when(recRepo.findSnapshotBuckets(any(), anyString(), any(), any(), anyString()))
        .thenReturn(List.<Object[]>of(
            new Object[]{pastB, 1L},
            new Object[]{pastA, 2L}));

    var result = service.findSnapshots("dict", null, null, "day");

    long isCurrentCount = result.snapshots().stream().filter(SnapshotsService.Snapshot::isCurrent).count();
    assertThat(isCurrentCount).isEqualTo(1L);
    // Newest bucket (pastB) — это "сейчас" label.
    var newest = result.snapshots().get(result.snapshots().size() - 1);
    assertThat(newest.isCurrent()).isTrue();
    assertThat(newest.label()).isEqualTo("сейчас");
  }

  // === Helpers ===

  private static DictionaryDefinition def(String name, DataScope scope) {
    JsonNode schema;
    try {
      schema = OM.readTree("{\"properties\":{}}");
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
    return new DictionaryDefinition(UUID.randomUUID(), name, scope, schema);
  }
}
