// ============================================================================
// test/test_mesh_routing/test_mesh_routing.cpp
//
// Unity tests for src/mesh_routing.{h,cpp} — MRSH multi-hop LoRa mesh
// routing (framing, CRC, neighbor table, forwarding decision, store-and-
// forward). Previously zero test coverage despite the module's own header
// explicitly marking several helpers "public for unit tests" and already
// having ARDUINO/native millis() guards + weak-linked mesh_lora_send/
// uav_relay_handle for stubbing -- clearly designed for eventual testing
// that never happened. NOTE: as of this writing, mesh_routing's entry
// points (mesh_routing_init/_tick/mesh_handle_*) are not actually called
// from main.cpp anywhere -- this module is currently dead code on Spencer01
// (included, compiled, but never invoked), so these tests protect logic
// that will matter once it's wired up, not something exercisable live on
// real hardware today.
//
// Writing these tests surfaced a real bug, fixed in mesh_routing.cpp:
// store_enqueue() memcpy'd an unbounded caller-supplied `len` into a fixed
// MRSH_ROUTE_MAX_BYTES buffer with no bounds check -- its only call site
// (the store-and-forward branch of mesh_handle_route_packet, reached when
// no next-hop neighbor is known) passes through the raw RX length with no
// upstream cap on that particular path (unlike the sibling "forward"
// branch a few lines later, which does check). See
// test_store_and_forward_rejects_oversized_packet below.
//
// Run with:
//   pio test -e native -f test_mesh_routing
// ============================================================================

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

// mesh_routing.cpp's native path (`#if defined(ARDUINO) || defined
// (ESP_PLATFORM)` not taken) uses its own chrono-based mrsh_now_ms(), which
// is not test-overridable (unlike network_id.cpp/threat_correlator.cpp's
// STUB_MILLIS_PROVIDED pattern). These tests therefore focus on everything
// deterministically testable without literal time control -- framing,
// CRC, routing decisions, neighbor-table population, store-and-forward
// bounds -- and skip mesh_routing_tick()'s own 15s/30s/60s interval gating
// and cross-call EWMA blending, which would need real elapsed wall-clock
// time to exercise meaningfully.

// ---------------------------------------------------------------------------
// Stubs for the two weak-linked externs mesh_routing.cpp calls.
// ---------------------------------------------------------------------------
static bool    g_tx_called = false;
static uint8_t g_tx_buf[128];
static size_t  g_tx_len = 0;
static int     g_tx_call_count = 0;
static bool    g_tx_fail = false;

extern "C" int mesh_lora_send(const uint8_t* data, size_t len) {
    g_tx_called = true;
    g_tx_call_count++;
    size_t copy = len < sizeof(g_tx_buf) ? len : sizeof(g_tx_buf);
    memcpy(g_tx_buf, data, copy);
    g_tx_len = len;
    return g_tx_fail ? -1 : 0;
}

static bool    g_deliver_called = false;
static uint8_t g_deliver_buf[64];
static size_t  g_deliver_len = 0;
extern "C" void uav_relay_handle(const uint8_t* data, size_t len) {
    g_deliver_called = true;
    size_t copy = len < sizeof(g_deliver_buf) ? len : sizeof(g_deliver_buf);
    memcpy(g_deliver_buf, data, copy);
    g_deliver_len = len;
}

#include "../../src/mesh_routing.cpp"

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
static void reset_tx_capture() {
    g_tx_called = false;
    g_tx_len = 0;
    g_tx_call_count = 0;
    g_tx_fail = false;
    memset(g_tx_buf, 0, sizeof(g_tx_buf));
    g_deliver_called = false;
    g_deliver_len = 0;
    memset(g_deliver_buf, 0, sizeof(g_deliver_buf));
}

void setUp(void) {
    reset_tx_capture();
    mesh_routing_init("selfnode0000000", MRSH_RADIO_LORA915);
    mesh_set_deliver_callback(nullptr);   // fall back to default_deliver (uav_relay_handle stub)
}
void tearDown(void) {}

// ---------------------------------------------------------------------------
// mesh_crc32 — standard CRC-32/ISO-HDLC known-answer test. This pins the
// polynomial/reflection/init/xorout choices as genuinely standard CRC-32,
// not a subtly different variant -- "123456789" -> 0xCBF43926 is the
// universally-cited CRC-32 check value.
// ---------------------------------------------------------------------------
void test_crc32_known_answer_test_vector(void) {
    const uint8_t data[] = "123456789";
    uint32_t crc = mesh_crc32(data, 9);
    TEST_ASSERT_EQUAL_UINT32(0xCBF43926u, crc);
}

void test_crc32_empty_input(void) {
    TEST_ASSERT_EQUAL_UINT32(0x00000000u, mesh_crc32(nullptr, 0));
}

// ---------------------------------------------------------------------------
// mesh_node_hash
// ---------------------------------------------------------------------------
void test_node_hash_is_deterministic(void) {
    uint8_t h1[6], h2[6];
    mesh_node_hash("heltec-v4-0a1b2c", h1);
    mesh_node_hash("heltec-v4-0a1b2c", h2);
    TEST_ASSERT_EQUAL_MEMORY(h1, h2, 6);
}

void test_node_hash_differs_for_different_ids(void) {
    uint8_t h1[6], h2[6];
    mesh_node_hash("node-alpha", h1);
    mesh_node_hash("node-beta", h2);
    TEST_ASSERT_TRUE(memcmp(h1, h2, 6) != 0);
}

// ---------------------------------------------------------------------------
// mesh_build_route_packet / mesh_parse_route_packet round-trip
// ---------------------------------------------------------------------------
void test_route_packet_round_trip(void) {
    const uint8_t payload[] = {0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02};
    uint8_t buf[MRSH_ROUTE_MAX_BYTES];
    size_t len = mesh_build_route_packet(
        buf, sizeof(buf), /*packet_id=*/0x1234,
        "src-node", "dest-node",
        /*ttl=*/8, MRSH_FLAG_REQUIRE_ACK, /*next_hop_hint=*/0,
        payload, sizeof(payload));

    TEST_ASSERT_TRUE(len > 0);
    TEST_ASSERT_EQUAL_UINT8(MSG_TYPE_MESH_ROUTE, buf[0]);

    MrshRouteHeader hdr;
    TEST_ASSERT_TRUE(mesh_parse_route_packet(buf, len, &hdr));
    TEST_ASSERT_EQUAL_UINT16(0x1234, hdr.packet_id);
    TEST_ASSERT_EQUAL_UINT8(8, hdr.ttl);
    TEST_ASSERT_EQUAL_UINT8(MRSH_FLAG_REQUIRE_ACK, hdr.flags);
    TEST_ASSERT_EQUAL_UINT8(sizeof(payload), hdr.payload_len);
    TEST_ASSERT_EQUAL_MEMORY(payload, hdr.payload, sizeof(payload));
    TEST_ASSERT_EQUAL_UINT32(hdr.crc32_computed, hdr.crc32_claimed);

    uint8_t expect_src_hash[6];
    mesh_node_hash("src-node", expect_src_hash);
    TEST_ASSERT_EQUAL_MEMORY(expect_src_hash, hdr.src_hash, 6);
}

void test_route_packet_null_dest_is_broadcast_hash(void) {
    uint8_t buf[MRSH_ROUTE_MAX_BYTES];
    size_t len = mesh_build_route_packet(
        buf, sizeof(buf), 1, "src-node", nullptr,
        8, 0, 0, nullptr, 0);
    TEST_ASSERT_TRUE(len > 0);

    MrshRouteHeader hdr;
    TEST_ASSERT_TRUE(mesh_parse_route_packet(buf, len, &hdr));
    uint8_t all_ff[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
    TEST_ASSERT_EQUAL_MEMORY(all_ff, hdr.dest_hash, 6);
}

void test_route_packet_rejects_oversized_payload(void) {
    uint8_t buf[MRSH_ROUTE_MAX_BYTES];
    uint8_t payload[MRSH_MAX_PAYLOAD + 1] = {0};
    size_t len = mesh_build_route_packet(
        buf, sizeof(buf), 1, "src", "dst", 8, 0, 0,
        payload, sizeof(payload));
    TEST_ASSERT_EQUAL_UINT32(0, len);
}

void test_route_packet_rejects_insufficient_out_buffer(void) {
    uint8_t buf[8];   // far too small
    size_t len = mesh_build_route_packet(
        buf, sizeof(buf), 1, "src", "dst", 8, 0, 0, nullptr, 0);
    TEST_ASSERT_EQUAL_UINT32(0, len);
}

void test_parse_rejects_wrong_msg_type(void) {
    uint8_t buf[MRSH_ROUTE_MAX_BYTES];
    size_t len = mesh_build_route_packet(buf, sizeof(buf), 1, "src", "dst", 8, 0, 0, nullptr, 0);
    buf[0] = MSG_TYPE_MESH_ACK;   // corrupt the type byte
    MrshRouteHeader hdr;
    TEST_ASSERT_FALSE(mesh_parse_route_packet(buf, len, &hdr));
}

void test_parse_rejects_truncated_buffer(void) {
    uint8_t buf[MRSH_ROUTE_MAX_BYTES];
    size_t len = mesh_build_route_packet(buf, sizeof(buf), 1, "src", "dst", 8, 0, 0, nullptr, 0);
    MrshRouteHeader hdr;
    TEST_ASSERT_FALSE(mesh_parse_route_packet(buf, len - 1, &hdr));
}

void test_parse_rejects_corrupted_crc(void) {
    const uint8_t payload[] = {0x01, 0x02, 0x03};
    uint8_t buf[MRSH_ROUTE_MAX_BYTES];
    size_t len = mesh_build_route_packet(buf, sizeof(buf), 1, "src", "dst", 8, 0, 0, payload, sizeof(payload));
    buf[len - 1] ^= 0xFF;   // flip a CRC byte
    MrshRouteHeader hdr;
    TEST_ASSERT_FALSE(mesh_parse_route_packet(buf, len, &hdr));
}

// ---------------------------------------------------------------------------
// mesh_build_ack_packet / mesh_build_discover_packet layout
// ---------------------------------------------------------------------------
void test_ack_packet_layout(void) {
    uint8_t buf[MRSH_ACK_BYTES];
    size_t len = mesh_build_ack_packet(buf, sizeof(buf), 0x0042, "acker-node",
                                        MRSH_ACK_DELIVERED, 3, -72);
    TEST_ASSERT_EQUAL_UINT32(MRSH_ACK_BYTES, len);
    TEST_ASSERT_EQUAL_UINT8(MSG_TYPE_MESH_ACK, buf[0]);
    TEST_ASSERT_EQUAL_UINT8(MRSH_ACK_DELIVERED, buf[9]);
    TEST_ASSERT_EQUAL_UINT8(3, buf[10]);
    TEST_ASSERT_EQUAL_UINT8((uint8_t)(int8_t)-72, buf[11]);
    uint32_t crc_claim = read_u32_le(buf + 12);
    TEST_ASSERT_EQUAL_UINT32(mesh_crc32(buf, 12), crc_claim);
}

void test_discover_packet_round_trip_via_handle(void) {
    uint8_t buf[MRSH_DISCOVER_BYTES];
    size_t len = mesh_build_discover_packet(buf, sizeof(buf), "peer-node-000001",
                                             MRSH_RADIO_LORA915 | MRSH_RADIO_BT5,
                                             /*hop_to_gateway=*/2, /*link_quality=*/77);
    TEST_ASSERT_EQUAL_UINT32(MRSH_DISCOVER_BYTES, len);

    mesh_handle_discover(buf, len);

    NeighborEntry neighbors[8];
    size_t n = mesh_get_neighbors(neighbors, 8);
    TEST_ASSERT_EQUAL_UINT32(1, n);
    TEST_ASSERT_EQUAL_UINT8(MRSH_RADIO_LORA915 | MRSH_RADIO_BT5, neighbors[0].radios_bitmap);
    TEST_ASSERT_EQUAL_UINT8(2, neighbors[0].hop_to_gateway);
}

void test_discover_with_corrupted_crc_does_not_add_neighbor(void) {
    uint8_t buf[MRSH_DISCOVER_BYTES];
    mesh_build_discover_packet(buf, sizeof(buf), "peer-node", MRSH_RADIO_LORA915, 1, 50);
    buf[MRSH_DISCOVER_BYTES - 1] ^= 0xFF;
    mesh_handle_discover(buf, MRSH_DISCOVER_BYTES);

    NeighborEntry neighbors[8];
    TEST_ASSERT_EQUAL_UINT32(0, mesh_get_neighbors(neighbors, 8));
}

// ---------------------------------------------------------------------------
// mesh_handle_route_packet — delivery / TTL drop / no-route / store-forward
// ---------------------------------------------------------------------------
void test_route_packet_addressed_to_self_is_delivered(void) {
    const uint8_t payload[] = {0xAA, 0xBB};
    uint8_t buf[MRSH_ROUTE_MAX_BYTES];
    size_t len = mesh_build_route_packet(buf, sizeof(buf), 1,
        "other-node", "selfnode0000000", 8, 0, 0, payload, sizeof(payload));

    mesh_handle_route_packet(buf, len);

    TEST_ASSERT_TRUE_MESSAGE(g_deliver_called, "packet addressed to our own node hash must be delivered");
    TEST_ASSERT_EQUAL_UINT32(sizeof(payload), g_deliver_len);
    TEST_ASSERT_EQUAL_MEMORY(payload, g_deliver_buf, sizeof(payload));

    MrshStats stats{};
    mesh_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.pkts_delivered_last_60s);
}

void test_route_packet_delivered_with_require_ack_sends_ack(void) {
    uint8_t buf[MRSH_ROUTE_MAX_BYTES];
    size_t len = mesh_build_route_packet(buf, sizeof(buf), 7,
        "other-node", "selfnode0000000", 8, MRSH_FLAG_REQUIRE_ACK, 0, nullptr, 0);

    mesh_handle_route_packet(buf, len);

    TEST_ASSERT_TRUE_MESSAGE(g_tx_called, "REQUIRE_ACK delivery must transmit an ACK");
    TEST_ASSERT_EQUAL_UINT8(MSG_TYPE_MESH_ACK, g_tx_buf[0]);
    TEST_ASSERT_EQUAL_UINT8(MRSH_ACK_DELIVERED, g_tx_buf[9]);
}

void test_route_packet_ttl_zero_is_dropped_with_ack(void) {
    uint8_t buf[MRSH_ROUTE_MAX_BYTES];
    size_t len = mesh_build_route_packet(buf, sizeof(buf), 2,
        "other-node", "unreachable-dest", /*ttl=*/0, 0, 0, nullptr, 0);

    mesh_handle_route_packet(buf, len);

    TEST_ASSERT_FALSE(g_deliver_called);
    TEST_ASSERT_TRUE(g_tx_called);
    TEST_ASSERT_EQUAL_UINT8(MRSH_ACK_DROPPED_TTL, g_tx_buf[9]);

    MrshStats stats{};
    mesh_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.pkts_dropped_last_60s);
}

void test_route_packet_no_route_no_flags_drops_with_nak(void) {
    uint8_t buf[MRSH_ROUTE_MAX_BYTES];
    size_t len = mesh_build_route_packet(buf, sizeof(buf), 3,
        "other-node", "unreachable-dest", 8, /*flags=*/0, 0, nullptr, 0);

    mesh_handle_route_packet(buf, len);

    TEST_ASSERT_TRUE(g_tx_called);
    TEST_ASSERT_EQUAL_UINT8(MRSH_ACK_NO_ROUTE, g_tx_buf[9]);
}

void test_route_packet_no_route_store_and_forward_enqueues(void) {
    uint8_t buf[MRSH_ROUTE_MAX_BYTES];
    size_t len = mesh_build_route_packet(buf, sizeof(buf), 4,
        "other-node", "unreachable-dest", 8, MRSH_FLAG_STORE_AND_FORWARD, 0, nullptr, 0);

    mesh_handle_route_packet(buf, len);

    TEST_ASSERT_TRUE(g_tx_called);
    TEST_ASSERT_EQUAL_UINT8(MRSH_ACK_STORE_FORWARD_QUEUED, g_tx_buf[9]);

    MrshStats stats{};
    mesh_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.store_forward_depth);
}

// Regression test for the real bug this pass fixed: store_enqueue() must
// reject an oversized packet rather than overflow its fixed pkt[] buffer.
// mesh_parse_route_packet only enforces a LOWER bound on len (>= what the
// declared payload_len needs), so a caller can legitimately pass a `len`
// larger than the real packet (e.g. an oversized/malformed RX buffer) and
// still have it parse successfully -- exercising exactly the path that
// used to reach store_enqueue's unbounded memcpy.
void test_store_and_forward_rejects_oversized_packet(void) {
    uint8_t oversized[300];
    memset(oversized, 0, sizeof(oversized));
    size_t real_len = mesh_build_route_packet(
        oversized, sizeof(oversized), 5, "other-node", "unreachable-dest",
        8, MRSH_FLAG_STORE_AND_FORWARD, 0, nullptr, 0);
    TEST_ASSERT_TRUE(real_len > 0);
    TEST_ASSERT_TRUE(real_len < MRSH_ROUTE_MAX_BYTES);

    // Inflate the claimed length far past MRSH_ROUTE_MAX_BYTES while still
    // passing mesh_parse_route_packet's own (lower-bound-only) length check.
    size_t inflated_len = 200;
    TEST_ASSERT_TRUE(inflated_len > MRSH_ROUTE_MAX_BYTES);

    // Must not crash (would previously have overflowed StoreSlot::pkt via
    // an unbounded memcpy inside store_enqueue). The packet must be
    // rejected rather than queued.
    mesh_handle_route_packet(oversized, inflated_len);

    MrshStats stats{};
    mesh_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(0, stats.store_forward_depth,
        "an oversized packet must be rejected by store_enqueue's bounds check, not queued");
}

void test_route_packet_broadcast_orphan_forwards_with_decremented_ttl(void) {
    uint8_t buf[MRSH_ROUTE_MAX_BYTES];
    size_t len = mesh_build_route_packet(buf, sizeof(buf), 6,
        "other-node", "unreachable-dest", /*ttl=*/5, MRSH_FLAG_BROADCAST_ORPHAN, 0, nullptr, 0);

    mesh_handle_route_packet(buf, len);

    TEST_ASSERT_TRUE(g_tx_called);
    TEST_ASSERT_EQUAL_UINT8(MSG_TYPE_MESH_ROUTE, g_tx_buf[0]);
    TEST_ASSERT_EQUAL_UINT8_MESSAGE(4, g_tx_buf[15], "TTL must be decremented in the rebroadcast copy");

    MrshStats stats{};
    mesh_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.pkts_forwarded_last_60s);
}

void test_route_packet_forwards_to_known_neighbor(void) {
    // Establish a neighbor for "unreachable-dest" via a DISCOVER frame first.
    uint8_t disc[MRSH_DISCOVER_BYTES];
    mesh_build_discover_packet(disc, sizeof(disc), "unreachable-dest", MRSH_RADIO_LORA915, 1, 90);
    mesh_handle_discover(disc, sizeof(disc));

    reset_tx_capture();   // discover doesn't transmit; capture is already clean, but be explicit

    uint8_t buf[MRSH_ROUTE_MAX_BYTES];
    size_t len = mesh_build_route_packet(buf, sizeof(buf), 8,
        "other-node", "unreachable-dest", /*ttl=*/5, 0, 0, nullptr, 0);
    mesh_handle_route_packet(buf, len);

    TEST_ASSERT_TRUE(g_tx_called);
    TEST_ASSERT_EQUAL_UINT8(4, g_tx_buf[15]);   // TTL decremented

    MrshStats stats{};
    mesh_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.pkts_forwarded_last_60s);
    TEST_ASSERT_EQUAL_UINT8(1, stats.active_neighbor_count);
}

// ---------------------------------------------------------------------------
// mesh_send
// ---------------------------------------------------------------------------
void test_send_transmits_and_returns_nonzero_packet_id(void) {
    const uint8_t payload[] = {0x01, 0x02, 0x03};
    uint16_t id = mesh_send("dest-node", payload, sizeof(payload), 0);
    TEST_ASSERT_TRUE(id != 0);
    TEST_ASSERT_TRUE(g_tx_called);
    TEST_ASSERT_EQUAL_UINT8(MSG_TYPE_MESH_ROUTE, g_tx_buf[0]);
}

void test_send_ids_increment_across_calls(void) {
    uint16_t id1 = mesh_send("dest-node", nullptr, 0, 0);
    uint16_t id2 = mesh_send("dest-node", nullptr, 0, 0);
    TEST_ASSERT_EQUAL_UINT16(id1 + 1, id2);
}

void test_send_rejects_oversized_payload(void) {
    uint8_t payload[MRSH_MAX_PAYLOAD + 1] = {0};
    uint16_t id = mesh_send("dest-node", payload, sizeof(payload), 0);
    TEST_ASSERT_EQUAL_UINT16(0, id);
}

void test_send_tx_failure_without_store_forward_flag_returns_zero(void) {
    g_tx_fail = true;
    uint16_t id = mesh_send("dest-node", nullptr, 0, /*flags=*/0);
    TEST_ASSERT_EQUAL_UINT16(0, id);

    MrshStats stats{};
    mesh_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.pkts_dropped_last_60s);
}

void test_send_tx_failure_with_store_forward_flag_queues_instead(void) {
    g_tx_fail = true;
    uint16_t id = mesh_send("dest-node", nullptr, 0, MRSH_FLAG_STORE_AND_FORWARD);
    // Still returns the allocated packet_id even though immediate TX failed
    // -- it was queued for retry, not dropped.
    TEST_ASSERT_TRUE(id != 0);

    MrshStats stats{};
    mesh_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.store_forward_depth);
}

// ---------------------------------------------------------------------------
// mesh_routing_init resets state
// ---------------------------------------------------------------------------
void test_init_resets_neighbor_table(void) {
    uint8_t disc[MRSH_DISCOVER_BYTES];
    mesh_build_discover_packet(disc, sizeof(disc), "peer-node", MRSH_RADIO_LORA915, 1, 50);
    mesh_handle_discover(disc, sizeof(disc));

    NeighborEntry neighbors[8];
    TEST_ASSERT_EQUAL_UINT32(1, mesh_get_neighbors(neighbors, 8));

    mesh_routing_init("selfnode0000000", MRSH_RADIO_LORA915);
    TEST_ASSERT_EQUAL_UINT32(0, mesh_get_neighbors(neighbors, 8));
}

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

    RUN_TEST(test_crc32_known_answer_test_vector);
    RUN_TEST(test_crc32_empty_input);

    RUN_TEST(test_node_hash_is_deterministic);
    RUN_TEST(test_node_hash_differs_for_different_ids);

    RUN_TEST(test_route_packet_round_trip);
    RUN_TEST(test_route_packet_null_dest_is_broadcast_hash);
    RUN_TEST(test_route_packet_rejects_oversized_payload);
    RUN_TEST(test_route_packet_rejects_insufficient_out_buffer);
    RUN_TEST(test_parse_rejects_wrong_msg_type);
    RUN_TEST(test_parse_rejects_truncated_buffer);
    RUN_TEST(test_parse_rejects_corrupted_crc);

    RUN_TEST(test_ack_packet_layout);
    RUN_TEST(test_discover_packet_round_trip_via_handle);
    RUN_TEST(test_discover_with_corrupted_crc_does_not_add_neighbor);

    RUN_TEST(test_route_packet_addressed_to_self_is_delivered);
    RUN_TEST(test_route_packet_delivered_with_require_ack_sends_ack);
    RUN_TEST(test_route_packet_ttl_zero_is_dropped_with_ack);
    RUN_TEST(test_route_packet_no_route_no_flags_drops_with_nak);
    RUN_TEST(test_route_packet_no_route_store_and_forward_enqueues);
    RUN_TEST(test_store_and_forward_rejects_oversized_packet);
    RUN_TEST(test_route_packet_broadcast_orphan_forwards_with_decremented_ttl);
    RUN_TEST(test_route_packet_forwards_to_known_neighbor);

    RUN_TEST(test_send_transmits_and_returns_nonzero_packet_id);
    RUN_TEST(test_send_ids_increment_across_calls);
    RUN_TEST(test_send_rejects_oversized_payload);
    RUN_TEST(test_send_tx_failure_without_store_forward_flag_returns_zero);
    RUN_TEST(test_send_tx_failure_with_store_forward_flag_queues_instead);

    RUN_TEST(test_init_resets_neighbor_table);

    return UNITY_END();
}
