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

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.DictionaryRecord;
import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecordRepository;
import cloud.nstart.terravault.ordinis.validation.ValidationError;
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.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.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;

/**
 * REGRESSION mandatory: existing close path с null {@code x-references-on-close}
 * field MUST behave identical к v1.2.1.
 *
 * <p>Покрывает gap: bundle v1.2.1 (без новой schema field) loaded в новый build —
 * убедиться что:
 * <ol>
 *   <li>{@link ReferenceValidator#validate} возвращает те же error codes что и
 *       до Phase 1 prep commit (никаких новых x-references-on-close-related
 *       codes не появилось).</li>
 *   <li>{@link OnCloseAction#BLOCK} default'ится автоматически — schema parser
 *       не падает и не меняет error response shape.</li>
 *   <li>Active record на которое есть references просто закрывается — на этом
 *       этапе close не обращается к onClose (cascade engine = Phase 3).</li>
 * </ol>
 *
 * <p>Без этого теста риск молча сломать live consumers (см. design doc test
 * plan REGRESSION section).
 */
class ReferenceCloseRegressionTest {

  private static final ObjectMapper OM = new ObjectMapper();

  private DictionaryDefinitionRepository defRepo;
  private DictionaryRecordRepository recRepo;
  private ReferenceValidator validator;

  @BeforeEach
  void setUp() {
    defRepo = mock(DictionaryDefinitionRepository.class);
    recRepo = mock(DictionaryRecordRepository.class);
    validator = new ReferenceValidator(defRepo, recRepo);
  }

  /**
   * v1.2.1 schema = x-references есть, x-references-on-close нет.
   * validate() должен:
   * — пропустить запись с валидным reference (no errors);
   * — отклонить запись с invalid reference с тем же кодом
   *   {@code x_references_target_not_found} (без новых fields в error).
   */
  @Test
  void v1_2_1_schemaWithoutOnClose_validateBehavesIdentical() throws Exception {
    UUID targetId = UUID.randomUUID();
    DictionaryDefinition target = new DictionaryDefinition(
        targetId, "satellite_type", DataScope.PUBLIC, OM.readTree("{}"));
    when(defRepo.findByName("satellite_type")).thenReturn(Optional.of(target));
    // Active record с code=LEO-001 существует, NONEXISTENT — нет.
    when(recRepo.findActiveByJsonField(eq(targetId), any(), eq("code"), eq("LEO-001")))
        .thenReturn(List.of(stubRecord(targetId)));
    when(recRepo.findActiveByJsonField(any(), any(), anyString(), eq("NONEXISTENT")))
        .thenReturn(List.of());

    // V1.2.1 schema: x-references есть, x-references-on-close ОТСУТСТВУЕТ.
    JsonNode schema = OM.readTree("""
        {"type":"object","properties":{
          "satTypeCode":{"type":"string","x-references":"satellite_type.code"}
        }}""");

    // Valid reference — нет errors.
    JsonNode goodData = OM.readTree("{\"satTypeCode\":\"LEO-001\"}");
    List<ValidationError> goodErrors = validator.validate(
        schema, goodData, List.of(DataScope.PUBLIC), OffsetDateTime.now());
    assertThat(goodErrors).isEmpty();

    // Invalid reference — точно такой же error code и message structure как в v1.
    JsonNode badData = OM.readTree("{\"satTypeCode\":\"NONEXISTENT\"}");
    List<ValidationError> badErrors = validator.validate(
        schema, badData, List.of(DataScope.PUBLIC), OffsetDateTime.now());
    assertThat(badErrors).hasSize(1);
    ValidationError err = badErrors.get(0);
    assertThat(err.code()).isEqualTo("x_references_target_not_found");
    assertThat(err.path()).isEqualTo("/satTypeCode");
    assertThat(err.message()).contains("satellite_type");
    assertThat(err.message()).contains("NONEXISTENT");
    // Никаких новых полей в error: x_references_on_close_* кодов не появилось.
    assertThat(err.code()).doesNotContain("on_close");
    assertThat(err.code()).doesNotContain("cascade");
  }

  /**
   * Schema parser применяется к v1.2.1 bundle — onClose default'ится в BLOCK
   * автоматически. Никаких малформ-related parse errors.
   */
  @Test
  void v1_2_1_schemaParser_defaultsOnCloseToBlock() throws Exception {
    JsonNode propV1 = OM.readTree("""
        {"type":"string","x-references":"d.f"}""");
    var parsed = ReferenceValidator.parseRef("d.f", propV1);
    assertThat(parsed.onClose()).isEqualTo(OnCloseAction.BLOCK);
    assertThat(parsed.dictionaryName()).isEqualTo("d");
    assertThat(parsed.fieldName()).isEqualTo("f");
  }

  /**
   * Defensive: legacy single-arg overload {@code parseRef(spec)} (без property
   * schema) сохраняет старый contract. Используется внешними тестами и orphan
   * scanner (не v1 API но shared parser).
   */
  @Test
  void v1_2_1_schemaParser_singleArgStillWorks() {
    var parsed = ReferenceValidator.parseRef("satellite_type.code");
    assertThat(parsed.onClose()).isEqualTo(OnCloseAction.BLOCK);
    assertThat(parsed.dictionaryName()).isEqualTo("satellite_type");
    assertThat(parsed.fieldName()).isEqualTo("code");
  }

  /**
   * Schema без x-references вообще — full no-op, validate возвращает пустой
   * list errors. Покрывает 30+ legacy справочников что не имеют FK.
   */
  @Test
  void v1_2_1_schemaWithoutXReferences_isFullNoOp() throws Exception {
    JsonNode schema = OM.readTree("""
        {"type":"object","properties":{
          "name":{"type":"string"},"code":{"type":"string"}
        }}""");
    JsonNode data = OM.readTree("{\"name\":\"Spacecraft Alpha\",\"code\":\"SC-001\"}");

    List<ValidationError> errors = validator.validate(
        schema, data, List.of(DataScope.PUBLIC), OffsetDateTime.now());
    assertThat(errors).isEmpty();
  }

  // === Helpers ===

  private static DictionaryRecord stubRecord(UUID dictId) {
    return new DictionaryRecord(
        UUID.randomUUID(), dictId, "stub-key",
        OM.createObjectNode(), DataScope.PUBLIC, OffsetDateTime.now());
  }
}
