// tql_apply_test.cpp — Tests for tql_apply (TQL Lua parser).
// Run with: cmake --build cpp/build --target tql_apply_test && cpp/build/tql_apply_test
// Or via: ./fn run tql_apply_cpp_core

#include "core/tql_apply.h"
#include "core/tql_emit.h"
#include "core/tql_helpers.h"

#include <cassert>
#include <cstdio>
#include <string>
#include <vector>

using namespace data_table;

// ---------------------------------------------------------------------------
// Minimal assertions
// ---------------------------------------------------------------------------
static int g_pass = 0, g_fail = 0;

static void check(bool cond, const char* label) {
    if (cond) {
        std::printf("PASS: %s\n", label);
        ++g_pass;
    } else {
        std::printf("FAIL: %s\n", label);
        ++g_fail;
    }
}

// ---------------------------------------------------------------------------
// Test: parse minimal TQL chunk
// ---------------------------------------------------------------------------
static void test_parse_minimal() {
    const char* tql_text = R"(
return {
  version = 1,
  display = "table",
  stages = { {} },
  columns = {},
  views = {},
  visualization_settings = {},
}
)";
    auto res = tql::apply(tql_text, {});

    check(res.ok,                              "parse minimal: ok=true");
    check(res.error.empty(),                   "parse minimal: no error");
    check(res.state.display == ViewMode::Table, "parse minimal: display=Table");
    check(!res.state.stages.empty(),           "parse minimal: stages not empty");
}

// ---------------------------------------------------------------------------
// Test: parse display=bar
// ---------------------------------------------------------------------------
static void test_parse_display() {
    const char* tql_text = R"(
return {
  version = 1,
  display = "bar",
  stages = { {} },
  columns = {},
  views = {},
  visualization_settings = {},
}
)";
    auto res = tql::apply(tql_text, {});
    check(res.ok,                            "parse display: ok");
    check(res.state.display == ViewMode::Bar, "parse display: display=Bar");
}

// ---------------------------------------------------------------------------
// Test: filter parsing with known header
// ---------------------------------------------------------------------------
static void test_parse_filter_known_col() {
    const char* tql_text = R"(
return {
  version = 1,
  display = "table",
  stages = {
    {
      filter = {
        {"=", "name", "Alice"},
      },
    },
  },
  columns = {},
  views = {},
  visualization_settings = {},
}
)";
    std::vector<std::string> headers = {"name", "age"};
    auto res = tql::apply(tql_text, headers);

    check(res.ok,                                       "filter known col: ok");
    check(res.warnings.empty(),                         "filter known col: no warnings");
    check(!res.state.stages.empty(),                    "filter known col: has stages");
    check(res.state.stages[0].filters.size() == 1,     "filter known col: 1 filter");
    check(res.state.stages[0].filters[0].col == 0,     "filter known col: col=0");
    check(res.state.stages[0].filters[0].op  == Op::Eq,"filter known col: op=Eq");
    check(res.state.stages[0].filters[0].value == "Alice", "filter known col: value=Alice");
}

// ---------------------------------------------------------------------------
// Test: unknown column generates warning
// ---------------------------------------------------------------------------
static void test_parse_filter_unknown_col() {
    const char* tql_text = R"(
return {
  version = 1,
  display = "table",
  stages = {
    {
      filter = {
        {"=", "nonexistent_col", "x"},
      },
    },
  },
  columns = {},
  views = {},
  visualization_settings = {},
}
)";
    std::vector<std::string> headers = {"name", "age"};
    auto res = tql::apply(tql_text, headers);

    check(res.ok,                         "unknown col: ok (warning not error)");
    check(!res.warnings.empty(),          "unknown col: warning generated");
}

// ---------------------------------------------------------------------------
// Test: sort parsing
// ---------------------------------------------------------------------------
static void test_parse_sort() {
    const char* tql_text = R"(
return {
  version = 1,
  display = "table",
  stages = {
    {
      sort = {
        {"desc", "age"},
      },
    },
  },
  columns = {},
  views = {},
  visualization_settings = {},
}
)";
    auto res = tql::apply(tql_text, {});

    check(res.ok,                                         "sort: ok");
    check(res.state.stages[0].sorts.size() == 1,         "sort: 1 sort clause");
    check(res.state.stages[0].sorts[0].col == "age",     "sort: col=age");
    check(res.state.stages[0].sorts[0].desc == true,     "sort: desc=true");
}

// ---------------------------------------------------------------------------
// Test: aggregation stage parsing
// ---------------------------------------------------------------------------
static void test_parse_aggregation() {
    const char* tql_text = R"(
return {
  version = 1,
  display = "table",
  stages = {
    {},
    {
      breakout = {"dept"},
      aggregation = {
        {"sum", "salary"},
        {"count"},
      },
      sort = {
        {"asc", "dept"},
      },
    },
  },
  columns = {},
  views = {},
  visualization_settings = {},
}
)";
    auto res = tql::apply(tql_text, {});

    check(res.ok,                                             "aggregation: ok");
    check(res.state.stages.size() == 2,                      "aggregation: 2 stages");
    const Stage& s1 = res.state.stages[1];
    check(s1.breakouts.size() == 1,                          "aggregation: 1 breakout");
    check(s1.breakouts[0] == "dept",                         "aggregation: breakout=dept");
    check(s1.aggregations.size() == 2,                       "aggregation: 2 aggs");
    check(s1.aggregations[0].fn  == AggFn::Sum,              "aggregation: [0] fn=Sum");
    check(s1.aggregations[0].col == "salary",                "aggregation: [0] col=salary");
    check(s1.aggregations[1].fn  == AggFn::Count,            "aggregation: [1] fn=Count");
    check(s1.sorts.size() == 1,                              "aggregation: 1 sort");
    check(s1.sorts[0].col == "dept",                         "aggregation: sort col=dept");
    check(s1.sorts[0].desc == false,                         "aggregation: sort asc");
}

// ---------------------------------------------------------------------------
// Test: expression (formula) stored verbatim, lua_id=-1
// ---------------------------------------------------------------------------
static void test_parse_expressions() {
    const char* tql_text = R"(
return {
  version = 1,
  display = "table",
  stages = {
    {
      expressions = {
        ["total"] = "return [price] * [qty]",
      },
    },
  },
  columns = {},
  views = {},
  visualization_settings = {},
}
)";
    auto res = tql::apply(tql_text, {});

    check(res.ok,                                                    "expr: ok");
    check(res.state.stages[0].derived.size() == 1,                  "expr: 1 derived col");
    check(res.state.stages[0].derived[0].name == "total",           "expr: name=total");
    check(res.state.stages[0].derived[0].formula == "return [price] * [qty]",
          "expr: formula stored verbatim");
    check(res.state.stages[0].derived[0].lua_id == -1,              "expr: lua_id=-1 (not compiled)");
}

// ---------------------------------------------------------------------------
// Test: views parsing (extra viz panel)
// ---------------------------------------------------------------------------
static void test_parse_views() {
    const char* tql_text = R"(
return {
  version = 1,
  display = "table",
  stages = { {} },
  columns = {},
  views = {
    {display = "bar", x_col = "month", y_cols = {"revenue", "cost"}},
    {display = "line", x_col = "date"},
  },
  visualization_settings = {},
}
)";
    auto res = tql::apply(tql_text, {});

    check(res.ok,                                          "views: ok");
    // Panel 0 sets state.display + state.viz_config.
    check(res.state.display == ViewMode::Bar,              "views: display=Bar (from panel 0)");
    check(res.state.viz_config.x_col == "month",          "views: x_col=month");
    check(res.state.viz_config.y_cols.size() == 2,        "views: y_cols has 2 entries");
    check(res.state.viz_config.y_cols[0] == "revenue",    "views: y_cols[0]=revenue");
    // Panel 1 goes into extra_panels.
    check(res.state.extra_panels.size() == 1,             "views: 1 extra panel");
    check(res.state.extra_panels[0].display == ViewMode::Line, "views: extra=Line");
}

// ---------------------------------------------------------------------------
// Test: join parsing
// ---------------------------------------------------------------------------
static void test_parse_join() {
    const char* tql_text = R"(
return {
  version = 1,
  display = "table",
  joins = {
    {alias = "d", source = "departments", strategy = "inner",
     on = {{"dept_id", "id"}}, fields = {"dept_name"}},
  },
  stages = { {} },
  columns = {},
  views = {},
  visualization_settings = {},
}
)";
    auto res = tql::apply(tql_text, {});

    check(res.ok,                                         "join: ok");
    check(res.state.joins.size() == 1,                    "join: 1 join");
    check(res.state.joins[0].alias == "d",                "join: alias=d");
    check(res.state.joins[0].source == "departments",     "join: source=departments");
    check(res.state.joins[0].strategy == JoinStrategy::Inner, "join: strategy=Inner");
    check(res.state.joins[0].on.size() == 1,              "join: 1 on clause");
    check(res.state.joins[0].on[0].first == "dept_id",    "join: on left=dept_id");
    check(res.state.joins[0].on[0].second == "id",        "join: on right=id");
    check(res.state.joins[0].fields.size() == 1,          "join: 1 field");
    check(res.state.joins[0].fields[0] == "dept_name",    "join: field=dept_name");
}

// ---------------------------------------------------------------------------
// Test: version mismatch returns error
// ---------------------------------------------------------------------------
static void test_parse_version_mismatch() {
    const char* tql_text = R"(
return { version = 99, display = "table", stages = {{}}, columns = {}, views = {} }
)";
    auto res = tql::apply(tql_text, {});

    check(!res.ok,              "version mismatch: ok=false");
    check(!res.error.empty(),   "version mismatch: error set");
}

// ---------------------------------------------------------------------------
// Test: invalid Lua syntax returns error
// ---------------------------------------------------------------------------
static void test_parse_invalid_lua() {
    const char* bad = "this is not lua %%%";
    auto res = tql::apply(bad, {});

    check(!res.ok,              "invalid lua: ok=false");
    check(!res.error.empty(),   "invalid lua: error set");
}

// ---------------------------------------------------------------------------
// Test: roundtrip emit -> apply -> emit produces same display/stages
// ---------------------------------------------------------------------------
static void test_roundtrip() {
    // Build a state with a filter and a grouped stage.
    State st;

    Stage s0;
    s0.filters.push_back({3, Op::Neq, "inactive"}); // status is index 3
    s0.sorts.push_back({"name", false});
    st.stages.push_back(s0);

    Stage s1;
    s1.breakouts.push_back("dept");
    Aggregation a;
    a.fn  = AggFn::Avg;
    a.col = "salary";
    s1.aggregations.push_back(a);
    st.stages.push_back(s1);

    st.display = ViewMode::Bar;
    st.viz_config.x_col = "dept";
    st.viz_config.y_cols = {"avg_salary"};

    std::vector<std::string> headers = {"name", "dept", "salary", "status"};
    std::vector<ColumnType>  types   = {
        ColumnType::String, ColumnType::String, ColumnType::Float, ColumnType::String
    };

    // Emit.
    std::string tql_text = tql::emit(st, headers, types);
    check(!tql_text.empty(), "roundtrip: emit produced text");

    // Apply with the same headers.
    auto res = tql::apply(tql_text, headers);
    check(res.ok,                              "roundtrip: apply ok");
    check(res.warnings.empty(),                "roundtrip: no warnings");

    const State& st2 = res.state;

    check(st2.display == ViewMode::Bar,        "roundtrip: display=Bar");
    check(st2.stages.size() == 2,              "roundtrip: 2 stages");

    // Stage 0 filter.
    check(st2.stages[0].filters.size() == 1,              "roundtrip: s0 1 filter");
    check(st2.stages[0].filters[0].col == 3,              "roundtrip: s0 filter col=3 (status)");
    check(st2.stages[0].filters[0].op  == Op::Neq,        "roundtrip: s0 filter op=Neq");
    check(st2.stages[0].filters[0].value == "inactive",   "roundtrip: s0 filter value=inactive");

    // Stage 0 sort.
    check(st2.stages[0].sorts.size() == 1,                "roundtrip: s0 1 sort");
    check(st2.stages[0].sorts[0].col == "name",           "roundtrip: s0 sort col=name");
    check(st2.stages[0].sorts[0].desc == false,           "roundtrip: s0 sort asc");

    // Stage 1 breakout.
    check(st2.stages[1].breakouts.size() == 1,            "roundtrip: s1 breakout");
    check(st2.stages[1].breakouts[0] == "dept",           "roundtrip: s1 breakout=dept");

    // Stage 1 aggregation.
    check(st2.stages[1].aggregations.size() == 1,         "roundtrip: s1 1 agg");
    check(st2.stages[1].aggregations[0].fn == AggFn::Avg, "roundtrip: s1 agg fn=Avg");
    check(st2.stages[1].aggregations[0].col == "salary",  "roundtrip: s1 agg col=salary");

    // viz_config.
    check(st2.viz_config.x_col == "dept",                 "roundtrip: viz x_col=dept");
    check(st2.viz_config.y_cols.size() == 1,              "roundtrip: viz 1 y_col");
    check(st2.viz_config.y_cols[0] == "avg_salary",       "roundtrip: viz y_col=avg_salary");
}

// ---------------------------------------------------------------------------
// Test: column visibility and order preserved through roundtrip
// ---------------------------------------------------------------------------
static void test_roundtrip_col_order() {
    State st;
    st.stages.push_back(Stage{});
    st.col_visible = {true, false, true};
    st.col_order   = {2, 0, 1};  // custom order

    std::vector<std::string> headers = {"a", "b", "c"};
    std::vector<ColumnType>  types   = {ColumnType::String, ColumnType::Int, ColumnType::Float};

    std::string tql_text = tql::emit(st, headers, types);
    auto res = tql::apply(tql_text, headers);

    check(res.ok,                               "col order roundtrip: ok");
    check(res.state.col_visible.size() == 3,    "col order roundtrip: 3 visible entries");
    check(res.state.col_visible[1] == false,    "col order roundtrip: col b invisible");
    // col_order after roundtrip should match the original (sorted by order value).
    check(res.state.col_order.size() == 3,      "col order roundtrip: 3 order entries");
    check(res.state.col_order[0] == 2,          "col order roundtrip: first=c (idx 2)");
    check(res.state.col_order[1] == 0,          "col order roundtrip: second=a (idx 0)");
    check(res.state.col_order[2] == 1,          "col order roundtrip: third=b (idx 1)");
}

// ---------------------------------------------------------------------------
// Test: color rule roundtrip
// ---------------------------------------------------------------------------
static void test_roundtrip_color_rules() {
    State st;
    st.stages.push_back(Stage{});
    ColorRule cr{0, "Alice", 0xFF2244FFu};
    st.color_rules.push_back(cr);

    std::vector<std::string> headers = {"name"};
    std::vector<ColumnType>  types   = {ColumnType::String};

    std::string tql_text = tql::emit(st, headers, types);
    auto res = tql::apply(tql_text, headers);

    check(res.ok,                                       "color rule roundtrip: ok");
    check(res.state.color_rules.size() == 1,            "color rule roundtrip: 1 rule");
    check(res.state.color_rules[0].col == 0,            "color rule roundtrip: col=0");
    check(res.state.color_rules[0].equals == "Alice",   "color rule roundtrip: equals=Alice");
    check(res.state.color_rules[0].color == cr.color,   "color rule roundtrip: color matches");
}

// ---------------------------------------------------------------------------
// Test: column_specs roundtrip — Badge
// ---------------------------------------------------------------------------
static void test_roundtrip_column_specs_badge() {
    State st;
    st.stages.push_back(Stage{});

    ColumnSpec cs;
    cs.id       = "status";
    cs.renderer = CellRenderer::Badge;
    cs.badges   = { BadgeRule{"ok", "#22c55e", "OK"}, BadgeRule{"error", "#ef4444", ""} };
    st.aux_column_specs.push_back({cs});

    std::vector<std::string> headers = {"status"};
    std::vector<ColumnType>  types   = {ColumnType::String};

    std::string tql_text = tql::emit(st, headers, types);
    auto res = tql::apply(tql_text, headers);

    check(res.ok,                                                    "cs badge roundtrip: ok");
    check(!res.state.aux_column_specs.empty(),                       "cs badge roundtrip: aux_column_specs non-empty");
    check(!res.state.aux_column_specs[0].empty(),                    "cs badge roundtrip: specs[0] non-empty");
    check(res.state.aux_column_specs[0][0].renderer == CellRenderer::Badge,
          "cs badge roundtrip: renderer=Badge");
    check(res.state.aux_column_specs[0][0].id == "status",          "cs badge roundtrip: id=status");
    check(res.state.aux_column_specs[0][0].badges.size() == 2,      "cs badge roundtrip: 2 badge rules");
    check(res.state.aux_column_specs[0][0].badges[0].value == "ok", "cs badge roundtrip: badge[0].value=ok");
    check(res.state.aux_column_specs[0][0].badges[0].label == "OK", "cs badge roundtrip: badge[0].label=OK");
}

// ---------------------------------------------------------------------------
// Test: column_specs roundtrip — Button
// ---------------------------------------------------------------------------
static void test_roundtrip_column_specs_button() {
    State st;
    st.stages.push_back(Stage{});

    ColumnSpec cs;
    cs.id               = "actions";
    cs.renderer         = CellRenderer::Button;
    cs.button_action    = "cancel";
    cs.button_label     = "Cancel";
    cs.button_color_hex = "#ef4444";
    st.aux_column_specs.push_back({cs});

    std::vector<std::string> headers = {"name", "actions"};
    std::vector<ColumnType>  types   = {ColumnType::String, ColumnType::String};

    std::string tql_text = tql::emit(st, headers, types);
    auto res = tql::apply(tql_text, headers);

    check(res.ok,                                               "cs button roundtrip: ok");
    check(!res.state.aux_column_specs.empty() &&
          !res.state.aux_column_specs[0].empty(),              "cs button roundtrip: specs present");
    const auto& cs2 = res.state.aux_column_specs[0][0];
    check(cs2.renderer      == CellRenderer::Button,           "cs button roundtrip: renderer=Button");
    check(cs2.button_action == "cancel",                       "cs button roundtrip: button_action=cancel");
    check(cs2.button_label  == "Cancel",                       "cs button roundtrip: button_label=Cancel");
    check(cs2.button_color_hex == "#ef4444",                   "cs button roundtrip: button_color");
}

// ---------------------------------------------------------------------------
// Test: column_specs roundtrip — Tooltip
// ---------------------------------------------------------------------------
static void test_roundtrip_column_specs_tooltip() {
    State st;
    st.stages.push_back(Stage{});

    ColumnSpec cs;
    cs.id              = "name";
    cs.renderer        = CellRenderer::Text;
    cs.tooltip         = "auto";
    cs.tooltip_on_hover = true;
    st.aux_column_specs.push_back({cs});

    std::vector<std::string> headers = {"name"};
    std::vector<ColumnType>  types   = {ColumnType::String};

    std::string tql_text = tql::emit(st, headers, types);
    auto res = tql::apply(tql_text, headers);

    check(res.ok,                                               "cs tooltip roundtrip: ok");
    check(!res.state.aux_column_specs.empty() &&
          !res.state.aux_column_specs[0].empty(),              "cs tooltip roundtrip: specs present");
    const auto& cs2 = res.state.aux_column_specs[0][0];
    check(cs2.tooltip         == "auto",                       "cs tooltip roundtrip: tooltip=auto");
    check(cs2.tooltip_on_hover == true,                        "cs tooltip roundtrip: tooltip_on_hover=true");
}

// ---------------------------------------------------------------------------
// Test: column_specs roundtrip — Dots (v1.3.0)
// ---------------------------------------------------------------------------
static void test_roundtrip_column_specs_dots() {
    State st;
    st.stages.push_back(Stage{});

    ColumnSpec cs;
    cs.id              = "recent";
    cs.renderer        = CellRenderer::Dots;
    cs.badges          = {{"ok", "#22c55e", ""}, {"error", "#ef4444", "failed"}};
    cs.dots_max        = 5;
    cs.dots_show_count = false;
    cs.tooltip_on_hover = true;
    st.aux_column_specs.push_back({cs});

    std::vector<std::string> headers = {"name", "recent"};
    std::vector<ColumnType>  types   = {ColumnType::String, ColumnType::String};

    std::string tql_text = tql::emit(st, headers, types);
    auto res = tql::apply(tql_text, headers);

    check(res.ok,                                               "cs dots roundtrip: ok");
    check(!res.state.aux_column_specs.empty() &&
          !res.state.aux_column_specs[0].empty(),              "cs dots roundtrip: specs present");
    const auto& cs2 = res.state.aux_column_specs[0][0];
    check(cs2.renderer        == CellRenderer::Dots,           "cs dots roundtrip: renderer=Dots");
    check(cs2.dots_max        == 5,                            "cs dots roundtrip: dots_max=5");
    check(cs2.tooltip_on_hover == true,                        "cs dots roundtrip: tooltip_on_hover=true");
    check(cs2.badges.size()   == 2,                            "cs dots roundtrip: 2 badge rules");
    check(cs2.badges[0].value == "ok",                         "cs dots roundtrip: badge[0].value=ok");
    check(cs2.badges[1].label == "failed",                     "cs dots roundtrip: badge[1].label=failed");
}

// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
int main() {
    test_parse_minimal();
    test_parse_display();
    test_parse_filter_known_col();
    test_parse_filter_unknown_col();
    test_parse_sort();
    test_parse_aggregation();
    test_parse_expressions();
    test_parse_views();
    test_parse_join();
    test_parse_version_mismatch();
    test_parse_invalid_lua();
    test_roundtrip();
    test_roundtrip_col_order();
    test_roundtrip_color_rules();
    test_roundtrip_column_specs_badge();
    test_roundtrip_column_specs_button();
    test_roundtrip_column_specs_tooltip();
    test_roundtrip_column_specs_dots();

    std::printf("---\nResults: %d passed, %d failed\n", g_pass, g_fail);
    return g_fail == 0 ? 0 : 1;
}
