// Tests for data_table::compute_stage (cpp/functions/core/compute_stage.cpp).
// Pure logic: no ImGui, no I/O. Exercises filter, group+sum, sort desc.

#define CATCH_CONFIG_MAIN
#include "catch_amalgamated.hpp"

#include "core/compute_stage.h"
#include <string>
#include <vector>

using namespace data_table;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
namespace {

// Build a flat row-major cell array from a vector-of-rows of cstrings.
// Keeps string storage alive via `backing`.
struct Table {
    std::vector<std::string>  backing;
    std::vector<const char*>  cells;
    std::vector<std::string>  headers;
    std::vector<ColumnType>   types;
    int rows = 0, cols = 0;

    void add_row(std::initializer_list<const char*> row) {
        for (const char* s : row) {
            backing.emplace_back(s ? s : "");
        }
        ++rows;
    }
    // Must call after all add_row calls.
    void finalize() {
        cols = headers.empty() ? 0 : (int)headers.size();
        cells.clear();
        cells.reserve(backing.size());
        for (const auto& s : backing) cells.push_back(s.c_str());
    }
};

} // anon

// ---------------------------------------------------------------------------
// Test: simple passthrough with no filter/group/sort
// ---------------------------------------------------------------------------
TEST_CASE("compute_stage passthrough returns all rows") {
    Table t;
    t.headers = {"name", "value"};
    t.types   = {ColumnType::String, ColumnType::Float};
    t.add_row({"alice", "10.0"});
    t.add_row({"bob",   "20.0"});
    t.add_row({"carol", "30.0"});
    t.finalize();

    Stage stage; // empty stage = passthrough
    StageOutput out = compute_stage(t.cells.data(), t.rows, t.cols,
                                    t.headers, t.types, stage);
    REQUIRE(out.rows == 3);
    REQUIRE(out.cols == 2);
    REQUIRE(out.headers[0] == "name");
    REQUIRE(out.headers[1] == "value");
}

// ---------------------------------------------------------------------------
// Test: filter equals — simple filter keeps matching rows only
// ---------------------------------------------------------------------------
TEST_CASE("compute_stage filter eq keeps matching rows") {
    Table t;
    t.headers = {"dept", "salary"};
    t.types   = {ColumnType::String, ColumnType::Int};
    t.add_row({"eng",  "90000"});
    t.add_row({"mktg", "70000"});
    t.add_row({"eng",  "95000"});
    t.finalize();

    Stage stage;
    Filter f; f.col = 0; f.op = Op::Eq; f.value = "eng";
    stage.filters.push_back(f);

    StageOutput out = compute_stage(t.cells.data(), t.rows, t.cols,
                                    t.headers, t.types, stage);
    REQUIRE(out.rows == 2);
    for (int r = 0; r < out.rows; ++r) {
        REQUIRE(std::string(out.cells[r * out.cols + 0]) == "eng");
    }
}

// ---------------------------------------------------------------------------
// Test: group + sum aggregation
// ---------------------------------------------------------------------------
TEST_CASE("compute_stage group sum aggregates correctly") {
    Table t;
    t.headers = {"dept", "amount"};
    t.types   = {ColumnType::String, ColumnType::Float};
    t.add_row({"eng",  "100"});
    t.add_row({"mktg", "200"});
    t.add_row({"eng",  "150"});
    t.add_row({"mktg", "50"});
    t.finalize();

    Stage stage;
    stage.breakouts = {"dept"};
    Aggregation agg;
    agg.fn  = AggFn::Sum;
    agg.col = "amount";
    stage.aggregations.push_back(agg);

    StageOutput out = compute_stage(t.cells.data(), t.rows, t.cols,
                                    t.headers, t.types, stage);
    REQUIRE(out.rows == 2);
    REQUIRE(out.cols == 2);
    REQUIRE(out.headers[0] == "dept");
    REQUIRE(out.headers[1] == "sum_amount");

    // Find eng and mktg rows (order may vary without sort).
    double eng_total = -1.0, mktg_total = -1.0;
    for (int r = 0; r < out.rows; ++r) {
        std::string dept = out.cells[r * out.cols + 0];
        double val = std::stod(out.cells[r * out.cols + 1]);
        if (dept == "eng")  eng_total  = val;
        if (dept == "mktg") mktg_total = val;
    }
    REQUIRE(eng_total  == 250.0);
    REQUIRE(mktg_total == 250.0);
}

// ---------------------------------------------------------------------------
// Test: sort descending
// ---------------------------------------------------------------------------
TEST_CASE("compute_stage sort desc reorders rows") {
    Table t;
    t.headers = {"score"};
    t.types   = {ColumnType::Int};
    t.add_row({"10"});
    t.add_row({"50"});
    t.add_row({"30"});
    t.finalize();

    Stage stage;
    SortClause sc; sc.col = "score"; sc.desc = true;
    stage.sorts.push_back(sc);

    StageOutput out = compute_stage(t.cells.data(), t.rows, t.cols,
                                    t.headers, t.types, stage);
    REQUIRE(out.rows == 3);
    REQUIRE(std::string(out.cells[0]) == "50");
    REQUIRE(std::string(out.cells[1]) == "30");
    REQUIRE(std::string(out.cells[2]) == "10");
}

// ---------------------------------------------------------------------------
// Test: filter + group + sort combined
// ---------------------------------------------------------------------------
TEST_CASE("compute_stage filter then group then sort asc") {
    Table t;
    t.headers = {"region", "type", "revenue"};
    t.types   = {ColumnType::String, ColumnType::String, ColumnType::Float};
    t.add_row({"EU", "A", "100"});
    t.add_row({"US", "A", "200"});
    t.add_row({"EU", "B", "300"});
    t.add_row({"EU", "A", "50"});
    t.finalize();

    Stage stage;
    // Keep only EU rows.
    Filter f; f.col = 0; f.op = Op::Eq; f.value = "EU";
    stage.filters.push_back(f);
    // Group by type.
    stage.breakouts = {"type"};
    Aggregation agg; agg.fn = AggFn::Sum; agg.col = "revenue";
    stage.aggregations.push_back(agg);
    // Sort by sum_revenue asc.
    SortClause sc; sc.col = "sum_revenue"; sc.desc = false;
    stage.sorts.push_back(sc);

    StageOutput out = compute_stage(t.cells.data(), t.rows, t.cols,
                                    t.headers, t.types, stage);
    REQUIRE(out.rows == 2);
    // type A: 100+50=150; type B: 300. Sorted asc -> A first.
    REQUIRE(std::string(out.cells[0 * out.cols + 0]) == "A");
    REQUIRE(std::string(out.cells[1 * out.cols + 0]) == "B");
    double rev_a = std::stod(out.cells[0 * out.cols + 1]);
    double rev_b = std::stod(out.cells[1 * out.cols + 1]);
    REQUIRE(rev_a == 150.0);
    REQUIRE(rev_b == 300.0);
}

// ---------------------------------------------------------------------------
// Test: apply_filters standalone
// ---------------------------------------------------------------------------
TEST_CASE("apply_filters returns correct row indices") {
    Table t;
    t.headers = {"val"};
    t.types   = {ColumnType::Int};
    t.add_row({"5"});
    t.add_row({"10"});
    t.add_row({"15"});
    t.finalize();

    Filter f; f.col = 0; f.op = Op::Gt; f.value = "7";
    auto idx = apply_filters(t.cells.data(), t.rows, t.cols, {f});
    REQUIRE(idx.size() == 2);
    REQUIRE(idx[0] == 1);
    REQUIRE(idx[1] == 2);
}

// ---------------------------------------------------------------------------
// Test: aggregation_alias default names
// ---------------------------------------------------------------------------
TEST_CASE("aggregation_alias produces expected names") {
    Aggregation count_agg;
    count_agg.fn = AggFn::Count;
    REQUIRE(aggregation_alias(count_agg) == "count");

    Aggregation sum_agg;
    sum_agg.fn = AggFn::Sum; sum_agg.col = "revenue";
    REQUIRE(aggregation_alias(sum_agg) == "sum_revenue");

    Aggregation pct_agg;
    pct_agg.fn = AggFn::Percentile; pct_agg.col = "latency"; pct_agg.arg = 0.95;
    REQUIRE(aggregation_alias(pct_agg) == "p95_latency");

    Aggregation custom;
    custom.fn = AggFn::Sum; custom.col = "x"; custom.alias = "my_alias";
    REQUIRE(aggregation_alias(custom) == "my_alias");
}

// ---------------------------------------------------------------------------
// Test: empty stage on empty table
// ---------------------------------------------------------------------------
TEST_CASE("compute_stage empty table empty stage") {
    Stage stage;
    StageOutput out = compute_stage(nullptr, 0, 0, {}, {}, stage);
    REQUIRE(out.rows == 0);
    REQUIRE(out.cols == 0);
}
