// Tests for parse_md_frontmatter (cpp/functions/core/parse_md_frontmatter).
// Pure function — no ImGui context, no I/O (except the golden-run test which
// reads dev/issues/ using FN_TEST_REPO_ROOT defined at compile time).

#define CATCH_CONFIG_MAIN
#include "catch_amalgamated.hpp"

#include "core/parse_md_frontmatter.h"

#include <fstream>
#include <sstream>
#include <string>
#include <vector>

using namespace fn_md;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

static std::string str(const YamlValue& v) {
    if (auto* s = std::get_if<std::string>(&v)) return *s;
    return "";
}

static std::vector<std::string> lst(const YamlValue& v) {
    if (auto* l = std::get_if<std::vector<std::string>>(&v)) return *l;
    return {};
}

static bool has(const Frontmatter& fm, const std::string& key) {
    return fm.fields.count(key) > 0;
}

// ---------------------------------------------------------------------------
// Case 1 — No frontmatter
// ---------------------------------------------------------------------------
TEST_CASE("parses_no_frontmatter") {
    const std::string content = "# Just a markdown file\n\nSome body text.\n";
    auto fm = parse_md_frontmatter(content);
    REQUIRE_FALSE(fm.has_frontmatter);
    REQUIRE(fm.body == content);
    REQUIRE(fm.fields.empty());
}

// ---------------------------------------------------------------------------
// Case 2 — Simple key:value
// ---------------------------------------------------------------------------
TEST_CASE("parses_simple_key_value") {
    const std::string content =
        "---\n"
        "title: Hello\n"
        "status: pending\n"
        "---\n"
        "Body here.\n";
    auto fm = parse_md_frontmatter(content);
    REQUIRE(fm.has_frontmatter);
    REQUIRE(str(fm.fields.at("title")) == "Hello");
    REQUIRE(str(fm.fields.at("status")) == "pending");
    REQUIRE(fm.body == "Body here.\n");
}

// ---------------------------------------------------------------------------
// Case 3 — Quoted string (double and single quotes)
// ---------------------------------------------------------------------------
TEST_CASE("parses_quoted_strings") {
    const std::string content =
        "---\n"
        "description: \"foo bar\"\n"
        "note: 'baz qux'\n"
        "id: \"0109\"\n"
        "---\n";
    auto fm = parse_md_frontmatter(content);
    REQUIRE(fm.has_frontmatter);
    REQUIRE(str(fm.fields.at("description")) == "foo bar");
    REQUIRE(str(fm.fields.at("note")) == "baz qux");
    REQUIRE(str(fm.fields.at("id")) == "0109");
}

// ---------------------------------------------------------------------------
// Case 4 — Inline list
// ---------------------------------------------------------------------------
TEST_CASE("parses_inline_list") {
    const std::string content =
        "---\n"
        "tags: [meta, cpp, imgui]\n"
        "---\n";
    auto fm = parse_md_frontmatter(content);
    REQUIRE(fm.has_frontmatter);
    const auto v = lst(fm.fields.at("tags"));
    REQUIRE(v.size() == 3);
    REQUIRE(v[0] == "meta");
    REQUIRE(v[1] == "cpp");
    REQUIRE(v[2] == "imgui");
}

// ---------------------------------------------------------------------------
// Case 5 — Multiline list
// ---------------------------------------------------------------------------
TEST_CASE("parses_multiline_list") {
    const std::string content =
        "---\n"
        "domain:\n"
        "  - meta\n"
        "  - cpp-stack\n"
        "---\n";
    auto fm = parse_md_frontmatter(content);
    REQUIRE(fm.has_frontmatter);
    const auto v = lst(fm.fields.at("domain"));
    REQUIRE(v.size() == 2);
    REQUIRE(v[0] == "meta");
    REQUIRE(v[1] == "cpp-stack");
}

// ---------------------------------------------------------------------------
// Case 6 — Body after frontmatter
// ---------------------------------------------------------------------------
TEST_CASE("parses_body_after_frontmatter") {
    const std::string content =
        "---\n"
        "key: v\n"
        "---\n"
        "body text\n";
    auto fm = parse_md_frontmatter(content);
    REQUIRE(fm.has_frontmatter);
    REQUIRE(str(fm.fields.at("key")) == "v");
    REQUIRE(fm.body == "body text\n");
}

// ---------------------------------------------------------------------------
// Case 7 — Empty list
// ---------------------------------------------------------------------------
TEST_CASE("parses_empty_inline_list") {
    const std::string content =
        "---\n"
        "blocks: []\n"
        "depends: []\n"
        "---\n";
    auto fm = parse_md_frontmatter(content);
    REQUIRE(fm.has_frontmatter);
    REQUIRE(lst(fm.fields.at("blocks")).empty());
    REQUIRE(lst(fm.fields.at("depends")).empty());
}

// ---------------------------------------------------------------------------
// Case 8 — Trailing comments stripped
// ---------------------------------------------------------------------------
TEST_CASE("parses_strips_trailing_comment") {
    const std::string content =
        "---\n"
        "key: value  # this is a comment\n"
        "other: hello # another\n"
        "---\n";
    auto fm = parse_md_frontmatter(content);
    REQUIRE(fm.has_frontmatter);
    REQUIRE(str(fm.fields.at("key")) == "value");
    REQUIRE(str(fm.fields.at("other")) == "hello");
}

// ---------------------------------------------------------------------------
// Case 9 — Real issue sample (issue 0109)
// ---------------------------------------------------------------------------
TEST_CASE("parses_real_issue_0109") {
    const std::string content =
        "---\n"
        "id: \"0109\"\n"
        "title: \"App skill_tree: mapa interactivo de issues+flows en anillos concentricos por estado (roadmap)\"\n"
        "status: in-progress\n"
        "type: epic\n"
        "domain:\n"
        "  - meta\n"
        "  - cpp-stack\n"
        "scope: cross-stack\n"
        "priority: media\n"
        "depends: []\n"
        "blocks: []\n"
        "related:\n"
        "  - \"0069\"\n"
        "  - \"0085\"\n"
        "created: 2026-05-17\n"
        "updated: 2026-05-17\n"
        "tags:\n"
        "  - skill-tree\n"
        "  - roadmap\n"
        "  - meta\n"
        "  - cpp\n"
        "  - imgui\n"
        "  - gamification\n"
        "---\n"
        "\n"
        "# Body\n";

    auto fm = parse_md_frontmatter(content);
    REQUIRE(fm.has_frontmatter);
    REQUIRE_FALSE(fm.fields.empty());

    REQUIRE(str(fm.fields.at("id")) == "0109");
    REQUIRE(str(fm.fields.at("status")) == "in-progress");

    const auto domain = lst(fm.fields.at("domain"));
    REQUIRE(domain.size() == 2);
    REQUIRE(domain[0] == "meta");
    REQUIRE(domain[1] == "cpp-stack");

    const auto tags = lst(fm.fields.at("tags"));
    REQUIRE(tags.size() == 6);
    REQUIRE(tags[0] == "skill-tree");
}

// ---------------------------------------------------------------------------
// Case 10 — Golden run: parse ALL dev/issues/*.md and dev/flows/*.md
//
// Requires the compile-time definition FN_TEST_REPO_ROOT pointing to the
// root of the fn_registry repo (set by CMakeLists.txt via
//   target_compile_definitions(... PRIVATE FN_TEST_REPO_ROOT="...")  ).
//
// The test reads every .md file it finds, parses it, and verifies:
//   1. No crash / no exception.
//   2. If the file starts with `---`, has_frontmatter == true.
//   3. fields is not empty.
//   4. Cumulative parse_errors == 0.
// ---------------------------------------------------------------------------

#ifndef FN_TEST_REPO_ROOT
#define FN_TEST_REPO_ROOT ""
#endif

#include <filesystem>

TEST_CASE("parses_real_issues_golden") {
    const std::string repo_root = FN_TEST_REPO_ROOT;
    if (repo_root.empty()) {
        WARN("FN_TEST_REPO_ROOT not set — skipping golden run");
        return;
    }

    std::vector<std::filesystem::path> md_files;
    for (const auto& dir : {"dev/issues", "dev/flows"}) {
        const auto p = std::filesystem::path(repo_root) / dir;
        if (!std::filesystem::is_directory(p)) continue;
        for (const auto& entry : std::filesystem::directory_iterator(p)) {
            if (entry.path().extension() == ".md")
                md_files.push_back(entry.path());
        }
    }

    REQUIRE_FALSE(md_files.empty());

    int parse_errors = 0;
    for (const auto& path : md_files) {
        std::ifstream f(path);
        REQUIRE(f.is_open());
        std::ostringstream ss;
        ss << f.rdbuf();
        const std::string content = ss.str();

        Frontmatter fm;
        // Must not throw
        REQUIRE_NOTHROW(fm = parse_md_frontmatter(content));

        if (content.rfind("---", 0) == 0) {
            // File starts with `---`: expect frontmatter was found
            INFO("File: " << path.string());
            REQUIRE(fm.has_frontmatter);
            REQUIRE_FALSE(fm.fields.empty());
        }
    }

    REQUIRE(parse_errors == 0);
}
