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

import cloud.nstart.terravault.ordinis.restapi.dto.BulkCloseRequest;
import cloud.nstart.terravault.ordinis.restapi.dto.BulkCloseResponse;
import cloud.nstart.terravault.ordinis.restapi.error.OrdinisException;
import org.junit.jupiter.api.Test;

import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;

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

/**
 * Тесты {@link BulkRecordService}: per-record транзакция, дедуп, classification
 * exception'ов в skipped vs errors.
 *
 * <p>{@link DictionaryRecordService} НЕ мокается через Mockito — на Java 17+
 * inline-mock не работает для классов из standard distribution. Вместо этого
 * создаётся анонимный subclass который переопределяет {@code close()} —
 * dependencies в конструкторе передаём null'ом (close override не обращается
 * к ним).
 */
class BulkRecordServiceTest {

  private static final String DICT = "ground_station";

  /**
   * Создаёт {@link DictionaryRecordService} stub: конструктор принимает 7
   * null deps, единственный perевопреlojavkaedoperedrebooled close() метод
   * — диспатчит вызов в {@code closeStub} (либо записывает в captures,
   * либо бросает заготовленный exception).
   */
  private static DictionaryRecordService stubService(BiConsumer<String, String> closeStub) {
    return new DictionaryRecordService(null, null, null, null, null, null, null) {
      @Override
      public void close(String dictionaryName, String businessKey, OffsetDateTime closeAt, String reason) {
        closeStub.accept(dictionaryName, businessKey);
      }
    };
  }

  /** Stub с per-key поведением: lookup map[bk] → behavior. Default success. */
  private static DictionaryRecordService routedStub(
      Map<String, RuntimeException> errorByKey,
      List<String> calls) {
    return stubService((dict, bk) -> {
      calls.add(bk);
      RuntimeException ex = errorByKey.get(bk);
      if (ex != null) throw ex;
    });
  }

  @Test
  void allSucceed_whenServiceClosesEachRecord() {
    List<String> calls = new ArrayList<>();
    DictionaryRecordService rs = routedStub(Map.of(), calls);
    BulkRecordService bulk = new BulkRecordService(rs, null, null);

    BulkCloseResponse res = bulk.bulkClose(DICT,
        new BulkCloseRequest(List.of("a", "b", "c"), null, "test reason"));

    assertThat(res.closed()).containsExactly("a", "b", "c");
    assertThat(res.skipped()).isEmpty();
    assertThat(res.errors()).isEmpty();
    assertThat(calls).containsExactly("a", "b", "c");
  }

  @Test
  void notActive_classifiedAsSkipped() {
    List<String> calls = new ArrayList<>();
    DictionaryRecordService rs = routedStub(
        Map.of("b", OrdinisException.notFound("record_not_active", "уже закрыто")),
        calls);
    BulkRecordService bulk = new BulkRecordService(rs, null, null);

    BulkCloseResponse res = bulk.bulkClose(DICT,
        new BulkCloseRequest(List.of("a", "b", "c"), null, null));

    assertThat(res.closed()).containsExactly("a", "c");
    assertThat(res.skipped()).hasSize(1);
    assertThat(res.skipped().get(0).businessKey()).isEqualTo("b");
    assertThat(res.skipped().get(0).reason()).isEqualTo("already_closed");
    assertThat(res.errors()).isEmpty();
  }

  @Test
  void otherOrdinisException_classifiedAsError() {
    List<String> calls = new ArrayList<>();
    DictionaryRecordService rs = routedStub(
        Map.of("a", OrdinisException.forbidden("scope_insufficient", "нет прав")),
        calls);
    BulkRecordService bulk = new BulkRecordService(rs, null, null);

    BulkCloseResponse res = bulk.bulkClose(DICT,
        new BulkCloseRequest(List.of("a"), null, null));

    assertThat(res.closed()).isEmpty();
    assertThat(res.skipped()).isEmpty();
    assertThat(res.errors()).hasSize(1);
    assertThat(res.errors().get(0).businessKey()).isEqualTo("a");
    assertThat(res.errors().get(0).reason()).contains("scope_insufficient").contains("нет прав");
  }

  @Test
  void unexpectedException_classifiedAsError() {
    List<String> calls = new ArrayList<>();
    DictionaryRecordService rs = routedStub(
        Map.of("a", new RuntimeException("DB connection lost")),
        calls);
    BulkRecordService bulk = new BulkRecordService(rs, null, null);

    BulkCloseResponse res = bulk.bulkClose(DICT,
        new BulkCloseRequest(List.of("a"), null, null));

    assertThat(res.closed()).isEmpty();
    assertThat(res.skipped()).isEmpty();
    assertThat(res.errors()).hasSize(1);
    assertThat(res.errors().get(0).reason())
        .contains("RuntimeException")
        .contains("DB connection lost");
  }

  @Test
  void duplicates_silentlyDeduplicated() {
    List<String> calls = new ArrayList<>();
    DictionaryRecordService rs = routedStub(Map.of(), calls);
    BulkRecordService bulk = new BulkRecordService(rs, null, null);

    BulkCloseResponse res = bulk.bulkClose(DICT,
        new BulkCloseRequest(List.of("a", "a", "b", "a"), null, null));

    assertThat(res.closed()).containsExactly("a", "b");
    assertThat(res.skipped()).isEmpty();
    assertThat(res.errors()).isEmpty();
    // service.close для "a" вызвался ровно 1 раз (дубликаты дедуплицированы)
    assertThat(calls).containsExactly("a", "b");
  }

  @Test
  void blankBusinessKey_classifiedAsSkipped_invalidKey() {
    List<String> calls = new ArrayList<>();
    DictionaryRecordService rs = routedStub(Map.of(), calls);
    BulkRecordService bulk = new BulkRecordService(rs, null, null);

    BulkCloseResponse res = bulk.bulkClose(DICT,
        new BulkCloseRequest(java.util.Arrays.asList("a", null, "", "   ", "b"), null, null));

    assertThat(res.closed()).containsExactly("a", "b");
    assertThat(res.skipped()).hasSize(3);
    assertThat(res.skipped()).extracting(BulkCloseResponse.SkippedItem::reason)
        .containsOnly("invalid_key");
    // service.close НЕ должен вызваться для невалидных ключей
    assertThat(calls).containsExactly("a", "b");
  }

  @Test
  void atTimestamp_passedThroughToServiceClose() {
    List<String> capturedKeys = new ArrayList<>();
    List<OffsetDateTime> capturedAt = new ArrayList<>();
    List<String> capturedReason = new ArrayList<>();
    DictionaryRecordService rs = new DictionaryRecordService(null, null, null, null, null, null, null) {
      @Override
      public void close(String dict, String bk, OffsetDateTime at, String reason) {
        capturedKeys.add(bk);
        capturedAt.add(at);
        capturedReason.add(reason);
      }
    };
    BulkRecordService bulk = new BulkRecordService(rs, null, null);

    OffsetDateTime when = OffsetDateTime.parse("2026-01-01T00:00:00Z");
    bulk.bulkClose(DICT, new BulkCloseRequest(List.of("a"), when, "scheduled deactivation"));

    assertThat(capturedKeys).containsExactly("a");
    assertThat(capturedAt).containsExactly(when);
    assertThat(capturedReason).containsExactly("scheduled deactivation");
  }

  @Test
  void mixedResults_allCategoriesPopulated() {
    List<String> calls = new ArrayList<>();
    DictionaryRecordService rs = routedStub(
        Map.of(
            "dup", OrdinisException.notFound("record_not_active", "x"),
            "bad", new IllegalStateException("boom")),
        calls);
    BulkRecordService bulk = new BulkRecordService(rs, null, null);

    BulkCloseResponse res = bulk.bulkClose(DICT,
        new BulkCloseRequest(List.of("ok1", "dup", "bad", "ok2"), null, null));

    assertThat(res.closed()).containsExactly("ok1", "ok2");
    assertThat(res.skipped()).extracting(BulkCloseResponse.SkippedItem::businessKey)
        .containsExactly("dup");
    assertThat(res.errors()).extracting(BulkCloseResponse.ErrorItem::businessKey)
        .containsExactly("bad");
  }

  @Test
  void emptyBatch_returnsEmptyResultWithoutCallingService() {
    List<String> calls = new ArrayList<>();
    DictionaryRecordService rs = routedStub(Map.of(), calls);
    BulkRecordService bulk = new BulkRecordService(rs, null, null);

    BulkCloseResponse res = bulk.bulkClose(DICT, new BulkCloseRequest(List.of(), null, null));

    assertThat(res.closed()).isEmpty();
    assertThat(res.skipped()).isEmpty();
    assertThat(res.errors()).isEmpty();
    assertThat(calls).isEmpty();
  }

  @Test
  void closeOrder_preservesInputOrder() {
    List<String> calls = new ArrayList<>();
    DictionaryRecordService rs = routedStub(Map.of(), calls);
    BulkRecordService bulk = new BulkRecordService(rs, null, null);

    BulkCloseResponse res = bulk.bulkClose(DICT,
        new BulkCloseRequest(List.of("z", "m", "a"), null, null));

    // closed[] сохраняет порядок входа — не пересортировано
    assertThat(res.closed()).containsExactly("z", "m", "a");
    // service.close вызывался в том же порядке (для audit log consistency)
    assertThat(calls).containsExactly("z", "m", "a");
  }
}
