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

import cloud.nstart.terravault.ordinis.domain.DataScope;
import cloud.nstart.terravault.ordinis.domain.definition.DictionaryDefinition;
import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecord;
import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecordRepository;
import cloud.nstart.terravault.ordinis.outbox.OutboxRecorder;
import cloud.nstart.terravault.ordinis.restapi.dto.CreateRecordRequest;
import cloud.nstart.terravault.ordinis.restapi.error.OrdinisException;
import cloud.nstart.terravault.ordinis.validation.RecordValidator;
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.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 DictionaryRecordServiceTest {

  private final ObjectMapper om = new ObjectMapper();
  private DictionaryRecordRepository repo;
  private DictionaryRecordService service;

  private static final UUID DICT_ID = UUID.fromString("00000000-0000-0000-0000-000000000001");

  @BeforeEach
  void setUp() {
    repo = mock(DictionaryRecordRepository.class);
    // resolveBusinessKey/enforceUniqueFields не дёргают остальные деп — передаём null,
    // mocking concrete Spring services через Mockito-inline на JDK 25 ломается.
    service = new DictionaryRecordService(repo, null, null, null, null, null, null);
  }

  // ============= resolveBusinessKey =============

  @Test
  void resolveReturnsExplicitKeyWhenNoIdSource() throws Exception {
    var d = definitionWithSchema("""
        { "type":"object", "properties": {} }
        """);
    var req = new CreateRecordRequest("MY_KEY", om.readTree("{}"), null, null, null);
    assertThat(service.resolveBusinessKey(d, req)).isEqualTo("MY_KEY");
  }

  @Test
  void resolveThrowsWhenNoIdSourceAndNoExplicitKey() throws Exception {
    var d = definitionWithSchema("""
        { "type":"object", "properties": {} }
        """);
    var req = new CreateRecordRequest(null, om.readTree("{}"), null, null, null);
    assertThatThrownBy(() -> service.resolveBusinessKey(d, req))
        .isInstanceOfSatisfying(OrdinisException.class,
            ex -> assertThat(ex.getCode()).isEqualTo("business_key_required"));
  }

  @Test
  void resolveDerivesKeyFromIdSourceField() throws Exception {
    var d = definitionWithSchema("""
        { "type":"object", "x-id-source":"code",
          "properties": {"code":{"type":"string"}} }
        """);
    var req = new CreateRecordRequest(null, om.readTree("{\"code\":\"AUTO_KEY\"}"), null, null, null);
    assertThat(service.resolveBusinessKey(d, req)).isEqualTo("AUTO_KEY");
  }

  @Test
  void resolveAcceptsExplicitKeyWhenItMatchesIdSource() throws Exception {
    var d = definitionWithSchema("""
        { "type":"object", "x-id-source":"code",
          "properties": {"code":{"type":"string"}} }
        """);
    var req = new CreateRecordRequest("AUTO_KEY", om.readTree("{\"code\":\"AUTO_KEY\"}"), null, null, null);
    assertThat(service.resolveBusinessKey(d, req)).isEqualTo("AUTO_KEY");
  }

  @Test
  void resolveThrowsOnMismatchBetweenExplicitAndDerived() throws Exception {
    var d = definitionWithSchema("""
        { "type":"object", "x-id-source":"code",
          "properties": {"code":{"type":"string"}} }
        """);
    var req = new CreateRecordRequest("WRONG", om.readTree("{\"code\":\"AUTO_KEY\"}"), null, null, null);
    assertThatThrownBy(() -> service.resolveBusinessKey(d, req))
        .isInstanceOfSatisfying(OrdinisException.class,
            ex -> assertThat(ex.getCode()).isEqualTo("business_key_mismatch"));
  }

  @Test
  void resolveThrowsWhenIdSourceFieldMissingInData() throws Exception {
    var d = definitionWithSchema("""
        { "type":"object", "x-id-source":"code",
          "properties": {"code":{"type":"string"}} }
        """);
    var req = new CreateRecordRequest(null, om.readTree("{}"), null, null, null);
    assertThatThrownBy(() -> service.resolveBusinessKey(d, req))
        .isInstanceOfSatisfying(OrdinisException.class,
            ex -> assertThat(ex.getCode()).isEqualTo("id_source_missing"));
  }

  // ============= enforceUniqueFields =============

  @Test
  void enforceNoUniqueFieldsIsNoOp() throws Exception {
    var d = definitionWithSchema("""
        { "type":"object",
          "properties": {"code":{"type":"string"}} }
        """);
    JsonNode data = om.readTree("{\"code\":\"X\"}");
    service.enforceUniqueFields(d, data, OffsetDateTime.now(), null);
    // нет вызовов в repo — just no exception
  }

  @Test
  void enforceUniqueFieldNoConflict() throws Exception {
    var d = definitionWithSchema("""
        { "type":"object",
          "properties": {"code":{"type":"string","x-unique":true}} }
        """);
    when(repo.findActiveByJsonField(eq(DICT_ID), any(), eq("code"), eq("X")))
        .thenReturn(List.of());

    JsonNode data = om.readTree("{\"code\":\"X\"}");
    service.enforceUniqueFields(d, data, OffsetDateTime.now(), null);
  }

  @Test
  void enforceUniqueFieldConflictThrows409() throws Exception {
    var d = definitionWithSchema("""
        { "type":"object",
          "properties": {"code":{"type":"string","x-unique":true}} }
        """);
    var conflict = new DictionaryRecord(
        UUID.randomUUID(), DICT_ID, "OTHER",
        om.readTree("{\"code\":\"X\"}"), DataScope.PUBLIC, OffsetDateTime.now());
    when(repo.findActiveByJsonField(eq(DICT_ID), any(), eq("code"), eq("X")))
        .thenReturn(List.of(conflict));

    JsonNode data = om.readTree("{\"code\":\"X\"}");
    assertThatThrownBy(() -> service.enforceUniqueFields(d, data, OffsetDateTime.now(), null))
        .isInstanceOfSatisfying(OrdinisException.class,
            ex -> assertThat(ex.getCode()).isEqualTo("unique_field_violation"));
  }

  @Test
  void enforceUniqueFieldExcludesOwnRecordOnUpdate() throws Exception {
    var d = definitionWithSchema("""
        { "type":"object",
          "properties": {"code":{"type":"string","x-unique":true}} }
        """);
    UUID ownId = UUID.randomUUID();
    var own = new DictionaryRecord(
        ownId, DICT_ID, "MY_KEY",
        om.readTree("{\"code\":\"X\"}"), DataScope.PUBLIC, OffsetDateTime.now());
    when(repo.findActiveByJsonField(eq(DICT_ID), any(), eq("code"), eq("X")))
        .thenReturn(List.of(own));

    JsonNode data = om.readTree("{\"code\":\"X\"}");
    // Не должно бросать — собственная запись исключается из проверки
    service.enforceUniqueFields(d, data, OffsetDateTime.now(), ownId);
  }

  @Test
  void enforceSkipsNullValueFields() throws Exception {
    var d = definitionWithSchema("""
        { "type":"object",
          "properties": {
            "code":{"type":"string","x-unique":true},
            "alias":{"type":"string","x-unique":true}
          }
        }
        """);
    when(repo.findActiveByJsonField(eq(DICT_ID), any(), anyString(), anyString()))
        .thenReturn(List.of());

    JsonNode data = om.readTree("{\"code\":\"X\"}"); // alias absent
    service.enforceUniqueFields(d, data, OffsetDateTime.now(), null);
    // Repository вызывается только для code, не для alias — проверка проходит
  }

  // ============= helpers =============

  private DictionaryDefinition definitionWithSchema(String schemaJson) throws Exception {
    return new DictionaryDefinition(DICT_ID, "test_dict", DataScope.PUBLIC, om.readTree(schemaJson));
  }
}
