package cloud.nstart.terravault.ordinis.app.e2e;

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 com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.Test;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.Point;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.web.servlet.MockMvc;

import java.util.List;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.offset;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

/**
 * E2E: backend auto-derive geometry из data.lat/lon при create. Покрывает
 * {@code DictionaryRecordService.resolveGeometry()} — все три ветки (явный
 * geometryWkt, auto-derive из lat/lon, alias latitude/longitude и lat/lng).
 *
 * <p>Тест отражает Liquibase migration 0015: backfill для legacy записей
 * без geometry при наличии lat/lon в data — чтобы AOI spatial filter работал
 * на ground_station и similar dictionaries.
 *
 * <p>Postgres testcontainer уже включает PostGIS (см. {@link E2ESupport}).
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
    properties = {
        "ordinis.outbox.enabled=false",
        "spring.autoconfigure.exclude="
            + "org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration",
    })
@AutoConfigureMockMvc
@ActiveProfiles("test")
class AutoDeriveGeometryE2ETest {

  @DynamicPropertySource
  static void props(DynamicPropertyRegistry registry) {
    // Postgres-only: outbox disabled → Kafka не нужна, экономим ~5s startup
    E2ESupport.registerPostgresOnlyProperties(registry);
  }

  @Autowired MockMvc mvc;
  @Autowired ObjectMapper om;
  @Autowired DictionaryRecordRepository recordRepo;
  @Autowired DictionaryDefinitionRepository definitionRepo;

  /** Schema с lat/lon (или alias) числовыми полями. */
  private String setupDict(String latKey, String lonKey) throws Exception {
    String dict = "geo_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8);
    JsonNode schema = om.readTree("""
        {
          "$schema": "http://json-schema.org/draft-07/schema#",
          "type": "object",
          "additionalProperties": false,
          "x-id-source": "code",
          "required": ["code"],
          "properties": {
            "code":     { "type": "string", "x-unique": true },
            "%s": { "type": "number" },
            "%s": { "type": "number" }
          }
        }
        """.formatted(latKey, lonKey));

    var body = om.createObjectNode()
        .put("name", dict)
        .put("scope", "PUBLIC")
        .put("schemaVersion", "1.0.0")
        .put("bundle", "test");
    body.set("schemaJson", schema);
    mvc.perform(post("/api/v1/dictionaries")
            .contentType(MediaType.APPLICATION_JSON)
            .content(om.writeValueAsBytes(body)))
        .andExpect(status().is2xxSuccessful());

    return dict;
  }

  private ObjectNode recordBody(String code, double lat, double lon, String latKey, String lonKey) {
    ObjectNode body = om.createObjectNode().put("businessKey", code);
    ObjectNode data = om.createObjectNode().put("code", code);
    data.put(latKey, lat);
    data.put(lonKey, lon);
    body.set("data", data);
    return body;
  }

  @Test
  void latLon_autoDerivedToPoint() throws Exception {
    String dict = setupDict("lat", "lon");
    String code = "MOSCOW";

    // Москва: 55.75, 37.62
    mvc.perform(post("/api/v1/dictionaries/{n}/records", dict)
            .contentType(MediaType.APPLICATION_JSON)
            .content(om.writeValueAsBytes(recordBody(code, 55.75, 37.62, "lat", "lon"))))
        .andExpect(status().is2xxSuccessful());

    Point pt = assertOnePointSaved(dict);
    // PostGIS возвращает coords в JTS Point: getX = longitude, getY = latitude
    assertThat(pt.getX()).isCloseTo(37.62, offset(0.001));
    assertThat(pt.getY()).isCloseTo(55.75, offset(0.001));
    assertThat(pt.getSRID()).isEqualTo(4326);
  }

  @Test
  void latitudeLongitude_aliasAutoDerived() throws Exception {
    String dict = setupDict("latitude", "longitude");
    String code = "SPB";

    // СПб: 59.93, 30.31
    mvc.perform(post("/api/v1/dictionaries/{n}/records", dict)
            .contentType(MediaType.APPLICATION_JSON)
            .content(om.writeValueAsBytes(
                recordBody(code, 59.93, 30.31, "latitude", "longitude"))))
        .andExpect(status().is2xxSuccessful());

    Point pt = assertOnePointSaved(dict);
    assertThat(pt.getX()).isCloseTo(30.31, offset(0.001));
    assertThat(pt.getY()).isCloseTo(59.93, offset(0.001));
  }

  @Test
  void latLng_aliasAutoDerived() throws Exception {
    String dict = setupDict("lat", "lng");
    String code = "TOKYO";

    // Токио: 35.68, 139.65
    mvc.perform(post("/api/v1/dictionaries/{n}/records", dict)
            .contentType(MediaType.APPLICATION_JSON)
            .content(om.writeValueAsBytes(recordBody(code, 35.68, 139.65, "lat", "lng"))))
        .andExpect(status().is2xxSuccessful());

    Point pt = assertOnePointSaved(dict);
    assertThat(pt.getX()).isCloseTo(139.65, offset(0.001));
    assertThat(pt.getY()).isCloseTo(35.68, offset(0.001));
  }

  @Test
  void explicitGeometryWkt_takesPriorityOverData() throws Exception {
    String dict = setupDict("lat", "lon");
    String code = "EXPLICIT";

    // data говорит "Москва", но geometryWkt явно указан как "СПб".
    // Backend WKTReader (JTS) не принимает SRID=4326; prefix — SRID хардкодится
    // на 4326 в коде (parseWkt). WKT без prefix'а: POINT(lon lat).
    ObjectNode body = recordBody(code, 55.75, 37.62, "lat", "lon");
    body.put("geometryWkt", "POINT(30.31 59.93)");

    mvc.perform(post("/api/v1/dictionaries/{n}/records", dict)
            .contentType(MediaType.APPLICATION_JSON)
            .content(om.writeValueAsBytes(body)))
        .andExpect(status().is2xxSuccessful());

    Point pt = assertOnePointSaved(dict);
    // Должен быть СПб (explicit), а не Москва (data)
    assertThat(pt.getX()).isCloseTo(30.31, offset(0.001));
    assertThat(pt.getY()).isCloseTo(59.93, offset(0.001));
  }

  @Test
  void outOfRangeLatLon_silentlySkipped() throws Exception {
    String dict = setupDict("lat", "lon");

    // lat=200 — out of [-90,90] range. Backend silent skip → geometry null.
    mvc.perform(post("/api/v1/dictionaries/{n}/records", dict)
            .contentType(MediaType.APPLICATION_JSON)
            .content(om.writeValueAsBytes(recordBody("BAD_LAT", 200.0, 50.0, "lat", "lon"))))
        .andExpect(status().is2xxSuccessful());

    // lon=300 — out of [-180,180] range
    mvc.perform(post("/api/v1/dictionaries/{n}/records", dict)
            .contentType(MediaType.APPLICATION_JSON)
            .content(om.writeValueAsBytes(recordBody("BAD_LON", 50.0, 300.0, "lat", "lon"))))
        .andExpect(status().is2xxSuccessful());

    List<DictionaryRecord> records = recordsOfDict(dict);
    assertThat(records).hasSize(2);
    assertThat(records).allMatch(r -> r.getGeometry() == null,
        "записи с out-of-range coords должны иметь geometry == null");
  }

  @Test
  void noLatLon_geometryRemainsNull() throws Exception {
    String dict = setupDict("lat", "lon");
    // Запись без lat/lon в data — geometry должна быть null
    ObjectNode body = om.createObjectNode().put("businessKey", "NO_GEO");
    body.set("data", om.createObjectNode().put("code", "NO_GEO"));
    mvc.perform(post("/api/v1/dictionaries/{n}/records", dict)
            .contentType(MediaType.APPLICATION_JSON)
            .content(om.writeValueAsBytes(body)))
        .andExpect(status().is2xxSuccessful());

    List<DictionaryRecord> records = recordsOfDict(dict);
    assertThat(records).hasSize(1);
    assertThat(records.get(0).getGeometry()).isNull();
  }

  // ---------- helpers ----------

  /** Достаёт все записи словаря из repository. Берём всё, dict свежий. */
  private List<DictionaryRecord> recordsOfDict(String dictName) {
    UUID dictId = definitionRepo.findByName(dictName)
        .orElseThrow(() -> new AssertionError("dict not found: " + dictName))
        .getId();
    return recordRepo.findAll().stream()
        .filter(r -> dictId.equals(r.getDictionaryId()))
        .toList();
  }

  /** Ожидает ровно одну запись в словаре с не-null Point geometry. */
  private Point assertOnePointSaved(String dict) {
    List<DictionaryRecord> records = recordsOfDict(dict);
    assertThat(records).hasSize(1);
    Geometry g = records.get(0).getGeometry();
    assertThat(g).isNotNull().isInstanceOf(Point.class);
    return (Point) g;
  }
}
