// ============================================================================
// test/test_model_manager/test_model_manager.cpp
//
// Unity tests for src/model_manager.{h,cpp} -- multi-model TFLite-Micro
// paging manager. Previously zero test coverage (flagged 2026-07-25 as
// "572 lines, TFLite-Micro coupled, needs a dedicated future session, not a
// quick pass").
//
// SCOPE — read this before trusting a green run as "model_manager is
// covered":
//
//   1. CRC-32/BZIP2 (model_manager_crc32_bzip2, hoisted to unconditional
//      file scope in model_manager.cpp so it's testable without a live
//      TFLite build) -- REAL coverage, verified against the official
//      CRC-32/BZIP2 catalog check value for "123456789" (0xFC891918), not
//      just "doesn't crash". This is the integrity check commit_slot() uses
//      to validate a staged OTA model before activating it -- genuinely
//      safety-relevant.
//
//   2. Everything else tested here exercises the PDL_DISABLE_TFLITE stub
//      branch (all slot operations no-op; see the #ifdef in
//      model_manager.cpp) -- this verifies the stub's safe-no-op CONTRACT
//      (never crashes, always reports "nothing available/loaded/possible"
//      rather than garbage), which matters because that's what a firmware
//      build with TFLite-Micro disabled actually ships with. It does NOT
//      exercise the real (non-stub) branch's registration bookkeeping,
//      arena sizing, eviction policy (find_eviction_candidate), or paging
//      logic against actual TFLite interpreters -- that logic is coupled to
//      tflite::MicroInterpreter/tflite::Model, which isn't natively
//      host-buildable in this toolchain (CMSIS-NN kernels are ESP32-target-
//      only, per the ARM lib output in a real `pio run` build). Testing
//      that path for real needs either an x86-buildable TFLite-Micro build
//      or decoupling the eviction/registration bookkeeping from the
//      TFLite-typed ModelSlot fields -- a real refactor, not a quick pass,
//      exactly as flagged. Do not read this file's green run as closing
//      that gap; it narrows it (real CRC coverage + a verified stub
//      contract) without claiming the coupled logic is now safe.
//
// Run with:
//   pio test -e native -f test_model_manager
// ============================================================================

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

#define PDL_DISABLE_TFLITE
#include "../../src/model_manager.cpp"

// ---------------------------------------------------------------------------
// Unity lifecycle
// ---------------------------------------------------------------------------
void setUp(void) {}
void tearDown(void) {}

// ============================================================================
// CRC-32/BZIP2 — real, known-answer coverage
// ============================================================================

// Official check value for the CRC-32/BZIP2 catalog entry (poly 0x04C11DB7,
// init 0xFFFFFFFF, refin=false, refout=false, xorout 0xFFFFFFFF) computed
// over the standard ASCII test string "123456789" is 0xFC891918. This is
// the exact algorithm model_manager_crc32_bzip2() implements (see its
// header comment) -- matching this catalog value means the bit-level
// implementation (poly, init, no reflection, xorout) is correct, not just
// "produces a number".
void test_crc32_bzip2_matches_catalog_check_value(void) {
    const uint8_t data[] = "123456789";
    uint32_t crc = model_manager_crc32_bzip2(data, 9);
    TEST_ASSERT_EQUAL_HEX32(0xFC891918u, crc);
}

void test_crc32_bzip2_empty_input(void) {
    // CRC-32/BZIP2 of zero-length input is xorout applied to init:
    // 0xFFFFFFFF ^ 0xFFFFFFFF = 0.
    uint32_t crc = model_manager_crc32_bzip2(nullptr, 0);
    TEST_ASSERT_EQUAL_HEX32(0x00000000u, crc);
}

void test_crc32_bzip2_deterministic(void) {
    const uint8_t data[] = { 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03 };
    uint32_t a = model_manager_crc32_bzip2(data, sizeof(data));
    uint32_t b = model_manager_crc32_bzip2(data, sizeof(data));
    TEST_ASSERT_EQUAL_HEX32(a, b);
}

void test_crc32_bzip2_single_bit_change_flips_result(void) {
    // Integrity checks are worthless if a corrupted byte doesn't change the
    // CRC -- assert a single flipped bit produces a different value.
    uint8_t data[] = { 0x01, 0x02, 0x03, 0x04 };
    uint32_t before = model_manager_crc32_bzip2(data, sizeof(data));
    data[2] ^= 0x01;
    uint32_t after = model_manager_crc32_bzip2(data, sizeof(data));
    TEST_ASSERT_NOT_EQUAL(before, after);
}

// ============================================================================
// PDL_DISABLE_TFLITE stub contract — safe no-ops, never garbage
// ============================================================================

void test_stub_init_returns_zero(void) {
    TEST_ASSERT_EQUAL_INT(0, model_manager_init());
}

void test_stub_register_always_fails(void) {
    // Stub mode has no registry at all -- every registration attempt must
    // report failure (-1), never a fabricated slot id.
    int slot = model_manager_register("drone_clf", "/models/drone.tflite",
                                       256, 4, MODEL_PRIORITY_TRIAGE);
    TEST_ASSERT_EQUAL_INT(-1, slot);
}

void test_stub_register_multiple_all_fail(void) {
    for (int i = 0; i < 5; i++) {
        int slot = model_manager_register("m", "/m.tflite", 64, 2, MODEL_PRIORITY_OPTIONAL);
        TEST_ASSERT_EQUAL_INT(-1, slot);
    }
}

void test_stub_infer_returns_invalid(void) {
    float input[4] = { 0.1f, 0.2f, 0.3f, 0.4f };
    ModelInferenceResult r = model_manager_infer(0, input, 4);
    TEST_ASSERT_FALSE(r.valid);
}

void test_stub_infer_invalid_slot_id_returns_invalid(void) {
    float input[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
    ModelInferenceResult r = model_manager_infer(255, input, 4);
    TEST_ASSERT_FALSE(r.valid);
}

void test_stub_infer_all_resident_returns_zero(void) {
    float input[4] = { 0.0f, 0.0f, 0.0f, 0.0f };
    ModelInferenceResult results[MODEL_MAX_SLOTS];
    int count = model_manager_infer_all_resident(input, 4, results, MODEL_MAX_SLOTS);
    TEST_ASSERT_EQUAL_INT(0, count);
}

void test_stub_page_in_always_false(void) {
    TEST_ASSERT_FALSE(model_manager_page_in(0));
}

void test_stub_page_out_does_not_crash(void) {
    model_manager_page_out(0);
    TEST_ASSERT_TRUE(true); // reaching here means it didn't crash
}

void test_stub_is_loaded_always_false(void) {
    TEST_ASSERT_FALSE(model_manager_is_loaded(0));
    TEST_ASSERT_FALSE(model_manager_is_loaded(MODEL_MAX_SLOTS - 1));
}

void test_stub_get_stats_returns_false_and_zeroes_output(void) {
    ModelStats stats;
    // Poison the struct first so a genuine memset is distinguishable from
    // stack garbage that happens to look zeroed.
    memset(&stats, 0xAA, sizeof(stats));
    bool ok = model_manager_get_stats(0, stats);
    TEST_ASSERT_FALSE(ok);
    TEST_ASSERT_EQUAL_UINT8(0, stats.slot_id);
    TEST_ASSERT_EQUAL_UINT8(0, stats.priority);
    TEST_ASSERT_FALSE(stats.loaded);
    TEST_ASSERT_EQUAL_UINT32(0, stats.inference_count);
    TEST_ASSERT_EQUAL_UINT32(0, stats.avg_latency_us);
    TEST_ASSERT_EQUAL_UINT32(0, stats.model_size_bytes);
    TEST_ASSERT_EQUAL_UINT32(0, stats.arena_used_bytes);
}

void test_stub_psram_used_always_zero(void) {
    TEST_ASSERT_EQUAL_UINT32(0, model_manager_psram_used());
}

void test_stub_count_always_zero(void) {
    TEST_ASSERT_EQUAL_UINT8(0, model_manager_count());
}

void test_stub_commit_slot_always_false(void) {
    TEST_ASSERT_FALSE(model_manager_commit_slot(0, 0x12345678u));
}

void test_stub_commit_slot_zero_crc_still_false_in_stub_mode(void) {
    // On the real (non-stub) path, expected_crc32==0 means "skip the CRC
    // check" -- but the stub never has a loaded slot to commit regardless,
    // so this must still be false, not accidentally true from a bypassed
    // check.
    TEST_ASSERT_FALSE(model_manager_commit_slot(0, 0));
}

void test_stub_deinit_does_not_crash(void) {
    model_manager_deinit();
    TEST_ASSERT_TRUE(true);
}

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

    RUN_TEST(test_crc32_bzip2_matches_catalog_check_value);
    RUN_TEST(test_crc32_bzip2_empty_input);
    RUN_TEST(test_crc32_bzip2_deterministic);
    RUN_TEST(test_crc32_bzip2_single_bit_change_flips_result);

    RUN_TEST(test_stub_init_returns_zero);
    RUN_TEST(test_stub_register_always_fails);
    RUN_TEST(test_stub_register_multiple_all_fail);
    RUN_TEST(test_stub_infer_returns_invalid);
    RUN_TEST(test_stub_infer_invalid_slot_id_returns_invalid);
    RUN_TEST(test_stub_infer_all_resident_returns_zero);
    RUN_TEST(test_stub_page_in_always_false);
    RUN_TEST(test_stub_page_out_does_not_crash);
    RUN_TEST(test_stub_is_loaded_always_false);
    RUN_TEST(test_stub_get_stats_returns_false_and_zeroes_output);
    RUN_TEST(test_stub_psram_used_always_zero);
    RUN_TEST(test_stub_count_always_zero);
    RUN_TEST(test_stub_commit_slot_always_false);
    RUN_TEST(test_stub_commit_slot_zero_crc_still_false_in_stub_mode);
    RUN_TEST(test_stub_deinit_does_not_crash);

    return UNITY_END();
}
