// test_llm_anthropic.cpp — Catch2 tests for llm_anthropic pure helpers.
// Issue 0081. Does NOT make real HTTP calls. Uses FN_LLM_MOCK_RESPONSE for
// call_api injection. Real roundtrip is a manual_test (requires API key).
#include "catch_amalgamated.hpp"
#include "core/llm_anthropic.h"

#include <cstdlib>
#include <string>
#include <vector>

using namespace llm_anthropic;
using namespace data_table;

// ============================================================================
// build_request_body (pure)
// ============================================================================

TEST_CASE("build_request_body: contains model field", "[llm_anthropic]") {
    AskInput in;
    in.question  = "show me total sales by region";
    in.col_names = {"region", "amount"};
    in.col_types = {ColumnType::String, ColumnType::Float};
    in.mode      = OutputMode::TQL;

    std::string body = build_request_body(in);
    REQUIRE(body.find("\"model\"") != std::string::npos);
    REQUIRE(body.find("claude") != std::string::npos);
}

TEST_CASE("build_request_body: contains messages array", "[llm_anthropic]") {
    AskInput in;
    in.question = "top 10 by revenue";
    std::string body = build_request_body(in);
    REQUIRE(body.find("\"messages\"") != std::string::npos);
    REQUIRE(body.find("\"user\"")     != std::string::npos);
}

TEST_CASE("build_request_body: question appears in user content", "[llm_anthropic]") {
    AskInput in;
    in.question = "unique_question_marker_xyz";
    std::string body = build_request_body(in);
    // The question is JSON-escaped inside "content", so the marker must appear.
    REQUIRE(body.find("unique_question_marker_xyz") != std::string::npos);
}

TEST_CASE("build_request_body: col_names appear in schema block", "[llm_anthropic]") {
    AskInput in;
    in.question  = "q";
    in.col_names = {"customer_id", "purchase_date", "total_amount"};
    in.col_types = {ColumnType::Int, ColumnType::Date, ColumnType::Float};
    std::string body = build_request_body(in);
    REQUIRE(body.find("customer_id")  != std::string::npos);
    REQUIRE(body.find("purchase_date") != std::string::npos);
    REQUIRE(body.find("total_amount")  != std::string::npos);
}

TEST_CASE("build_request_body: custom model overrides default", "[llm_anthropic]") {
    AskInput in;
    in.question = "q";
    in.model    = "claude-opus-4-5";
    std::string body = build_request_body(in);
    REQUIRE(body.find("claude-opus-4-5") != std::string::npos);
}

TEST_CASE("build_request_body: SQL mode produces SQL system prompt", "[llm_anthropic]") {
    AskInput in;
    in.question = "q";
    in.mode     = OutputMode::SQL;
    std::string body = build_request_body(in);
    REQUIRE(body.find("DuckDB") != std::string::npos);
}

TEST_CASE("build_request_body: current TQL appears in body", "[llm_anthropic]") {
    AskInput in;
    in.question    = "q";
    in.tql_current = "return { version=1, stages={} }";
    std::string body = build_request_body(in);
    REQUIRE(body.find("version=1") != std::string::npos);
}

TEST_CASE("build_request_body: joinable tables appear in schema", "[llm_anthropic]") {
    AskInput in;
    in.question        = "q";
    in.joinable_names  = {"products_table", "customers_table"};
    std::string body   = build_request_body(in);
    REQUIRE(body.find("products_table")  != std::string::npos);
    REQUIRE(body.find("customers_table") != std::string::npos);
}

// ============================================================================
// extract_code_block (pure)
// ============================================================================

TEST_CASE("extract_code_block: extracts lua fence", "[llm_anthropic]") {
    std::string raw = "Here is the TQL:\n```lua\nreturn { version=1 }\n```\nDone.";
    std::string code = extract_code_block(raw, "lua");
    REQUIRE(code == "return { version=1 }");
}

TEST_CASE("extract_code_block: extracts sql fence", "[llm_anthropic]") {
    std::string raw = "```sql\nSELECT * FROM t;\n```";
    std::string code = extract_code_block(raw, "sql");
    REQUIRE(code == "SELECT * FROM t;");
}

TEST_CASE("extract_code_block: fallback to plain fence without lang", "[llm_anthropic]") {
    std::string raw = "```\nSOME CODE\n```";
    std::string code = extract_code_block(raw, "lua");
    REQUIRE(code.find("SOME CODE") != std::string::npos);
}

TEST_CASE("extract_code_block: no fence returns stripped raw", "[llm_anthropic]") {
    std::string raw = "  return { version=1 }  ";
    std::string code = extract_code_block(raw, "lua");
    REQUIRE(code == "return { version=1 }");
}

// ============================================================================
// parse_response_text (pure)
// ============================================================================

TEST_CASE("parse_response_text: extracts text from Anthropic response JSON", "[llm_anthropic]") {
    std::string json =
        R"({"id":"msg_01","content":[{"type":"text","text":"Hello world"}],"model":"claude-sonnet-4-6"})";
    std::string text = parse_response_text(json);
    REQUIRE(text == "Hello world");
}

TEST_CASE("parse_response_text: handles escaped newline in text", "[llm_anthropic]") {
    std::string json =
        R"({"content":[{"type":"text","text":"line1\nline2"}]})";
    std::string text = parse_response_text(json);
    REQUIRE(text.find("line1") != std::string::npos);
    REQUIRE(text.find("line2") != std::string::npos);
}

TEST_CASE("parse_response_text: empty text on missing field", "[llm_anthropic]") {
    std::string json = R"({"error":"no_key"})";
    std::string text = parse_response_text(json);
    REQUIRE(text.empty());
}

// ============================================================================
// call_api: mock injection via FN_LLM_MOCK_RESPONSE
// ============================================================================

TEST_CASE("call_api: mock response injection via env var", "[llm_anthropic]") {
    // Set mock response so no real HTTP call is made.
    setenv("FN_LLM_MOCK_RESPONSE",
           R"({"content":[{"type":"text","text":"```lua\nreturn {}\n```"}]})",
           1);

    std::string err;
    std::string resp = call_api("{}", "", err);
    REQUIRE(err.empty());
    REQUIRE(resp.find("content") != std::string::npos);

    unsetenv("FN_LLM_MOCK_RESPONSE");
}

// ============================================================================
// ask: end-to-end with mock (no real HTTP)
// ============================================================================

TEST_CASE("ask: mock roundtrip produces code block", "[llm_anthropic]") {
    setenv("FN_LLM_MOCK_RESPONSE",
           R"({"content":[{"type":"text","text":"```lua\nreturn { version=1 }\n```"}]})",
           1);

    AskInput in;
    in.question  = "show all rows";
    in.col_names = {"name", "value"};
    in.col_types = {ColumnType::String, ColumnType::Float};
    in.mode      = OutputMode::TQL;

    AskResult r = ask(in);
    REQUIRE(r.error.empty());
    REQUIRE(r.code.find("version=1") != std::string::npos);

    unsetenv("FN_LLM_MOCK_RESPONSE");
}
