#include <doctest/doctest.h>

#include "fgc/DumpParser.h"

#include <algorithm>
#include <string>

using namespace fgc;

namespace {

// A realistic two-axis firmware DUMP block (the on-wire format from
// firmware/src/motor.cpp printDump): header, then per axis a state line and a
// TMC register line, bracketed by DUMP BEGIN / DUMP END.
const char* kDump =
    "DUMP BEGIN build=11dd3ce-dirty uptime=11740067 mcusr=0x01 free_ram=1852\n"
    "DUMP Y state=3 hsub=10 enabled=1 lim_neg=-82919 lim_pos=98687 hold_target=-90 "
    "speed=50000 eeprom_restored=0 has_encoder=1\n"
    "DUMP Y TMC GCONF=0x0000000C GSTAT=0x00000000 IOIN=0x30000008 TSTEP=0x000FFFFF "
    "RAMPMODE=0x00000000 XACTUAL=0xFFFFFFA6 VACTUAL=0x00000000 XTARGET=0xFFFFFFA6 "
    "SW_MODE=0x000008A0 RAMP_STAT=0x00001680 X_ENC=0xFFFFFF7F ENC_STATUS=0x00000002 "
    "CHOPCONF=0x00410043 DRV_STATUS=0x80084000 PWM_SCALE=0x00000011 PWM_AUTO=0x003F0039\n"
    "DUMP P state=3 hsub=10 enabled=1 lim_neg=495183 lim_pos=1069042 hold_target=500183 "
    "speed=150000 eeprom_restored=0 has_encoder=1\n"
    "DUMP P TMC GCONF=0x0000000C GSTAT=0x00000000 IOIN=0x30000008 TSTEP=0x000FFFFF "
    "RAMPMODE=0x00000000 XACTUAL=0x0007A1C8 VACTUAL=0x00000000 XTARGET=0x0007A1C8 "
    "SW_MODE=0x000008A3 RAMP_STAT=0x00001688 X_ENC=0x0007A231 ENC_STATUS=0x00000002 "
    "CHOPCONF=0x00410043 DRV_STATUS=0x80084000 PWM_SCALE=0x00010011 PWM_AUTO=0x0021003C\n"
    "DUMP END\n";

bool has(const std::vector<std::string>& v, const std::string& s) {
    return std::find(v.begin(), v.end(), s) != v.end();
}

const DumpAxis* axis(const DumpData& d, char a) {
    for (const auto& ax : d.axes)
        if (ax.axis == a) return &ax;
    return nullptr;
}

}  // namespace

TEST_CASE("parseDump decodes the header") {
    DumpData d = parseDump(kDump);
    REQUIRE(d.valid);
    CHECK(d.build == "11dd3ce-dirty");
    CHECK(d.uptime_ms == 11740067);
    CHECK(d.mcusr == 0x01u);
    CHECK(d.free_ram == 1852);
    CHECK(has(d.reset_flags, "PORF (power-on)"));
    REQUIRE(d.axes.size() == 2);
}

TEST_CASE("parseDump decodes the per-axis state line") {
    DumpData d = parseDump(kDump);
    const DumpAxis* y = axis(d, 'Y');
    REQUIRE(y != nullptr);
    CHECK(y->state_name == "READY");   // state=3
    CHECK(y->enabled);
    CHECK(y->has_encoder);
    CHECK_FALSE(y->eeprom_restored);
    CHECK(y->lim_neg == -82919);
    CHECK(y->lim_pos == 98687);
    CHECK(y->hold_target == -90);
    CHECK(y->speed == 50000);
    CHECK(y->hsub == 10);

    const DumpAxis* p = axis(d, 'P');
    REQUIRE(p != nullptr);
    CHECK(p->lim_neg == 495183);
    CHECK(p->lim_pos == 1069042);
}

TEST_CASE("parseDump keeps all 16 TMC registers (regression: TMC-line whitespace)") {
    // The "DUMP Y TMC ..." line was once misclassified due to a leading-space
    // slice, dropping every register. Guard that all of them survive.
    DumpData d = parseDump(kDump);
    const DumpAxis* y = axis(d, 'Y');
    REQUIRE(y != nullptr);
    for (const char* r : {"GCONF", "GSTAT", "IOIN", "TSTEP", "RAMPMODE", "XACTUAL",
                          "VACTUAL", "XTARGET", "SW_MODE", "RAMP_STAT", "X_ENC",
                          "ENC_STATUS", "CHOPCONF", "DRV_STATUS", "PWM_SCALE", "PWM_AUTO"}) {
        CHECK_MESSAGE(y->regs.count(r) == 1, "missing register ", r);
    }
    CHECK(y->regs.at("RAMP_STAT") == "0x00001680");
    CHECK(y->regs.at("DRV_STATUS") == "0x80084000");
}

TEST_CASE("parseDump decodes status-register bit flags") {
    DumpData d = parseDump(kDump);
    const DumpAxis* y = axis(d, 'Y');
    REQUIRE(y != nullptr);

    // DRV_STATUS = 0x80084000 -> bit31 stst, bit14 stealth; CS_ACTUAL=(>>16)&0x1F=8.
    CHECK(y->drv_status == 0x80084000u);
    CHECK(has(y->drv_flags, "stst"));
    CHECK(has(y->drv_flags, "stealth"));
    CHECK(y->cs_actual == 8);
    CHECK(y->sg_result == 0);

    // GSTAT = 0 -> no flags (placeholder "-").
    CHECK(has(y->gstat_flags, "-"));

    // RAMP_STAT = 0x1680 -> bit7 event_pos_reached, bit9 position_reached, bit10 vzero.
    CHECK(y->ramp_stat == 0x00001680u);
    CHECK(has(y->ramp_flags, "event_pos_reached"));
    CHECK(has(y->ramp_flags, "position_reached"));
    CHECK(has(y->ramp_flags, "vzero"));
}

TEST_CASE("parseDump tolerates a corrupted/merged DUMP END (best-effort)") {
    // The host detects DUMP END anywhere in a line; a clean header still parses
    // even when later content is mangled — but a block with no BEGIN/END is invalid.
    CHECK_FALSE(parseDump("").valid);
    CHECK_FALSE(parseDump("garbage with no markers\n").valid);
    CHECK_FALSE(parseDump("DUMP Y state=3\nno end marker\n").valid);
}

TEST_CASE("formatDump renders valid and invalid dumps") {
    auto bad = formatDump(DumpData{});
    REQUIRE(bad.size() == 1);
    CHECK(bad[0].find("no firmware dump") != std::string::npos);

    auto good = formatDump(parseDump(kDump));
    std::string joined;
    for (const auto& l : good) joined += l + "\n";
    CHECK(joined.find("build=11dd3ce-dirty") != std::string::npos);
    CHECK(joined.find("[Y]") != std::string::npos);
    CHECK(joined.find("[P]") != std::string::npos);
    CHECK(joined.find("RAMP_STAT") != std::string::npos);
}
