// ============================================================================
// test/test_batch_d/test_batch_d.cpp
//
// Unity tests for Batch-D firmware modules:
//   Module 1: src/talon_federated.{h,cpp}   — TALON federated-learning receiver
//   Module 2: src/cognitive_rf.{h,cpp}       — local RF EWMA + CUSUM baseline
//
// Run with:
//   pio test -e native -f test_batch_d
//
// Both modules are pulled in as single translation units (same pattern as
// test_markov_scorer / test_wfx_features).  All Arduino / ESP32 dependencies
// are stubbed below.
// ============================================================================

#include <unity.h>
#include <cstdint>
#include <cstring>
#include <cmath>
#include <cstdlib>    // malloc / free for talon PSRAM stub

// ---------------------------------------------------------------------------
// Shared stubs
// ---------------------------------------------------------------------------

// millis() stub — test controls time via g_millis
static uint32_t g_millis_val = 0u;
extern "C" unsigned long millis() { return (unsigned long)g_millis_val; }

// mesh_publish_raw stub — records last call
static bool    g_publish_called = false;
static uint8_t g_publish_buf[128];
static size_t  g_publish_len    = 0u;
static bool    g_publish_ret    = true;

bool mesh_publish_raw(const uint8_t* data, size_t len) {
    g_publish_called = true;
    if (len > sizeof(g_publish_buf)) len = sizeof(g_publish_buf);
    memcpy(g_publish_buf, data, len);
    g_publish_len = len;
    return g_publish_ret;
}

// heap_caps_malloc / free — redirect to system heap in native build
#include <cstdlib>
#define MALLOC_CAP_SPIRAM 0
static inline void* heap_caps_malloc(size_t size, int /*caps*/) {
    return malloc(size);
}

// model_manager_commit_slot stub — always succeeds in test
bool model_manager_commit_slot(uint8_t /*slot_id*/, uint32_t /*expected_crc32*/) {
    return true;
}

// ---------------------------------------------------------------------------
// Include implementations under test
// ---------------------------------------------------------------------------
#include "../../src/talon_federated.cpp"
#include "../../src/cognitive_rf.cpp"

// ---------------------------------------------------------------------------
// CRC-32 helper (mirror of crc32_update in talon_federated.cpp)
// Used to build valid test packets without white-box access.
// ---------------------------------------------------------------------------
static uint32_t test_crc32(const uint8_t* data, size_t len) {
    uint32_t crc = ~0u;
    for (size_t i = 0; i < len; i++) {
        uint8_t b = data[i];
        for (int bit = 0; bit < 8; bit++) {
            uint32_t mask = (uint32_t)(-(int32_t)((crc ^ b) & 1u));
            crc = (crc >> 1u) ^ (0xEDB88320u & mask);
            b >>= 1u;
        }
    }
    return ~crc;
}

// Build a MSG_TYPE_FED_UPDATE chunk into buf[].
// Returns total length of the chunk packet.
// weight_data / weight_len carry the payload for this chunk.
static size_t make_chunk(uint8_t* buf,
                         uint16_t round_id,
                         uint32_t total_chunks,
                         uint32_t chunk_idx,
                         uint32_t delta_crc32,
                         uint32_t apply_ts,
                         const uint8_t* weight_data,
                         size_t weight_len) {
    buf[0] = 0x1Bu;  // MSG_TYPE_FED_UPDATE
    buf[1] = (uint8_t)(round_id >> 8u);
    buf[2] = (uint8_t)(round_id);
    buf[3] = (uint8_t)(total_chunks >> 24u);
    buf[4] = (uint8_t)(total_chunks >> 16u);
    buf[5] = (uint8_t)(total_chunks >>  8u);
    buf[6] = (uint8_t)(total_chunks);
    buf[7] = (uint8_t)(chunk_idx >> 24u);
    buf[8] = (uint8_t)(chunk_idx >> 16u);
    buf[9] = (uint8_t)(chunk_idx >>  8u);
    buf[10] = (uint8_t)(chunk_idx);
    buf[11] = (uint8_t)(delta_crc32 >> 24u);
    buf[12] = (uint8_t)(delta_crc32 >> 16u);
    buf[13] = (uint8_t)(delta_crc32 >>  8u);
    buf[14] = (uint8_t)(delta_crc32);
    buf[15] = (uint8_t)(apply_ts >> 24u);
    buf[16] = (uint8_t)(apply_ts >> 16u);
    buf[17] = (uint8_t)(apply_ts >>  8u);
    buf[18] = (uint8_t)(apply_ts);
    // bytes 19..27 = reserved
    memset(buf + 19u, 0u, 9u);
    if (weight_data && weight_len > 0u) {
        memcpy(buf + 1u + TALON_CHUNK_HEADER_BYTES, weight_data, weight_len);
    }
    return 1u + TALON_CHUNK_HEADER_BYTES + weight_len;
}

// ---------------------------------------------------------------------------
// setUp / tearDown
// ---------------------------------------------------------------------------
void setUp(void) {
    g_millis_val     = 0u;
    g_publish_called = false;
    g_publish_len    = 0u;
    g_publish_ret    = true;
    memset(g_publish_buf, 0u, sizeof(g_publish_buf));
    talon_init();
    cogrf_init();
}

void tearDown(void) {}

// ===========================================================================
// Module 1 tests — TALON federated-learning receiver
// ===========================================================================

// ---------------------------------------------------------------------------
// test_talon_chunk_reassembly
//
// Sends a 4-chunk round.  Each chunk carries 10 bytes of payload.
// After all 4 chunks the CRC must match and talon_apply_round() must
// return true (commit_slot stub accepts unconditionally in test build).
// ---------------------------------------------------------------------------
void test_talon_chunk_reassembly(void) {
    // Build synthetic 40-byte delta split into 4 × 10-byte chunks
    const uint32_t NCHUNKS   = 4u;
    const size_t   CHUNK_PL  = 10u;
    uint8_t delta[NCHUNKS * CHUNK_PL];
    for (size_t i = 0u; i < sizeof(delta); i++) {
        delta[i] = (uint8_t)(i + 1u);   // 1..40
    }

    uint32_t crc = test_crc32(delta, sizeof(delta));
    const uint16_t ROUND = 0x0042u;

    uint8_t pkt[1u + TALON_CHUNK_HEADER_BYTES + CHUNK_PL];

    for (uint32_t ci = 0u; ci < NCHUNKS; ci++) {
        size_t pkt_len = make_chunk(
            pkt, ROUND, NCHUNKS, ci, crc, 0u,
            delta + ci * CHUNK_PL, CHUNK_PL);
        talon_handle_chunk(pkt, pkt_len);
    }

    // All chunks received, CRC should match
    TEST_ASSERT_TRUE_MESSAGE(talon_round_complete(),
                             "Round should be complete after 4 chunks with valid CRC");

    TEST_ASSERT_EQUAL_UINT16(ROUND, talon_current_round());

    bool applied = talon_apply_round();
    TEST_ASSERT_TRUE_MESSAGE(applied,
                             "talon_apply_round() should return true for valid round");

    // After apply, round state resets
    TEST_ASSERT_FALSE_MESSAGE(talon_round_complete(),
                              "Round state should reset after successful apply");
}

// ---------------------------------------------------------------------------
// test_talon_bad_crc_rejected
//
// Same 4-chunk round but with one payload byte flipped — CRC will mismatch.
// talon_round_complete() must return false and talon_apply_round() must
// return false.
// ---------------------------------------------------------------------------
void test_talon_bad_crc_rejected(void) {
    const uint32_t NCHUNKS  = 4u;
    const size_t   CHUNK_PL = 10u;
    uint8_t delta[NCHUNKS * CHUNK_PL];
    for (size_t i = 0u; i < sizeof(delta); i++) {
        delta[i] = (uint8_t)(i + 5u);   // 5..44
    }

    // Compute CRC over the *correct* delta
    uint32_t crc = test_crc32(delta, sizeof(delta));

    // Corrupt byte 7 of chunk 2 (will be written at offset 2×10 + 7 = 27)
    uint8_t corrupt_delta[NCHUNKS * CHUNK_PL];
    memcpy(corrupt_delta, delta, sizeof(delta));
    corrupt_delta[27u] ^= 0xFFu;   // flip all bits

    const uint16_t ROUND = 0x0099u;
    uint8_t pkt[1u + TALON_CHUNK_HEADER_BYTES + CHUNK_PL];

    for (uint32_t ci = 0u; ci < NCHUNKS; ci++) {
        const uint8_t* src = corrupt_delta + ci * CHUNK_PL;
        size_t pkt_len = make_chunk(pkt, ROUND, NCHUNKS, ci, crc, 0u,
                                    src, CHUNK_PL);
        talon_handle_chunk(pkt, pkt_len);
    }

    // CRC mismatch — round must NOT be complete
    TEST_ASSERT_FALSE_MESSAGE(talon_round_complete(),
                              "Round with corrupted payload should fail CRC check");

    // Apply must also fail
    bool applied = talon_apply_round();
    TEST_ASSERT_FALSE_MESSAGE(applied,
                              "talon_apply_round() must return false on CRC mismatch");
}

// ===========================================================================
// Module 2 tests — Cognitive RF baseline learner
// ===========================================================================

// ---------------------------------------------------------------------------
// test_cogrf_baseline_warmup
//
// Feed 100 samples at -70 dBm on channel 6.
// The EWMA baseline for channel 6 must converge to within 3 dBm of -70.
// ---------------------------------------------------------------------------
void test_cogrf_baseline_warmup(void) {
    // Feed 100 observations at -70 dBm on channel 6
    for (int i = 0; i < 100; i++) {
        cogrf_observe(6u, (int16_t)-70);
    }

    float baselines[COGRF_NUM_CHANNELS];
    cogrf_get_baseline(baselines);

    // baselines[5] = channel 6 (0-indexed)
    float val = baselines[5];

    // After 100 samples with EWMA alpha=0.035:
    // Steady-state convergence: |val - target| < 3 dBm
    TEST_ASSERT_FLOAT_WITHIN(3.0f, -70.0f, val);
}

// ---------------------------------------------------------------------------
// test_cogrf_change_detection
//
// Feed 60 samples at -70 dBm (steady baseline), then 40 samples at -50 dBm
// (sudden +20 dBm step).  The CUSUM detector must trip on channel 4.
// ---------------------------------------------------------------------------
void test_cogrf_change_detection(void) {
    const uint8_t CH = 4u;   // wire channel 4

    // Warm-up phase: feed 60 samples at -70 dBm to establish baseline
    // (> COGRF_MIN_OBS = 12, so the CUSUM becomes active)
    for (int i = 0; i < 60; i++) {
        cogrf_observe(CH, (int16_t)-70);
    }

    // Clear any trip flags that might have fired during warm-up (edge case:
    // the initial EWMA starts at -80 so the first -70 sample is slightly
    // above the initial mean; CUSUM might trip transiently).
    uint8_t dummy_ch;
    float   dummy_score;
    cogrf_anomalous_emitter(&dummy_ch, &dummy_score);   // clears flags

    // Inject step change: 40 samples at -50 dBm (+20 dBm above baseline).
    // Each sample contributes: x - mu - k = -50 - (-70) - 5 = +15 to CUSUM.
    // After ceil(50/15) ≈ 4 samples the accumulator crosses COGRF_CUSUM_THRESHOLD=50.
    for (int i = 0; i < 40; i++) {
        cogrf_observe(CH, (int16_t)-50);
    }

    uint8_t tripped_ch  = 0u;
    float   score       = 0.0f;
    bool    detected    = cogrf_anomalous_emitter(&tripped_ch, &score);

    TEST_ASSERT_TRUE_MESSAGE(detected,
                             "CUSUM should trip on a +20 dBm step after baseline warmup");
    TEST_ASSERT_EQUAL_UINT8_MESSAGE(CH, tripped_ch,
                             "Tripped channel should match the injected channel");
    TEST_ASSERT_TRUE_MESSAGE(score > 0.0f,
                             "Anomaly score should be positive");
}

// ===========================================================================
// Unity main
// ===========================================================================
int main(int, char**) {
    UNITY_BEGIN();

    // Module 1 — TALON federated receiver
    RUN_TEST(test_talon_chunk_reassembly);
    RUN_TEST(test_talon_bad_crc_rejected);

    // Module 2 — Cognitive RF baseline learner
    RUN_TEST(test_cogrf_baseline_warmup);
    RUN_TEST(test_cogrf_change_detection);

    return UNITY_END();
}
