// Tests for the pure helpers of fn_viz::agent_runs_timeline (issue 0118).
//
// Render path (ImGui) is intentionally NOT tested here — only filter / sort /
// duration / status mapping logic.

#define CATCH_CONFIG_MAIN
#include "catch_amalgamated.hpp"

#include "viz/agent_runs_timeline_helpers.h"

#include <algorithm>
#include <string>
#include <vector>

using fn_viz::AgentRun;
using fn_viz::TimelineFilter;
using namespace fn_viz::timeline;

static std::vector<AgentRun> make_mock() {
    std::vector<AgentRun> runs;
    AgentRun r;

    r = {}; r.id = "r1"; r.app = "kanban_cpp"; r.issue_id = "0118"; r.card_id = "c1";
    r.branch = "issue/0118"; r.status = "running"; r.started_at = 1000;
    r.finished_at = 0; r.dod_total = 5; r.dod_done = 2; r.dod_validated = 0;
    runs.push_back(r);

    r = {}; r.id = "r2"; r.app = "skill_tree"; r.issue_id = "0107"; r.card_id = "c2";
    r.branch = "issue/0107"; r.status = "done"; r.started_at = 800;
    r.finished_at = 860; r.dod_total = 3; r.dod_done = 3; r.dod_validated = 0;
    runs.push_back(r);

    r = {}; r.id = "r3"; r.app = "kanban_cpp"; r.issue_id = "0100"; r.card_id = "c3";
    r.branch = "issue/0100"; r.status = "merged"; r.started_at = 600;
    r.finished_at = 4200; r.dod_total = 4; r.dod_done = 4; r.dod_validated = 4;
    runs.push_back(r);

    r = {}; r.id = "r4"; r.app = "skill_tree"; r.issue_id = "0090"; r.card_id = "c4";
    r.branch = "quick/x"; r.status = "failed"; r.started_at = 1200;
    r.finished_at = 1205; r.dod_total = 2; r.dod_done = 0; r.dod_validated = 0;
    runs.push_back(r);

    r = {}; r.id = "r5"; r.app = "graph_explorer"; r.issue_id = "0080"; r.card_id = "c5";
    r.branch = "issue/0080"; r.status = "validated"; r.started_at = 1500;
    r.finished_at = 5000; r.dod_total = 6; r.dod_done = 6; r.dod_validated = 5;
    runs.push_back(r);

    return runs;
}

TEST_CASE("passes_filter — empty filter matches everything", "[timeline]") {
    auto runs = make_mock();
    TimelineFilter f;
    for (const auto& r : runs) REQUIRE(passes_filter(r, f));
}

TEST_CASE("passes_filter — app filter", "[timeline]") {
    auto runs = make_mock();
    TimelineFilter f;
    f.apps = {"kanban_cpp"};
    int n = 0;
    for (const auto& r : runs) if (passes_filter(r, f)) ++n;
    REQUIRE(n == 2);

    f.apps = {"kanban_cpp", "graph_explorer"};
    n = 0;
    for (const auto& r : runs) if (passes_filter(r, f)) ++n;
    REQUIRE(n == 3);
}

TEST_CASE("passes_filter — status filter combined with app", "[timeline]") {
    auto runs = make_mock();
    TimelineFilter f;
    f.apps = {"kanban_cpp"};
    f.statuses = {"merged"};
    int n = 0;
    AgentRun match{};
    for (const auto& r : runs) {
        if (passes_filter(r, f)) { ++n; match = r; }
    }
    REQUIRE(n == 1);
    REQUIRE(match.id == "r3");
}

TEST_CASE("passes_filter — since_ts lower bound", "[timeline]") {
    auto runs = make_mock();
    TimelineFilter f;
    f.since_ts = 1000;          // keep r1 (1000), r4 (1200), r5 (1500)
    int n = 0;
    for (const auto& r : runs) if (passes_filter(r, f)) ++n;
    REQUIRE(n == 3);
}

TEST_CASE("filter_and_sort — sorts by started_at desc", "[timeline]") {
    auto runs = make_mock();
    TimelineFilter f;  // no filter
    auto out = filter_and_sort(runs, f);
    REQUIRE(out.size() == 5);
    // expected order by started_at desc: r5(1500), r4(1200), r1(1000), r2(800), r3(600)
    REQUIRE(out[0].id == "r5");
    REQUIRE(out[1].id == "r4");
    REQUIRE(out[2].id == "r1");
    REQUIRE(out[3].id == "r2");
    REQUIRE(out[4].id == "r3");
}

TEST_CASE("filter_and_sort — combines filter and sort", "[timeline]") {
    auto runs = make_mock();
    TimelineFilter f;
    f.apps = {"kanban_cpp"};
    auto out = filter_and_sort(runs, f);
    REQUIRE(out.size() == 2);
    REQUIRE(out[0].id == "r1");  // 1000
    REQUIRE(out[1].id == "r3");  // 600
}

TEST_CASE("format_duration — running when finished_at=0", "[timeline]") {
    REQUIRE(format_duration(1000, 0) == "running");
}

TEST_CASE("format_duration — seconds", "[timeline]") {
    REQUIRE(format_duration(1000, 1045) == "45s");
    REQUIRE(format_duration(1000, 1000) == "0s");
}

TEST_CASE("format_duration — minutes + seconds", "[timeline]") {
    REQUIRE(format_duration(0, 65)  == "1m05s");
    REQUIRE(format_duration(0, 305) == "5m05s");
    REQUIRE(format_duration(0, 3599) == "59m59s");
}

TEST_CASE("format_duration — hours + minutes", "[timeline]") {
    REQUIRE(format_duration(0, 3600) == "1h00m");
    REQUIRE(format_duration(0, 3600 + 720) == "1h12m");
    REQUIRE(format_duration(0, 7200 + 1800) == "2h30m");
}

TEST_CASE("format_duration — inverted timestamps", "[timeline]") {
    REQUIRE(format_duration(2000, 1000) == "—");
}

TEST_CASE("status_color_token — known statuses", "[timeline]") {
    REQUIRE(status_color_token("pending")   == 1);
    REQUIRE(status_color_token("running")   == 1);
    REQUIRE(status_color_token("done")      == 2);
    REQUIRE(status_color_token("validated") == 2);
    REQUIRE(status_color_token("merged")    == 3);
    REQUIRE(status_color_token("aborted")   == 5);
    REQUIRE(status_color_token("failed")    == 6);
}

TEST_CASE("status_color_token — unknown falls back to neutral", "[timeline]") {
    REQUIRE(status_color_token("")        == 0);
    REQUIRE(status_color_token("zombie")  == 0);
}

TEST_CASE("status_icon_id — non-empty for known statuses", "[timeline]") {
    for (const std::string s : {"pending", "running", "done", "validated",
                                 "merged", "aborted", "failed"}) {
        REQUIRE_FALSE(status_icon_id(s).empty());
    }
    // unknown still returns a glyph (hourglass), never empty
    REQUIRE_FALSE(status_icon_id("nope").empty());
}

TEST_CASE("status_icon_id — distinct for distinct statuses", "[timeline]") {
    auto a = status_icon_id("running");
    auto b = status_icon_id("done");
    auto c = status_icon_id("failed");
    REQUIRE(a != b);
    REQUIRE(b != c);
    REQUIRE(a != c);
}

TEST_CASE("app_chip_hex — known apps", "[timeline]") {
    REQUIRE(app_chip_hex("kanban_cpp") == "#a855f7");
    REQUIRE(app_chip_hex("skill_tree") == "#0ea5e9");
}

TEST_CASE("app_chip_hex — unknown falls back to gray", "[timeline]") {
    REQUIRE(app_chip_hex("never_seen") == "#5C5F66");
    REQUIRE(app_chip_hex("") == "#5C5F66");
}
