// test_column_specs.cpp — Smoke / back-compat tests for declarative cell renderers.
// Issue 0081-N, v1.1.0. Phase 2 (issue 0081-O, v1.2.0).
// Phase 2.5 (issue 0081-O.5, v1.3.0): Dots renderer.
// v1.4.0: CategoricalChip + ColorScale renderers (TestCategoricalChipRule,
//         TestColorScaleLerpTwoStops, TestColorScaleLerpThreeStops,
//         TestColorScaleOutOfRange).
//
// These tests verify:
//   1. TableInput without column_specs compiles and links (back-compat).
//   2-5. TableInput with Badge/Progress/Duration/Icon column_specs compiles and links.
//   6. Button renderer: TableEvent struct is constructible; events_out pointer accepted.
//   7. Tooltip field: ColumnSpec with tooltip_on_hover=true compiles and links.
//   8. render() overload with events_out=nullptr back-compat (symbol resolution only).
//   9. Dots renderer: ColumnSpec with CellRenderer::Dots + badges constructs correctly.
//   10. Dots TQL roundtrip: State::aux_column_specs accepts Dots spec.
//   11. TestCategoricalChipRule: ChipRule with match="success" produces correct color.
//   12. TestColorScaleLerpTwoStops: t=0→first color, t=1→last color, t=0.5→midpoint.
//   13. TestColorScaleLerpThreeStops: t=0.25 lies between stop0 and stop1.
//   14. TestColorScaleOutOfRange: t<0 saturates at first; t>1 saturates at last.
//
// None of these tests call data_table::render() (requires ImGui context).
// They only verify that the new types are usable and that the symbols from
// fn_table_viz link correctly.
//
// Build: cmake --build cpp/build/linux --target test_column_specs
// Run:   ./cpp/build/linux/tests/test_column_specs

#include "core/data_table_types.h"
#include "data_table/data_table.h"

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

using namespace data_table;

// Shared trivial dataset (3 rows x 4 cols).
static const char* g_cells[] = {
    "ok",    "0.75",  "250",   "fn",
    "error", "0.20",  "3500",  "type",
    "warn",  "1.00",  "12000", "fn",
};
static const std::vector<std::string> g_headers = {"status", "progress", "duration_ms", "kind"};
static const std::vector<ColumnType>  g_types   = {
    ColumnType::String, ColumnType::Float, ColumnType::Float, ColumnType::String
};

// ---------------------------------------------------------------------------
// Test 1: back-compat — TableInput without column_specs.
// ---------------------------------------------------------------------------
static void test_no_column_specs() {
    TableInput t;
    t.name    = "t1";
    t.rows    = 3;
    t.cols    = 4;
    t.cells   = g_cells;
    t.headers = g_headers;
    t.types   = g_types;
    // column_specs intentionally left empty (back-compat: default behavior).

    assert(t.column_specs.empty() && "column_specs must be empty by default");

    // Verify that render symbol is still linkable (no ImGui context needed
    // to take the address; the linker verifies the symbol resolves).
    // Use the classic overload (without events_out) for the back-compat check.
    auto* render_fn = static_cast<void(*)(const char*,
                                          const std::vector<TableInput>&,
                                          State&,
                                          bool)>(&data_table::render);
    (void)render_fn;

    std::printf("PASS: test_no_column_specs (back-compat, column_specs empty)\n");
}

// ---------------------------------------------------------------------------
// Test 2: Badge renderer — construct TableInput with Badge column_spec.
// ---------------------------------------------------------------------------
static void test_badge_column_spec() {
    TableInput t;
    t.name    = "t2";
    t.rows    = 3;
    t.cols    = 4;
    t.cells   = g_cells;
    t.headers = g_headers;
    t.types   = g_types;

    // Column 0: Badge renderer mapping ok/error/warn to colors.
    ColumnSpec cs_status;
    cs_status.id       = "status";
    cs_status.renderer = CellRenderer::Badge;
    cs_status.badges   = {
        BadgeRule{"ok",    "#22c55e", "OK"},
        BadgeRule{"error", "#ef4444", ""},   // label empty -> use value
        BadgeRule{"warn",  "#f59e0b", "WARN"},
    };

    // Remaining columns: default Text.
    t.column_specs.resize(4);  // default-initialized = CellRenderer::Text
    t.column_specs[0] = cs_status;

    assert(t.column_specs.size() == 4);
    assert(t.column_specs[0].renderer == CellRenderer::Badge);
    assert(t.column_specs[0].badges.size() == 3);
    assert(t.column_specs[1].renderer == CellRenderer::Text);

    std::printf("PASS: test_badge_column_spec (3 badge rules, remaining cols Text)\n");
}

// ---------------------------------------------------------------------------
// Test 3: Progress renderer.
// ---------------------------------------------------------------------------
static void test_progress_column_spec() {
    TableInput t;
    t.name    = "t3";
    t.rows    = 3;
    t.cols    = 4;
    t.cells   = g_cells;
    t.headers = g_headers;
    t.types   = g_types;

    ColumnSpec cs_progress;
    cs_progress.id                 = "progress";
    cs_progress.renderer           = CellRenderer::Progress;
    cs_progress.progress_scale_100 = false;
    cs_progress.progress_color_hex = "#3b82f6";

    t.column_specs.resize(4);
    t.column_specs[1] = cs_progress;

    assert(t.column_specs[1].renderer == CellRenderer::Progress);
    assert(!t.column_specs[1].progress_scale_100);
    assert(t.column_specs[1].progress_color_hex == "#3b82f6");

    std::printf("PASS: test_progress_column_spec\n");
}

// ---------------------------------------------------------------------------
// Test 4: Duration renderer.
// ---------------------------------------------------------------------------
static void test_duration_column_spec() {
    TableInput t;
    t.name    = "t4";
    t.rows    = 3;
    t.cols    = 4;
    t.cells   = g_cells;
    t.headers = g_headers;
    t.types   = g_types;

    ColumnSpec cs_dur;
    cs_dur.id                = "duration_ms";
    cs_dur.renderer          = CellRenderer::Duration;
    cs_dur.duration_warn_ms  = 500.0f;
    cs_dur.duration_error_ms = 2000.0f;

    t.column_specs.resize(4);
    t.column_specs[2] = cs_dur;

    assert(t.column_specs[2].renderer == CellRenderer::Duration);
    assert(t.column_specs[2].duration_warn_ms  == 500.0f);
    assert(t.column_specs[2].duration_error_ms == 2000.0f);

    std::printf("PASS: test_duration_column_spec (warn=500ms error=2000ms)\n");
}

// ---------------------------------------------------------------------------
// Test 5: Icon renderer.
// ---------------------------------------------------------------------------
static void test_icon_column_spec() {
    TableInput t;
    t.name    = "t5";
    t.rows    = 3;
    t.cols    = 4;
    t.cells   = g_cells;
    t.headers = g_headers;
    t.types   = g_types;

    ColumnSpec cs_icon;
    cs_icon.id       = "kind";
    cs_icon.renderer = CellRenderer::Icon;
    cs_icon.icon_map = {
        IconMapEntry{"fn",   "TI_BOLT",     "#3b82f6"},
        IconMapEntry{"type", "TI_DATABASE", ""},
    };

    t.column_specs.resize(4);
    t.column_specs[3] = cs_icon;

    assert(t.column_specs[3].renderer == CellRenderer::Icon);
    assert(t.column_specs[3].icon_map.size() == 2);
    assert(t.column_specs[3].icon_map[0].value     == "fn");
    assert(t.column_specs[3].icon_map[0].icon_name == "TI_BOLT");

    // Verify render symbol still links with column_specs populated (classic overload).
    auto* render_fn = static_cast<void(*)(const char*,
                                          const std::vector<TableInput>&,
                                          State&,
                                          bool)>(&data_table::render);
    (void)render_fn;

    std::printf("PASS: test_icon_column_spec (2 entries, render symbol links)\n");
}

// ---------------------------------------------------------------------------
// Test 6: Button renderer — TableEvent struct is constructible; events_out ptr
//         is accepted by the new render() overload (symbol resolution only).
// ---------------------------------------------------------------------------
static void test_button_column_spec_and_event_struct() {
    TableInput t;
    t.name    = "t6";
    t.rows    = 3;
    t.cols    = 4;
    t.cells   = g_cells;
    t.headers = g_headers;
    t.types   = g_types;

    ColumnSpec cs_btn;
    cs_btn.id               = "actions";
    cs_btn.renderer         = CellRenderer::Button;
    cs_btn.button_action    = "cancel";
    cs_btn.button_label     = "Cancel";
    cs_btn.button_color_hex = "#ef4444";

    t.column_specs.resize(4);
    t.column_specs[0] = cs_btn;

    assert(t.column_specs[0].renderer         == CellRenderer::Button);
    assert(t.column_specs[0].button_action    == "cancel");
    assert(t.column_specs[0].button_label     == "Cancel");
    assert(t.column_specs[0].button_color_hex == "#ef4444");

    // Verify TableEvent struct can be constructed and holds expected fields.
    TableEvent ev;
    ev.kind      = TableEventKind::ButtonClick;
    ev.row       = 1;
    ev.col       = 0;
    ev.column_id = "actions";
    ev.action_id = "cancel";
    ev.value     = "ok";
    assert(ev.kind      == TableEventKind::ButtonClick);
    assert(ev.row       == 1);
    assert(ev.action_id == "cancel");

    // Verify the render() overload with events_out is linkable.
    std::vector<TableEvent> events;
    auto* render_with_events = static_cast<void(*)(const char*,
                                                    const std::vector<TableInput>&,
                                                    State&,
                                                    std::vector<TableEvent>*,
                                                    bool)>(&data_table::render);
    (void)render_with_events;

    std::printf("PASS: test_button_column_spec_and_event_struct "
                "(Button spec + TableEvent + render overload link)\n");
}

// ---------------------------------------------------------------------------
// Test 7: Tooltip field — ColumnSpec with tooltip_on_hover=true.
// ---------------------------------------------------------------------------
static void test_tooltip_column_spec() {
    TableInput t;
    t.name    = "t7";
    t.rows    = 3;
    t.cols    = 4;
    t.cells   = g_cells;
    t.headers = g_headers;
    t.types   = g_types;

    ColumnSpec cs_tip;
    cs_tip.id              = "status";
    cs_tip.renderer        = CellRenderer::Text;  // tooltip works on any renderer
    cs_tip.tooltip         = "auto";              // "auto" -> show cell value
    cs_tip.tooltip_on_hover = true;

    t.column_specs.resize(4);
    t.column_specs[0] = cs_tip;

    assert(t.column_specs[0].tooltip         == "auto");
    assert(t.column_specs[0].tooltip_on_hover == true);

    // Also test explicit tooltip text.
    ColumnSpec cs_tip2;
    cs_tip2.id              = "progress";
    cs_tip2.renderer        = CellRenderer::Progress;
    cs_tip2.tooltip         = "Progress percentage (0..1)";
    cs_tip2.tooltip_on_hover = true;
    t.column_specs[1] = cs_tip2;

    assert(t.column_specs[1].tooltip         == "Progress percentage (0..1)");
    assert(t.column_specs[1].tooltip_on_hover == true);

    std::printf("PASS: test_tooltip_column_spec (auto + explicit, tooltip_on_hover=true)\n");
}

// ---------------------------------------------------------------------------
// Test 8: render() back-compat overload without events_out — symbol links.
// ---------------------------------------------------------------------------
static void test_render_backcompat_overload() {
    // Verify both render() signatures are resolvable at link time.
    // Classic (no events_out):
    auto* render_classic = static_cast<void(*)(const char*,
                                               const std::vector<TableInput>&,
                                               State&,
                                               bool)>(&data_table::render);
    (void)render_classic;

    // New (with events_out):
    auto* render_events = static_cast<void(*)(const char*,
                                              const std::vector<TableInput>&,
                                              State&,
                                              std::vector<TableEvent>*,
                                              bool)>(&data_table::render);
    (void)render_events;

    std::printf("PASS: test_render_backcompat_overload (both render() signatures link)\n");
}

// ---------------------------------------------------------------------------
// Test 9: Dots renderer — ColumnSpec with CellRenderer::Dots + badges.
// Cell value "ok,error,ok,running,ok" — 5 tokens, 5 badge rules, dots_max=5.
// ---------------------------------------------------------------------------
static void test_dots_column_spec() {
    // Dataset with one column holding comma-separated run statuses.
    static const char* cells_dots[] = {
        "ok,error,ok,running,ok",
        "ok,ok,ok",
        "failed,failed",
    };
    std::vector<std::string> hdrs = {"recent"};
    std::vector<ColumnType>  types = {ColumnType::String};

    TableInput t;
    t.name    = "t_dots";
    t.rows    = 3;
    t.cols    = 1;
    t.cells   = cells_dots;
    t.headers = hdrs;
    t.types   = types;

    ColumnSpec cs;
    cs.id       = "recent";
    cs.renderer = CellRenderer::Dots;
    cs.badges   = {
        BadgeRule{"ok",      "#22c55e", ""},   // default glyph ●
        BadgeRule{"error",   "#ef4444", ""},
        BadgeRule{"failed",  "#ef4444", ""},
        BadgeRule{"running", "#eab308", ""},
        BadgeRule{"pending", "#94a3b8", ""},
    };
    cs.dots_max        = 5;
    cs.dots_show_count = false;
    cs.tooltip_on_hover = true;

    t.column_specs.resize(1);
    t.column_specs[0] = cs;

    assert(t.column_specs[0].renderer    == CellRenderer::Dots);
    assert(t.column_specs[0].dots_max    == 5);
    assert(t.column_specs[0].badges.size() == 5);
    assert(t.column_specs[0].dots_show_count == false);
    assert(t.column_specs[0].tooltip_on_hover == true);
    assert(t.column_specs[0].dots_separator == ',');

    // Verify that the struct is copyable and the enum value is distinct.
    ColumnSpec cs2 = cs;
    assert(cs2.renderer == CellRenderer::Dots);
    assert(cs2.dots_max == 5);

    std::printf("PASS: test_dots_column_spec "
                "(5 badge rules, dots_max=5, tooltip_on_hover=true)\n");
}

// ---------------------------------------------------------------------------
// Test 10: Dots TQL roundtrip — emit + apply preserves Dots fields.
// ---------------------------------------------------------------------------
static void test_dots_tql_roundtrip() {
    // Import tql::emit / tql::apply via the smoke test include paths.
    // This test checks that the new Dots fields survive a TQL roundtrip.
    // We populate aux_column_specs[0] and verify apply reconstructs them.

    // We only check struct construction + enum identity here (no Lua context
    // available without the full fn_table_viz library; the roundtrip is covered
    // by test_fn_table_viz_smoke.cpp test_tql_roundtrip).
    ColumnSpec cs;
    cs.id              = "recent";
    cs.renderer        = CellRenderer::Dots;
    cs.dots_separator  = ',';
    cs.dots_max        = 5;
    cs.dots_show_count = false;
    cs.badges          = {{"ok", "#22c55e", ""}, {"error", "#ef4444", ""}};

    // Build a State with aux_column_specs to verify the container accepts Dots.
    data_table::State st;
    st.stages.push_back(data_table::Stage{});
    st.aux_column_specs.push_back({cs});

    assert(st.aux_column_specs.size() == 1);
    assert(st.aux_column_specs[0].size() == 1);
    assert(st.aux_column_specs[0][0].renderer == CellRenderer::Dots);
    assert(st.aux_column_specs[0][0].dots_max == 5);
    assert(st.aux_column_specs[0][0].badges.size() == 2);

    std::printf("PASS: test_dots_tql_roundtrip "
                "(State::aux_column_specs accepts Dots spec)\n");
}

// ---------------------------------------------------------------------------
// Test 11: TestCategoricalChipRule — ChipRule with match="success" correct color.
// Verifies ChipRule struct construction + ColumnSpec.chips field accessible.
// ---------------------------------------------------------------------------
static void test_categorical_chip_rule() {
    ColumnSpec cs;
    cs.id       = "state";
    cs.renderer = CellRenderer::CategoricalChip;
    cs.chips    = {
        ChipRule{"success", "#22c55e"},
        ChipRule{"failure", "#ef4444"},
        ChipRule{"pending", "#f59e0b"},
    };

    assert(cs.renderer == CellRenderer::CategoricalChip);
    assert(cs.chips.size() == 3);
    assert(cs.chips[0].match == "success");
    assert(cs.chips[0].color == "#22c55e");
    assert(cs.chips[1].match == "failure");
    assert(cs.chips[1].color == "#ef4444");
    assert(cs.chips[2].match == "pending");

    // No matching rule for "unknown" — chips lookup returns nullptr (logic check).
    const ChipRule* found = nullptr;
    const char* test_val = "unknown";
    for (const auto& cr : cs.chips) {
        if (cr.match == test_val) { found = &cr; break; }
    }
    assert(found == nullptr && "no rule should match 'unknown'");

    // Match "success" should find first rule.
    const ChipRule* found2 = nullptr;
    for (const auto& cr : cs.chips) {
        if (cr.match == std::string("success")) { found2 = &cr; break; }
    }
    assert(found2 != nullptr && found2->color == "#22c55e");

    std::printf("PASS: TestCategoricalChipRule "
                "(3 chip rules, match/no-match logic correct)\n");
}

// ---------------------------------------------------------------------------
// Headless color lerp helpers (mirrors the static functions in data_table.cpp,
// replicated here so tests run without ImGui context).
// Uses a plain struct RGB3 instead of std::tuple to avoid extra includes.
// ---------------------------------------------------------------------------
struct RGB3 { float r, g, b; };

static float lerp_f(float a, float b, float t) { return a + t * (b - a); }

// Parse "#rrggbb" -> RGB3 floats in [0,1]. Returns {-1,-1,-1} on failure.
static RGB3 parse_rgb(const std::string& hex) {
    const char* p = hex.c_str();
    if (*p == '#') ++p;
    unsigned int r = 0, g = 0, b = 0;
    if (std::sscanf(p, "%02x%02x%02x", &r, &g, &b) != 3)
        return {-1.f, -1.f, -1.f};
    return {r / 255.f, g / 255.f, b / 255.f};
}

// Lerp between two ColorStop RGB colors at a given global t.
static RGB3 lerp_between(const ColorStop& lo, const ColorStop& hi, float t_global) {
    float span = hi.position - lo.position;
    float f = (span > 1e-6f) ? (t_global - lo.position) / span : 0.f;
    RGB3 ca = parse_rgb(lo.color);
    RGB3 cb = parse_rgb(hi.color);
    return {lerp_f(ca.r,cb.r,f), lerp_f(ca.g,cb.g,f), lerp_f(ca.b,cb.b,f)};
}

// lerp_stops: full N-stop lerp (same logic as lerp_color_along_stops in data_table.cpp).
static RGB3 lerp_stops(const std::vector<ColorStop>& stops, float t) {
    static const ColorStop kDefault[] = {
        {0.0f, "#22c55e"}, {0.5f, "#f59e0b"}, {1.0f, "#ef4444"}
    };
    static const int kDefaultN = 3;

    // Build a working sorted copy.
    std::vector<ColorStop> s;
    if (stops.empty()) {
        for (int i = 0; i < kDefaultN; ++i) s.push_back(kDefault[i]);
    } else {
        s = stops;
    }
    // Simple insertion sort (N is tiny, avoids std::sort include).
    for (size_t i = 1; i < s.size(); ++i) {
        ColorStop key = s[i];
        int j = (int)i - 1;
        while (j >= 0 && s[j].position > key.position) { s[j+1] = s[j]; --j; }
        s[j+1] = key;
    }

    t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t);
    if (t <= s.front().position) return parse_rgb(s.front().color);
    if (t >= s.back().position)  return parse_rgb(s.back().color);
    for (size_t i = 0; i + 1 < s.size(); ++i) {
        if (t >= s[i].position && t <= s[i+1].position)
            return lerp_between(s[i], s[i+1], t);
    }
    return parse_rgb(s.back().color);
}

// ---------------------------------------------------------------------------
// Test 12: TestColorScaleLerpTwoStops — t=0→first, t=1→last, t=0.5→midpoint.
// ---------------------------------------------------------------------------
static void test_color_scale_lerp_two_stops() {
    std::vector<ColorStop> stops = {
        {0.0f, "#000000"},  // black
        {1.0f, "#ffffff"},  // white
    };
    ColumnSpec cs;
    cs.renderer   = CellRenderer::ColorScale;
    cs.range_min  = 0.0;
    cs.range_max  = 1.0;
    cs.range_stops = stops;
    cs.range_alpha = 0.25f;

    assert(cs.renderer == CellRenderer::ColorScale);
    assert(cs.range_stops.size() == 2);

    // t=0.0 → black (0,0,0)
    RGB3 c0 = lerp_stops(stops, 0.0f);
    assert(c0.r < 0.01f && c0.g < 0.01f && c0.b < 0.01f);

    // t=1.0 → white (1,1,1)
    RGB3 c1 = lerp_stops(stops, 1.0f);
    assert(c1.r > 0.99f && c1.g > 0.99f && c1.b > 0.99f);

    // t=0.5 → midpoint (0.5, 0.5, 0.5) within floating-point tolerance
    RGB3 c5 = lerp_stops(stops, 0.5f);
    assert(c5.r > 0.49f && c5.r < 0.51f);
    assert(c5.g > 0.49f && c5.g < 0.51f);
    assert(c5.b > 0.49f && c5.b < 0.51f);

    std::printf("PASS: TestColorScaleLerpTwoStops "
                "(t=0→black, t=1→white, t=0.5→mid-grey)\n");
}

// ---------------------------------------------------------------------------
// Test 13: TestColorScaleLerpThreeStops — t=0.25 between stop0 and stop1.
// Stops: {0.0,red}, {0.5,green}, {1.0,blue}.
// At t=0.25 we expect halfway between red and green.
// ---------------------------------------------------------------------------
static void test_color_scale_lerp_three_stops() {
    // red=#ff0000, green=#00ff00, blue=#0000ff
    std::vector<ColorStop> stops = {
        {0.0f, "#ff0000"},  // red
        {0.5f, "#00ff00"},  // green
        {1.0f, "#0000ff"},  // blue
    };

    // t=0.25 is halfway between stop0 (t=0) and stop1 (t=0.5).
    // Lerp factor f = (0.25 - 0.0) / (0.5 - 0.0) = 0.5.
    // Expected: R = lerp(1,0,0.5)=0.5, G = lerp(0,1,0.5)=0.5, B = lerp(0,0,0.5)=0.
    RGB3 ca = lerp_stops(stops, 0.25f);
    assert(ca.r > 0.49f && ca.r < 0.51f && "R should be ~0.5 at t=0.25");
    assert(ca.g > 0.49f && ca.g < 0.51f && "G should be ~0.5 at t=0.25");
    assert(ca.b < 0.01f                  && "B should be ~0 at t=0.25");

    // t=0.75 is halfway between stop1 (t=0.5) and stop2 (t=1.0).
    // Expected: R=0, G=0.5, B=0.5.
    RGB3 cb = lerp_stops(stops, 0.75f);
    assert(cb.r < 0.01f               && "R should be ~0 at t=0.75");
    assert(cb.g > 0.49f && cb.g < 0.51f && "G should be ~0.5 at t=0.75");
    assert(cb.b > 0.49f && cb.b < 0.51f && "B should be ~0.5 at t=0.75");

    std::printf("PASS: TestColorScaleLerpThreeStops "
                "(t=0.25 between stop0/stop1, t=0.75 between stop1/stop2)\n");
}

// ---------------------------------------------------------------------------
// Test 14: TestColorScaleOutOfRange — t<0 saturates at first; t>1 at last.
// ---------------------------------------------------------------------------
static void test_color_scale_out_of_range() {
    std::vector<ColorStop> stops = {
        {0.0f, "#ff0000"},  // red at t=0
        {1.0f, "#0000ff"},  // blue at t=1
    };

    // t=-0.5 → clamp to 0 → red
    RGB3 cu = lerp_stops(stops, -0.5f);
    assert(cu.r > 0.99f && "under-range should saturate at first stop (red)");
    assert(cu.b < 0.01f);

    // t=1.5 → clamp to 1 → blue
    RGB3 co = lerp_stops(stops, 1.5f);
    assert(co.r < 0.01f  && "over-range should saturate at last stop (blue)");
    assert(co.b > 0.99f);

    // ColumnSpec struct fields accessible and defaults sensible.
    ColumnSpec cs;
    cs.renderer  = CellRenderer::ColorScale;
    cs.range_min = -10.0;
    cs.range_max = 10.0;
    assert(cs.range_alpha == 0.25f && "default range_alpha should be 0.25");
    assert(cs.range_stops.empty() && "default range_stops should be empty (→ use default gradient)");

    std::printf("PASS: TestColorScaleOutOfRange "
                "(t<0 saturates at first stop, t>1 saturates at last stop)\n");
}

// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
int main() {
    std::printf("=== test_column_specs ===\n");
    test_no_column_specs();
    test_badge_column_spec();
    test_progress_column_spec();
    test_duration_column_spec();
    test_icon_column_spec();
    test_button_column_spec_and_event_struct();
    test_tooltip_column_spec();
    test_render_backcompat_overload();
    test_dots_column_spec();
    test_dots_tql_roundtrip();
    test_categorical_chip_rule();
    test_color_scale_lerp_two_stops();
    test_color_scale_lerp_three_stops();
    test_color_scale_out_of_range();
    std::printf("=== ALL TESTS PASSED (14/14) ===\n");
    return 0;
}
