// ============================================================================
// Native (host) unit tests for src/command_handler.cpp
//
// Run with: pio test -e native -f test_command_handler
//
// Strategy: include command_handler.cpp directly as a single translation unit
// so the UNIT_TEST stubs take effect and no linker hunting for mesh_publish /
// rf_classifier / etc. is required. Tests exercise:
//   - packet parse (valid, CRC mismatch, size mismatch)
//   - target filtering (unicast mismatch, broadcast accept)
//   - ACK serialize round-trip
//   - dispatch: NOOP / REBOOT magic / SELF_TEST bitmap
// ============================================================================

#include <unity.h>
#include <cstdint>
#include <cstring>

// Pull the handler in directly. UNIT_TEST is set in platformio.ini.
#include "../../src/command_handler.cpp"

// ---------------------------------------------------------------------------
// Packet-builder helpers
// ---------------------------------------------------------------------------
static uint32_t test_crc32_bzip2(const uint8_t* data, size_t len) {
    // Mirror of cmd_crc32_bzip2 — duplicated so the test catches regressions
    // if someone changes the CRC in command_handler.cpp without updating here.
    uint32_t crc = 0xFFFFFFFFu;
    for (size_t i = 0; i < len; i++) {
        crc ^= ((uint32_t)data[i]) << 24;
        for (int b = 0; b < 8; b++) {
            if (crc & 0x80000000u) crc = (crc << 1) ^ 0x04C11DB7u;
            else                   crc = (crc << 1);
        }
    }
    return crc ^ 0xFFFFFFFFu;
}

static void build_cmd_packet(uint8_t* buf32,
                             uint16_t target,
                             uint16_t cmd_id,
                             SensorCmdType cmd_type,
                             uint8_t flags,
                             const uint8_t* params,
                             uint8_t param_len) {
    std::memset(buf32, 0, CMD_PACKET_SIZE);
    buf32[0] = MSG_TYPE_COMMAND;
    buf32[1] = (uint8_t)((target >> 8) & 0xFF);
    buf32[2] = (uint8_t)(target & 0xFF);
    buf32[3] = (uint8_t)((cmd_id >> 8) & 0xFF);
    buf32[4] = (uint8_t)(cmd_id & 0xFF);
    buf32[5] = (uint8_t)cmd_type;
    buf32[6] = flags;
    buf32[7] = param_len;
    if (param_len > 0 && params != nullptr) {
        std::memcpy(&buf32[8], params, param_len);
    }
    uint32_t crc = test_crc32_bzip2(buf32, 28);
    buf32[28] = (uint8_t)((crc >> 24) & 0xFF);
    buf32[29] = (uint8_t)((crc >> 16) & 0xFF);
    buf32[30] = (uint8_t)((crc >> 8)  & 0xFF);
    buf32[31] = (uint8_t)(crc & 0xFF);
}

// ---------------------------------------------------------------------------
// setUp / tearDown — Unity lifecycle hooks
// ---------------------------------------------------------------------------
void setUp(void) {
    commandHandlerInit(0x1234);
}

void tearDown(void) {}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

void test_parse_valid_command(void) {
    uint8_t buf[CMD_PACKET_SIZE];
    uint8_t params[2] = { 0x05, 0x50 };  // SET_THRESHOLD: model 5, 80%
    build_cmd_packet(buf, 0x1234, 0xBEEF,
                     SensorCmdType::SET_THRESHOLD,
                     CMD_FLAG_REQUIRE_ACK, params, 2);

    CmdPacket pkt;
    TEST_ASSERT_TRUE(parseCmdPacket(buf, CMD_PACKET_SIZE, pkt));
    TEST_ASSERT_EQUAL_UINT16(0x1234, pkt.target_sensor_id);
    TEST_ASSERT_EQUAL_UINT16(0xBEEF, pkt.cmd_id);
    TEST_ASSERT_EQUAL_UINT8((uint8_t)SensorCmdType::SET_THRESHOLD, pkt.cmd_type);
    TEST_ASSERT_EQUAL_UINT8(CMD_FLAG_REQUIRE_ACK, pkt.flags);
    TEST_ASSERT_EQUAL_UINT8(2, pkt.param_len);
    TEST_ASSERT_EQUAL_UINT8(0x05, pkt.params[0]);
    TEST_ASSERT_EQUAL_UINT8(0x50, pkt.params[1]);
}

void test_parse_crc_mismatch_rejects(void) {
    uint8_t buf[CMD_PACKET_SIZE];
    build_cmd_packet(buf, 0x1234, 0x0001, SensorCmdType::NOOP, 0, nullptr, 0);

    // Corrupt the last CRC byte
    buf[31] ^= 0xFF;

    CmdPacket pkt;
    TEST_ASSERT_FALSE(parseCmdPacket(buf, CMD_PACKET_SIZE, pkt));
}

void test_parse_size_mismatch_rejects(void) {
    uint8_t buf[CMD_PACKET_SIZE];
    build_cmd_packet(buf, 0x1234, 0x0001, SensorCmdType::NOOP, 0, nullptr, 0);

    CmdPacket pkt;
    // Too-small buffer
    TEST_ASSERT_FALSE(parseCmdPacket(buf, CMD_PACKET_SIZE - 1, pkt));
    // Too-large buffer
    TEST_ASSERT_FALSE(parseCmdPacket(buf, CMD_PACKET_SIZE + 4, pkt));
    // Not a COMMAND type
    buf[0] = 0xAA;
    TEST_ASSERT_FALSE(parseCmdPacket(buf, CMD_PACKET_SIZE, pkt));
}

void test_parse_rejects_oversize_param_len(void) {
    uint8_t buf[CMD_PACKET_SIZE];
    build_cmd_packet(buf, 0x1234, 0x0001, SensorCmdType::NOOP, 0, nullptr, 0);
    buf[7] = 21;  // > CMD_PARAMS_SIZE (20)
    // Re-CRC so that's not what fails
    uint32_t crc = test_crc32_bzip2(buf, 28);
    buf[28] = (uint8_t)((crc >> 24) & 0xFF);
    buf[29] = (uint8_t)((crc >> 16) & 0xFF);
    buf[30] = (uint8_t)((crc >> 8)  & 0xFF);
    buf[31] = (uint8_t)(crc & 0xFF);

    CmdPacket pkt;
    TEST_ASSERT_FALSE(parseCmdPacket(buf, CMD_PACKET_SIZE, pkt));
}

void test_target_mismatch_ignored(void) {
    // own_sensor_id = 0x1234 via setUp. Send to 0x9999.
    uint8_t buf[CMD_PACKET_SIZE];
    uint8_t params[2] = { 0, 0xA5 };  // REBOOT with correct magic
    build_cmd_packet(buf, 0x9999, 0x0001, SensorCmdType::REBOOT,
                     CMD_FLAG_REQUIRE_ACK, params, 2);

    TEST_ASSERT_FALSE(commandHandlerPendingReboot());
    handleIncomingCommand(buf, CMD_PACKET_SIZE);
    // Wrong target — reboot must NOT be scheduled
    TEST_ASSERT_FALSE(commandHandlerPendingReboot());
}

void test_broadcast_target_accepted(void) {
    uint8_t buf[CMD_PACKET_SIZE];
    uint8_t params[2] = { 5, 0xA5 };  // REBOOT delay 5s, correct magic
    build_cmd_packet(buf, CMD_BROADCAST_SENSOR_ID, 0x0002,
                     SensorCmdType::REBOOT, 0, params, 2);

    TEST_ASSERT_FALSE(commandHandlerPendingReboot());
    handleIncomingCommand(buf, CMD_PACKET_SIZE);
    // Broadcast — reboot scheduled
    TEST_ASSERT_TRUE(commandHandlerPendingReboot());
}

void test_ack_serialization_roundtrip(void) {
    AckPacket ack{};
    ack.sensor_id   = 0x1234;
    ack.cmd_id      = 0xBEEF;
    ack.status      = (uint8_t)CmdAckStatus::OK;
    ack.result_code = 0x42;
    ack.result_data[0] = 0x11;
    ack.result_data[1] = 0x22;
    ack.result_data[2] = 0x33;
    ack.result_data[3] = 0x44;
    ack.result_data[4] = 0x55;

    uint8_t buf[CMD_ACK_PACKET_SIZE];
    size_t n = serializeAckPacket(ack, buf, sizeof(buf));
    TEST_ASSERT_EQUAL_size_t(CMD_ACK_PACKET_SIZE, n);
    TEST_ASSERT_EQUAL_UINT8(MSG_TYPE_CMD_ACK, buf[0]);
    TEST_ASSERT_EQUAL_UINT8(0x12, buf[1]);
    TEST_ASSERT_EQUAL_UINT8(0x34, buf[2]);
    TEST_ASSERT_EQUAL_UINT8(0xBE, buf[3]);
    TEST_ASSERT_EQUAL_UINT8(0xEF, buf[4]);
    TEST_ASSERT_EQUAL_UINT8(0x00, buf[5]);  // status
    TEST_ASSERT_EQUAL_UINT8(0x42, buf[6]);  // result_code
    TEST_ASSERT_EQUAL_UINT8(0x11, buf[7]);
    TEST_ASSERT_EQUAL_UINT8(0x55, buf[11]);

    // Verify CRC is self-consistent
    uint32_t crc_on_wire = ((uint32_t)buf[12] << 24) | ((uint32_t)buf[13] << 16) |
                           ((uint32_t)buf[14] << 8)  | (uint32_t)buf[15];
    uint32_t crc_expected = test_crc32_bzip2(buf, 12);
    TEST_ASSERT_EQUAL_UINT32(crc_expected, crc_on_wire);

    // Too-small buffer returns 0
    uint8_t tiny[8];
    TEST_ASSERT_EQUAL_size_t(0, serializeAckPacket(ack, tiny, sizeof(tiny)));
}

void test_dispatch_noop_returns_ok(void) {
    CmdPacket pkt{};
    pkt.target_sensor_id = 0x1234;
    pkt.cmd_id           = 0x0001;
    pkt.cmd_type         = (uint8_t)SensorCmdType::NOOP;
    pkt.flags            = 0;
    pkt.param_len        = 0;

    AckPacket ack{};
    bool ok = dispatchCommand(pkt, ack);
    TEST_ASSERT_TRUE(ok);
    TEST_ASSERT_EQUAL_UINT8((uint8_t)CmdAckStatus::OK, ack.status);
    TEST_ASSERT_EQUAL_UINT16(0x1234, ack.sensor_id);
    TEST_ASSERT_EQUAL_UINT16(0x0001, ack.cmd_id);
}

void test_dispatch_reboot_requires_magic_byte(void) {
    // Wrong magic -> REJECTED, no reboot scheduled
    CmdPacket bad{};
    bad.target_sensor_id = 0x1234;
    bad.cmd_id           = 0x0010;
    bad.cmd_type         = (uint8_t)SensorCmdType::REBOOT;
    bad.param_len        = 2;
    bad.params[0]        = 5;      // delay_sec
    bad.params[1]        = 0x00;   // wrong magic

    AckPacket ack{};
    TEST_ASSERT_TRUE(dispatchCommand(bad, ack));
    TEST_ASSERT_EQUAL_UINT8((uint8_t)CmdAckStatus::REJECTED, ack.status);
    TEST_ASSERT_FALSE(commandHandlerPendingReboot());

    // Correct magic -> OK, reboot scheduled
    CmdPacket good = bad;
    good.cmd_id   = 0x0011;
    good.params[1] = 0xA5;
    AckPacket ack2{};
    TEST_ASSERT_TRUE(dispatchCommand(good, ack2));
    TEST_ASSERT_EQUAL_UINT8((uint8_t)CmdAckStatus::OK, ack2.status);
    TEST_ASSERT_TRUE(commandHandlerPendingReboot());
    TEST_ASSERT_EQUAL_UINT8(5, ack2.result_data[0]);
}

void test_dispatch_self_test_returns_bitmap(void) {
    CmdPacket pkt{};
    pkt.target_sensor_id = 0x1234;
    pkt.cmd_id           = 0x0020;
    pkt.cmd_type         = (uint8_t)SensorCmdType::SELF_TEST;
    pkt.param_len        = 1;
    pkt.params[0]        = 0x1F;  // all five components requested

    AckPacket ack{};
    TEST_ASSERT_TRUE(dispatchCommand(pkt, ack));
    TEST_ASSERT_EQUAL_UINT8((uint8_t)CmdAckStatus::OK, ack.status);

    // Under UNIT_TEST the build_self_test_result function sets deterministic
    // values: bits 0, 2, 3, 4 from the mask (no bit 1 GPS).
    // mask=0x1F -> radio(0x01) | model(0x04) | psram(0x08) | nvs(0x10) = 0x1D
    TEST_ASSERT_EQUAL_UINT8(0x1D, ack.result_data[0]);
    TEST_ASSERT_EQUAL_UINT8(0x00, ack.result_data[1]);
    TEST_ASSERT_EQUAL_UINT8(100,  ack.result_data[2]);  // heap KB
    TEST_ASSERT_EQUAL_UINT8(200,  ack.result_data[3]);  // psram KB
    TEST_ASSERT_EQUAL_UINT8(42,   ack.result_data[4]);  // CPU temp
}

void test_dispatch_unknown_cmd_returns_invalid(void) {
    CmdPacket pkt{};
    pkt.target_sensor_id = 0x1234;
    pkt.cmd_id           = 0x0030;
    pkt.cmd_type         = 0xFE;  // not a defined cmd
    pkt.param_len        = 0;

    AckPacket ack{};
    bool ok = dispatchCommand(pkt, ack);
    TEST_ASSERT_FALSE(ok);
    TEST_ASSERT_EQUAL_UINT8((uint8_t)CmdAckStatus::INVALID, ack.status);
}

void test_dispatch_set_tx_power_range(void) {
    // Out-of-range low
    CmdPacket low{};
    low.cmd_type = (uint8_t)SensorCmdType::SET_TX_POWER;
    low.param_len = 1;
    low.params[0] = 1;  // below 2 dBm minimum
    AckPacket ack_low{};
    TEST_ASSERT_TRUE(dispatchCommand(low, ack_low));
    TEST_ASSERT_EQUAL_UINT8((uint8_t)CmdAckStatus::INVALID, ack_low.status);

    // In-range
    CmdPacket ok{};
    ok.cmd_type = (uint8_t)SensorCmdType::SET_TX_POWER;
    ok.param_len = 1;
    ok.params[0] = 14;
    AckPacket ack_ok{};
    TEST_ASSERT_TRUE(dispatchCommand(ok, ack_ok));
    TEST_ASSERT_EQUAL_UINT8((uint8_t)CmdAckStatus::OK, ack_ok.status);
    TEST_ASSERT_EQUAL_UINT8(14, ack_ok.result_data[0]);

    // Out-of-range high
    CmdPacket hi{};
    hi.cmd_type = (uint8_t)SensorCmdType::SET_TX_POWER;
    hi.param_len = 1;
    hi.params[0] = 23;  // above 22 dBm max
    AckPacket ack_hi{};
    TEST_ASSERT_TRUE(dispatchCommand(hi, ack_hi));
    TEST_ASSERT_EQUAL_UINT8((uint8_t)CmdAckStatus::INVALID, ack_hi.status);
}

// ---------------------------------------------------------------------------
// Unity main
// ---------------------------------------------------------------------------
int main(int, char**) {
    UNITY_BEGIN();
    RUN_TEST(test_parse_valid_command);
    RUN_TEST(test_parse_crc_mismatch_rejects);
    RUN_TEST(test_parse_size_mismatch_rejects);
    RUN_TEST(test_parse_rejects_oversize_param_len);
    RUN_TEST(test_target_mismatch_ignored);
    RUN_TEST(test_broadcast_target_accepted);
    RUN_TEST(test_ack_serialization_roundtrip);
    RUN_TEST(test_dispatch_noop_returns_ok);
    RUN_TEST(test_dispatch_reboot_requires_magic_byte);
    RUN_TEST(test_dispatch_self_test_returns_bitmap);
    RUN_TEST(test_dispatch_unknown_cmd_returns_invalid);
    RUN_TEST(test_dispatch_set_tx_power_range);
    return UNITY_END();
}
