// ============================================================================
// test/test_active/test_active.cpp
//
// Native Unity test suite for the T1 ACTIVE subsystem modules:
//   - network_id    (802.11 frame parser)
//   - payload_buffer (LoRa→PSRAM chunk reassembly)
//   - auth_gate      (Ed25519 token verification)
//   - wifi_inject    (802.11 frame injection state save/restore)
//
// Run with:  pio test -e native -f test_active
//
// Strategy: include each .cpp directly as a single translation unit with
// UNIT_TEST defined (set by platformio.ini native env), so that host-side
// stubs take effect and no ESP-IDF / Arduino libraries are required.
// ============================================================================

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

// ===========================================================================
// ---- Global stubs used across multiple modules ----------------------------
// ===========================================================================

// mesh_publish_raw stub: records the last published packet
static bool    g_publish_called = false;
static uint8_t g_publish_buf[256] = {};
static size_t  g_publish_len  = 0;
static bool    g_publish_ret  = true;

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 = copy;
    return g_publish_ret;
}

// millis stub shared by network_id and payload_buffer
#define STUB_MILLIS_PROVIDED
static uint32_t g_stub_millis = 5000;
static inline uint32_t millis() { return g_stub_millis; }

// ===========================================================================
// ---- Pull in implementation units -----------------------------------------
// ===========================================================================
#include "../../src/network_id.cpp"
#include "../../src/payload_buffer.cpp"

// auth_gate needs its own millis stub (uint64_t) — reuse a wrapper
// and expose the test-injectable Ed25519 controls before including.
extern bool g_ed25519_verify_result;
extern uint8_t g_test_pubkey[32];
extern bool    g_test_pubkey_set;
#include "../../src/auth_gate.cpp"

// wifi_inject: its stubs are embedded inside the TU under UNIT_TEST.
// Pull in the wifi call log struct before including.
#include "../../src/wifi_inject.cpp"
// After inclusion, EspWifiCallLog g_wifi_call_log is accessible.

// ===========================================================================
// ---- Helpers --------------------------------------------------------------
// ===========================================================================

// CRC32/BZIP2 (mirrors the production implementation — used to build valid
// test chunks without importing the production TU twice).
static uint32_t test_crc32(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;
}

// Build a 24-byte chunk header + data into buf (buf must be >= 24 + data_len)
static size_t make_chunk(uint8_t* buf,
                         uint32_t payload_id,
                         uint16_t total_chunks,
                         uint16_t chunk_idx,
                         uint32_t total_size,
                         const uint8_t* data,
                         size_t   data_len) {
    uint32_t chunk_crc = test_crc32(data, data_len);
    buf[0] = MSG_TYPE_PAYLOAD_CHUNK;
    memcpy(buf + 1,  &payload_id,   4);
    memcpy(buf + 5,  &total_chunks, 2);
    memcpy(buf + 7,  &chunk_idx,    2);
    memcpy(buf + 9,  &total_size,   4);
    memcpy(buf + 13, &chunk_crc,    4);
    memset(buf + 17, 0,             7);  // reserved
    memcpy(buf + 24, data, data_len);
    return 24 + data_len;
}

// Build the same chunk but with a deliberately bad per-chunk CRC
static size_t make_chunk_bad_crc(uint8_t* buf,
                                  uint32_t payload_id,
                                  uint16_t total_chunks,
                                  uint16_t chunk_idx,
                                  uint32_t total_size,
                                  const uint8_t* data,
                                  size_t   data_len) {
    size_t len = make_chunk(buf, payload_id, total_chunks, chunk_idx,
                            total_size, data, data_len);
    // Flip one CRC byte (bytes 13..16)
    buf[13] ^= 0xFF;
    return len;
}

// Build a raw 802.11 beacon frame (minimal) with known BSSID / SSID / channel
// Layout matches what the ESP32 promiscuous sniffer delivers:
//   FCF(2) Dur(2) DA(6) SA(6) BSSID(6) SeqCtrl(2) Timestamp(8) BI(2) Cap(2) IEs...
static size_t make_beacon_frame(uint8_t* buf,
                                 const uint8_t bssid[6],
                                 const char*   ssid,
                                 uint8_t       channel) {
    memset(buf, 0, 120);
    // Frame Control: type=mgmt(0), subtype=beacon(8) → 0x80 0x00
    buf[0] = 0x80; buf[1] = 0x00;
    // Duration = 0 (bytes 2..3)
    // DA = broadcast
    memset(buf + 4, 0xFF, 6);
    // SA = BSSID
    memcpy(buf + 10, bssid, 6);
    // BSSID (Address 3)
    memcpy(buf + 16, bssid, 6);
    // SeqCtrl = 0 (bytes 22..23)
    // Fixed beacon params: Timestamp(8) + BeaconInterval(2) + Capability(2) = 12B
    // (bytes 24..35 are all zero — OK for test)
    uint8_t* ie = buf + 36;
    size_t ie_len = 0;
    // SSID IE
    size_t slen = strlen(ssid);
    ie[ie_len++] = 0x00;               // IE ID = SSID
    ie[ie_len++] = (uint8_t)slen;
    memcpy(ie + ie_len, ssid, slen);
    ie_len += slen;
    // DS Param IE
    ie[ie_len++] = 0x03;               // IE ID = DS Param
    ie[ie_len++] = 1;
    ie[ie_len++] = channel;
    // RSN IE (minimal WPA2-CCMP)
    ie[ie_len++] = 0x30;               // IE ID = RSN
    // length: 18 real bytes follow (version[2] + group[4] + pairwise_count[2]
    // + pairwise[4] + akm_count[2] + akm[4]). This previously said 20 (2
    // bytes too many) -- harmless under the old flat-350-byte parse cap
    // (parse_ies just walked into pre-zeroed padding), but became a hard
    // failure once network_id.cpp's IE parse was correctly bounded to the
    // real frame length (sig_len fix): the overstated length made the
    // parser see this IE as truncated and skip it entirely.
    ie[ie_len++] = 18;                 // length
    // version=1
    ie[ie_len++] = 0x01; ie[ie_len++] = 0x00;
    // Group cipher OUI 00:0F:AC suite 4 = CCMP
    ie[ie_len++] = 0x00; ie[ie_len++] = 0x0F; ie[ie_len++] = 0xAC;
    ie[ie_len++] = 0x04; // CCMP
    // Pairwise count = 1
    ie[ie_len++] = 0x01; ie[ie_len++] = 0x00;
    // Pairwise OUI 00:0F:AC:4 = CCMP
    ie[ie_len++] = 0x00; ie[ie_len++] = 0x0F; ie[ie_len++] = 0xAC;
    ie[ie_len++] = 0x04;
    // AKM count = 1
    ie[ie_len++] = 0x01; ie[ie_len++] = 0x00;
    // AKM OUI = 00:0F:AC:2
    ie[ie_len++] = 0x00; ie[ie_len++] = 0x0F; ie[ie_len++] = 0xAC;
    ie[ie_len++] = 0x02;

    return 36 + ie_len;
}

// Build an 84-byte auth token (minimal: all fields valid, sig controlled by
// g_ed25519_verify_result stub).
// "now_ms" used to set expiry = now_ms + 60000.
static void make_auth_token(uint8_t        token[84],
                             uint32_t       sensor_id,
                             const uint8_t  bssid[6],
                             uint32_t       payload_id,
                             uint64_t       now_ms,
                             int64_t        expiry_offset_ms) {
    memset(token, 0, 84);
    uint64_t expiry = (uint64_t)((int64_t)now_ms + expiry_offset_ms);
    memcpy(token + 0,  &sensor_id,  4);
    memcpy(token + 4,  bssid,       6);
    memcpy(token + 10, &payload_id, 4);
    memcpy(token + 14, &expiry,     8);
    // token[22..23] = reserved = 0
    // token[24..87] = sig — filled with 0xAB so the stub can see something
    memset(token + 24, 0xAB, 64);
}

// ===========================================================================
// ---- setUp / tearDown ------------------------------------------------------
// ===========================================================================
void setUp(void) {
    g_publish_called = false;
    g_publish_len    = 0;
    g_publish_ret    = true;
    g_stub_millis    = 5000;
    network_id_init();
    payload_buffer_init();
    // auth_gate state
    g_ed25519_verify_result = true;
    g_test_pubkey_set       = false;
    memset(g_test_pubkey, 0, 32);
    authgate_reload_pubkey();
    // wifi_inject
    memset(&g_wifi_call_log, 0, sizeof(g_wifi_call_log));
    wifi_inject_init();
}

void tearDown(void) {}

// ===========================================================================
// ===========================================================================
// ---- Test: network_id_parses_beacon ----------------------------------------
// ===========================================================================
// Feed a synthesized beacon frame through network_id_sniffer_cb, then verify
// the table entry has the correct SSID, BSSID, and channel.
// ===========================================================================
void test_network_id_parses_beacon(void) {
    const uint8_t bssid[6] = { 0xAA, 0xBB, 0xCC, 0x11, 0x22, 0x33 };
    const char*   ssid     = "TestNetwork";
    const uint8_t channel  = 6;

    uint8_t frame_buf[120];
    size_t  frame_len = make_beacon_frame(frame_buf, bssid, ssid, channel);
    (void)frame_len;

    // Build a fake wifi_promiscuous_pkt_t wrapping the frame.
    // The real struct packs: rx_ctrl then payload. We need to match the
    // offsets the production code uses. In the stub type mirror:
    //   struct wifi_pkt_rx_ctrl_t { int8_t rssi; uint8_t channel; uint16_t sig_len; };
    //   struct wifi_promiscuous_pkt_t { wifi_pkt_rx_ctrl_t rx_ctrl; uint8_t payload[0]; };
    // sig_len must be set to the real frame length -- network_id.cpp now
    // bounds its IE parse to it (a real over-read bug fix: it previously
    // always scanned a flat 350-byte cap regardless of actual frame size).
    struct FakePkt {
        int8_t   rssi;
        uint8_t  channel;
        uint16_t sig_len;
        uint8_t  payload[120];
    } fake;
    fake.rssi    = -65;
    fake.channel = channel;
    fake.sig_len = (uint16_t)frame_len;
    memcpy(fake.payload, frame_buf, frame_len);

    // WIFI_PKT_MGMT = 0 in the stub enum
    network_id_sniffer_cb(&fake, WIFI_PKT_MGMT);

    TEST_ASSERT_EQUAL_INT(1, network_id_count());

    NetworkEntry entry;
    bool got = network_id_get(0, &entry);
    TEST_ASSERT_TRUE(got);
    TEST_ASSERT_EQUAL_MEMORY(bssid, entry.bssid, 6);
    TEST_ASSERT_EQUAL_STRING(ssid, entry.ssid);
    TEST_ASSERT_EQUAL_UINT8(channel, entry.channel);
    TEST_ASSERT_EQUAL_INT8(-65, entry.last_rssi);
    // Should have detected CCMP from RSN IE
    TEST_ASSERT_NOT_EQUAL(CIPHER_OPEN, entry.cipher_id);
}

// ===========================================================================
// ---- Test: payload_chunk_reassembly ----------------------------------------
// ===========================================================================
// Send 5 chunks of a 1 KB payload. Verify PB_COMPLETE on the last chunk and
// that the reassembled buffer matches the original data.
// ===========================================================================
void test_payload_chunk_reassembly(void) {
    const uint32_t PAYLOAD_ID   = 0x12345678u;
    const size_t   TOTAL_SIZE   = 1000;  // 1000 bytes
    // 5 chunks of 200 bytes each (200 * 5 = 1000)
    const uint16_t TOTAL_CHUNKS = 5;
    const size_t   CHUNK_SIZE   = 200;

    // Build original data
    uint8_t original[TOTAL_SIZE];
    for (size_t i = 0; i < TOTAL_SIZE; i++) original[i] = (uint8_t)(i & 0xFF);

    // Compute full-payload CRC32 (will be sent in the final chunk's crc field)
    uint32_t full_crc = test_crc32(original, TOTAL_SIZE);

    uint8_t pkt[PAYLOAD_CHUNK_HDR_LEN + CHUNK_SIZE];
    PayloadBufStatus status = PB_OK;

    for (uint16_t ci = 0; ci < TOTAL_CHUNKS; ci++) {
        size_t offset    = ci * CHUNK_SIZE;
        const uint8_t* chunk_data = original + offset;

        if (ci < TOTAL_CHUNKS - 1) {
            // Normal chunk: per-chunk CRC
            size_t pkt_len = make_chunk(pkt, PAYLOAD_ID, TOTAL_CHUNKS, ci,
                                        (uint32_t)TOTAL_SIZE,
                                        chunk_data, CHUNK_SIZE);
            status = payload_buffer_handle_chunk(pkt, pkt_len);
            TEST_ASSERT_EQUAL_UINT8(PB_OK, status);
        } else {
            // Final chunk: chunk_crc32 field carries the full-payload CRC32.
            // Build header manually.
            pkt[0] = MSG_TYPE_PAYLOAD_CHUNK;
            memcpy(pkt + 1,  &PAYLOAD_ID,   4);
            memcpy(pkt + 5,  &TOTAL_CHUNKS, 2);
            memcpy(pkt + 7,  &ci,           2);
            uint32_t ts = (uint32_t)TOTAL_SIZE;
            memcpy(pkt + 9,  &ts,           4);
            // Put full_crc in the chunk_crc32 field (double-purpose for final chunk)
            memcpy(pkt + 13, &full_crc,     4);
            memset(pkt + 17, 0,             7);
            memcpy(pkt + 24, chunk_data, CHUNK_SIZE);
            size_t pkt_len = PAYLOAD_CHUNK_HDR_LEN + CHUNK_SIZE;
            status = payload_buffer_handle_chunk(pkt, pkt_len);
            TEST_ASSERT_EQUAL_UINT8(PB_COMPLETE, status);
        }
    }

    const uint8_t* out_data = nullptr;
    size_t         out_len  = 0;
    bool got = payload_buffer_get(PAYLOAD_ID, &out_data, &out_len);
    TEST_ASSERT_TRUE(got);
    TEST_ASSERT_EQUAL_size_t(TOTAL_SIZE, out_len);
    TEST_ASSERT_EQUAL_MEMORY(original, out_data, TOTAL_SIZE);

    payload_buffer_release(PAYLOAD_ID);
}

// ===========================================================================
// ---- Test: payload_chunk_bad_crc_rejected ----------------------------------
// ===========================================================================
// Flip a byte in the first chunk's CRC; verify PB_BAD_CHUNK_CRC.
// ===========================================================================
void test_payload_chunk_bad_crc_rejected(void) {
    const uint32_t PAYLOAD_ID   = 0xDEADBEEFu;
    const uint16_t TOTAL_CHUNKS = 2;
    const size_t   CHUNK_SIZE   = 50;
    const size_t   TOTAL_SIZE   = CHUNK_SIZE * 2;

    uint8_t data[CHUNK_SIZE];
    memset(data, 0x55, CHUNK_SIZE);

    uint8_t pkt[PAYLOAD_CHUNK_HDR_LEN + CHUNK_SIZE];
    size_t pkt_len = make_chunk_bad_crc(pkt, PAYLOAD_ID, TOTAL_CHUNKS, 0,
                                         (uint32_t)TOTAL_SIZE, data, CHUNK_SIZE);

    PayloadBufStatus status = payload_buffer_handle_chunk(pkt, pkt_len);
    TEST_ASSERT_EQUAL_UINT8(PB_BAD_CHUNK_CRC, status);
}

// ===========================================================================
// ---- Test: authgate_valid_token_passes ------------------------------------
// ===========================================================================
// Tests the happy path: valid sensor_id, bssid, payload_id, future expiry,
// and the Ed25519 stub returns true.
// ===========================================================================
void test_authgate_valid_token_passes(void) {
    const uint32_t SENSOR_ID  = 0x00000042u;
    const uint8_t  BSSID[6]   = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66 };
    const uint32_t PAYLOAD_ID = 0xCAFEBABEu;
    const uint64_t NOW_MS     = 1000000ULL;

    // Provide a pubkey to the stub
    memset(g_test_pubkey, 0xAA, 32);
    g_test_pubkey_set       = true;
    g_ed25519_verify_result = true;
    authgate_reload_pubkey();

    uint8_t token[84];
    make_auth_token(token, SENSOR_ID, BSSID, PAYLOAD_ID, NOW_MS, +60000);

    bool result = authgate_verify(SENSOR_ID, BSSID, PAYLOAD_ID,
                                  NOW_MS, token, 84);
    TEST_ASSERT_TRUE(result);
}

// ===========================================================================
// ---- Test: authgate_expired_token_fails ------------------------------------
// ===========================================================================
// Set expiry in the past; verify false return.
// ===========================================================================
void test_authgate_expired_token_fails(void) {
    const uint32_t SENSOR_ID  = 0x00000042u;
    const uint8_t  BSSID[6]   = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66 };
    const uint32_t PAYLOAD_ID = 0xCAFEBABEu;
    const uint64_t NOW_MS     = 1000000ULL;

    memset(g_test_pubkey, 0xAA, 32);
    g_test_pubkey_set       = true;
    g_ed25519_verify_result = true;
    authgate_reload_pubkey();

    uint8_t token[84];
    // expiry = NOW_MS - 1 → already expired
    make_auth_token(token, SENSOR_ID, BSSID, PAYLOAD_ID, NOW_MS, -1);

    bool result = authgate_verify(SENSOR_ID, BSSID, PAYLOAD_ID,
                                  NOW_MS, token, 84);
    TEST_ASSERT_FALSE(result);
}

// ===========================================================================
// ---- Test: authgate_wrong_pubkey_fails -------------------------------------
// ===========================================================================
// The Ed25519 stub is configured to return false (simulating wrong key).
// ===========================================================================
void test_authgate_wrong_pubkey_fails(void) {
    const uint32_t SENSOR_ID  = 0x00000042u;
    const uint8_t  BSSID[6]   = { 0x11, 0x22, 0x33, 0x44, 0x55, 0x66 };
    const uint32_t PAYLOAD_ID = 0xCAFEBABEu;
    const uint64_t NOW_MS     = 1000000ULL;

    // Pubkey present but Ed25519 verify says no
    memset(g_test_pubkey, 0xBB, 32);
    g_test_pubkey_set       = true;
    g_ed25519_verify_result = false;  // wrong key / bad sig
    authgate_reload_pubkey();

    uint8_t token[84];
    make_auth_token(token, SENSOR_ID, BSSID, PAYLOAD_ID, NOW_MS, +60000);

    bool result = authgate_verify(SENSOR_ID, BSSID, PAYLOAD_ID,
                                  NOW_MS, token, 84);
    TEST_ASSERT_FALSE(result);
}

// ===========================================================================
// ---- Test: wifi_inject_state_save_restore_idempotent -----------------------
// ===========================================================================
// Verify that save_state captures the channel, restore_state puts it back,
// and doing save→restore twice leaves the state identical.
// ===========================================================================
void test_wifi_inject_state_save_restore_idempotent(void) {
    // Pre-set a known channel via the stub
    g_wifi_call_log.last_channel = 11;

    // First save
    bool saved1 = wifi_inject_save_state();
    TEST_ASSERT_TRUE(saved1);
    InjectorSavedState s1 = wifi_inject_get_saved_state();
    TEST_ASSERT_TRUE(s1.valid);
    TEST_ASSERT_EQUAL_UINT8(11, s1.channel);

    // Restore
    bool restored = wifi_inject_restore_state();
    TEST_ASSERT_TRUE(restored);
    // Restore should have called set_channel back to 11
    TEST_ASSERT_EQUAL_UINT8(11, g_wifi_call_log.last_channel);

    // Second save → same result
    bool saved2 = wifi_inject_save_state();
    TEST_ASSERT_TRUE(saved2);
    InjectorSavedState s2 = wifi_inject_get_saved_state();
    TEST_ASSERT_EQUAL_UINT8(s1.channel, s2.channel);

    // Inject a RAW frame and verify the channel is restored afterwards
    uint8_t dummy_frame[10] = { 0x80, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
    wifi_inject_set_channel(6);  // switch to ch 6 for injection
    int sent = wifi_inject_send(dummy_frame, sizeof(dummy_frame), INJECT_RAW, 1);
    TEST_ASSERT_GREATER_OR_EQUAL_INT(1, sent);
    // After send, restore_state should have brought back channel 6
    // (save captured ch=6 at time of send entry)
    // The important invariant: restore was called (set_channel_called == true)
    TEST_ASSERT_TRUE(g_wifi_call_log.set_channel_called);
    // And tx_calls incremented
    TEST_ASSERT_GREATER_OR_EQUAL_INT(1, g_wifi_call_log.tx_calls);
}

// ===========================================================================
// ---- Additional integration smoke test ------------------------------------
// ===========================================================================
// Verify network_id_publish emits a MSG_TYPE_NETWORK_LIST packet after a
// beacon is parsed.
// ===========================================================================
void test_network_id_publish_emits_packet(void) {
    const uint8_t bssid[6] = { 0x10, 0x20, 0x30, 0x40, 0x50, 0x60 };
    const char*   ssid     = "Smoky";
    const uint8_t channel  = 1;

    struct FakePkt {
        int8_t   rssi;
        uint8_t  channel;
        uint16_t sig_len;
        uint8_t  payload[120];
    } fake;
    fake.rssi    = -70;
    fake.channel = channel;
    uint8_t frame_buf[120];
    size_t  frame_len = make_beacon_frame(frame_buf, bssid, ssid, channel);
    fake.sig_len = (uint16_t)frame_len;
    memcpy(fake.payload, frame_buf, frame_len);
    network_id_sniffer_cb(&fake, WIFI_PKT_MGMT);

    bool pub = network_id_publish(0x00000001u);
    TEST_ASSERT_TRUE(pub);
    TEST_ASSERT_TRUE(g_publish_called);
    TEST_ASSERT_EQUAL_UINT8(MSG_TYPE_NETWORK_LIST, g_publish_buf[0]);
    // sensor_id LE = 0x00000001
    TEST_ASSERT_EQUAL_UINT8(0x01, g_publish_buf[1]);
    TEST_ASSERT_EQUAL_UINT8(0x00, g_publish_buf[2]);
    // entry count = 1
    TEST_ASSERT_EQUAL_UINT8(1, g_publish_buf[5]);
    // BSSID at offset 6
    TEST_ASSERT_EQUAL_MEMORY(bssid, g_publish_buf + 6, 6);
}

// ===========================================================================
// ---- Main -----------------------------------------------------------------
// ===========================================================================
int main(int /*argc*/, char** /*argv*/) {
    UNITY_BEGIN();

    RUN_TEST(test_network_id_parses_beacon);
    RUN_TEST(test_payload_chunk_reassembly);
    RUN_TEST(test_payload_chunk_bad_crc_rejected);
    RUN_TEST(test_authgate_valid_token_passes);
    RUN_TEST(test_authgate_expired_token_fails);
    RUN_TEST(test_authgate_wrong_pubkey_fails);
    RUN_TEST(test_wifi_inject_state_save_restore_idempotent);
    RUN_TEST(test_network_id_publish_emits_packet);

    return UNITY_END();
}
