// ============================================================================
// test/test_uav_relay/test_uav_relay.cpp
//
// Unity tests for src/uav_relay.{h,cpp} — STANAG 4586 LoRa -> MAVLink UAV
// command relay. Only compiled into the separate PDL_ENABLE_UAV_RELAY
// build variant (env:heltec_wifi_lora_v4_uav_bridge), NOT into Spencer01's
// active firmware today -- but the parsing/dispatch/ack logic should still
// be correctness-tested for whenever it is deployed. Had zero test
// coverage and its PDL_ENABLE_UAV_RELAY block unconditionally included
// <HardwareSerial.h> with no ARDUINO/native split at all -- restructured
// to match the #ifdef ARDUINO native-stub pattern used throughout this
// sweep (a minimal native HardwareSerial stand-in with a weak, test-
// overridable write() capture hook).
//
// *** REAL HMAC VERIFICATION NOW IMPLEMENTED ***
// A prior pass through this file found HMAC verification was a documented
// no-op stub and deliberately did NOT implement it blind (no visibility
// into the real gateway signing spec at the time). That spec has since
// been confirmed to already exist and match exactly: drone-companion/
// SPEC.md + src/stanag_decoder.cpp implement the counterpart side of this
// SAME wire protocol, with a working, tested HMAC-SHA256 implementation.
// uav_relay.cpp now ports that same implementation (self-contained, no
// mbedtls dependency) plus a replay guard drone-companion also has that
// T1's relay previously lacked entirely. Every command now requires a
// valid HMAC over bytes[0..28) computed with the configured key, and a
// non-replayed sequence number; a relay with no key configured rejects
// everything with POLICY_BLOCK (previously only EMERGENCY_COMMAND was
// gated on key presence -- see test_non_emergency_command_also_blocked_
// without_key below for the behavior change this implies).
//
// UPDATE 2026-07-30: key provisioning IS now addressed -- see
// src/uav_pairing.{h,cpp} (own test suite: test_uav_pairing) for the real
// PAIR_REQ/PAIR_RSP handshake + NVS-persisted root, and
// uav_relay_handle_pair_request()/uav_relay_init() below for how this
// module wires into it. This file's own tests below continue to cover
// the crypto/replay LOGIC once a key IS configured (via direct injection,
// same as before) PLUS a few integration tests for the new pairing
// wiring specifically -- test_uav_pairing owns exhaustive coverage of the
// pairing state machine itself, so this file doesn't duplicate that.
//
// Run with:
//   pio test -e native -f test_uav_relay
// ============================================================================

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

#define PDL_ENABLE_UAV_RELAY

#define STUB_MILLIS_PROVIDED
static uint32_t g_stub_millis = 1000;
static inline uint32_t millis() { return g_stub_millis; }

// uav_relay.cpp includes mesh_publish.h, which unconditionally pulls in
// RadioLib.h -- not native-includable. Same header-avoidance trick used in
// test_ffps.cpp/test_health_report.cpp: pre-define its include guard and
// declare exactly the one function actually called.
#define MESH_PUBLISH_H
#include "../../src/config.h"
static bool    g_publish_called = false;
static uint8_t g_publish_buf[32];
static size_t  g_publish_len = 0;
bool mesh_publish_raw(const uint8_t* data, size_t len) {
    g_publish_called = true;
    size_t copy = len < sizeof(g_publish_buf) ? len : sizeof(g_publish_buf);
    memcpy(g_publish_buf, data, copy);
    g_publish_len = len;
    return true;
}

// Captures whatever uav_relay.cpp's native HardwareSerial stub would have
// written to the drone modem UART (the synthesized MAVLink frame).
static bool    g_uart_write_called = false;
static uint8_t g_uart_buf[64];
static size_t  g_uart_len = 0;
extern "C" void uav_relay_test_capture_uart_write(const uint8_t* data, size_t len) {
    g_uart_write_called = true;
    size_t copy = len < sizeof(g_uart_buf) ? len : sizeof(g_uart_buf);
    memcpy(g_uart_buf, data, copy);
    g_uart_len = len;
}

#include "../../src/uav_pairing.cpp"
#include "../../src/uav_relay.cpp"

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static void build_cmd_frame(uint8_t out[32], uint8_t msg_code, uint16_t sequence,
                             const uint8_t vehicle_hash[8], uint32_t issued_at_mod,
                             const uint8_t body[13], const uint8_t hmac_trunc[4]) {
    out[0] = msg_code;
    out[1] = (sequence >> 8) & 0xFF;
    out[2] = sequence & 0xFF;
    memcpy(out + 3, vehicle_hash, 8);
    out[11] = (issued_at_mod >> 24) & 0xFF;
    out[12] = (issued_at_mod >> 16) & 0xFF;
    out[13] = (issued_at_mod >> 8)  & 0xFF;
    out[14] = issued_at_mod & 0xFF;
    memcpy(out + 15, body, 13);
    memcpy(out + 28, hmac_trunc, 4);
}

// Builds a frame with a GENUINELY VALID HMAC, computed the same way
// production's verify_hmac() will check it (HMAC-SHA256(key, bytes[0..28))
// truncated to 4 bytes) -- uses the anonymous-namespace hmac_sha256_trunc4
// directly (visible here via the single-TU #include above).
static void build_cmd_frame_signed(uint8_t out[32], uint8_t msg_code, uint16_t sequence,
                                    const uint8_t vehicle_hash[8], uint32_t issued_at_mod,
                                    const uint8_t body[13],
                                    const uint8_t* key, size_t key_len) {
    uint8_t placeholder[4] = {0, 0, 0, 0};
    build_cmd_frame(out, msg_code, sequence, vehicle_hash, issued_at_mod, body, placeholder);
    uint8_t computed[4];
    hmac_sha256_trunc4(key, key_len, out, 28, computed);
    memcpy(out + 28, computed, 4);
}

static const uint8_t kVehicleHash[8] = {1, 2, 3, 4, 5, 6, 7, 8};
static const uint8_t kHmac[4] = {0xAA, 0xBB, 0xCC, 0xDD};   // placeholder, NOT a valid HMAC
static const uint8_t kTestKey[16] = {
    0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
    0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
};

static void reset_captures() {
    g_publish_called = false;
    g_publish_len = 0;
    memset(g_publish_buf, 0, sizeof(g_publish_buf));
    g_uart_write_called = false;
    g_uart_len = 0;
    memset(g_uart_buf, 0, sizeof(g_uart_buf));
}

void setUp(void) {
    reset_captures();
    // uav_relay_init() now also touches uav_pairing's state (loads any
    // "persisted" root before applying the explicit key argument) -- must
    // reset that FIRST, or a prior test that completed a pairing (e.g.
    // test_handle_pair_request_completes_pairing_and_refreshes_key below)
    // would leak uav_pairing_is_paired()==true into every later test's
    // uav_relay_init(nullptr, 0) call, silently reloading a stale key and
    // breaking the "starts unconfigured" invariant the rest of this file
    // relies on.
    uav_pairing_test_reset();
    // White-box reset (single-TU include gives direct access to uav_relay.cpp's
    // anonymous-namespace statics) -- no public reset API exists, and
    // uav_relay_init(nullptr, 0) intentionally does NOT clear a previously
    // configured key (see test_init_without_key_preserves_prior_key below),
    // so tests reset state directly for isolation.
    s_hmac_key_len = 0;
    memset(s_hmac_key, 0, sizeof(s_hmac_key));
    s_initialized = false;
    s_seq_counter = 0;
    s_last_seq = 0;
    s_last_mod = 0;
    s_have_seen_seq = false;
    uav_relay_init(nullptr, 0);
}
void tearDown(void) {}

// ---------------------------------------------------------------------------
// uav_relay_parse — pure framing, no HMAC/replay involvement
// ---------------------------------------------------------------------------
void test_parse_32_byte_frame_round_trip(void) {
    uint8_t body[13] = {0};
    int32_t lat = 384500000;   // 38.45 deg * 1e7
    memcpy(body, &lat, 4);
    uint8_t frame[32];
    build_cmd_frame(frame, UAV_MSG_WAYPOINT_LOAD, 0x1234, kVehicleHash, 999, body, kHmac);

    UavCmdPacket pkt{};
    TEST_ASSERT_TRUE(uav_relay_parse(frame, sizeof(frame), pkt));
    TEST_ASSERT_EQUAL_UINT8(UAV_MSG_WAYPOINT_LOAD, pkt.msg_code);
    TEST_ASSERT_EQUAL_UINT16(0x1234, pkt.sequence);
    TEST_ASSERT_EQUAL_MEMORY(kVehicleHash, pkt.vehicle_hash, 8);
    TEST_ASSERT_EQUAL_UINT32(999, pkt.issued_at_mod);
    TEST_ASSERT_EQUAL_MEMORY(body, pkt.body, 13);
    TEST_ASSERT_EQUAL_MEMORY(kHmac, pkt.hmac_trunc, 4);
}

void test_parse_accepts_33_byte_frame_with_prefix(void) {
    uint8_t body[13] = {0};
    uint8_t frame32[32];
    build_cmd_frame(frame32, UAV_MSG_LOI_REQUEST, 5, kVehicleHash, 1, body, kHmac);
    uint8_t frame33[33];
    frame33[0] = MSG_TYPE_UAV_CMD;
    memcpy(frame33 + 1, frame32, 32);

    UavCmdPacket pkt{};
    TEST_ASSERT_TRUE(uav_relay_parse(frame33, sizeof(frame33), pkt));
    TEST_ASSERT_EQUAL_UINT8(UAV_MSG_LOI_REQUEST, pkt.msg_code);
    TEST_ASSERT_EQUAL_UINT16(5, pkt.sequence);
}

void test_parse_rejects_wrong_length(void) {
    uint8_t frame[31] = {0};
    UavCmdPacket pkt{};
    TEST_ASSERT_FALSE(uav_relay_parse(frame, sizeof(frame), pkt));
}

// ---------------------------------------------------------------------------
// uav_relay_send_ack
// ---------------------------------------------------------------------------
void test_send_ack_packs_sequence_and_code(void) {
    uav_relay_send_ack(0xBEEF, UavAckCode::POLICY_BLOCK);
    TEST_ASSERT_TRUE(g_publish_called);
    TEST_ASSERT_EQUAL_UINT32(UAV_ACK_FRAME_SIZE, g_publish_len);
    TEST_ASSERT_EQUAL_UINT8(MSG_TYPE_UAV_ACK, g_publish_buf[0]);
    TEST_ASSERT_EQUAL_UINT8(0xBE, g_publish_buf[1]);
    TEST_ASSERT_EQUAL_UINT8(0xEF, g_publish_buf[2]);
    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavAckCode::POLICY_BLOCK, g_publish_buf[3]);
}

// ---------------------------------------------------------------------------
// uav_relay_handle — parse failure (before any HMAC/key check)
// ---------------------------------------------------------------------------
void test_handle_malformed_frame_sends_bad_crc_ack(void) {
    uint8_t bad_frame[10] = {0};
    uav_relay_handle(bad_frame, sizeof(bad_frame));
    TEST_ASSERT_TRUE(g_publish_called);
    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavAckCode::BAD_CRC, g_publish_buf[3]);
    TEST_ASSERT_FALSE(g_uart_write_called);
}

// ---------------------------------------------------------------------------
// No key configured -> everything rejected (not just EMERGENCY_COMMAND)
// ---------------------------------------------------------------------------
void test_no_key_configured_blocks_emergency_command(void) {
    uint8_t body[13] = {0};
    uint8_t frame[32];
    build_cmd_frame(frame, UAV_MSG_EMERGENCY_COMMAND, 7, kVehicleHash, 1, body, kHmac);

    uav_relay_handle(frame, sizeof(frame));

    TEST_ASSERT_TRUE(g_publish_called);
    TEST_ASSERT_EQUAL_UINT8_MESSAGE((uint8_t)UavAckCode::POLICY_BLOCK, g_publish_buf[3],
        "an emergency command with no key configured must be policy-blocked");
    TEST_ASSERT_FALSE(g_uart_write_called);
}

void test_no_key_configured_also_blocks_ordinary_command(void) {
    // Behavior change from the pre-HMAC-fix version of this module: with
    // real verification in place, EVERY command needs a key, not just
    // EMERGENCY_COMMAND -- an unauthenticated relay can't verify anything.
    uint8_t body[13] = {0};
    uint8_t frame[32];
    build_cmd_frame(frame, UAV_MSG_WAYPOINT_LOAD, 1, kVehicleHash, 1, body, kHmac);

    uav_relay_handle(frame, sizeof(frame));

    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavAckCode::POLICY_BLOCK, g_publish_buf[3]);
    TEST_ASSERT_FALSE(g_uart_write_called);
}

// ---------------------------------------------------------------------------
// HMAC verification -- the actual fix
// ---------------------------------------------------------------------------
void test_valid_hmac_is_accepted_and_dispatched(void) {
    uav_relay_init(kTestKey, sizeof(kTestKey));

    uint8_t body[13] = {0};
    uint8_t frame[32];
    build_cmd_frame_signed(frame, UAV_MSG_WAYPOINT_LOAD, 1, kVehicleHash, 1000,
                           body, kTestKey, sizeof(kTestKey));

    uav_relay_handle(frame, sizeof(frame));

    TEST_ASSERT_TRUE_MESSAGE(g_uart_write_called, "a correctly-signed frame must dispatch");
    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavAckCode::OK, g_publish_buf[3]);
}

void test_garbage_hmac_is_rejected(void) {
    uav_relay_init(kTestKey, sizeof(kTestKey));

    uint8_t body[13] = {0};
    uint8_t frame[32];
    // kHmac is an arbitrary placeholder, not a real HMAC of this content --
    // must now be rejected (this is the actual bug fix this pass makes:
    // previously this exact scenario was silently accepted).
    build_cmd_frame(frame, UAV_MSG_WAYPOINT_EXECUTE, 1, kVehicleHash, 1000, body, kHmac);
    uav_relay_handle(frame, sizeof(frame));

    TEST_ASSERT_FALSE_MESSAGE(g_uart_write_called,
        "a frame with an invalid HMAC must NOT be dispatched to the drone");
    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavAckCode::BAD_HMAC, g_publish_buf[3]);
}

void test_hmac_signed_with_wrong_key_is_rejected(void) {
    const uint8_t wrong_key[16] = {0xFF, 0xEE, 0xDD, 0xCC, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
    uav_relay_init(kTestKey, sizeof(kTestKey));   // relay configured with kTestKey

    uint8_t body[13] = {0};
    uint8_t frame[32];
    // Signed with a DIFFERENT key than the relay has -- must fail.
    build_cmd_frame_signed(frame, UAV_MSG_WAYPOINT_LOAD, 1, kVehicleHash, 1000,
                           body, wrong_key, sizeof(wrong_key));
    uav_relay_handle(frame, sizeof(frame));

    TEST_ASSERT_FALSE(g_uart_write_called);
    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavAckCode::BAD_HMAC, g_publish_buf[3]);
}

void test_tampered_body_after_signing_is_rejected(void) {
    uav_relay_init(kTestKey, sizeof(kTestKey));

    uint8_t body[13] = {0};
    uint8_t frame[32];
    build_cmd_frame_signed(frame, UAV_MSG_WAYPOINT_LOAD, 1, kVehicleHash, 1000,
                           body, kTestKey, sizeof(kTestKey));
    // Flip a body byte AFTER signing -- the HMAC no longer matches the
    // (now-different) content it's supposed to cover.
    frame[15] ^= 0xFF;
    uav_relay_handle(frame, sizeof(frame));

    TEST_ASSERT_FALSE(g_uart_write_called);
    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavAckCode::BAD_HMAC, g_publish_buf[3]);
}

// ---------------------------------------------------------------------------
// Recognized command dispatch (now requires a valid signed frame)
// ---------------------------------------------------------------------------
void test_handle_waypoint_load_emits_mavlink_and_ok_ack(void) {
    uav_relay_init(kTestKey, sizeof(kTestKey));

    uint8_t body[13] = {0};
    int32_t lat = 384500000, lon = -782300000;
    int16_t alt = 150;
    uint16_t heading = 900;   // 90.0 deg x10
    memcpy(body + 0, &lat, 4);
    memcpy(body + 4, &lon, 4);
    // alt/heading are read big-endian by decode_body -- write big-endian.
    body[8]  = (alt >> 8) & 0xFF; body[9]  = alt & 0xFF;
    body[10] = (heading >> 8) & 0xFF; body[11] = heading & 0xFF;

    uint8_t frame[32];
    build_cmd_frame_signed(frame, UAV_MSG_WAYPOINT_LOAD, 42, kVehicleHash, 1000,
                           body, kTestKey, sizeof(kTestKey));
    uav_relay_handle(frame, sizeof(frame));

    TEST_ASSERT_TRUE(g_uart_write_called);
    TEST_ASSERT_EQUAL_UINT32(32, g_uart_len);
    TEST_ASSERT_EQUAL_UINT8(0xFD, g_uart_buf[0]);   // MAVLink v2 STX

    TEST_ASSERT_TRUE(g_publish_called);
    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavAckCode::OK, g_publish_buf[3]);
    TEST_ASSERT_EQUAL_UINT8(42, g_publish_buf[2]);   // sequence low byte
}

void test_handle_position_lla_decodes_lat_lon_into_mavlink_frame(void) {
    uav_relay_init(kTestKey, sizeof(kTestKey));

    uint8_t body[13] = {0};
    // decode_body reads body[0..3] big-endian -- write it that way (matches
    // the pattern already used for alt/heading in the waypoint test above).
    uint32_t lat = 123456789u;
    body[0] = (lat >> 24) & 0xFF; body[1] = (lat >> 16) & 0xFF;
    body[2] = (lat >> 8)  & 0xFF; body[3] = lat & 0xFF;

    uint8_t frame[32];
    build_cmd_frame_signed(frame, UAV_MSG_POSITION_LLA, 1, kVehicleHash, 1000,
                           body, kTestKey, sizeof(kTestKey));
    uav_relay_handle(frame, sizeof(frame));

    TEST_ASSERT_TRUE(g_uart_write_called);
    int32_t lat_in_frame;
    memcpy(&lat_in_frame, g_uart_buf + 11, 4);
    TEST_ASSERT_EQUAL_INT32(lat, lat_in_frame);
}

void test_handle_unknown_msg_code_sends_unknown_type_ack(void) {
    uav_relay_init(kTestKey, sizeof(kTestKey));

    uint8_t body[13] = {0};
    uint8_t frame[32];
    build_cmd_frame_signed(frame, /*msg_code=*/0x99, 1, kVehicleHash, 1000,
                           body, kTestKey, sizeof(kTestKey));
    uav_relay_handle(frame, sizeof(frame));
    TEST_ASSERT_TRUE(g_publish_called);
    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavAckCode::UNKNOWN_TYPE, g_publish_buf[3]);
    TEST_ASSERT_FALSE(g_uart_write_called);
}

// ---------------------------------------------------------------------------
// Emergency-command policy gate (still present, now layered under real auth)
// ---------------------------------------------------------------------------
void test_emergency_command_allowed_with_valid_signature(void) {
    uav_relay_init(kTestKey, sizeof(kTestKey));

    uint8_t body[13] = {0};
    uint8_t frame[32];
    build_cmd_frame_signed(frame, UAV_MSG_EMERGENCY_COMMAND, 8, kVehicleHash, 1000,
                           body, kTestKey, sizeof(kTestKey));
    uav_relay_handle(frame, sizeof(frame));

    TEST_ASSERT_TRUE(g_publish_called);
    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavAckCode::OK, g_publish_buf[3]);
    TEST_ASSERT_TRUE(g_uart_write_called);
}

// ---------------------------------------------------------------------------
// Replay guard (new capability -- drone-companion had this, T1's relay
// previously had no sequence/freshness checking of any kind)
// ---------------------------------------------------------------------------
void test_first_frame_ever_seen_is_accepted(void) {
    uav_relay_init(kTestKey, sizeof(kTestKey));
    uint8_t body[13] = {0};
    uint8_t frame[32];
    build_cmd_frame_signed(frame, UAV_MSG_WAYPOINT_LOAD, 100, kVehicleHash, 5000,
                           body, kTestKey, sizeof(kTestKey));
    uav_relay_handle(frame, sizeof(frame));
    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavAckCode::OK, g_publish_buf[3]);
}

void test_replayed_identical_sequence_is_rejected(void) {
    uav_relay_init(kTestKey, sizeof(kTestKey));
    uint8_t body[13] = {0};
    uint8_t frame[32];
    build_cmd_frame_signed(frame, UAV_MSG_WAYPOINT_LOAD, 100, kVehicleHash, 5000,
                           body, kTestKey, sizeof(kTestKey));

    uav_relay_handle(frame, sizeof(frame));
    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavAckCode::OK, g_publish_buf[3]);

    reset_captures();
    // Exact same frame (same sequence, same issued_at_mod) replayed.
    uav_relay_handle(frame, sizeof(frame));
    TEST_ASSERT_EQUAL_UINT8_MESSAGE((uint8_t)UavAckCode::BAD_HMAC, g_publish_buf[3],
        "a replayed identical sequence must be rejected");
    TEST_ASSERT_FALSE(g_uart_write_called);
}

void test_advancing_sequence_is_accepted(void) {
    uav_relay_init(kTestKey, sizeof(kTestKey));
    uint8_t body[13] = {0};
    uint8_t frame1[32], frame2[32];
    build_cmd_frame_signed(frame1, UAV_MSG_WAYPOINT_LOAD, 100, kVehicleHash, 5000,
                           body, kTestKey, sizeof(kTestKey));
    build_cmd_frame_signed(frame2, UAV_MSG_WAYPOINT_LOAD, 101, kVehicleHash, 5001,
                           body, kTestKey, sizeof(kTestKey));

    uav_relay_handle(frame1, sizeof(frame1));
    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavAckCode::OK, g_publish_buf[3]);

    reset_captures();
    uav_relay_handle(frame2, sizeof(frame2));
    TEST_ASSERT_EQUAL_UINT8_MESSAGE((uint8_t)UavAckCode::OK, g_publish_buf[3],
        "a genuinely newer sequence must be accepted, not treated as a replay");
}

void test_older_sequence_within_window_is_rejected(void) {
    uav_relay_init(kTestKey, sizeof(kTestKey));
    uint8_t body[13] = {0};
    uint8_t frame_new[32], frame_old[32];
    build_cmd_frame_signed(frame_new, UAV_MSG_WAYPOINT_LOAD, 100, kVehicleHash, 5000,
                           body, kTestKey, sizeof(kTestKey));
    build_cmd_frame_signed(frame_old, UAV_MSG_WAYPOINT_LOAD, 50, kVehicleHash, 4990,
                           body, kTestKey, sizeof(kTestKey));

    uav_relay_handle(frame_new, sizeof(frame_new));
    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavAckCode::OK, g_publish_buf[3]);

    reset_captures();
    // Lower sequence AND issued_at_mod within the replay window of the
    // last accepted frame -- must be rejected, not treated as fresh.
    uav_relay_handle(frame_old, sizeof(frame_old));
    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavAckCode::BAD_HMAC, g_publish_buf[3]);
    TEST_ASSERT_FALSE(g_uart_write_called);
}

// ---------------------------------------------------------------------------
// uav_relay_init
// ---------------------------------------------------------------------------
void test_init_rejects_oversized_key(void) {
    uint8_t oversized_key[64] = {0};
    uav_relay_init(oversized_key, sizeof(oversized_key));
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, s_hmac_key_len,
        "a key longer than the internal buffer must be rejected, not partially copied");
}

void test_init_without_key_preserves_prior_key(void) {
    // Documents actual current behavior (not necessarily ideal): re-calling
    // uav_relay_init(nullptr, 0) does NOT clear a previously configured key.
    const uint8_t key[16] = {0xAB};
    uav_relay_init(key, sizeof(key));
    TEST_ASSERT_EQUAL_UINT32(16, s_hmac_key_len);

    uav_relay_init(nullptr, 0);
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(16, s_hmac_key_len,
        "current behavior: init(nullptr,0) does not clear a prior key -- documented, not changed");
}

// ---------------------------------------------------------------------------
// Pairing wiring -- uav_relay_set_own_sensor_id() / uav_relay_handle_pair_
// request(). The state-machine/frame-format logic itself (window timing,
// parse/build, NVS persistence) is exhaustively covered by test_uav_pairing;
// these tests only cover this module's glue: does it publish the response,
// does it stamp the configured sensor id into vehicle_hash, and does a
// successful pairing actually take effect for subsequent uav_relay_handle()
// calls without requiring a reboot.
// ---------------------------------------------------------------------------
static void build_pair_req_frame(uint8_t out[UAV_PAIR_REQ_FRAME_SIZE], const uint8_t root32[32]) {
    memset(out, 0, UAV_PAIR_REQ_FRAME_SIZE);
    out[0] = MSG_TYPE_UAV_PAIR_REQ;
    memcpy(out + 1, root32, 32);
    strncpy((char*)out + 41, "t1-drone", 23);
    strncpy((char*)out + 65, "OPX", 23);
    out[121] = 1;   // allow_terminate
    out[122] = (uint8_t)UavDroneProfile::PX4;
}

void test_set_own_sensor_id_stamped_into_pair_response_vehicle_hash(void) {
    uav_relay_set_own_sensor_id(0x11223344);

    uint8_t root[32];
    memset(root, 0xAB, sizeof(root));
    uint8_t frame[UAV_PAIR_REQ_FRAME_SIZE];
    build_pair_req_frame(frame, root);

    uav_relay_handle_pair_request(frame, sizeof(frame));

    TEST_ASSERT_TRUE(g_publish_called);
    TEST_ASSERT_EQUAL_UINT32(UAV_PAIR_RSP_FRAME_SIZE, g_publish_len);
    TEST_ASSERT_EQUAL_UINT8(MSG_TYPE_UAV_PAIR_RSP, g_publish_buf[0]);
    uint32_t sensor_id_le;
    memcpy(&sensor_id_le, g_publish_buf + 1, 4);
    TEST_ASSERT_EQUAL_UINT32(0x11223344, sensor_id_le);
    TEST_ASSERT_EQUAL_UINT8(0, g_publish_buf[5]);   // upper 4 vehicle_hash bytes untouched
}

void test_handle_pair_request_completes_pairing_and_refreshes_key(void) {
    // setUp() already left a fresh pairing window open (cold-boot uav_pairing_
    // init(0) inside uav_relay_init(nullptr, 0), never yet paired).
    uint8_t root[32];
    for (int i = 0; i < 32; i++) root[i] = (uint8_t)(i * 3 + 1);
    uint8_t frame[UAV_PAIR_REQ_FRAME_SIZE];
    build_pair_req_frame(frame, root);

    uav_relay_handle_pair_request(frame, sizeof(frame));

    TEST_ASSERT_TRUE(g_publish_called);
    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavPairRspStatus::OK, g_publish_buf[12]);
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(32, s_hmac_key_len,
        "a successful pairing must refresh this module's in-RAM HMAC key immediately");
    TEST_ASSERT_EQUAL_MEMORY(root, s_hmac_key, 32);

    // Prove it actually takes effect: a command signed with the newly
    // paired root must now verify and dispatch, with no reboot/re-init.
    reset_captures();
    uint8_t body[13] = {0};
    uint8_t cmd[32];
    build_cmd_frame_signed(cmd, UAV_MSG_WAYPOINT_LOAD, 1, kVehicleHash, 1000,
                           body, root, sizeof(root));
    uav_relay_handle(cmd, sizeof(cmd));
    TEST_ASSERT_TRUE_MESSAGE(g_uart_write_called,
        "a command signed with the freshly-paired root must verify without a reboot");
}

void test_handle_pair_request_rejected_when_window_closed_does_not_refresh_key(void) {
    // Complete one pairing (closes the window), then attempt a second
    // PAIR_REQ with a different root -- must be rejected and must NOT
    // clobber the already-paired key.
    uint8_t root1[32];
    memset(root1, 0x11, sizeof(root1));
    uint8_t frame1[UAV_PAIR_REQ_FRAME_SIZE];
    build_pair_req_frame(frame1, root1);
    uav_relay_handle_pair_request(frame1, sizeof(frame1));
    TEST_ASSERT_EQUAL_UINT8((uint8_t)UavPairRspStatus::OK, g_publish_buf[12]);

    reset_captures();
    uint8_t root2[32];
    memset(root2, 0x22, sizeof(root2));
    uint8_t frame2[UAV_PAIR_REQ_FRAME_SIZE];
    build_pair_req_frame(frame2, root2);
    uav_relay_handle_pair_request(frame2, sizeof(frame2));

    TEST_ASSERT_TRUE(g_publish_called);
    TEST_ASSERT_EQUAL_UINT8_MESSAGE((uint8_t)UavPairRspStatus::PAIR_MODE_REQUIRED, g_publish_buf[12],
        "a second PAIR_REQ after the window has closed must be rejected");
    TEST_ASSERT_EQUAL_MEMORY_MESSAGE(root1, s_hmac_key, 32,
        "a rejected re-pair attempt must not overwrite the already-paired key");
}

void test_handle_pair_request_malformed_frame_sends_nothing(void) {
    uint8_t frame[UAV_PAIR_REQ_FRAME_SIZE] = {0};
    frame[0] = MSG_TYPE_UAV_CMD;   // wrong msg_type -> parse fails
    uav_relay_handle_pair_request(frame, sizeof(frame));
    TEST_ASSERT_FALSE_MESSAGE(g_publish_called,
        "a frame that fails to parse as PAIR_REQ must not produce any response");
}

// ---------------------------------------------------------------------------
// Telemetry uplink (drone -> mesh, MSG_TYPE_UAV_TELEM/0x0C) -- the
// software-only "SiK telemetry" equivalent: no new hardware, receive +
// relay built entirely on the existing LoRa mesh / uav_relay wire
// protocol shared with drone-companion.
// ---------------------------------------------------------------------------
static void build_telem_frame(uint8_t out[32], uint16_t sequence,
                               const uint8_t vehicle_hash[8], uint32_t issued_at_mod,
                               int32_t lat_e7, int32_t lon_e7, int16_t alt_m,
                               uint16_t battery_mv, uint8_t battery_pct,
                               uint8_t flight_mode, uint8_t armed_flags) {
    out[0] = MSG_TYPE_UAV_TELEM;
    out[1] = (uint8_t)((sequence >> 8) & 0xFF);
    out[2] = (uint8_t)(sequence & 0xFF);
    memcpy(out + 3, vehicle_hash, 8);
    out[11] = (uint8_t)((issued_at_mod >> 24) & 0xFF);
    out[12] = (uint8_t)((issued_at_mod >> 16) & 0xFF);
    out[13] = (uint8_t)((issued_at_mod >> 8)  & 0xFF);
    out[14] = (uint8_t)(issued_at_mod & 0xFF);
    out[15] = (uint8_t)(((uint32_t)lat_e7 >> 24) & 0xFF);
    out[16] = (uint8_t)(((uint32_t)lat_e7 >> 16) & 0xFF);
    out[17] = (uint8_t)(((uint32_t)lat_e7 >> 8)  & 0xFF);
    out[18] = (uint8_t)((uint32_t)lat_e7 & 0xFF);
    out[19] = (uint8_t)(((uint32_t)lon_e7 >> 24) & 0xFF);
    out[20] = (uint8_t)(((uint32_t)lon_e7 >> 16) & 0xFF);
    out[21] = (uint8_t)(((uint32_t)lon_e7 >> 8)  & 0xFF);
    out[22] = (uint8_t)((uint32_t)lon_e7 & 0xFF);
    out[23] = (uint8_t)(((uint16_t)alt_m >> 8) & 0xFF);
    out[24] = (uint8_t)((uint16_t)alt_m & 0xFF);
    out[25] = (uint8_t)((battery_mv >> 8) & 0xFF);
    out[26] = (uint8_t)(battery_mv & 0xFF);
    out[27] = battery_pct;
    out[28] = flight_mode;
    out[29] = armed_flags;
    // crc16_xmodem is in uav_relay.cpp's anonymous namespace -- accessible
    // here via the single-TU #include, giving a genuinely valid CRC.
    uint16_t crc = crc16_xmodem(out, 30);
    out[30] = (uint8_t)((crc >> 8) & 0xFF);
    out[31] = (uint8_t)(crc & 0xFF);
}

void test_parse_telemetry_valid_beacon_round_trip(void) {
    uint8_t frame[32];
    build_telem_frame(frame, 500, kVehicleHash, 123456, 384500000, -782300000,
                      150, 12600, 88, (uint8_t)FlightModeAgnostic::GUIDED, 0x03);

    UavTelemetryBeacon beacon{};
    TEST_ASSERT_TRUE(uav_relay_parse_telemetry(frame, sizeof(frame), beacon));
    TEST_ASSERT_EQUAL_UINT16(500, beacon.sequence);
    TEST_ASSERT_EQUAL_MEMORY(kVehicleHash, beacon.vehicle_hash, 8);
    TEST_ASSERT_EQUAL_UINT32(123456, beacon.issued_at_mod);
    TEST_ASSERT_EQUAL_INT32(384500000, beacon.lat_e7);
    TEST_ASSERT_EQUAL_INT32(-782300000, beacon.lon_e7);
    TEST_ASSERT_EQUAL_INT16(150, beacon.alt_m);
    TEST_ASSERT_EQUAL_UINT16(12600, beacon.battery_mv);
    TEST_ASSERT_EQUAL_UINT8(88, beacon.battery_pct);
    TEST_ASSERT_EQUAL_UINT8((uint8_t)FlightModeAgnostic::GUIDED, beacon.flight_mode);
    TEST_ASSERT_EQUAL_UINT8(0x03, beacon.armed_flags);
}

void test_parse_telemetry_rejects_wrong_length(void) {
    uint8_t frame[31] = {0};
    UavTelemetryBeacon beacon{};
    TEST_ASSERT_FALSE(uav_relay_parse_telemetry(frame, sizeof(frame), beacon));
}

void test_parse_telemetry_rejects_wrong_msg_type(void) {
    uint8_t frame[32];
    build_telem_frame(frame, 1, kVehicleHash, 1, 0, 0, 0, 0, 0, 0, 0);
    frame[0] = MSG_TYPE_UAV_CMD;   // corrupt the type byte
    UavTelemetryBeacon beacon{};
    TEST_ASSERT_FALSE(uav_relay_parse_telemetry(frame, sizeof(frame), beacon));
}

void test_parse_telemetry_rejects_bad_crc(void) {
    uint8_t frame[32];
    build_telem_frame(frame, 1, kVehicleHash, 1, 0, 0, 0, 0, 0, 0, 0);
    frame[31] ^= 0xFF;   // corrupt the CRC
    UavTelemetryBeacon beacon{};
    TEST_ASSERT_FALSE(uav_relay_parse_telemetry(frame, sizeof(frame), beacon));
}

void test_parse_telemetry_rejects_tampered_payload(void) {
    uint8_t frame[32];
    build_telem_frame(frame, 1, kVehicleHash, 1, 384500000, 0, 0, 0, 0, 0, 0);
    frame[15] ^= 0xFF;   // corrupt a payload byte, CRC now stale
    UavTelemetryBeacon beacon{};
    TEST_ASSERT_FALSE(uav_relay_parse_telemetry(frame, sizeof(frame), beacon));
}

void test_handle_telemetry_relays_valid_beacon_verbatim(void) {
    uint8_t frame[32];
    build_telem_frame(frame, 42, kVehicleHash, 1000, 384500000, -782300000,
                      150, 12600, 88, (uint8_t)FlightModeAgnostic::AUTO, 0x01);

    uav_relay_handle_telemetry(frame, sizeof(frame));

    TEST_ASSERT_TRUE_MESSAGE(g_publish_called, "a valid telemetry beacon must be relayed onto the mesh");
    TEST_ASSERT_EQUAL_UINT32(32, g_publish_len);
    TEST_ASSERT_EQUAL_MEMORY(frame, g_publish_buf, 32);
}

void test_handle_telemetry_does_not_relay_invalid_crc(void) {
    uint8_t frame[32];
    build_telem_frame(frame, 1, kVehicleHash, 1, 0, 0, 0, 0, 0, 0, 0);
    frame[31] ^= 0xFF;

    uav_relay_handle_telemetry(frame, sizeof(frame));

    TEST_ASSERT_FALSE_MESSAGE(g_publish_called, "a beacon with an invalid CRC must not be relayed");
}

void test_handle_telemetry_works_without_key_configured(void) {
    // Telemetry has no HMAC field (CRC-only, per drone-companion/SPEC.md
    // 2.3) -- unlike STANAG commands, it must relay even when no HMAC key
    // has been provisioned (s_hmac_key_len stays 0 from setUp()'s reset).
    TEST_ASSERT_EQUAL_UINT32(0, s_hmac_key_len);
    uint8_t frame[32];
    build_telem_frame(frame, 1, kVehicleHash, 1, 0, 0, 0, 0, 0, 0, 0);

    uav_relay_handle_telemetry(frame, sizeof(frame));

    TEST_ASSERT_TRUE(g_publish_called);
}

// Known-answer test for the ported CRC-16-CCITT/XMODEM algorithm, using
// the standard published check value for "123456789" -- independently
// verified via a from-scratch Python reimplementation of the same
// algorithm. Catches an algorithm-level bug independent of anything
// UAV-specific.
void test_crc16_xmodem_matches_standard_check_value(void) {
    const uint8_t data[] = "123456789";
    TEST_ASSERT_EQUAL_HEX16(0x31C3, crc16_xmodem(data, 9));
}

// ---------------------------------------------------------------------------
// HMAC implementation correctness -- known-answer test against a standard
// HMAC-SHA256 test vector (RFC 4231 test case 1), independent of anything
// UAV-specific. If this fails, the SHA-256/HMAC construction itself is
// broken, not just its integration into this module.
// ---------------------------------------------------------------------------
void test_hmac_sha256_matches_rfc4231_test_vector_1(void) {
    // RFC 4231 #1: key = 0x0b * 20, data = "Hi There"
    uint8_t key[20];
    memset(key, 0x0b, sizeof(key));
    const uint8_t data[] = "Hi There";
    uint8_t full[32];
    hmac_sha256(key, sizeof(key), data, 8, full);

    // Expected: b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7
    // (independently verified via Python's hmac module -- an earlier
    // version of this test had a transcription typo in the final byte,
    // 0xff instead of the correct 0xf7)
    const uint8_t expected[32] = {
        0xb0,0x34,0x4c,0x61,0xd8,0xdb,0x38,0x53,0x5c,0xa8,0xaf,0xce,0xaf,0x0b,0xf1,0x2b,
        0x88,0x1d,0xc2,0x00,0xc9,0x83,0x3d,0xa7,0x26,0xe9,0x37,0x6c,0x2e,0x32,0xcf,0xf7,
    };
    TEST_ASSERT_EQUAL_MEMORY(expected, full, 32);
}

int main(int, char**) {
    UNITY_BEGIN();

    RUN_TEST(test_parse_32_byte_frame_round_trip);
    RUN_TEST(test_parse_accepts_33_byte_frame_with_prefix);
    RUN_TEST(test_parse_rejects_wrong_length);

    RUN_TEST(test_send_ack_packs_sequence_and_code);

    RUN_TEST(test_handle_malformed_frame_sends_bad_crc_ack);

    RUN_TEST(test_no_key_configured_blocks_emergency_command);
    RUN_TEST(test_no_key_configured_also_blocks_ordinary_command);

    RUN_TEST(test_valid_hmac_is_accepted_and_dispatched);
    RUN_TEST(test_garbage_hmac_is_rejected);
    RUN_TEST(test_hmac_signed_with_wrong_key_is_rejected);
    RUN_TEST(test_tampered_body_after_signing_is_rejected);

    RUN_TEST(test_handle_waypoint_load_emits_mavlink_and_ok_ack);
    RUN_TEST(test_handle_position_lla_decodes_lat_lon_into_mavlink_frame);
    RUN_TEST(test_handle_unknown_msg_code_sends_unknown_type_ack);

    RUN_TEST(test_emergency_command_allowed_with_valid_signature);

    RUN_TEST(test_first_frame_ever_seen_is_accepted);
    RUN_TEST(test_replayed_identical_sequence_is_rejected);
    RUN_TEST(test_advancing_sequence_is_accepted);
    RUN_TEST(test_older_sequence_within_window_is_rejected);

    RUN_TEST(test_init_rejects_oversized_key);
    RUN_TEST(test_init_without_key_preserves_prior_key);

    RUN_TEST(test_set_own_sensor_id_stamped_into_pair_response_vehicle_hash);
    RUN_TEST(test_handle_pair_request_completes_pairing_and_refreshes_key);
    RUN_TEST(test_handle_pair_request_rejected_when_window_closed_does_not_refresh_key);
    RUN_TEST(test_handle_pair_request_malformed_frame_sends_nothing);

    RUN_TEST(test_parse_telemetry_valid_beacon_round_trip);
    RUN_TEST(test_parse_telemetry_rejects_wrong_length);
    RUN_TEST(test_parse_telemetry_rejects_wrong_msg_type);
    RUN_TEST(test_parse_telemetry_rejects_bad_crc);
    RUN_TEST(test_parse_telemetry_rejects_tampered_payload);
    RUN_TEST(test_handle_telemetry_relays_valid_beacon_verbatim);
    RUN_TEST(test_handle_telemetry_does_not_relay_invalid_crc);
    RUN_TEST(test_handle_telemetry_works_without_key_configured);
    RUN_TEST(test_crc16_xmodem_matches_standard_check_value);

    RUN_TEST(test_hmac_sha256_matches_rfc4231_test_vector_1);

    return UNITY_END();
}
