// ============================================================================
// Native (host) unit tests — spoof_gate_client + pec_feedback
//
// Run with: pio test -e native -f test_spoof_pec
//
// Strategy mirrors test_command_handler: include the .cpp files directly so
// the UNIT_TEST / non-ARDUINO stubs take effect and no Arduino linker stubs
// are required.
//
// Tests cover:
//   - Spoof gate: good packet → correct lookup
//   - Spoof gate: undersized packet ignored
//   - Spoof gate: cache expiry (advance mock clock past TTL)
//   - Spoof gate: should_promote threshold logic
//   - Spoof gate: LRU overflow eviction (fill 9 tracks, assert oldest gone)
//   - PEC feedback: good packet → get_latest returns correct probs
//   - PEC feedback: undersized packet ignored
//   - PEC feedback: max_age_ms parameter rejects stale entry
//   - PEC feedback: num_classes=0 and num_classes>16 rejected
//   - PEC feedback: LRU overflow eviction (fill 5 tracks, oldest gone)
//   - PEC feedback: cache expiry (advance mock clock past TTL)
// ============================================================================

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

// ---------------------------------------------------------------------------
// Shared simulated clock
// Both .cpp files are included directly into this single translation unit.
// They each do `extern uint32_t millis()` under UNIT_TEST, so we define
// millis() exactly once here and both modules share it.
// ---------------------------------------------------------------------------

static uint32_t s_mock_millis_shared = 0;

// millis() — required by spoof_gate_client.cpp and pec_feedback.cpp
uint32_t millis() { return s_mock_millis_shared; }

// Single clock setter used by test cases
static void set_time(uint32_t t) { s_mock_millis_shared = t; }

// Pull implementation files in directly (UNIT_TEST is set in platformio.ini
// native env build_flags, so the Arduino paths are skipped).
#include "../../src/spoof_gate_client.cpp"
#include "../../src/pec_feedback.cpp"

// ---------------------------------------------------------------------------
// Packet-builder helpers
// ---------------------------------------------------------------------------

// Build a 9-byte MSG_TYPE_SPOOF_SCORE packet.
// Layout: [msg_type=0x11][track_id LE 4 bytes][score_q8][reserved 0x00 0x00 0x00]
// Total: 1 (msg_type) + sizeof(SpoofScoreMsg)(4+1+3=8) = 9 bytes
static void build_spoof_packet(uint8_t* buf9,
                               uint32_t track_id,
                               uint8_t  score_q8) {
    buf9[0] = 0x11; // MSG_TYPE_SPOOF_SCORE
    memcpy(buf9 + 1, &track_id, 4);
    buf9[5] = score_q8;
    buf9[6] = 0x00; // reserved[0]
    buf9[7] = 0x00; // reserved[1]
    buf9[8] = 0x00; // reserved[2]
}

// Build a 22-byte MSG_TYPE_PEC_FEEDBACK packet.
// Layout: [0x12] [num_classes] [class_probs_q8 × 16] [source_track_id LE 4 bytes]
static void build_pec_packet(uint8_t*       buf22,
                             uint32_t       track_id,
                             uint8_t        num_classes,
                             const uint8_t* probs_q8) {
    buf22[0] = 0x12; // MSG_TYPE_PEC_FEEDBACK
    buf22[1] = num_classes;
    for (int i = 0; i < 16; i++) {
        buf22[2 + i] = (i < num_classes) ? probs_q8[i] : 0;
    }
    memcpy(buf22 + 18, &track_id, 4);
}

// ---------------------------------------------------------------------------
// setUp / tearDown
// ---------------------------------------------------------------------------

void setUp() {
    set_time(0);
    spoof_gate_init();
    pec_feedback_init();
}

void tearDown() {}

// ===========================================================================
// SPOOF GATE TESTS
// ===========================================================================

// SG-01: Insert one track, retrieve correct score
static void test_sg_basic_lookup() {
    uint8_t pkt[9];  // 1 msg_type + 8 (SpoofScoreMsg) = 9 bytes total
    build_spoof_packet(pkt, /*track_id=*/0xDEAD0001, /*score_q8=*/178);

    spoof_gate_handle_packet(pkt, 9);

    uint8_t score = spoof_gate_score_for_track(0xDEAD0001);
    TEST_ASSERT_EQUAL_UINT8(178, score);
}

// SG-02: Unknown track returns 0
static void test_sg_unknown_track_returns_zero() {
    TEST_ASSERT_EQUAL_UINT8(0, spoof_gate_score_for_track(0xFFFFFFFF));
}

// SG-03: Undersized packet (8 bytes, need 9) is silently ignored
static void test_sg_short_packet_ignored() {
    uint8_t pkt[9];
    build_spoof_packet(pkt, 0x00000042, 200);
    spoof_gate_handle_packet(pkt, 8); // pass only 8 of the 9 bytes
    TEST_ASSERT_EQUAL_UINT8(0, spoof_gate_score_for_track(0x00000042));
}

// SG-04: Null buffer is silently ignored
static void test_sg_null_buf_ignored() {
    spoof_gate_handle_packet(nullptr, 9);
    // No crash — test passes if we reach here
    TEST_PASS();
}

// SG-05: Entry expires after SPOOF_GATE_ENTRY_TTL_MS (30 000 ms)
static void test_sg_expiry() {
    uint8_t pkt[9];
    build_spoof_packet(pkt, 0xABCD1234, 99);
    set_time(1000);
    spoof_gate_handle_packet(pkt, 9);

    // Just before expiry — should still be there
    set_time(1000 + SPOOF_GATE_ENTRY_TTL_MS - 1);
    TEST_ASSERT_EQUAL_UINT8(99, spoof_gate_score_for_track(0xABCD1234));

    // At or past expiry — should return 0
    set_time(1000 + SPOOF_GATE_ENTRY_TTL_MS);
    TEST_ASSERT_EQUAL_UINT8(0, spoof_gate_score_for_track(0xABCD1234));
}

// SG-06: should_promote threshold
static void test_sg_should_promote() {
    uint8_t pkt[9];
    build_spoof_packet(pkt, 0x00000001, 128);
    spoof_gate_handle_packet(pkt, 9);

    TEST_ASSERT_TRUE(spoof_gate_should_promote(0x00000001, 128));  // at threshold
    TEST_ASSERT_TRUE(spoof_gate_should_promote(0x00000001, 100));  // above threshold
    TEST_ASSERT_FALSE(spoof_gate_should_promote(0x00000001, 129)); // just above score
    TEST_ASSERT_FALSE(spoof_gate_should_promote(0x00000001, 255)); // max threshold
}

// SG-07: Update overwrites existing entry score
static void test_sg_update_score() {
    uint8_t pkt[9];
    build_spoof_packet(pkt, 0x99990001, 50);
    spoof_gate_handle_packet(pkt, 9);
    TEST_ASSERT_EQUAL_UINT8(50, spoof_gate_score_for_track(0x99990001));

    build_spoof_packet(pkt, 0x99990001, 200);
    spoof_gate_handle_packet(pkt, 9);
    TEST_ASSERT_EQUAL_UINT8(200, spoof_gate_score_for_track(0x99990001));
}

// SG-08: LRU overflow — fill 8 slots then insert a 9th; the oldest (lowest
// ts_ms) entry should be evicted.  We insert at different timestamps so
// the eviction target is deterministic.
static void test_sg_lru_overflow_evicts_oldest() {
    uint8_t pkt[9];
    // Insert 8 tracks at t=100..800 (step 100 ms)
    for (uint8_t i = 0; i < SPOOF_GATE_CACHE_SIZE; i++) {
        set_time((i + 1) * 100);
        build_spoof_packet(pkt, /*track_id=*/(uint32_t)(0xBB000000 + i), /*score=*/(uint8_t)(10 + i));
        spoof_gate_handle_packet(pkt, 9);
    }

    // Verify all 8 are present (score for track i = 10+i, all > 0)
    for (uint8_t i = 0; i < SPOOF_GATE_CACHE_SIZE; i++) {
        TEST_ASSERT_NOT_EQUAL(0, spoof_gate_score_for_track(0xBB000000 + i));
    }

    // Insert 9th at t=900
    set_time(900);
    build_spoof_packet(pkt, 0xBB000099, 77);
    spoof_gate_handle_packet(pkt, 9);

    // 9th must be present
    TEST_ASSERT_EQUAL_UINT8(77, spoof_gate_score_for_track(0xBB000099));

    // The oldest (track 0xBB000000, inserted at t=100) must be gone
    TEST_ASSERT_EQUAL_UINT8(0, spoof_gate_score_for_track(0xBB000000));
}

// ===========================================================================
// PEC FEEDBACK TESTS
// ===========================================================================

// PEC-01: Insert one track, retrieve correct probability vector
static void test_pec_basic_lookup() {
    const uint8_t q8[16] = { 255, 127, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
    uint8_t pkt[22];
    build_pec_packet(pkt, /*track_id=*/0xCAFE0001, /*num_classes=*/3, q8);

    pec_feedback_handle_packet(pkt, 22);

    float probs[MODEL_MAX_OUTPUT_CLASSES];
    uint8_t n = 0;
    bool ok = pec_feedback_get_latest(0xCAFE0001, probs, &n, 60000);

    TEST_ASSERT_TRUE(ok);
    TEST_ASSERT_EQUAL_UINT8(3, n);

    // q8=255 → 1.0, q8=127 → ~0.498, q8=63 → ~0.247
    TEST_ASSERT_FLOAT_WITHIN(0.005f, 1.0f,  probs[0]);
    TEST_ASSERT_FLOAT_WITHIN(0.005f, 0.498f, probs[1]);
    TEST_ASSERT_FLOAT_WITHIN(0.005f, 0.247f, probs[2]);
}

// PEC-02: Unknown track returns false
static void test_pec_unknown_track_returns_false() {
    float probs[MODEL_MAX_OUTPUT_CLASSES] = {};
    uint8_t n = 0;
    TEST_ASSERT_FALSE(pec_feedback_get_latest(0xDEADBEEF, probs, &n, 60000));
}

// PEC-03: Undersized packet silently ignored
static void test_pec_short_packet_ignored() {
    const uint8_t q8[16] = { 200 };
    uint8_t pkt[22];
    build_pec_packet(pkt, 0x11220001, 1, q8);
    // Pass only 10 bytes (need 22)
    pec_feedback_handle_packet(pkt, 10);

    float probs[MODEL_MAX_OUTPUT_CLASSES] = {};
    uint8_t n = 0;
    TEST_ASSERT_FALSE(pec_feedback_get_latest(0x11220001, probs, &n, 60000));
}

// PEC-04: num_classes=0 rejected
static void test_pec_zero_classes_rejected() {
    const uint8_t q8[16] = {};
    uint8_t pkt[22];
    build_pec_packet(pkt, 0x55550001, 0, q8);
    pec_feedback_handle_packet(pkt, 22);

    float probs[MODEL_MAX_OUTPUT_CLASSES] = {};
    uint8_t n = 0;
    TEST_ASSERT_FALSE(pec_feedback_get_latest(0x55550001, probs, &n, 60000));
}

// PEC-05: num_classes > MODEL_MAX_OUTPUT_CLASSES rejected
static void test_pec_too_many_classes_rejected() {
    const uint8_t q8[16] = { 128 };
    uint8_t pkt[22];
    // Manually set num_classes=17 > 16
    pkt[0] = 0x12;
    pkt[1] = 17; // invalid
    memset(pkt + 2, 128, 16);
    uint32_t tid = 0x44440001;
    memcpy(pkt + 18, &tid, 4);

    pec_feedback_handle_packet(pkt, 22);

    float probs[MODEL_MAX_OUTPUT_CLASSES] = {};
    uint8_t n = 0;
    TEST_ASSERT_FALSE(pec_feedback_get_latest(0x44440001, probs, &n, 60000));
}

// PEC-06: max_age_ms parameter rejects entries older than caller's age limit
static void test_pec_max_age_rejects_stale() {
    const uint8_t q8[16] = { 200, 55 };
    uint8_t pkt[22];
    build_pec_packet(pkt, 0x22220001, 2, q8);

    set_time(5000);
    pec_feedback_handle_packet(pkt, 22);

    // 2 seconds later — within TTL but caller asks for ≤1s freshness
    set_time(7000);
    float probs[MODEL_MAX_OUTPUT_CLASSES] = {};
    uint8_t n = 0;

    // max_age_ms=1000 → entry is 2000ms old → rejected
    TEST_ASSERT_FALSE(pec_feedback_get_latest(0x22220001, probs, &n, 1000));

    // max_age_ms=3000 → entry is 2000ms old → accepted
    TEST_ASSERT_TRUE(pec_feedback_get_latest(0x22220001, probs, &n, 3000));
}

// PEC-07: Entry expires after PEC_FEEDBACK_ENTRY_TTL_MS
static void test_pec_expiry() {
    const uint8_t q8[16] = { 100 };
    uint8_t pkt[22];
    build_pec_packet(pkt, 0x33330001, 1, q8);

    set_time(2000);
    pec_feedback_handle_packet(pkt, 22);

    // Just before TTL expiry
    set_time(2000 + PEC_FEEDBACK_ENTRY_TTL_MS - 1);
    float probs[MODEL_MAX_OUTPUT_CLASSES] = {};
    uint8_t n = 0;
    TEST_ASSERT_TRUE(pec_feedback_get_latest(0x33330001, probs, &n, PEC_FEEDBACK_ENTRY_TTL_MS));

    // At expiry boundary
    set_time(2000 + PEC_FEEDBACK_ENTRY_TTL_MS);
    TEST_ASSERT_FALSE(pec_feedback_get_latest(0x33330001, probs, &n, PEC_FEEDBACK_ENTRY_TTL_MS));
}

// PEC-08: LRU overflow — fill 4 slots then insert a 5th; oldest evicted
static void test_pec_lru_overflow_evicts_oldest() {
    const uint8_t q8[16] = { 111 };
    uint8_t pkt[22];

    for (uint8_t i = 0; i < PEC_FEEDBACK_CACHE_SIZE; i++) {
        set_time((i + 1) * 200);
        build_pec_packet(pkt, (uint32_t)(0xCC000000 + i), 1, q8);
        pec_feedback_handle_packet(pkt, 22);
    }

    // All 4 present
    for (uint8_t i = 0; i < PEC_FEEDBACK_CACHE_SIZE; i++) {
        float probs[MODEL_MAX_OUTPUT_CLASSES] = {};
        uint8_t n = 0;
        TEST_ASSERT_TRUE(pec_feedback_get_latest(0xCC000000 + i, probs, &n, 60000));
    }

    // Insert 5th at t=1000
    set_time(1000);
    build_pec_packet(pkt, 0xCC000099, 1, q8);
    pec_feedback_handle_packet(pkt, 22);

    float probs[MODEL_MAX_OUTPUT_CLASSES] = {};
    uint8_t n = 0;

    // 5th must be present
    TEST_ASSERT_TRUE(pec_feedback_get_latest(0xCC000099, probs, &n, 60000));

    // Oldest (track 0xCC000000, inserted at t=200) must be evicted
    TEST_ASSERT_FALSE(pec_feedback_get_latest(0xCC000000, probs, &n, 60000));
}

// PEC-09: Null output pointers handled gracefully
static void test_pec_null_out_pointers() {
    TEST_ASSERT_FALSE(pec_feedback_get_latest(0x00000001, nullptr, nullptr, 60000));
}

// ===========================================================================
// Main
// ===========================================================================

int main() {
    UNITY_BEGIN();

    // Spoof gate
    RUN_TEST(test_sg_basic_lookup);
    RUN_TEST(test_sg_unknown_track_returns_zero);
    RUN_TEST(test_sg_short_packet_ignored);
    RUN_TEST(test_sg_null_buf_ignored);
    RUN_TEST(test_sg_expiry);
    RUN_TEST(test_sg_should_promote);
    RUN_TEST(test_sg_update_score);
    RUN_TEST(test_sg_lru_overflow_evicts_oldest);

    // PEC feedback
    RUN_TEST(test_pec_basic_lookup);
    RUN_TEST(test_pec_unknown_track_returns_false);
    RUN_TEST(test_pec_short_packet_ignored);
    RUN_TEST(test_pec_zero_classes_rejected);
    RUN_TEST(test_pec_too_many_classes_rejected);
    RUN_TEST(test_pec_max_age_rejects_stale);
    RUN_TEST(test_pec_expiry);
    RUN_TEST(test_pec_lru_overflow_evicts_oldest);
    RUN_TEST(test_pec_null_out_pointers);

    return UNITY_END();
}
