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.restapi.dto.PatchSchemaRequest;
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.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.Mockito.mock;
import static org.mockito.Mockito.when;

class SchemaPatchServiceTest {

  private static final ObjectMapper OM = new ObjectMapper();

  private DictionaryDefinitionRepository repo;
  private DictionaryDefinitionService defService;
  private TestScopeContext scopeCtx;
  private SchemaPatchService service;

  @BeforeEach
  void setUp() {
    repo = mock(DictionaryDefinitionRepository.class);
    defService = new DictionaryDefinitionService(repo, null);
    scopeCtx = new TestScopeContext();
    service = new SchemaPatchService(repo, defService, scopeCtx);
    when(repo.save(org.mockito.ArgumentMatchers.any(DictionaryDefinition.class)))
        .thenAnswer(inv -> inv.getArgument(0));
  }

  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 patch_versionMismatch_409() throws Exception {
    var d = def("dict", "1.4.0",
        "{\"properties\":{\"altitude\":{\"type\":\"integer\"}}}");
    when(repo.findByName("dict")).thenReturn(Optional.of(d));
    var req = new PatchSchemaRequest("1.0.0", "altitude",
        OM.readTree("{\"description\":\"updated\"}"));
    assertThatThrownBy(() -> service.patchSchema("dict", req))
        .isInstanceOfSatisfying(OrdinisException.class,
            e -> assertThat(e.getCode()).isEqualTo("version_mismatch"));
  }

  @Test
  void patch_fieldNotFound_404() throws Exception {
    var d = def("dict", "1.4.0",
        "{\"properties\":{\"altitude\":{\"type\":\"integer\"}}}");
    when(repo.findByName("dict")).thenReturn(Optional.of(d));
    var req = new PatchSchemaRequest("1.4.0", "mass",
        OM.readTree("{\"description\":\"x\"}"));
    assertThatThrownBy(() -> service.patchSchema("dict", req))
        .isInstanceOfSatisfying(OrdinisException.class,
            e -> assertThat(e.getCode()).isEqualTo("field_not_found"));
  }

  @Test
  void patch_breakingKey_422() throws Exception {
    var d = def("dict", "1.4.0",
        "{\"properties\":{\"altitude\":{\"type\":\"integer\"}}}");
    when(repo.findByName("dict")).thenReturn(Optional.of(d));
    var req = new PatchSchemaRequest("1.4.0", "altitude",
        OM.readTree("{\"type\":\"string\"}"));
    assertThatThrownBy(() -> service.patchSchema("dict", req))
        .isInstanceOfSatisfying(OrdinisException.class,
            e -> assertThat(e.getCode()).isEqualTo("breaking_change_requires_draft"));
  }

  @Test
  void patch_descriptionUpdate_bumpsPatchVersion() throws Exception {
    var d = def("dict", "1.4.0",
        "{\"properties\":{\"altitude\":{\"type\":\"integer\"}}}");
    when(repo.findByName("dict")).thenReturn(Optional.of(d));
    var req = new PatchSchemaRequest("1.4.0", "altitude",
        OM.readTree("{\"description\":{\"ru\":\"Высота орбиты\",\"en\":\"Altitude\"}}"));

    var result = service.patchSchema("dict", req);

    assertThat(result.newVersion()).isEqualTo("1.4.1");
    JsonNode altitude = result.schemaJson().get("properties").get("altitude");
    assertThat(altitude.get("description").get("ru").asText()).isEqualTo("Высота орбиты");
  }

  @Test
  void patch_minimumTighter_422() throws Exception {
    var d = def("dict", "1.4.0",
        "{\"properties\":{\"altitude\":{\"type\":\"integer\",\"minimum\":0}}}");
    when(repo.findByName("dict")).thenReturn(Optional.of(d));
    var req = new PatchSchemaRequest("1.4.0", "altitude",
        OM.readTree("{\"minimum\":100}"));
    assertThatThrownBy(() -> service.patchSchema("dict", req))
        .isInstanceOfSatisfying(OrdinisException.class,
            e -> assertThat(e.getCode()).isEqualTo("validation_failed"));
  }

  @Test
  void patch_minimumExpanded_ok() throws Exception {
    var d = def("dict", "1.4.0",
        "{\"properties\":{\"altitude\":{\"type\":\"integer\",\"minimum\":100}}}");
    when(repo.findByName("dict")).thenReturn(Optional.of(d));
    var req = new PatchSchemaRequest("1.4.0", "altitude",
        OM.readTree("{\"minimum\":0}"));

    var result = service.patchSchema("dict", req);

    assertThat(result.newVersion()).isEqualTo("1.4.1");
  }

  @Test
  void patch_enumRemove_422() throws Exception {
    var d = def("dict", "1.4.0", """
        {"properties":{"status":{"type":"string","enum":["live","draft","archived"]}}}""");
    when(repo.findByName("dict")).thenReturn(Optional.of(d));
    var req = new PatchSchemaRequest("1.4.0", "status",
        OM.readTree("{\"enum\":[\"live\",\"draft\"]}"));
    assertThatThrownBy(() -> service.patchSchema("dict", req))
        .isInstanceOfSatisfying(OrdinisException.class,
            e -> assertThat(e.getCode()).isEqualTo("validation_failed"));
  }

  @Test
  void patch_enumAdd_ok() throws Exception {
    var d = def("dict", "1.4.0",
        "{\"properties\":{\"status\":{\"type\":\"string\",\"enum\":[\"live\"]}}}");
    when(repo.findByName("dict")).thenReturn(Optional.of(d));
    var req = new PatchSchemaRequest("1.4.0", "status",
        OM.readTree("{\"enum\":[\"live\",\"draft\",\"archived\"]}"));

    var result = service.patchSchema("dict", req);

    assertThat(result.newVersion()).isEqualTo("1.4.1");
    assertThat(result.schemaJson().get("properties").get("status").get("enum"))
        .hasSize(3);
  }

  @Test
  void bumpPatchVersion_basic() {
    assertThat(SchemaPatchService.bumpPatchVersion("1.4.0")).isEqualTo("1.4.1");
    assertThat(SchemaPatchService.bumpPatchVersion("0.0.99")).isEqualTo("0.0.100");
    assertThat(SchemaPatchService.bumpPatchVersion("2.0.0")).isEqualTo("2.0.1");
  }

  @Test
  void bumpPatchVersion_nonSemver() {
    assertThat(SchemaPatchService.bumpPatchVersion("v1")).isEqualTo("v1.1");
    assertThat(SchemaPatchService.bumpPatchVersion("")).isEqualTo("1.0.1");
    assertThat(SchemaPatchService.bumpPatchVersion(null)).isEqualTo("1.0.1");
  }

  private static DictionaryDefinition def(String name, String version, String schemaJson) {
    JsonNode schema;
    try {
      schema = OM.readTree(schemaJson);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
    var d = new DictionaryDefinition(UUID.randomUUID(), name, DataScope.PUBLIC, schema);
    d.setSchemaVersion(version);
    return d;
  }
}
