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

import cloud.nstart.terravault.ordinis.domain.audit.AuditLogRepository;
import cloud.nstart.terravault.ordinis.domain.record.DictionaryRecordRepository;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.test.web.servlet.MvcResult;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;

import java.util.List;

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

/**
 * Smoke e2e: Spring Boot context поднимается с реальным Postgres + Kafka
 * (Testcontainers), создаём dictionary через POST /api/v1/dictionaries,
 * добавляем record через POST .../records, проверяем что:
 * - Запись персистнулась в Postgres (через repo)
 * - audit_log получил CREATE-entry
 * - GET /api/v1/dictionaries/{name}/records/{key} возвращает её обратно
 *
 * <p>Этап 1 покрытия. Bitemporal flows / idempotency / outbox→Kafka — Этап 2.
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ActiveProfiles("test")
class SmokeE2ETest {

  @DynamicPropertySource
  static void props(DynamicPropertyRegistry registry) {
    E2ESupport.registerProperties(registry);
  }

  @Autowired MockMvc mvc;
  @Autowired ObjectMapper om;
  @Autowired DictionaryRecordRepository recordRepo;
  @Autowired AuditLogRepository auditRepo;

  @Test
  void contextLoadsAndCreatesRecord_endToEnd() throws Exception {
    // Уникальный suffix чтобы при testcontainers reuse=true тест был идемпотентен.
    String dictName = "smoke_" + java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 8);
    String businessKey = "SMK_" + java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase();
    long auditCountBefore = auditRepo.count();

    // 1. Создать dictionary
    JsonNode schema = om.readTree("""
        {
          "$schema": "http://json-schema.org/draft-07/schema#",
          "type": "object",
          "additionalProperties": false,
          "x-id-source": "code",
          "required": ["code", "label"],
          "properties": {
            "code":  { "type": "string", "pattern": "^[A-Z][A-Z0-9_]{0,31}$", "x-unique": true },
            "label": { "type": "string", "minLength": 1 }
          }
        }
        """);

    var createDictBody = om.createObjectNode()
        .put("name", dictName)
        .put("scope", "PUBLIC")
        .put("displayName", "Smoke test dict")
        .put("schemaVersion", "1.0.0")
        .put("bundle", "test")
        .set("schemaJson", schema);

    mvc.perform(post("/api/v1/dictionaries")
            .contentType(MediaType.APPLICATION_JSON)
            .content(om.writeValueAsBytes(createDictBody)))
        .andExpect(status().is2xxSuccessful());

    // 2. Создать record. CreateRecordRequest.businessKey @NotBlank — для
    // x-id-source-схем фронт деривит и передаёт явно. Бэк потом проверит
    // совпадение с data.code (см. resolveBusinessKey).
    var createRecBody = om.createObjectNode();
    createRecBody.put("businessKey", businessKey);
    createRecBody.set("data", om.createObjectNode()
        .put("code", businessKey)
        .put("label", "Alpha record"));

    MvcResult recResult = mvc.perform(post("/api/v1/dictionaries/{name}/records", dictName)
            .contentType(MediaType.APPLICATION_JSON)
            .content(om.writeValueAsBytes(createRecBody)))
        .andExpect(status().is2xxSuccessful())
        .andExpect(jsonPath("$.businessKey").value(businessKey))
        .andReturn();

    JsonNode recResp = om.readTree(recResult.getResponse().getContentAsString());
    String recordId = recResp.get("id").asText();
    assertThat(recordId).isNotBlank();

    // 3. Проверить что запись в репо
    var allForKey = recordRepo.findAll().stream()
        .filter(r -> businessKey.equals(r.getBusinessKey()))
        .toList();
    assertThat(allForKey).hasSize(1);
    assertThat(allForKey.get(0).getDataScope().name()).isEqualTo("PUBLIC");

    // 4. GET через writer (read-api контроллер живёт в отдельном модуле,
    // здесь admin-write путь)
    mvc.perform(get("/api/v1/dictionaries/{name}/records/{key}", dictName, businessKey))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.businessKey").value(businessKey))
        .andExpect(jsonPath("$.data.label").value("Alpha record"));

    // 5. audit_log пополнился (≥1 CREATE entry).
    long auditCountAfter = auditRepo.count();
    assertThat(auditCountAfter).isGreaterThan(auditCountBefore);
    var newEntries = auditRepo.findAll().stream()
        .filter(a -> dictName.equals(a.getDictionaryName()))
        .filter(a -> "CREATE".equals(a.getAction()))
        .toList();
    assertThat(newEntries).isNotEmpty();
    assertThat(newEntries.get(0).getBusinessKey()).isEqualTo(businessKey);
  }

  /**
   * Regression: unrouted path должен возвращать 404 not_found, не 500
   * internal_error.
   *
   * <p>До MR-fix: NoResourceFoundException попадал в generic Exception handler
   * в GlobalExceptionHandler → 500. Симптомы на prod включали:
   * <ul>
   *   <li>{@code /swagger-ui/index.html} при disabled springdoc → 500 вместо 404
   *       (UI не рендерился но logs спамились "Unhandled exception").</li>
   *   <li>Integrator'ы при typo в endpoint path получали 500 — затрудняло
   *       debugging без traceId correlation.</li>
   * </ul>
   *
   * <p>Тест проверяет API-typo путь — он независим от env (springdoc может
   * быть enabled/disabled, не влияет). Проверка swagger-ui→404 при disabled
   * springdoc относится к prod integration, не к unit-уровню handler'а.
   */
  @Test
  void unknownPath_returns404WithNotFoundCode() throws Exception {
    mvc.perform(get("/api/v1/this-endpoint-does-not-exist"))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.status").value(404))
        .andExpect(jsonPath("$.code").value("not_found"));

    // Generic non-API typo тоже должен 404'нуться (covers static resource
    // fallback branch NoResourceFoundException — этот путь не SPA-routed).
    mvc.perform(get("/totally-bogus-path-xyz-12345.unknown"))
        .andExpect(status().isNotFound())
        .andExpect(jsonPath("$.status").value(404))
        .andExpect(jsonPath("$.code").value("not_found"));
  }
}
