// ============================================================================
// test/test_c2_proxy_tunnel/test_c2_proxy_tunnel.cpp
//
// Unity tests for src/c2_proxy_tunnel.{h,cpp} — the BLE<->LoRa C2 API
// tunnel (phone -> T1 -> T2 -> OpenForge C2 fallback path). This module
// was ALREADY structured for native testing (a clean `#ifndef UNIT_TEST`
// guard with millis()/heap_caps_malloc host stubs built in, and
// c2_proxy_tunnel_test_reset()/_test_drain_*_queue() hooks declared in
// the header) -- but no test file had actually been written yet. No
// #ifdef ARDUINO restructuring was needed here (unlike most of this
// sweep); it compiles natively as-is.
//
// Writing these tests surfaced a real defense-in-depth gap, fixed in
// c2_proxy_tunnel.cpp: handle_chunk_generic() only enforced a LOWER bound
// on the chunk length. The "patched" BLE-write path (when the phone's
// characteristic write callback hadn't already stamped the correct
// msg_type byte) separately capped len at C2_PROXY_HDR_LEN +
// C2_PROXY_MAX_DATA before calling in, but the "direct" path (msg_type
// already correct) did not apply the same cap, so an oversized single
// "chunk" (violating the documented <=200-byte-per-chunk wire format)
// could pass length validation and potentially write further into the
// destination buffer than a well-formed chunk would. Fixed by enforcing
// the upper bound inside handle_chunk_generic itself, uniformly for both
// callers. See test_direct_path_rejects_oversized_chunk below.
//
// millis() in this module's UNIT_TEST branch uses a real steady_clock with
// no test-override hook (unlike most other modules in this sweep) -- so
// c2_proxy_tunnel_tick()'s 30s stale-slot-reap timing is not practically
// unit-testable without a real 30-second wait; skipped here for the same
// reason mesh_routing_tick()'s interval gating was skipped.
//
// Run with:
//   pio test -e native -f test_c2_proxy_tunnel
// ============================================================================

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

// ---------------------------------------------------------------------------
// Stubs for the module's three weak-linked hooks.
// ---------------------------------------------------------------------------
static bool g_authenticated = true;
extern "C" bool ble_companion_is_authenticated(uint32_t) { return g_authenticated; }

static bool     g_lora_tx_called = false;
static int      g_lora_tx_call_count = 0;
static uint8_t  g_lora_tx_buf[256];
static size_t   g_lora_tx_len = 0;
static bool     g_lora_tx_ret = true;
static bool     g_ble_notify_called = false;
static int      g_ble_notify_call_count = 0;
static uint8_t  g_ble_notify_buf[256];
static size_t   g_ble_notify_len = 0;
static bool     g_ble_notify_ret = true;

// extern "C" to match c2_proxy_tunnel.h's declarations (wrapped in
// `extern "C" { ... }` under __cplusplus) -- the module's own weak default
// bodies for these two are compiled out under UNIT_TEST (see the guard
// added in c2_proxy_tunnel.cpp) specifically so these are the only bodies.
extern "C" bool c2_proxy_lora_tx(const uint8_t* buf, size_t len) {
    g_lora_tx_called = true;
    g_lora_tx_call_count++;
    size_t copy = len < sizeof(g_lora_tx_buf) ? len : sizeof(g_lora_tx_buf);
    memcpy(g_lora_tx_buf, buf, copy);
    g_lora_tx_len = len;
    return g_lora_tx_ret;
}
extern "C" bool c2_proxy_ble_notify(const uint8_t* buf, size_t len) {
    g_ble_notify_called = true;
    g_ble_notify_call_count++;
    size_t copy = len < sizeof(g_ble_notify_buf) ? len : sizeof(g_ble_notify_buf);
    memcpy(g_ble_notify_buf, buf, copy);
    g_ble_notify_len = len;
    return g_ble_notify_ret;
}

#include "../../src/c2_proxy_tunnel.cpp"

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
// Mirrors production's crc32_bzip2 exactly so tests can build valid frames
// without white-box access to the static function.
static uint32_t test_crc32_bzip2(const uint8_t* data, size_t len) {
    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 <<= 1;
        }
    }
    return crc ^ 0xFFFFFFFFu;
}

static size_t build_chunk(uint8_t* out, uint8_t msg_type, uint32_t request_id,
                           uint16_t total_chunks, uint16_t chunk_idx,
                           uint32_t total_size, uint32_t chunk_crc32,
                           const uint8_t* data, size_t data_len) {
    out[0] = msg_type;
    memcpy(out + 1, &request_id, 4);
    memcpy(out + 5, &total_chunks, 2);
    memcpy(out + 7, &chunk_idx, 2);
    memcpy(out + 9, &total_size, 4);
    memcpy(out + 13, &chunk_crc32, 4);
    memset(out + 17, 0, 7);
    if (data && data_len) memcpy(out + C2_PROXY_HDR_LEN, data, data_len);
    return C2_PROXY_HDR_LEN + data_len;
}

// Builds a single-chunk (total_chunks=1) well-formed REQ envelope --
// simplest complete case: this one chunk is both first and final, so its
// crc32 field carries the full-payload CRC.
static size_t build_single_chunk_req(uint8_t* out, uint32_t request_id,
                                      const uint8_t* payload, size_t payload_len) {
    uint32_t full_crc = test_crc32_bzip2(payload, payload_len);
    return build_chunk(out, MSG_TYPE_C2_PROXY_REQ, request_id, 1, 0,
                        (uint32_t)payload_len, full_crc, payload, payload_len);
}

void setUp(void) {
    g_authenticated = true;
    g_lora_tx_called = false;
    g_lora_tx_call_count = 0;
    g_lora_tx_len = 0;
    g_lora_tx_ret = true;
    memset(g_lora_tx_buf, 0, sizeof(g_lora_tx_buf));
    g_ble_notify_called = false;
    g_ble_notify_call_count = 0;
    g_ble_notify_len = 0;
    g_ble_notify_ret = true;
    memset(g_ble_notify_buf, 0, sizeof(g_ble_notify_buf));
    c2_proxy_tunnel_test_reset();
}
void tearDown(void) {}

// ---------------------------------------------------------------------------
// Auth gate
// ---------------------------------------------------------------------------
void test_ble_write_rejected_when_not_authenticated(void) {
    g_authenticated = false;
    uint8_t payload[8] = {1, 2, 3, 4, 5, 6, 7, 8};
    uint8_t frame[C2_PROXY_HDR_LEN + 8];
    size_t len = build_single_chunk_req(frame, 1, payload, sizeof(payload));

    C2ProxyStatus rc = c2_proxy_handle_ble_write(frame, len);
    TEST_ASSERT_EQUAL(C2P_NOT_AUTH, rc);

    C2ProxyStats stats{};
    c2_proxy_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.dropped_not_auth);
    TEST_ASSERT_EQUAL_UINT32(0, stats.ble_in_envelopes);
}

// ---------------------------------------------------------------------------
// Single-chunk envelope: full round trip through to LoRa TX
// ---------------------------------------------------------------------------
void test_single_chunk_req_completes_and_queues_lora_tx(void) {
    uint8_t payload[8] = {1, 2, 3, 4, 5, 6, 7, 8};
    uint8_t frame[C2_PROXY_HDR_LEN + 8];
    size_t len = build_single_chunk_req(frame, 0xAAAA, payload, sizeof(payload));

    C2ProxyStatus rc = c2_proxy_handle_ble_write(frame, len);
    TEST_ASSERT_EQUAL(C2P_OK, rc);

    C2ProxyStats stats{};
    c2_proxy_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.ble_in_envelopes);
    TEST_ASSERT_EQUAL_UINT32(0, stats.in_flight_req);   // released after completion

    c2_proxy_tunnel_test_drain_lora_queue();
    TEST_ASSERT_TRUE(g_lora_tx_called);
    TEST_ASSERT_EQUAL_UINT32(len, g_lora_tx_len);
    TEST_ASSERT_EQUAL_MEMORY(frame, g_lora_tx_buf, len);
}

// ---------------------------------------------------------------------------
// Multi-chunk envelope reassembly
// ---------------------------------------------------------------------------
void test_multi_chunk_req_reassembles_across_two_chunks(void) {
    uint8_t full_payload[300];
    for (int i = 0; i < 300; i++) full_payload[i] = (uint8_t)i;

    // chunk 0: first 200 bytes (C2_PROXY_MAX_DATA), per-chunk CRC
    uint8_t frame0[C2_PROXY_HDR_LEN + C2_PROXY_MAX_DATA];
    uint32_t crc0 = test_crc32_bzip2(full_payload, C2_PROXY_MAX_DATA);
    size_t len0 = build_chunk(frame0, MSG_TYPE_C2_PROXY_REQ, 0xBEEF, 2, 0,
                               sizeof(full_payload), crc0, full_payload, C2_PROXY_MAX_DATA);

    C2ProxyStatus rc0 = c2_proxy_handle_ble_write(frame0, len0);
    TEST_ASSERT_EQUAL(C2P_OK, rc0);

    C2ProxyStats mid_stats{};
    c2_proxy_get_stats(&mid_stats);
    TEST_ASSERT_EQUAL_UINT32(0, mid_stats.ble_in_envelopes);   // not complete yet
    TEST_ASSERT_EQUAL_UINT32(1, mid_stats.in_flight_req);

    // chunk 1 (final): remaining 100 bytes, crc field = FULL payload CRC
    size_t remaining = sizeof(full_payload) - C2_PROXY_MAX_DATA;
    uint32_t full_crc = test_crc32_bzip2(full_payload, sizeof(full_payload));
    uint8_t frame1[C2_PROXY_HDR_LEN + 100];
    size_t len1 = build_chunk(frame1, MSG_TYPE_C2_PROXY_REQ, 0xBEEF, 2, 1,
                               sizeof(full_payload), full_crc,
                               full_payload + C2_PROXY_MAX_DATA, remaining);

    C2ProxyStatus rc1 = c2_proxy_handle_ble_write(frame1, len1);
    TEST_ASSERT_EQUAL(C2P_OK, rc1);

    C2ProxyStats stats{};
    c2_proxy_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.ble_in_envelopes);
    TEST_ASSERT_EQUAL_UINT32(0, stats.in_flight_req);
}

// ---------------------------------------------------------------------------
// CRC validation
// ---------------------------------------------------------------------------
void test_non_final_chunk_bad_crc_rejected(void) {
    uint8_t data[C2_PROXY_MAX_DATA] = {0};
    uint8_t frame[C2_PROXY_HDR_LEN + C2_PROXY_MAX_DATA];
    // Deliberately wrong CRC for a non-final chunk (total_chunks=2, idx=0).
    size_t len = build_chunk(frame, MSG_TYPE_C2_PROXY_REQ, 0x1111, 2, 0,
                              400, /*wrong crc=*/0xDEADBEEF, data, sizeof(data));

    C2ProxyStatus rc = c2_proxy_handle_ble_write(frame, len);
    TEST_ASSERT_EQUAL(C2P_BAD_CRC, rc);

    C2ProxyStats stats{};
    c2_proxy_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.dropped_bad_crc);
}

void test_final_chunk_bad_full_crc_rejected_and_slot_released(void) {
    uint8_t payload[8] = {1, 2, 3, 4, 5, 6, 7, 8};
    uint8_t frame[C2_PROXY_HDR_LEN + 8];
    // Single chunk (first == final) with a deliberately wrong full-payload CRC.
    size_t len = build_chunk(frame, MSG_TYPE_C2_PROXY_REQ, 0x2222, 1, 0,
                              sizeof(payload), /*wrong crc=*/0xCAFEBABE, payload, sizeof(payload));

    C2ProxyStatus rc = c2_proxy_handle_ble_write(frame, len);
    TEST_ASSERT_EQUAL(C2P_BAD_CRC, rc);

    C2ProxyStats stats{};
    c2_proxy_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.dropped_bad_crc);
    TEST_ASSERT_EQUAL_UINT32(0, stats.in_flight_req);   // slot released, not leaked
}

// ---------------------------------------------------------------------------
// Malformed header rejection
// ---------------------------------------------------------------------------
void test_too_short_frame_rejected(void) {
    uint8_t frame[C2_PROXY_HDR_LEN - 1] = {0};
    C2ProxyStatus rc = c2_proxy_handle_ble_write(frame, sizeof(frame));
    TEST_ASSERT_EQUAL(C2P_BAD_HDR, rc);
}

void test_zero_total_chunks_rejected(void) {
    uint8_t data[4] = {0};
    uint8_t frame[C2_PROXY_HDR_LEN + 4];
    size_t len = build_chunk(frame, MSG_TYPE_C2_PROXY_REQ, 1, /*total_chunks=*/0, 0,
                              4, 0, data, sizeof(data));
    TEST_ASSERT_EQUAL(C2P_BAD_HDR, c2_proxy_handle_ble_write(frame, len));
}

void test_chunk_idx_out_of_range_rejected(void) {
    uint8_t data[4] = {0};
    uint8_t frame[C2_PROXY_HDR_LEN + 4];
    // total_chunks=2 but chunk_idx=5 -- out of range.
    size_t len = build_chunk(frame, MSG_TYPE_C2_PROXY_REQ, 1, 2, 5,
                              400, 0, data, sizeof(data));
    TEST_ASSERT_EQUAL(C2P_BAD_HDR, c2_proxy_handle_ble_write(frame, len));
}

void test_oversized_total_size_rejected(void) {
    uint8_t data[4] = {0};
    uint8_t frame[C2_PROXY_HDR_LEN + 4];
    size_t len = build_chunk(frame, MSG_TYPE_C2_PROXY_REQ, 1, 1, 0,
                              C2_PROXY_MAX_PAYLOAD + 1, 0, data, sizeof(data));
    TEST_ASSERT_EQUAL(C2P_TOO_LARGE, c2_proxy_handle_ble_write(frame, len));
}

// Regression test for the real bug this pass fixed: the "direct" path
// (msg_type already correct on entry) must reject an oversized chunk the
// same way the "patched" path already did.
void test_direct_path_rejects_oversized_chunk(void) {
    uint8_t oversized[C2_PROXY_HDR_LEN + C2_PROXY_MAX_DATA + 50];
    memset(oversized, 0, sizeof(oversized));
    oversized[0] = MSG_TYPE_C2_PROXY_REQ;   // "direct" path: type already correct
    uint32_t rid = 1, tc = 1, ci = 0, ts = 4;
    memcpy(oversized + 1, &rid, 4);
    memcpy(oversized + 5, &tc, 2);
    memcpy(oversized + 7, &ci, 2);
    memcpy(oversized + 9, &ts, 4);

    C2ProxyStatus rc = c2_proxy_handle_ble_write(oversized, sizeof(oversized));
    TEST_ASSERT_EQUAL_MESSAGE(C2P_BAD_HDR, rc,
        "an oversized chunk on the direct (already-correct-msg_type) path must be rejected");
}

// ---------------------------------------------------------------------------
// Duplicate chunk handling
// ---------------------------------------------------------------------------
void test_duplicate_chunk_rejected(void) {
    uint8_t data[C2_PROXY_MAX_DATA] = {0};
    uint8_t frame[C2_PROXY_HDR_LEN + C2_PROXY_MAX_DATA];
    uint32_t crc = test_crc32_bzip2(data, sizeof(data));
    size_t len = build_chunk(frame, MSG_TYPE_C2_PROXY_REQ, 0x3333, 2, 0,
                              400, crc, data, sizeof(data));

    TEST_ASSERT_EQUAL(C2P_OK, c2_proxy_handle_ble_write(frame, len));
    TEST_ASSERT_EQUAL_MESSAGE(C2P_DUPLICATE, c2_proxy_handle_ble_write(frame, len),
        "re-delivering the same chunk_idx for an in-progress envelope must be rejected as duplicate");
}

// ---------------------------------------------------------------------------
// Slot exhaustion (C2_PROXY_CONCURRENT in-flight requests)
// ---------------------------------------------------------------------------
void test_slot_exhaustion_returns_full(void) {
    uint8_t data[C2_PROXY_MAX_DATA] = {0};
    uint32_t crc = test_crc32_bzip2(data, sizeof(data));

    for (int i = 0; i < C2_PROXY_CONCURRENT; i++) {
        uint8_t frame[C2_PROXY_HDR_LEN + C2_PROXY_MAX_DATA];
        // total_chunks=2 so the slot stays open (not completed by 1 chunk).
        size_t len = build_chunk(frame, MSG_TYPE_C2_PROXY_REQ, 1000 + i, 2, 0,
                                  400, crc, data, sizeof(data));
        TEST_ASSERT_EQUAL(C2P_OK, c2_proxy_handle_ble_write(frame, len));
    }

    // One more, distinct request_id: all slots are occupied.
    uint8_t frame[C2_PROXY_HDR_LEN + C2_PROXY_MAX_DATA];
    size_t len = build_chunk(frame, MSG_TYPE_C2_PROXY_REQ, 9999, 2, 0,
                              400, crc, data, sizeof(data));
    TEST_ASSERT_EQUAL(C2P_FULL, c2_proxy_handle_ble_write(frame, len));
}

// ---------------------------------------------------------------------------
// "Patched" path: phone/callback didn't stamp the correct msg_type byte
// ---------------------------------------------------------------------------
void test_patched_path_handles_wrong_leading_byte(void) {
    uint8_t payload[8] = {9, 8, 7, 6, 5, 4, 3, 2};
    uint8_t frame[C2_PROXY_HDR_LEN + 8];
    build_single_chunk_req(frame, 0x4444, payload, sizeof(payload));
    frame[0] = 0x00;   // NOT MSG_TYPE_C2_PROXY_REQ -- triggers the patch path

    C2ProxyStatus rc = c2_proxy_handle_ble_write(frame, sizeof(frame));
    TEST_ASSERT_EQUAL(C2P_OK, rc);

    C2ProxyStats stats{};
    c2_proxy_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.ble_in_envelopes);
}

// ---------------------------------------------------------------------------
// LoRa RSP -> BLE notify fan-out
// ---------------------------------------------------------------------------
void test_lora_rsp_queues_ble_notify(void) {
    uint8_t payload[8] = {1, 1, 2, 2, 3, 3, 4, 4};
    uint32_t full_crc = test_crc32_bzip2(payload, sizeof(payload));
    uint8_t frame[C2_PROXY_HDR_LEN + 8];
    size_t len = build_chunk(frame, MSG_TYPE_C2_PROXY_RSP, 0x5555, 1, 0,
                              sizeof(payload), full_crc, payload, sizeof(payload));

    C2ProxyStatus rc = c2_proxy_handle_lora_rsp(frame, len);
    TEST_ASSERT_EQUAL(C2P_OK, rc);

    C2ProxyStats stats{};
    c2_proxy_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.lora_in_chunks);
    TEST_ASSERT_EQUAL_UINT32(1, stats.lora_in_envelopes);

    c2_proxy_tunnel_test_drain_ble_queue();
    TEST_ASSERT_TRUE(g_ble_notify_called);
    TEST_ASSERT_EQUAL_UINT32(len, g_ble_notify_len);
    TEST_ASSERT_EQUAL_MEMORY(frame, g_ble_notify_buf, len);
}

// ---------------------------------------------------------------------------
// LoRa REQ passthrough (documented no-op today)
// ---------------------------------------------------------------------------
void test_lora_req_is_inert_passthrough(void) {
    uint8_t frame[C2_PROXY_HDR_LEN] = {0};
    TEST_ASSERT_EQUAL(C2P_OK, c2_proxy_handle_lora_req(frame, sizeof(frame)));

    C2ProxyStats stats{};
    c2_proxy_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(0, stats.ble_in_chunks);
    TEST_ASSERT_EQUAL_UINT32(0, stats.lora_in_chunks);
}

// ---------------------------------------------------------------------------
// Reset
// ---------------------------------------------------------------------------
void test_reset_clears_stats_and_slots(void) {
    uint8_t data[C2_PROXY_MAX_DATA] = {0};
    uint32_t crc = test_crc32_bzip2(data, sizeof(data));
    uint8_t frame[C2_PROXY_HDR_LEN + C2_PROXY_MAX_DATA];
    size_t len = build_chunk(frame, MSG_TYPE_C2_PROXY_REQ, 1, 2, 0, 400, crc, data, sizeof(data));
    c2_proxy_handle_ble_write(frame, len);

    C2ProxyStats before{};
    c2_proxy_get_stats(&before);
    TEST_ASSERT_EQUAL_UINT32(1, before.in_flight_req);

    c2_proxy_tunnel_test_reset();

    C2ProxyStats after{};
    c2_proxy_get_stats(&after);
    TEST_ASSERT_EQUAL_UINT32(0, after.in_flight_req);
    TEST_ASSERT_EQUAL_UINT32(0, after.ble_in_chunks);
}

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

    RUN_TEST(test_ble_write_rejected_when_not_authenticated);

    RUN_TEST(test_single_chunk_req_completes_and_queues_lora_tx);
    RUN_TEST(test_multi_chunk_req_reassembles_across_two_chunks);

    RUN_TEST(test_non_final_chunk_bad_crc_rejected);
    RUN_TEST(test_final_chunk_bad_full_crc_rejected_and_slot_released);

    RUN_TEST(test_too_short_frame_rejected);
    RUN_TEST(test_zero_total_chunks_rejected);
    RUN_TEST(test_chunk_idx_out_of_range_rejected);
    RUN_TEST(test_oversized_total_size_rejected);
    RUN_TEST(test_direct_path_rejects_oversized_chunk);

    RUN_TEST(test_duplicate_chunk_rejected);
    RUN_TEST(test_slot_exhaustion_returns_full);
    RUN_TEST(test_patched_path_handles_wrong_leading_byte);

    RUN_TEST(test_lora_rsp_queues_ble_notify);
    RUN_TEST(test_lora_req_is_inert_passthrough);

    RUN_TEST(test_reset_clears_stats_and_slots);

    return UNITY_END();
}
