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

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.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpHeaders;
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.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;

/**
 * Этап 2c: JWT auth + scope mapping.
 *
 * <ul>
 *   <li>{@code ORDINIS_AUTH_REQUIRED=true} — anonymous → 401</li>
 *   <li>JWT с {@code realm_access.roles=[ordinis:client:public]} → доступ к PUBLIC</li>
 *   <li>JWT с {@code ordinis:client:internal} → доступ к INTERNAL</li>
 *   <li>Public-юзер пытается читать INTERNAL — пустой результат (scope filter), не 403</li>
 * </ul>
 *
 * <p>Тестовый Keycloak — embedded JWKS HTTP server (см. {@link JwtTestSupport}).
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ActiveProfiles("test")
class AuthE2ETest {

  @DynamicPropertySource
  static void props(DynamicPropertyRegistry registry) {
    E2ESupport.registerProperties(registry);
    // Включаем JWT валидацию через embedded JWKS server.
    registry.add("ordinis.auth.jwk-set-uri", JwtTestSupport::jwksUrl);
    registry.add("ordinis.auth.require-authentication", () -> "true");
    registry.add("ordinis.auth.allow-query-scope", () -> "false");
    // issuerUri не задаём — JwtValidators.createDefault() (без iss проверки)
    registry.add("ordinis.auth.issuer-uri", () -> "");
  }

  @Autowired MockMvc mvc;
  @Autowired ObjectMapper om;

  @Test
  void anonymousRequest_returns401() throws Exception {
    mvc.perform(get("/api/v1/dictionaries"))
        .andExpect(status().isUnauthorized());
  }

  @Test
  void invalidJwt_returns401() throws Exception {
    mvc.perform(get("/api/v1/dictionaries")
            .header(HttpHeaders.AUTHORIZATION, "Bearer not.a.valid.jwt"))
        .andExpect(status().isUnauthorized());
  }

  @Test
  void publicUser_canListDictionaries() throws Exception {
    String token = JwtTestSupport.signJwt(List.of("ordinis:client:public"), "user-public");

    mvc.perform(get("/api/v1/dictionaries")
            .header(HttpHeaders.AUTHORIZATION, "Bearer " + token))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$").isArray());
  }

  @Test
  void publicUser_cannotSeeInternalRecords() throws Exception {
    // Создаём INTERNAL словарь от имени internal-юзера
    String dictName = "auth_int_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8);
    String internalToken = JwtTestSupport.signJwt(
        List.of("ordinis:client:internal"), "user-internal");

    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 } }
        }
        """);
    var dictBody = om.createObjectNode()
        .put("name", dictName)
        .put("scope", "INTERNAL")
        .put("schemaVersion", "1.0.0")
        .put("bundle", "test");
    dictBody.set("schemaJson", schema);
    mvc.perform(post("/api/v1/dictionaries")
            .header(HttpHeaders.AUTHORIZATION, "Bearer " + internalToken)
            .contentType(MediaType.APPLICATION_JSON)
            .content(om.writeValueAsBytes(dictBody)))
        .andExpect(status().is2xxSuccessful());

    String key = "INT_REC_1";
    var recBody = om.createObjectNode().put("businessKey", key);
    recBody.set("data", om.createObjectNode().put("code", key));
    mvc.perform(post("/api/v1/dictionaries/{n}/records", dictName)
            .header(HttpHeaders.AUTHORIZATION, "Bearer " + internalToken)
            .contentType(MediaType.APPLICATION_JSON)
            .content(om.writeValueAsBytes(recBody)))
        .andExpect(status().is2xxSuccessful());

    // Writer endpoint фильтрует по scope: PUBLIC-юзер не видит INTERNAL словарь.
    // Возвращаем 404 (а не 403) — намеренно скрываем существование INTERNAL
    // данных, как делает read-api. Защита от enumeration.
    String publicToken = JwtTestSupport.signJwt(List.of("ordinis:client:public"), "user-public");
    mvc.perform(get("/api/v1/dictionaries/{n}/records/{k}", dictName, key)
            .header(HttpHeaders.AUTHORIZATION, "Bearer " + publicToken))
        .andExpect(status().isNotFound());
  }

  @Test
  void internalUser_canSeeInternalRecord() throws Exception {
    String dictName = "auth_int2_" + UUID.randomUUID().toString().replace("-", "").substring(0, 8);
    String internalToken = JwtTestSupport.signJwt(
        List.of("ordinis:client:internal"), "user-internal");

    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 } }
        }
        """);
    var dictBody = om.createObjectNode()
        .put("name", dictName)
        .put("scope", "INTERNAL")
        .put("schemaVersion", "1.0.0")
        .put("bundle", "test");
    dictBody.set("schemaJson", schema);
    mvc.perform(post("/api/v1/dictionaries")
            .header(HttpHeaders.AUTHORIZATION, "Bearer " + internalToken)
            .contentType(MediaType.APPLICATION_JSON)
            .content(om.writeValueAsBytes(dictBody)))
        .andExpect(status().is2xxSuccessful());

    String key = "INT2_KEY";
    var recBody = om.createObjectNode().put("businessKey", key);
    recBody.set("data", om.createObjectNode().put("code", key));
    mvc.perform(post("/api/v1/dictionaries/{n}/records", dictName)
            .header(HttpHeaders.AUTHORIZATION, "Bearer " + internalToken)
            .contentType(MediaType.APPLICATION_JSON)
            .content(om.writeValueAsBytes(recBody)))
        .andExpect(status().is2xxSuccessful());

    // Internal токен видит свою же запись.
    mvc.perform(get("/api/v1/dictionaries/{n}/records/{k}", dictName, key)
            .header(HttpHeaders.AUTHORIZATION, "Bearer " + internalToken))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.businessKey").value(key));
  }

  @Test
  void unrecognizedRole_fallsBackToPublic() throws Exception {
    String token = JwtTestSupport.signJwt(List.of("admin", "viewer"), "user-misc");
    mvc.perform(get("/api/v1/dictionaries")
            .header(HttpHeaders.AUTHORIZATION, "Bearer " + token))
        .andExpect(status().isOk());
  }
}
