// ============================================================================
// test/test_ble_proxy_chars/test_ble_proxy_chars.cpp
//
// Unity tests for src/ble_proxy_chars.{h,cpp} -- the two GATT characteristics
// (ProxyTx write / ProxyRx notify) that let the bonded phone tunnel OpenForge
// C2 API calls through LoRa. This module was ALREADY structured for native
// testing (a clean `#ifndef UNIT_TEST` guard around all NimBLE code, plus
// ble_proxy_test_reset()/_inject_write()/_pop_notify() hooks declared in the
// header) -- same situation as c2_proxy_tunnel.cpp, no #ifdef ARDUINO
// restructuring needed, but no test file had been written yet.
//
// This also exercises the REAL glue between ble_proxy_chars.cpp and
// c2_proxy_tunnel.cpp (both .cpp files are pulled into this one TU) rather
// than mocking the boundary between them -- ble_proxy_chars.cpp's strong
// c2_proxy_ble_notify() override is left in place (not redefined here) so
// c2_proxy_handle_lora_rsp() -> tick-drain -> notify actually flows through
// the same code path production does, landing in ble_proxy's own in-memory
// notify queue.
//
// Run with:
//   pio test -e native -f test_ble_proxy_chars
// ============================================================================

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

// ---------------------------------------------------------------------------
// Hooks c2_proxy_tunnel.cpp needs that ble_proxy_chars.cpp does NOT supply:
//   - ble_companion_is_authenticated: weak-declared, c2_proxy_is_authenticated()
//     delegates to it when present.
//   - c2_proxy_lora_tx: guarded out of c2_proxy_tunnel.cpp under UNIT_TEST
//     (see the comment there -- same-TU weak/strong redefinition issue).
// c2_proxy_ble_notify is intentionally NOT stubbed here -- ble_proxy_chars.cpp
// supplies the real one under UNIT_TEST, and that's exactly what this file
// wants to exercise.
// ---------------------------------------------------------------------------
static bool g_authenticated = true;
extern "C" bool ble_companion_is_authenticated(uint32_t) { return g_authenticated; }

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;
extern "C" bool c2_proxy_lora_tx(const uint8_t* buf, size_t len) {
    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;
}

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

// Both .cpp files independently declare a file-scope `static ... s_stats;`.
// Across separate translation units in the real firmware build that's fine
// (internal linkage keeps them private to each .o) but pulling both .cpp
// files into this single test TU makes them collide as if redeclared.
// Rename ble_proxy_chars.cpp's copy for the duration of its include only --
// test-infrastructure-only workaround, no production code touched.
#define s_stats s_stats_ble_proxy
#include "../../src/ble_proxy_chars.cpp"
#undef s_stats

// ---------------------------------------------------------------------------
// Test helpers -- build valid single-chunk REQ/RSP wire frames using the
// SAME crc32_bzip2() the production code uses (visible here: it's `static`
// but this file is compiled into the same TU via the #include above).
// ---------------------------------------------------------------------------
static std::vector<uint8_t> build_chunk(
    uint8_t msg_type, uint32_t request_id,
    uint16_t total_chunks, uint16_t chunk_idx,
    const std::vector<uint8_t>& data, uint32_t total_size_override = 0
) {
    std::vector<uint8_t> pkt(C2_PROXY_HDR_LEN + data.size());
    uint32_t total_size = total_size_override ? total_size_override : (uint32_t)data.size();
    bool is_final = (chunk_idx == (uint16_t)(total_chunks - 1));
    uint32_t crc = is_final
        ? crc32_bzip2(data.data(), total_size <= data.size() ? total_size : data.size())
        : crc32_bzip2(data.data(), data.size());

    pkt[0] = msg_type;
    memcpy(&pkt[1], &request_id, 4);
    memcpy(&pkt[5], &total_chunks, 2);
    memcpy(&pkt[7], &chunk_idx, 2);
    memcpy(&pkt[9], &total_size, 4);
    memcpy(&pkt[13], &crc, 4);
    memset(&pkt[17], 0, 7);
    memcpy(&pkt[24], data.data(), data.size());
    return pkt;
}

static std::vector<uint8_t> make_data(size_t n, uint8_t fill = 0x5A) {
    return std::vector<uint8_t>(n, fill);
}

void setUp(void) {
    g_authenticated = true;
    g_lora_tx_call_count = 0;
    g_lora_tx_len = 0;
    g_lora_tx_ret = true;
    c2_proxy_tunnel_test_reset();
    ble_proxy_test_reset();
}

void tearDown(void) {}

// ── ble_proxy_register() ─────────────────────────────────────────────────

void test_register_rejects_null_service(void) {
    TEST_ASSERT_FALSE(ble_proxy_register(nullptr));
    BleProxyStats stats;
    ble_proxy_get_stats(&stats);
    TEST_ASSERT_FALSE(stats.registered);
}

void test_register_succeeds_and_sets_registered_flag(void) {
    TEST_ASSERT_TRUE(ble_proxy_register((void*)0x1234));
    BleProxyStats stats;
    ble_proxy_get_stats(&stats);
    TEST_ASSERT_TRUE(stats.registered);
}

void test_register_is_idempotent(void) {
    TEST_ASSERT_TRUE(ble_proxy_register((void*)0x1234));
    TEST_ASSERT_TRUE(ble_proxy_register((void*)0x5678));  // second call, different ptr
    BleProxyStats stats;
    ble_proxy_get_stats(&stats);
    TEST_ASSERT_TRUE(stats.registered);
}

void test_get_stats_null_out_does_not_crash(void) {
    ble_proxy_get_stats(nullptr);
    TEST_PASS();
}

// ── ble_proxy_test_inject_write() -- ProxyTx write validation ────────────

void test_inject_write_too_short_rejected(void) {
    std::vector<uint8_t> tiny(23, 0);  // one byte short of C2_PROXY_HDR_LEN
    TEST_ASSERT_FALSE(ble_proxy_test_inject_write(tiny.data(), tiny.size()));
    BleProxyStats stats;
    ble_proxy_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.proxy_tx_writes_bad);
    TEST_ASSERT_EQUAL_UINT32(0, stats.proxy_tx_writes);
}

void test_inject_write_too_long_rejected(void) {
    std::vector<uint8_t> huge(BLE_PROXY_MAX_WRITE_BYTES + 1, 0);
    TEST_ASSERT_FALSE(ble_proxy_test_inject_write(huge.data(), huge.size()));
    BleProxyStats stats;
    ble_proxy_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.proxy_tx_writes_bad);
}

void test_inject_write_null_buf_rejected(void) {
    TEST_ASSERT_FALSE(ble_proxy_test_inject_write(nullptr, 30));
}

void test_inject_write_not_authenticated_rejected(void) {
    g_authenticated = false;
    auto pkt = build_chunk(MSG_TYPE_C2_PROXY_REQ, 1, 1, 0, make_data(10));
    TEST_ASSERT_FALSE(ble_proxy_test_inject_write(pkt.data(), pkt.size()));
    BleProxyStats stats;
    ble_proxy_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.proxy_tx_writes_bad);
}

void test_inject_write_valid_single_chunk_accepted(void) {
    auto pkt = build_chunk(MSG_TYPE_C2_PROXY_REQ, 1, 1, 0, make_data(10));
    TEST_ASSERT_TRUE(ble_proxy_test_inject_write(pkt.data(), pkt.size()));
    BleProxyStats stats;
    ble_proxy_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.proxy_tx_writes);
    TEST_ASSERT_EQUAL_UINT32(0, stats.proxy_tx_writes_bad);
}

void test_inject_write_bad_crc_rejected(void) {
    auto pkt = build_chunk(MSG_TYPE_C2_PROXY_REQ, 1, 1, 0, make_data(10));
    pkt[13] ^= 0xFF;  // corrupt CRC byte
    TEST_ASSERT_FALSE(ble_proxy_test_inject_write(pkt.data(), pkt.size()));
    BleProxyStats stats;
    ble_proxy_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.proxy_tx_writes_bad);
}

// ── End-to-end: ProxyTx write -> c2_proxy_tunnel -> LoRa TX queue ────────

void test_valid_write_reaches_lora_tx_after_drain(void) {
    auto pkt = build_chunk(MSG_TYPE_C2_PROXY_REQ, 42, 1, 0, make_data(20));
    TEST_ASSERT_TRUE(ble_proxy_test_inject_write(pkt.data(), pkt.size()));

    c2_proxy_tunnel_test_drain_lora_queue();

    TEST_ASSERT_EQUAL_INT(1, g_lora_tx_call_count);
    TEST_ASSERT_EQUAL_UINT32(pkt.size(), (uint32_t)g_lora_tx_len);
    TEST_ASSERT_EQUAL_UINT8_ARRAY(pkt.data(), g_lora_tx_buf, pkt.size());
}

// ── End-to-end: LoRa RSP -> c2_proxy_tunnel -> ble_proxy's REAL notify ───

void test_lora_rsp_reaches_ble_proxy_notify_queue_after_drain(void) {
    auto pkt = build_chunk(MSG_TYPE_C2_PROXY_RSP, 7, 1, 0, make_data(15, 0xEE));
    TEST_ASSERT_EQUAL(C2P_OK, c2_proxy_handle_lora_rsp(pkt.data(), pkt.size()));

    c2_proxy_tunnel_test_drain_ble_queue();

    BleProxyStats stats;
    ble_proxy_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(1, stats.proxy_rx_notifies);

    uint8_t out[BLE_PROXY_MAX_WRITE_BYTES];
    size_t n = ble_proxy_test_pop_notify(out, sizeof(out));
    TEST_ASSERT_EQUAL_UINT32(pkt.size(), (uint32_t)n);
    TEST_ASSERT_EQUAL_UINT8_ARRAY(pkt.data(), out, pkt.size());
}

// ── c2_proxy_ble_notify() edge cases (via the real override) ────────────

void test_notify_null_buf_rejected(void) {
    TEST_ASSERT_FALSE(c2_proxy_ble_notify(nullptr, 10));
    BleProxyStats stats;
    ble_proxy_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(0, stats.proxy_rx_notifies);
}

void test_notify_zero_len_rejected(void) {
    uint8_t buf[4] = {1, 2, 3, 4};
    TEST_ASSERT_FALSE(c2_proxy_ble_notify(buf, 0));
}

void test_notify_oversized_rejected(void) {
    std::vector<uint8_t> huge(BLE_PROXY_MAX_WRITE_BYTES + 1, 0xAB);
    TEST_ASSERT_FALSE(c2_proxy_ble_notify(huge.data(), huge.size()));
}

void test_notify_queue_full_counts_drop(void) {
    uint8_t buf[8] = {1, 2, 3, 4, 5, 6, 7, 8};
    // Notify queue is 8 slots deep (s_test_notify_q[8]) -- fill it.
    for (int i = 0; i < 8; i++) {
        TEST_ASSERT_TRUE(c2_proxy_ble_notify(buf, sizeof(buf)));
    }
    // 9th notify has nowhere to go.
    TEST_ASSERT_FALSE(c2_proxy_ble_notify(buf, sizeof(buf)));
    BleProxyStats stats;
    ble_proxy_get_stats(&stats);
    TEST_ASSERT_EQUAL_UINT32(8, stats.proxy_rx_notifies);
    TEST_ASSERT_EQUAL_UINT32(1, stats.proxy_rx_notify_drops);
}

void test_pop_notify_null_out_buf_returns_zero(void) {
    uint8_t buf[4] = {9, 9, 9, 9};
    c2_proxy_ble_notify(buf, sizeof(buf));
    TEST_ASSERT_EQUAL_UINT32(0, (uint32_t)ble_proxy_test_pop_notify(nullptr, 16));
}

void test_pop_notify_empty_queue_returns_zero(void) {
    uint8_t out[16];
    TEST_ASSERT_EQUAL_UINT32(0, (uint32_t)ble_proxy_test_pop_notify(out, sizeof(out)));
}

void test_pop_notify_truncates_to_caller_cap(void) {
    uint8_t buf[8] = {1, 2, 3, 4, 5, 6, 7, 8};
    c2_proxy_ble_notify(buf, sizeof(buf));
    uint8_t out[4];
    size_t n = ble_proxy_test_pop_notify(out, sizeof(out));
    TEST_ASSERT_EQUAL_UINT32(4, (uint32_t)n);
    TEST_ASSERT_EQUAL_UINT8_ARRAY(buf, out, 4);
}

// ── reset ─────────────────────────────────────────────────────────────────

void test_reset_clears_stats_and_registration(void) {
    ble_proxy_register((void*)0x1);
    auto pkt = build_chunk(MSG_TYPE_C2_PROXY_REQ, 1, 1, 0, make_data(5));
    ble_proxy_test_inject_write(pkt.data(), pkt.size());

    ble_proxy_test_reset();

    BleProxyStats stats;
    ble_proxy_get_stats(&stats);
    TEST_ASSERT_FALSE(stats.registered);
    TEST_ASSERT_EQUAL_UINT32(0, stats.proxy_tx_writes);
}

void test_reset_clears_pending_notify_queue(void) {
    uint8_t buf[4] = {1, 2, 3, 4};
    c2_proxy_ble_notify(buf, sizeof(buf));

    ble_proxy_test_reset();

    uint8_t out[16];
    TEST_ASSERT_EQUAL_UINT32(0, (uint32_t)ble_proxy_test_pop_notify(out, sizeof(out)));
}

// ── main ──────────────────────────────────────────────────────────────────

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

    RUN_TEST(test_register_rejects_null_service);
    RUN_TEST(test_register_succeeds_and_sets_registered_flag);
    RUN_TEST(test_register_is_idempotent);
    RUN_TEST(test_get_stats_null_out_does_not_crash);

    RUN_TEST(test_inject_write_too_short_rejected);
    RUN_TEST(test_inject_write_too_long_rejected);
    RUN_TEST(test_inject_write_null_buf_rejected);
    RUN_TEST(test_inject_write_not_authenticated_rejected);
    RUN_TEST(test_inject_write_valid_single_chunk_accepted);
    RUN_TEST(test_inject_write_bad_crc_rejected);

    RUN_TEST(test_valid_write_reaches_lora_tx_after_drain);
    RUN_TEST(test_lora_rsp_reaches_ble_proxy_notify_queue_after_drain);

    RUN_TEST(test_notify_null_buf_rejected);
    RUN_TEST(test_notify_zero_len_rejected);
    RUN_TEST(test_notify_oversized_rejected);
    RUN_TEST(test_notify_queue_full_counts_drop);
    RUN_TEST(test_pop_notify_null_out_buf_returns_zero);
    RUN_TEST(test_pop_notify_empty_queue_returns_zero);
    RUN_TEST(test_pop_notify_truncates_to_caller_cap);

    RUN_TEST(test_reset_clears_stats_and_registration);
    RUN_TEST(test_reset_clears_pending_notify_queue);

    return UNITY_END();
}
