// ============================================================================
// test/test_blue_force/test_blue_force.cpp
//
// Unity tests for src/blue_force.{h,cpp} — the friendly-RID whitelist.
// A false negative here (a hostile emitter incorrectly reading as
// friendly, or a real friendly incorrectly reading as unmatched) is
// operationally significant in both directions: missing a real threat, or
// spamming an alert for a known-friendly asset. Previously zero test
// coverage, and previously unconditionally `#include <Arduino.h>`'d so it
// could not even be compiled natively -- fixed with the same #ifdef
// ARDUINO stub pattern already used by network_id.cpp / threat_
// correlator.cpp.
//
// Writing these tests surfaced a real latent inconsistency, fixed in
// blue_force.cpp: blue_force_add() computed the whitelist hash over the
// RAW input serial, but stored a truncated (<=20 char) copy -- for any
// out-of-spec input longer than the documented ANSI/CTA-2063-A 20-char
// format, the stored string and its hash would desync (blue_force_check_
// serial's truncated strncmp vs. blue_force_check_hash's full-string hash
// would disagree on whether two long serials sharing a 20-char prefix are
// "the same" whitelist entry). Fixed by hashing the stored (already-
// truncated) copy. See test_add_truncates_and_keeps_hash_consistent below
// for the regression test.
//
// Run with:
//   pio test -e native -f test_blue_force
// ============================================================================

#include <unity.h>
#include <cstring>
#include "../../src/blue_force.cpp"

void setUp(void) {
    blue_force_init();
}
void tearDown(void) {}

// ---------------------------------------------------------------------------
// add / count / duplicate handling
// ---------------------------------------------------------------------------
void test_add_increments_count(void) {
    TEST_ASSERT_EQUAL_UINT8(0, blue_force_count());
    TEST_ASSERT_TRUE(blue_force_add("1AVJTFB123AB4567890"));
    TEST_ASSERT_EQUAL_UINT8(1, blue_force_count());
}

void test_add_rejects_null_and_empty(void) {
    TEST_ASSERT_FALSE(blue_force_add(nullptr));
    TEST_ASSERT_FALSE(blue_force_add(""));
    TEST_ASSERT_EQUAL_UINT8(0, blue_force_count());
}

void test_add_duplicate_serial_does_not_double_count(void) {
    const char* serial = "1AVJTFB123AB4567890";
    TEST_ASSERT_TRUE(blue_force_add(serial));
    TEST_ASSERT_TRUE(blue_force_add(serial));   // re-add: returns true, no-op
    TEST_ASSERT_EQUAL_UINT8(1, blue_force_count());
}

void test_add_fails_when_whitelist_full(void) {
    char serial[BLUE_FORCE_SERIAL_LEN + 1];
    for (int i = 0; i < BLUE_FORCE_MAX_ENTRIES; i++) {
        snprintf(serial, sizeof(serial), "SERIAL%013d", i);
        TEST_ASSERT_TRUE_MESSAGE(blue_force_add(serial), "expected room in whitelist");
    }
    TEST_ASSERT_EQUAL_UINT8(BLUE_FORCE_MAX_ENTRIES, blue_force_count());

    // One more, distinct serial: whitelist is full, must fail cleanly.
    TEST_ASSERT_FALSE(blue_force_add("OVERFLOW0000000000A"));
    TEST_ASSERT_EQUAL_UINT8(BLUE_FORCE_MAX_ENTRIES, blue_force_count());
}

// ---------------------------------------------------------------------------
// remove / clear
// ---------------------------------------------------------------------------
void test_remove_existing_serial(void) {
    const char* serial = "1AVJTFB123AB4567890";
    blue_force_add(serial);
    TEST_ASSERT_TRUE(blue_force_remove(serial));
    TEST_ASSERT_EQUAL_UINT8(0, blue_force_count());
    TEST_ASSERT_FALSE(blue_force_check_serial(serial));
}

void test_remove_nonexistent_serial_returns_false(void) {
    TEST_ASSERT_FALSE(blue_force_remove("NOTPRESENT000000000"));
}

void test_remove_frees_a_slot_for_reuse(void) {
    char serial[BLUE_FORCE_SERIAL_LEN + 1];
    for (int i = 0; i < BLUE_FORCE_MAX_ENTRIES; i++) {
        snprintf(serial, sizeof(serial), "SERIAL%013d", i);
        blue_force_add(serial);
    }
    TEST_ASSERT_TRUE(blue_force_remove("SERIAL0000000000000"));
    TEST_ASSERT_EQUAL_UINT8(BLUE_FORCE_MAX_ENTRIES - 1, blue_force_count());
    TEST_ASSERT_TRUE_MESSAGE(blue_force_add("NEWENTRY00000000000"),
        "a freed slot must accept a new entry even when previously full");
}

void test_clear_empties_whitelist(void) {
    blue_force_add("1AVJTFB123AB4567890");
    blue_force_add("2AVJTFB123AB4567891");
    TEST_ASSERT_EQUAL_UINT8(2, blue_force_count());
    blue_force_clear();
    TEST_ASSERT_EQUAL_UINT8(0, blue_force_count());
    TEST_ASSERT_FALSE(blue_force_check_serial("1AVJTFB123AB4567890"));
}

// ---------------------------------------------------------------------------
// check_serial
// ---------------------------------------------------------------------------
void test_check_serial_matches_present_entry(void) {
    const char* serial = "1AVJTFB123AB4567890";
    blue_force_add(serial);
    TEST_ASSERT_TRUE(blue_force_check_serial(serial));
}

void test_check_serial_rejects_absent_entry(void) {
    blue_force_add("1AVJTFB123AB4567890");
    TEST_ASSERT_FALSE(blue_force_check_serial("2AVJTFB123AB4567891"));
}

void test_check_serial_is_case_sensitive(void) {
    blue_force_add("1AVJTFB123AB4567890");
    TEST_ASSERT_FALSE_MESSAGE(blue_force_check_serial("1avjtfb123ab4567890"),
        "serial matching must be exact/case-sensitive, not fuzzy");
}

void test_check_serial_rejects_shared_prefix_different_suffix(void) {
    // Guards against a truncation-related false positive: two serials
    // sharing the same first N characters but differing later must NOT
    // be considered a match.
    blue_force_add("1AVJTFB123AB4567890");
    TEST_ASSERT_FALSE(blue_force_check_serial("1AVJTFB123AB4567891"));
}

// ---------------------------------------------------------------------------
// check_hash / compute_hash
// ---------------------------------------------------------------------------
void test_check_hash_matches_whitelisted_serial(void) {
    const char* serial = "1AVJTFB123AB4567890";
    blue_force_add(serial);

    uint8_t hash[8];
    blue_force_compute_hash(serial, hash);
    TEST_ASSERT_TRUE(blue_force_check_hash(hash));
}

void test_check_hash_rejects_unrelated_hash(void) {
    blue_force_add("1AVJTFB123AB4567890");

    uint8_t hash[8];
    blue_force_compute_hash("2AVJTFB123AB4567891", hash);
    TEST_ASSERT_FALSE(blue_force_check_hash(hash));
}

void test_compute_hash_is_deterministic(void) {
    uint8_t h1[8], h2[8];
    blue_force_compute_hash("1AVJTFB123AB4567890", h1);
    blue_force_compute_hash("1AVJTFB123AB4567890", h2);
    TEST_ASSERT_EQUAL_MEMORY(h1, h2, 8);
}

void test_compute_hash_differs_for_different_serials(void) {
    uint8_t h1[8], h2[8];
    blue_force_compute_hash("1AVJTFB123AB4567890", h1);
    blue_force_compute_hash("2AVJTFB123AB4567891", h2);
    TEST_ASSERT_TRUE(memcmp(h1, h2, 8) != 0);
}

// Regression test for the fix described in the file header: an out-of-spec
// (>20 char) input's stored serial and its whitelist hash must stay
// consistent with each other, even though the real-world format is always
// exactly 20 chars.
void test_add_truncates_and_keeps_hash_consistent(void) {
    // 25 chars: exceeds BLUE_FORCE_SERIAL_LEN (20) -- out of spec, but the
    // module must not desync its two lookup paths for it regardless.
    const char* long_serial = "1AVJTFB123AB4567890EXTRA";
    TEST_ASSERT_TRUE(blue_force_add(long_serial));

    // The stored/matchable form is the truncated 20-char prefix.
    char truncated[BLUE_FORCE_SERIAL_LEN + 1];
    strncpy(truncated, long_serial, BLUE_FORCE_SERIAL_LEN);
    truncated[BLUE_FORCE_SERIAL_LEN] = '\0';
    TEST_ASSERT_TRUE_MESSAGE(blue_force_check_serial(truncated),
        "the truncated stored serial must be matchable via check_serial");

    // The hash for a query computed over the SAME truncated string must
    // match what got stored -- this is the fixed behavior (hash the
    // stored/truncated copy, not the untruncated raw input).
    uint8_t query_hash[8];
    blue_force_compute_hash(truncated, query_hash);
    TEST_ASSERT_TRUE_MESSAGE(blue_force_check_hash(query_hash),
        "hash computed over the truncated serial must match the stored entry's hash");
}

int main(int, char**) {
    UNITY_BEGIN();

    RUN_TEST(test_add_increments_count);
    RUN_TEST(test_add_rejects_null_and_empty);
    RUN_TEST(test_add_duplicate_serial_does_not_double_count);
    RUN_TEST(test_add_fails_when_whitelist_full);

    RUN_TEST(test_remove_existing_serial);
    RUN_TEST(test_remove_nonexistent_serial_returns_false);
    RUN_TEST(test_remove_frees_a_slot_for_reuse);
    RUN_TEST(test_clear_empties_whitelist);

    RUN_TEST(test_check_serial_matches_present_entry);
    RUN_TEST(test_check_serial_rejects_absent_entry);
    RUN_TEST(test_check_serial_is_case_sensitive);
    RUN_TEST(test_check_serial_rejects_shared_prefix_different_suffix);

    RUN_TEST(test_check_hash_matches_whitelisted_serial);
    RUN_TEST(test_check_hash_rejects_unrelated_hash);
    RUN_TEST(test_compute_hash_is_deterministic);
    RUN_TEST(test_compute_hash_differs_for_different_serials);
    RUN_TEST(test_add_truncates_and_keeps_hash_consistent);

    return UNITY_END();
}
