// ============================================================================
// Native (host) unit tests — ble_companion
//
// Run with: pio test -e native -f test_ble_companion
//
// Strategy: pull ble_companion.cpp directly. UNIT_TEST is set by the native
// env, which selects the host shim path (pure-C HMAC-SHA256, no NimBLE).
// We then exercise:
//   - HMAC challenge-response math against deterministic Python-side vectors
//   - Auth window expiry (8 hours from successful auth)
//   - Cmd packet round-trip via test_stub_handle_command
//   - Refusal when not authenticated
//   - JSON status payload byte budget
// ============================================================================

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

// The ble_companion impl checks BLE_HAVE_NIMBLE and falls back to the host
// HMAC + simple in-memory state when it's absent. UNIT_TEST is defined in
// platformio.ini for the native env.
#include "../../src/ble_companion.cpp"

// ---------------------------------------------------------------------------
// Test stub for handleIncomingCommand: capture the buffer so the round-trip
// test can verify the same bytes the phone wrote arrive at command_handler.
// ---------------------------------------------------------------------------
static uint8_t  g_captured_cmd[64];
static size_t   g_captured_len = 0;
static int      g_captured_count = 0;

void test_stub_handle_command(const uint8_t* data, size_t len) {
    if (data && len <= sizeof(g_captured_cmd)) {
        std::memcpy(g_captured_cmd, data, len);
        g_captured_len = len;
        g_captured_count++;
        // Synthesize a 16-byte ACK for the round-trip path.
        uint8_t ack[16];
        std::memset(ack, 0, sizeof(ack));
        ack[0] = 0x06;           // MSG_TYPE_CMD_ACK
        ack[1] = data[0];        // echo first byte for inspection
        ack[2] = (uint8_t)len;
        ble_companion_capture_cmd_ack(ack, 16);
    }
}

// ---------------------------------------------------------------------------
// Helper: build an auth-response payload for a given pin_hmac + challenge.
// ---------------------------------------------------------------------------
static void make_auth_response(uint8_t out_payload[BLE_PDL_AUTHRESP_TOTAL_BYTES],
                               const char* callsign,
                               const uint8_t pin_hmac[32],
                               const uint8_t challenge[BLE_PDL_CHALLENGE_BYTES]) {
    std::memset(out_payload, 0, BLE_PDL_AUTHRESP_TOTAL_BYTES);
    if (callsign) {
        size_t cl = std::strlen(callsign);
        if (cl > BLE_PDL_AUTHRESP_CALLSIGN_BYTES) cl = BLE_PDL_AUTHRESP_CALLSIGN_BYTES;
        std::memcpy(out_payload, callsign, cl);
    }
    ble_companion_hmac_sha256(pin_hmac, 32,
                              challenge, BLE_PDL_CHALLENGE_BYTES,
                              out_payload + BLE_PDL_AUTHRESP_CALLSIGN_BYTES);
}

// ---------------------------------------------------------------------------
// Unity hooks
// ---------------------------------------------------------------------------
void setUp(void) {
    ble_companion_test_reset();
    ble_companion_init();
    g_captured_len = 0;
    g_captured_count = 0;
    std::memset(g_captured_cmd, 0, sizeof(g_captured_cmd));
}
void tearDown(void) {}

// ============================================================================
// Tests
// ============================================================================

// --- T1. HMAC math is deterministic + matches RFC 4231 ----------------------
void test_hmac_sha256_matches_rfc_vector(void) {
    // RFC 4231 test case 1: key = 0x0b * 20, data = "Hi There"
    uint8_t key[20];
    std::memset(key, 0x0b, sizeof(key));
    const char* msg = "Hi There";
    uint8_t out[32];
    ble_companion_hmac_sha256(key, sizeof(key),
                              (const uint8_t*)msg, std::strlen(msg), out);
    // Expected: b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7
    static 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_UINT8_ARRAY(expected, out, 32);
}

// --- T2. Round-trip: Python-side computes HMAC, sensor accepts -------------
void test_auth_handshake_accepts_correct_hmac(void) {
    uint8_t pin_hmac[32];
    for (int i = 0; i < 32; i++) pin_hmac[i] = (uint8_t)(0xA0 ^ i);
    TEST_ASSERT_TRUE(ble_companion_add_authorized_pin_hmac(pin_hmac));

    uint8_t challenge[BLE_PDL_CHALLENGE_BYTES];
    for (int i = 0; i < BLE_PDL_CHALLENGE_BYTES; i++) challenge[i] = (uint8_t)i;
    ble_companion_set_challenge(challenge);

    uint8_t resp[BLE_PDL_AUTHRESP_TOTAL_BYTES];
    make_auth_response(resp, "MARK01", pin_hmac, challenge);

    TEST_ASSERT_TRUE(ble_companion_process_auth_response(resp, sizeof(resp), 1000));
    TEST_ASSERT_TRUE(ble_companion_is_authenticated(1000));
}

// --- T3. Wrong HMAC is rejected ---------------------------------------------
void test_auth_handshake_rejects_bad_hmac(void) {
    uint8_t pin_hmac[32];
    std::memset(pin_hmac, 0x42, 32);
    TEST_ASSERT_TRUE(ble_companion_add_authorized_pin_hmac(pin_hmac));

    uint8_t challenge[BLE_PDL_CHALLENGE_BYTES];
    for (int i = 0; i < BLE_PDL_CHALLENGE_BYTES; i++) challenge[i] = (uint8_t)(i*7);
    ble_companion_set_challenge(challenge);

    // Build response with the WRONG key
    uint8_t wrong_pin[32];
    std::memset(wrong_pin, 0x99, 32);
    uint8_t resp[BLE_PDL_AUTHRESP_TOTAL_BYTES];
    make_auth_response(resp, "BADGUY", wrong_pin, challenge);

    TEST_ASSERT_FALSE(ble_companion_process_auth_response(resp, sizeof(resp), 1000));
    TEST_ASSERT_FALSE(ble_companion_is_authenticated(1000));
}

// --- T4. Auth window expires at exactly 8 hours -----------------------------
void test_auth_window_expires_at_8h(void) {
    uint8_t pin_hmac[32];
    std::memset(pin_hmac, 0x55, 32);
    TEST_ASSERT_TRUE(ble_companion_add_authorized_pin_hmac(pin_hmac));

    uint8_t challenge[BLE_PDL_CHALLENGE_BYTES];
    std::memset(challenge, 0xAA, sizeof(challenge));
    ble_companion_set_challenge(challenge);

    uint8_t resp[BLE_PDL_AUTHRESP_TOTAL_BYTES];
    make_auth_response(resp, "OP1", pin_hmac, challenge);

    uint32_t t0 = 100000;
    TEST_ASSERT_TRUE(ble_companion_process_auth_response(resp, sizeof(resp), t0));
    TEST_ASSERT_TRUE(ble_companion_is_authenticated(t0));
    TEST_ASSERT_TRUE(ble_companion_is_authenticated(t0 + 1000));
    TEST_ASSERT_TRUE(ble_companion_is_authenticated(t0 + BLE_PDL_AUTH_WINDOW_MS - 1));
    // At exactly the deadline the window has closed.
    TEST_ASSERT_FALSE(ble_companion_is_authenticated(t0 + BLE_PDL_AUTH_WINDOW_MS));
    TEST_ASSERT_FALSE(ble_companion_is_authenticated(t0 + BLE_PDL_AUTH_WINDOW_MS + 1));
}

// --- T5. Cmd write round-trip: bytes reach command_handler verbatim --------
void test_cmd_round_trip_authenticated(void) {
    uint8_t pin_hmac[32];
    std::memset(pin_hmac, 0x33, 32);
    TEST_ASSERT_TRUE(ble_companion_add_authorized_pin_hmac(pin_hmac));
    uint8_t challenge[BLE_PDL_CHALLENGE_BYTES];
    std::memset(challenge, 0x11, sizeof(challenge));
    ble_companion_set_challenge(challenge);
    uint8_t resp[BLE_PDL_AUTHRESP_TOTAL_BYTES];
    make_auth_response(resp, "ERIC01", pin_hmac, challenge);
    TEST_ASSERT_TRUE(ble_companion_process_auth_response(resp, sizeof(resp), 5000));

    // Build a fake CMD_PACKET (we don't need it CRC-valid; ble_companion
    // does not parse — it just hands the bytes to command_handler).
    uint8_t cmd[32];
    for (int i = 0; i < 32; i++) cmd[i] = (uint8_t)(i + 1);

    uint8_t ack[16] = {};
    size_t  ack_len = 0;
    uint8_t rc = ble_companion_process_cmd(cmd, sizeof(cmd), 5000,
                                           ack, &ack_len);
    TEST_ASSERT_EQUAL_UINT8(BLE_PDL_OK, rc);
    TEST_ASSERT_EQUAL_INT(1, g_captured_count);
    TEST_ASSERT_EQUAL_size_t(32, g_captured_len);
    TEST_ASSERT_EQUAL_UINT8_ARRAY(cmd, g_captured_cmd, 32);
    // ACK was synthesized by the stub: byte 0 = 0x06, byte 1 = first cmd byte
    TEST_ASSERT_EQUAL_size_t(16, ack_len);
    TEST_ASSERT_EQUAL_UINT8(0x06, ack[0]);
    TEST_ASSERT_EQUAL_UINT8(cmd[0], ack[1]);
    TEST_ASSERT_EQUAL_UINT8(32,     ack[2]);
}

// --- T6. Cmd write rejected when not authenticated -------------------------
void test_cmd_rejected_when_not_authenticated(void) {
    uint8_t cmd[32] = {0};
    cmd[0] = 0x05;
    uint8_t ack[16] = {};
    size_t  ack_len = 99;
    uint8_t rc = ble_companion_process_cmd(cmd, sizeof(cmd), 12345,
                                           ack, &ack_len);
    TEST_ASSERT_EQUAL_UINT8(BLE_PDL_ERR_NOT_AUTH, rc);
    TEST_ASSERT_EQUAL_INT(0, g_captured_count);
    TEST_ASSERT_EQUAL_size_t(0, ack_len);
}

// --- T7. Cmd rejected after auth window expires ----------------------------
void test_cmd_rejected_after_window(void) {
    uint8_t pin_hmac[32];
    std::memset(pin_hmac, 0x77, 32);
    TEST_ASSERT_TRUE(ble_companion_add_authorized_pin_hmac(pin_hmac));
    uint8_t challenge[BLE_PDL_CHALLENGE_BYTES];
    std::memset(challenge, 0x88, sizeof(challenge));
    ble_companion_set_challenge(challenge);
    uint8_t resp[BLE_PDL_AUTHRESP_TOTAL_BYTES];
    make_auth_response(resp, "OP2", pin_hmac, challenge);
    uint32_t t0 = 1000;
    TEST_ASSERT_TRUE(ble_companion_process_auth_response(resp, sizeof(resp), t0));

    uint8_t cmd[32] = {0};
    uint8_t ack[16] = {};
    size_t  ack_len = 0;
    // Inside the window
    TEST_ASSERT_EQUAL_UINT8(BLE_PDL_OK,
        ble_companion_process_cmd(cmd, sizeof(cmd), t0 + 1000, ack, &ack_len));
    // After the window
    TEST_ASSERT_EQUAL_UINT8(BLE_PDL_ERR_NOT_AUTH,
        ble_companion_process_cmd(cmd, sizeof(cmd),
                                  t0 + BLE_PDL_AUTH_WINDOW_MS + 1,
                                  ack, &ack_len));
}

// --- T8. Cmd rejected on bad length ---------------------------------------
void test_cmd_rejected_bad_length(void) {
    uint8_t pin_hmac[32];
    std::memset(pin_hmac, 0x66, 32);
    ble_companion_add_authorized_pin_hmac(pin_hmac);
    uint8_t challenge[BLE_PDL_CHALLENGE_BYTES];
    std::memset(challenge, 0x99, sizeof(challenge));
    ble_companion_set_challenge(challenge);
    uint8_t resp[BLE_PDL_AUTHRESP_TOTAL_BYTES];
    make_auth_response(resp, "OP3", pin_hmac, challenge);
    TEST_ASSERT_TRUE(ble_companion_process_auth_response(resp, sizeof(resp), 100));

    uint8_t cmd[40] = {0};
    uint8_t ack[16] = {};
    size_t  ack_len = 0;
    // 33 bytes -> too long
    TEST_ASSERT_EQUAL_UINT8(BLE_PDL_ERR_BAD_LEN,
        ble_companion_process_cmd(cmd, 33, 100, ack, &ack_len));
    // 0 bytes -> too short
    TEST_ASSERT_EQUAL_UINT8(BLE_PDL_ERR_BAD_LEN,
        ble_companion_process_cmd(cmd, 0, 100, ack, &ack_len));
}

// --- T9. Status JSON byte budget fits in 244 bytes -------------------------
void test_status_json_fits_in_244_bytes(void) {
    // Worst case: 16-char sensor name + max counts. Build the JSON ourselves
    // (the firmware path is gated behind BLE_HAVE_NIMBLE; we replicate the
    // exact format string here so the budget is verified independent of the
    // NimBLE-only build_status_json()).
    char buf[256];
    int n = snprintf(buf, sizeof(buf),
        "{\"name\":\"%s\",\"sensor_id_hex\":\"%08X\","
        "\"mesh_state\":\"%s\",\"nll\":%.2f,\"networks_count\":%d,"
        "\"uptime_s\":%u,\"battery_pct\":%d}",
        "Spencer01XX",  // long-ish name
        0xDEADBEEFu,
        "enrolled",
        12345.67f,      // nll
        9999,           // networks_count (worst case)
        4294967295u,    // uptime_s (worst case)
        100);           // battery_pct
    TEST_ASSERT_TRUE(n > 0);
    TEST_ASSERT_TRUE_MESSAGE(n <= BLE_PDL_STATUS_JSON_MAX,
        "status JSON exceeded 244-byte BLE characteristic budget");
}

// --- T10. Detection JSON also fits in 244 bytes ---------------------------
void test_detection_json_fits(void) {
    DetectionEvent e = {};
    e.sensor_id     = 0xDEADBEEFu;
    e.timestamp_utc = 0xFFFFFFFFu;
    e.class_label   = 4;
    e.confidence    = 200;
    e.rssi          = -42;
    e.flags         = 0x01;
    for (int i = 0; i < 8; i++) e.signature_hash[i] = (uint8_t)(0xA0 ^ i);
    char buf[256];
    size_t n = build_detection_json(buf, sizeof(buf), e);
    TEST_ASSERT_TRUE(n > 0);
    TEST_ASSERT_TRUE_MESSAGE(n <= BLE_PDL_STATUS_JSON_MAX,
        "detection JSON exceeded 244-byte BLE characteristic budget");
}

// --- T10b. VqcSample JSON also fits in 244 bytes, publish is a clean no-crash op ---
void test_vqc_json_fits(void) {
    float feats[5] = {0.9999f, 0.9999f, 0.9999f, 0.9999f, 1.0f};
    char buf[256];
    size_t n = build_vqc_json(buf, sizeof(buf), "PDL-Spencer01", feats,
                              0.9999f, 0xFFFFFFFFu);
    TEST_ASSERT_TRUE(n > 0);
    TEST_ASSERT_TRUE_MESSAGE(n <= BLE_PDL_STATUS_JSON_MAX,
        "vqc sample JSON exceeded 244-byte BLE characteristic budget");
    // Sanity: the score and all 5 feats actually landed in the buffer.
    TEST_ASSERT_TRUE(std::strstr(buf, "\"vqc_score\":0.9999") != nullptr);
    TEST_ASSERT_TRUE(std::strstr(buf, "\"feats\":[0.9999") != nullptr);
}

void test_publish_vqc_sample_no_crash(void) {
    float feats[5] = {0.1f, 0.2f, 0.3f, 0.4f, 0.5f};
    // Host build has no NimBLE characteristic to notify -- this just
    // exercises the shared LOCK/build/pending-flag path without crashing.
    ble_companion_publish_vqc_sample(feats, 0.42f, 1234567u);
    TEST_ASSERT_TRUE(s_vqc_pending_notify);
    TEST_ASSERT_TRUE(s_vqc_json_len > 0);
}

// --- T11. Auth response with wrong size is rejected -----------------------
void test_auth_response_bad_length(void) {
    uint8_t pin_hmac[32];
    std::memset(pin_hmac, 0x44, 32);
    ble_companion_add_authorized_pin_hmac(pin_hmac);
    uint8_t resp[16] = {0};  // far too short
    TEST_ASSERT_FALSE(ble_companion_process_auth_response(resp, sizeof(resp), 1000));
}

// --- T12. Multi-operator allowlist: any authorized HMAC passes ------------
void test_multi_operator_allowlist(void) {
    uint8_t pinA[32], pinB[32], pinC[32];
    std::memset(pinA, 0x01, 32);
    std::memset(pinB, 0x02, 32);
    std::memset(pinC, 0x03, 32);
    ble_companion_add_authorized_pin_hmac(pinA);
    ble_companion_add_authorized_pin_hmac(pinB);
    ble_companion_add_authorized_pin_hmac(pinC);

    uint8_t challenge[BLE_PDL_CHALLENGE_BYTES];
    std::memset(challenge, 0xCC, sizeof(challenge));
    ble_companion_set_challenge(challenge);

    // B-operator's response should pass even though it's slot-1
    uint8_t resp[BLE_PDL_AUTHRESP_TOTAL_BYTES];
    make_auth_response(resp, "OPB", pinB, challenge);
    TEST_ASSERT_TRUE(ble_companion_process_auth_response(resp, sizeof(resp), 1));
    TEST_ASSERT_TRUE(ble_companion_is_authenticated(1));
}

// --- T13. Default-PIN provisioning: ble_companion_set_operator_pin("000000")
// authorizes the same SHA-256(pin) the phone side's factory-default
// operatorPinHmac uses -- this is the actual gap that made every real Cmd
// write time out (s_authorized_count was permanently 0 with no way to
// provision an allowlist entry at all before this function existed).
void test_set_operator_pin_provisions_matching_hmac(void) {
    ble_companion_set_operator_pin("000000");

    // Independently compute SHA-256("000000") using the same host-side pure-C
    // primitives ble_companion_set_operator_pin() itself uses (accessible
    // here since this file #includes ble_companion.cpp directly) -- this is
    // the cross-check, not just re-calling the function under test.
    uint8_t expected_hash[32];
    Sha256Ctx c; sha256_init(&c);
    const char* pin = "000000";
    sha256_update(&c, (const uint8_t*)pin, std::strlen(pin));
    sha256_final(&c, expected_hash);

    uint8_t challenge[BLE_PDL_CHALLENGE_BYTES];
    std::memset(challenge, 0x5A, sizeof(challenge));
    ble_companion_set_challenge(challenge);

    uint8_t resp[BLE_PDL_AUTHRESP_TOTAL_BYTES];
    make_auth_response(resp, "OPDEFAULT", expected_hash, challenge);

    TEST_ASSERT_TRUE(ble_companion_process_auth_response(resp, sizeof(resp), 42));
    TEST_ASSERT_TRUE(ble_companion_is_authenticated(42));
}

// --- T14. A blank/null PIN is a no-op (doesn't add a wildcard entry) -------
void test_set_operator_pin_ignores_empty(void) {
    ble_companion_set_operator_pin("");
    ble_companion_set_operator_pin(nullptr);

    uint8_t challenge[BLE_PDL_CHALLENGE_BYTES];
    std::memset(challenge, 0x11, sizeof(challenge));
    ble_companion_set_challenge(challenge);

    uint8_t zero_key[32] = {0};
    uint8_t resp[BLE_PDL_AUTHRESP_TOTAL_BYTES];
    make_auth_response(resp, "NOBODY", zero_key, challenge);
    TEST_ASSERT_FALSE(ble_companion_process_auth_response(resp, sizeof(resp), 1));
}

// ============================================================================
// Runner
// ============================================================================
int main(int /*argc*/, char** /*argv*/) {
    UNITY_BEGIN();
    RUN_TEST(test_hmac_sha256_matches_rfc_vector);
    RUN_TEST(test_auth_handshake_accepts_correct_hmac);
    RUN_TEST(test_auth_handshake_rejects_bad_hmac);
    RUN_TEST(test_auth_window_expires_at_8h);
    RUN_TEST(test_cmd_round_trip_authenticated);
    RUN_TEST(test_cmd_rejected_when_not_authenticated);
    RUN_TEST(test_cmd_rejected_after_window);
    RUN_TEST(test_cmd_rejected_bad_length);
    RUN_TEST(test_status_json_fits_in_244_bytes);
    RUN_TEST(test_detection_json_fits);
    RUN_TEST(test_vqc_json_fits);
    RUN_TEST(test_publish_vqc_sample_no_crash);
    RUN_TEST(test_auth_response_bad_length);
    RUN_TEST(test_multi_operator_allowlist);
    RUN_TEST(test_set_operator_pin_provisions_matching_hmac);
    RUN_TEST(test_set_operator_pin_ignores_empty);
    return UNITY_END();
}
