package com.cloudhandson.vpdbackoffice.service;

import static org.assertj.core.api.Assertions.assertThat;
import com.cloudhandson.vpdbackoffice.config.BackofficeProperties;
import com.cloudhandson.vpdbackoffice.config.GameScopeProperties;
import com.cloudhandson.vpdbackoffice.config.McpProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.Test;

class McpSseServiceTest {

  private final ObjectMapper objectMapper = new ObjectMapper();
  private final SelectAiService selectAiService = new CapturingSelectAiService();
  private final QaVectorService qaVectorService = new CapturingQaVectorService();
  private final GameScopeService gameScopeService = new CapturingGameScopeService();
  private final GameCatalogVectorService gameCatalogVectorService =
      new CapturingGameCatalogVectorService();
  private final McpSseService service = new McpSseService(
      selectAiService,
      objectMapper,
      new BackofficeProperties(
          null,
          null,
          null,
          null,
          new BackofficeProperties.SelectAi(
              "", "", "", "SGMP_POC_OCI_GPT54MINI", true, 3)
      ),
      new McpProperties(
          "oracle.select_ai.test_data_text2sql",
          "테스트 데이터 Text2SQL",
          "테스트 데이터용 읽기 전용 SQL을 생성하고 실행합니다.",
          "테스트 데이터의 조회 내용을 입력합니다.",
          "oracle.select_ai.test_data_showprompt",
          "테스트 데이터 SHOWPROMPT",
          "테스트 데이터의 Select AI prompt를 조회합니다.",
          "oracle.select_ai.test_qa_vector_search",
          "테스트 예제 SQL 조회",
          "테스트 질문에 사용할 예제 SQL을 조회합니다.",
          "oracle.select_ai.test_qa_vector_store",
          "테스트 예제 SQL 저장",
          "테스트 예제 SQL을 저장합니다.",
          "oracle.select_ai.test_fewshot_nl2sql",
          "테스트 Few-shot NL2SQL",
          "테스트 Few-shot 검색 및 실행 도구입니다.",
          "oracle.select_ai.test_game_scope_resolve",
          "테스트 게임 범위 확인",
          "테스트 DB 게임 범위 확인 도구입니다."
      ),
      qaVectorService,
      gameScopeService,
      gameCatalogVectorService
  );

  @Test
  void listsQueryShowpromptAndQaVectorToolsWithTheirInputs() {
    ObjectNode response = service.handle("default", request(1, "tools/list"));

    var tools = response.path("result").path("tools");
    assertThat(tools).hasSize(8);
    var selectAi = tools.get(0);
    assertThat(selectAi.path("name").asText()).isEqualTo("oracle.select_ai.test_data_text2sql");
    assertThat(selectAi.path("description").asText()).contains("SGMP_POC_OCI_GPT54MINI");
    assertThat(selectAi.path("inputSchema").path("required"))
        .extracting(node -> node.asText())
        .contains("prompt");
    assertThat(selectAi.path("inputSchema").path("properties").has("bearerToken")).isFalse();
    assertThat(selectAi.path("inputSchema").path("properties").has("limit")).isFalse();
    assertThat(selectAi.path("inputSchema").path("properties").has("conversationId")).isFalse();

    var showprompt = tools.get(1);
    assertThat(showprompt.path("name").asText())
        .isEqualTo("oracle.select_ai.test_data_showprompt");
    assertThat(showprompt.path("description").asText())
        .contains("SGMP_POC_OCI_GPT54MINI")
        .contains("Select AI prompt");
    assertThat(showprompt.path("inputSchema").path("required"))
        .extracting(node -> node.asText())
        .contains("prompt");

    var vectorSearch = tools.get(2);
    assertThat(vectorSearch.path("name").asText())
        .isEqualTo("oracle.select_ai.test_qa_vector_search");
    assertThat(vectorSearch.path("inputSchema").path("properties").path("question").path("type").asText())
        .isEqualTo("string");
    assertThat(vectorSearch.path("inputSchema").path("properties").path("topK").path("default").asInt())
        .isEqualTo(3);

    var vectorStore = tools.get(3);
    assertThat(vectorStore.path("name").asText())
        .isEqualTo("oracle.select_ai.test_qa_vector_store");
    assertThat(vectorStore.path("inputSchema").path("required"))
        .extracting(node -> node.asText())
        .contains("question", "answerSql");

    var fewShot = tools.get(4);
    assertThat(fewShot.path("name").asText())
        .isEqualTo("oracle.select_ai.test_fewshot_nl2sql");
    assertThat(fewShot.path("description").asText()).contains("Few-shot");

    var gameScope = tools.get(5);
    assertThat(gameScope.path("name").asText())
        .isEqualTo("oracle.select_ai.test_game_scope_resolve");
    assertThat(gameScope.path("inputSchema").path("required"))
        .extracting(node -> node.asText()).contains("question");

    assertThat(tools.get(6).path("name").asText())
        .isEqualTo("oracle.select_ai.game_catalog_resolve");
    assertThat(tools.get(7).path("name").asText())
        .isEqualTo("oracle.select_ai.game_query_plan");
  }

  @Test
  void callsShowpromptWithoutExecutingGeneratedSql() {
    ObjectNode request = request(4, "tools/call");
    ObjectNode params = (ObjectNode) request.putObject("params");
    params.put("name", "oracle.select_ai.test_data_showprompt");
    params.putObject("arguments").put("prompt", "버블리즈 유저 마스터 조회해줘.");

    ObjectNode response = service.handle("default", request, "user-bearer");

    CapturingSelectAiService agentService = (CapturingSelectAiService) selectAiService;
    assertThat(agentService.bearerToken).isEqualTo("user-bearer");
    assertThat(agentService.prompt).isEqualTo("버블리즈 유저 마스터 조회해줘.");
    assertThat(agentService.showpromptCalled).isTrue();
    assertThat(response.path("result").path("isError").asBoolean()).isFalse();
    assertThat(response.path("result").path("content").get(0).path("text").asText())
        .contains("oracle.select_ai.test_data_showprompt")
        .contains("SHOWPROMPT")
        .contains("assembled Select AI prompt");
  }

  @Test
  void callsVpdSelectAiThroughOrdsService() {
    ObjectNode request = request(2, "tools/call");
    ObjectNode params = (ObjectNode) request.putObject("params");
    params.put("name", "oracle.select_ai.test_data_text2sql");
    ObjectNode arguments = params.putObject("arguments");
    arguments.put("prompt", "카제나 AU를 조회해 줘");

    ObjectNode response = service.handle("default", request, "user-bearer");

    CapturingSelectAiService agentService = (CapturingSelectAiService) selectAiService;
    assertThat(agentService.bearerToken).isEqualTo("user-bearer");
    assertThat(agentService.prompt).isEqualTo("카제나 AU를 조회해 줘");
    assertThat(response.path("error").isMissingNode()).isTrue();
    assertThat(response.path("result").path("isError").asBoolean()).isFalse();
    String payload = response.path("result").path("content").get(0).path("text").asText();
    assertThat(payload)
        .contains("SGMP_POC_OCI_GPT54MINI")
        .contains("SELECT 1 FROM DUAL")
        .contains("SHOWSQL_AND_EXECUTED")
        .contains("READ_ONLY_EXECUTED")
        .contains("RESULT");
  }

  @Test
  void callsDedicatedFewShotNl2SqlToolWithGeneratedSqlAndExecutionResult() {
    ObjectNode request = request(7, "tools/call");
    ObjectNode params = (ObjectNode) request.putObject("params");
    params.put("name", "oracle.select_ai.test_fewshot_nl2sql");
    params.putObject("arguments").put("prompt", "카제나 AU를 조회해 줘");

    ObjectNode response = service.handle("default", request, "user-bearer");

    assertThat(response.path("result").path("isError").asBoolean()).isFalse();
    String payload = response.path("result").path("content").get(0).path("text").asText();
    assertThat(payload)
        .contains("oracle.select_ai.test_fewshot_nl2sql")
        .contains("SHOWSQL_AND_EXECUTED")
        .contains("generatedSql")
        .contains("READ_ONLY_EXECUTED");
  }

  @Test
  void returnsToolLevelDeniedResultWhenVpdTokenIsMissing() {
    ObjectNode request = request(3, "tools/call");
    ObjectNode params = (ObjectNode) request.putObject("params");
    params.put("name", "oracle.select_ai.test_data_text2sql");
    params.putObject("arguments").put("prompt", "카제나 AU를 조회해 줘");

    ObjectNode response = service.handle("default", request, "");

    assertThat(response.path("error").isMissingNode()).isTrue();
    assertThat(response.path("result").path("isError").asBoolean()).isTrue();
    assertThat(response.path("result").path("content").get(0).path("text").asText())
        .contains("VPD_TOKEN_DENIED")
        .contains("권한이 없습니다");
  }

  @Test
  void searchesQaVectorExamplesForFewShotContext() {
    ObjectNode request = request(5, "tools/call");
    ObjectNode params = (ObjectNode) request.putObject("params");
    params.put("name", "oracle.select_ai.test_qa_vector_search");
    params.putObject("arguments").put("question", "active users by game").put("topK", 2);

    ObjectNode response = service.handle("default", request, "user-bearer");

    CapturingQaVectorService vectorService = (CapturingQaVectorService) qaVectorService;
    assertThat(vectorService.bearerToken).isEqualTo("user-bearer");
    assertThat(vectorService.question).isEqualTo("active users by game");
    assertThat(vectorService.topK).isEqualTo(2);
    assertThat(response.path("result").path("content").get(0).path("text").asText())
        .contains("QA_VECTOR_SEARCH")
        .contains("few-shot")
        .contains("SELECT COUNT(*) FROM APP_USER");
  }

  @Test
  void storesReviewedSelectAiExampleSql() {
    ObjectNode request = request(6, "tools/call");
    ObjectNode params = (ObjectNode) request.putObject("params");
    params.put("name", "oracle.select_ai.test_qa_vector_store");
    params.putObject("arguments")
        .put("question", "active users by game")
        .put("answerSql", "SELECT COUNT(*) FROM APP_USER")
        .put("answer", "AU count");

    ObjectNode response = service.handle("default", request, "user-bearer");

    CapturingQaVectorService vectorService = (CapturingQaVectorService) qaVectorService;
    assertThat(vectorService.answerSql).isEqualTo("SELECT COUNT(*) FROM APP_USER");
    assertThat(response.path("result").path("content").get(0).path("text").asText())
        .contains("QA_VECTOR_STORED")
        .contains("exampleId");
  }

  @Test
  void resolvesGameScopeFromTheDatabaseBackedTool() {
    ObjectNode request = request(8, "tools/call");
    ObjectNode params = (ObjectNode) request.putObject("params");
    params.put("name", "oracle.select_ai.test_game_scope_resolve");
    params.putObject("arguments").put("question", "games requested");

    ObjectNode response = service.handle("default", request, "user-bearer");

    assertThat(response.path("result").path("isError").asBoolean()).isFalse();
    String payload = response.path("result").path("content").get(0).path("text").asText();
    assertThat(payload)
        .contains("RESOLVED")
        .contains("CALL_FEW_SHOT_NL2SQL")
        .contains("REPORT_UNSUPPORTED");
  }

  @Test
  void keepsUnmatchedMentionsInPartialGameQueryPlan() {
    ObjectNode request = request(9, "tools/call");
    ObjectNode params = (ObjectNode) request.putObject("params");
    params.put("name", "oracle.select_ai.game_query_plan");
    params.putObject("arguments")
        .put("question", "supported game and unknown game");

    ObjectNode response = service.handle("default", request, "user-bearer");

    assertThat(response.path("result").path("isError").asBoolean()).isFalse();
    String payload = response.path("result").path("content").get(0)
        .path("text").asText();
    assertThat(payload)
        .contains("\"status\" : \"PARTIAL\"")
        .contains("\"gameKey\" : \"SUPPORTED_GAME\"")
        .contains("\"mention\" : \"Unknown game\"")
        .contains("\"reasonCode\" : \"NO_CATALOG_IDENTITY_MATCH\"");
  }

  private ObjectNode request(int id, String method) {
    ObjectNode request = objectMapper.createObjectNode();
    request.put("jsonrpc", "2.0");
    request.put("id", id);
    request.put("method", method);
    return request;
  }

  private static final class CapturingSelectAiService extends SelectAiService {

    private String bearerToken;
    private String prompt;
    private boolean showpromptCalled;

    private CapturingSelectAiService() {
      super(null, null, null, new ObjectMapper(), null, null, null);
    }

    @Override
    public JsonNode generateAndExecute(String bearerToken, String prompt) {
      this.bearerToken = bearerToken;
      this.prompt = prompt;
      ObjectNode response = new ObjectMapper().createObjectNode()
          .put("profile", "SGMP_POC_OCI_GPT54MINI")
          .put("generatedSql", "SELECT 1 FROM DUAL")
          .put("status", "SHOWSQL_AND_EXECUTED")
          .put("execution", "READ_ONLY_EXECUTED")
          .put("rowCount", 1);
      response.putArray("items").addObject().put("RESULT", 1);
      return response;
    }

    @Override
    public JsonNode generateAndExecute(String bearerToken, String prompt, String scopeGameKey) {
      return generateAndExecute(bearerToken, prompt);
    }

    @Override
    public JsonNode generatePrompt(String bearerToken, String prompt) {
      this.bearerToken = bearerToken;
      this.prompt = prompt;
      this.showpromptCalled = true;
      return new ObjectMapper().createObjectNode()
          .put("profile", "SGMP_POC_OCI_GPT54MINI")
          .put("status", "SHOWPROMPT")
          .put("selectAiPrompt", "assembled Select AI prompt");
    }
  }

  private static final class CapturingQaVectorService extends QaVectorService {

    private String bearerToken;
    private String question;
    private String answerSql;
    private int topK;

    private CapturingQaVectorService() {
      super(null, null, null);
    }

    @Override
    public VectorSearchResult search(String bearerToken, String question, int topK) {
      this.bearerToken = bearerToken;
      this.question = question;
      this.topK = topK;
      return new VectorSearchResult(question, topK, java.util.List.of(new VectorExample(
          42L, "active user count", "SELECT COUNT(*) FROM APP_USER", "AU count",
          "cohere.embed-v4.0", 0.12
      )));
    }

    @Override
    public VectorStoreResult store(String bearerToken, String question, String answerSql, String answer) {
      this.bearerToken = bearerToken;
      this.question = question;
      this.answerSql = answerSql;
      return new VectorStoreResult(77L, question, "cohere.embed-v4.0");
    }
  }

  private static final class CapturingGameScopeService extends GameScopeService {

    private CapturingGameScopeService() {
      super(null, new GameScopeProperties(false, "", 8), null, null);
    }

    @Override
    public GameScopeResult resolve(String bearerToken, String question) {
      return new GameScopeResult(question, "RESOLVED", java.util.List.of(
          new GameScope("SUPPORTED_GAME", "Supported game", "Supported", "SUPPORTED",
              "APPROVED_OBJECT_AVAILABLE", 100, "100"),
          new GameScope("UNSUPPORTED_GAME", "Unsupported game", "Unsupported", "UNSUPPORTED",
              "OBJECT_LIST_NOT_AVAILABLE", 100, "100")
      ));
    }
  }

  private static final class CapturingGameCatalogVectorService
      extends GameCatalogVectorService {

    private CapturingGameCatalogVectorService() {
      super(null, null, new ObjectMapper());
    }

    @Override
    public Resolution resolve(String token, String question, int topK) {
      GameCandidate supported = new GameCandidate(
          "SUPPORTED_GAME",
          "SUPPORTED_GAME_ID",
          "SUPPORTED_PREFIX",
          "Supported game",
          "Supported",
          0.1
      );
      GameCandidate nearestUnknownCandidate = new GameCandidate(
          "OTHER_GAME",
          "OTHER_GAME_ID",
          "OTHER_PREFIX",
          "Other game",
          "Other",
          0.6
      );
      return new Resolution(
          "MULTI_GAME",
          java.util.List.of(supported),
          java.util.List.of(
              new MentionResolution(
                  "Supported game",
                  "MATCHED",
                  supported,
                  java.util.List.of(supported)
              ),
              new MentionResolution(
                  "Unknown game",
                  "UNMATCHED",
                  null,
                  java.util.List.of(nearestUnknownCandidate)
              )
          )
      );
    }
  }
}
