// ============================================================================
// test/test_batch_c/test_batch_c.cpp
//
// Unity tests for the Batch-C ports:
//   src/fhss_hop.{h,cpp}        — FHSS 3-gram hop predictor
//   src/threat_vqc.{h,cpp}      — classically-simulated 5-qubit VQC scorer
//   src/attack_graph_local.{h,cpp} — local self-diagnosis attack graph
//
// Run with:
//   pio test -e native -f test_batch_c
//
// IMPORTANT — Unity float comparisons:
//   Unity's TEST_ASSERT_GREATER_THAN / TEST_ASSERT_LESS_THAN macros coerce to
//   int.  All float threshold comparisons use TEST_ASSERT_TRUE with an explicit
//   boolean expression to avoid silent truncation.
// ============================================================================

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

// Pull in implementations directly (same pattern as test_markov_scorer)
#include "../../src/fhss_hop.cpp"
#include "../../src/threat_vqc.cpp"
#include "../../src/attack_graph_local.cpp"

// ---------------------------------------------------------------------------
// Unity lifecycle
// ---------------------------------------------------------------------------
void setUp(void) {
    fhss_init();
    threat_vqc_init();
    attackgraph_init();
}

void tearDown(void) {}

// ============================================================================
// Module 1 — FHSS 3-gram Markov predictor
// ============================================================================

// Deterministic hop sequence: cycles through 4 distinct frequencies that map
// to 4 distinct buckets.  After warmup the 3-gram [b0,b1,b2]→b3 should
// strongly predict the next hop.
//
// Chosen frequencies (all in 2.4 GHz band → linear bucket):
//   2402 → bucket 0
//   2412 → bucket 2
//   2422 → bucket 4
//   2432 → bucket 6
//   then repeats: 2402 → 2412 → 2422 → 2432 → ...
//
// After feeding 20 observations the 3-gram [2,4,6]→0 should dominate.
// fhss_predict_next should return bucket 0 → freq 2402 (or bucket-centre 2402).
// We only check the predicted frequency matches one of the sequence members.
void test_fhss_3gram_converges(void) {
    static const uint16_t seq[4] = { 2402u, 2412u, 2422u, 2432u };

    // Spacing = 10 MHz → IAT of 5 ms = 5000 µs (within dwell window)
    uint32_t ts_us = 1000000u;
    for (int i = 0; i < 20; ++i) {
        fhss_observe(seq[i % 4], ts_us);
        ts_us += 5000u;  // 5 ms hop interval
    }

    uint16_t pred_freq = 0;
    uint32_t pred_dwell = 0;
    bool got = fhss_predict_next(&pred_freq, &pred_dwell);
    TEST_ASSERT_TRUE_MESSAGE(got, "fhss_predict_next should return true after warmup");

    // The predicted frequency should be one of the four sequence members
    bool in_seq = (pred_freq == 2402u || pred_freq == 2412u ||
                   pred_freq == 2422u || pred_freq == 2432u ||
                   // Allow bucket-centre values (2402+step/2 etc.)
                   pred_freq == 2404u || pred_freq == 2414u ||
                   pred_freq == 2424u || pred_freq == 2434u);
    TEST_ASSERT_TRUE_MESSAGE(in_seq,
        "fhss_predict_next frequency should be one of the observed sequence members");

    // Dwell estimate should be near 5000 µs
    TEST_ASSERT_TRUE_MESSAGE(pred_dwell >= 1000u && pred_dwell <= 20000u,
        "Predicted dwell should be in valid OcuSync range [1ms, 20ms]");
}

// Verify that fhss_predict_next returns false before FHSS_MIN_OBS observations
void test_fhss_no_prediction_before_warmup(void) {
    uint16_t f = 0; uint32_t d = 0;
    // Feed fewer than FHSS_MIN_OBS (6) observations
    for (int i = 0; i < 4; ++i) {
        fhss_observe(static_cast<uint16_t>(2402u + i * 10u),
                     static_cast<uint32_t>(1000000u + i * 5000u));
    }
    bool got = fhss_predict_next(&f, &d);
    TEST_ASSERT_FALSE_MESSAGE(got,
        "fhss_predict_next should return false before warmup (< MIN_OBS)");
}

// fhss_reset must clear state so predict_next returns false afterwards
void test_fhss_reset_clears_state(void) {
    uint32_t ts = 1000000u;
    for (int i = 0; i < 20; ++i) {
        fhss_observe(static_cast<uint16_t>(2402u + (i % 4) * 10u), ts);
        ts += 5000u;
    }
    fhss_reset();
    uint16_t f = 0; uint32_t d = 0;
    bool got = fhss_predict_next(&f, &d);
    TEST_ASSERT_FALSE_MESSAGE(got,
        "fhss_predict_next should return false after fhss_reset()");
}

// ============================================================================
// Module 2 — threat_vqc classically-simulated VQC scorer
// ============================================================================

// Helper: generate a pseudo-random float in [0,1] with a simple LCG
static float lcg_float(uint32_t* seed) {
    *seed = (*seed) * 1664525u + 1013904223u;
    return static_cast<float>((*seed) >> 8) / static_cast<float>(0xFFFFFFu);
}

// For all-zero features, score should be a well-defined float in [0,1]
void test_threat_vqc_score_zero_feats(void) {
    float feats[5] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f};
    float score = threat_vqc_score(feats);
    TEST_ASSERT_TRUE_MESSAGE(score >= 0.0f && score <= 1.0f,
        "VQC score for zero features must be in [0,1]");
}

// For all-one features, score should be a well-defined float in [0,1]
void test_threat_vqc_score_one_feats(void) {
    float feats[5] = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f};
    float score = threat_vqc_score(feats);
    TEST_ASSERT_TRUE_MESSAGE(score >= 0.0f && score <= 1.0f,
        "VQC score for all-one features must be in [0,1]");
}

// Random feature vectors: output must always be in [0,1]
void test_threat_vqc_score_in_range(void) {
    uint32_t seed = 0xDEADBEEFu;
    for (int trial = 0; trial < 50; ++trial) {
        float feats[5];
        for (int i = 0; i < 5; ++i) feats[i] = lcg_float(&seed);
        float score = threat_vqc_score(feats);
        TEST_ASSERT_TRUE_MESSAGE(score >= 0.0f && score <= 1.0f,
            "VQC score must be in [0,1] for random feature vector");
    }
}

// Out-of-range inputs should be clamped, not crash
void test_threat_vqc_clamps_inputs(void) {
    float feats[5] = {-0.5f, 1.5f, 2.0f, -1.0f, 0.5f};
    float score = threat_vqc_score(feats);
    TEST_ASSERT_TRUE_MESSAGE(score >= 0.0f && score <= 1.0f,
        "VQC score must be in [0,1] even for out-of-range inputs");
}

// Determinism: same features produce same score
void test_threat_vqc_deterministic(void) {
    float feats[5] = {0.3f, 0.6f, 0.1f, 0.8f, 0.5f};
    float s1 = threat_vqc_score(feats);
    float s2 = threat_vqc_score(feats);
    TEST_ASSERT_FLOAT_WITHIN(1e-6f, s1, s2);
}

// ============================================================================
// Module 3 — attack_graph_local self-diagnosis
// ============================================================================

// Drive a node above 0.85 via observe() and verify should_failsafe()
void test_attackgraph_failsafe_trigger(void) {
    attackgraph_init();

    // Repeatedly fire EVT_SIG_FAIL at NODE_FIRMWARE.
    // Weight matrix entry: s_event_weight[EVT_SIG_FAIL][NODE_FIRMWARE] = 0.35
    // Each call adds 0.35 * 1.0 = 0.35; propagation adds 0.035 to adjacent nodes.
    // After 3 calls: 3 * 0.35 = 1.05 → clamped to 1.0 > 0.85
    for (int i = 0; i < 3; ++i) {
        attackgraph_observe(NODE_FIRMWARE, EVT_SIG_FAIL, 1.0f);
    }
    float risk = attackgraph_node_risk(NODE_FIRMWARE);
    TEST_ASSERT_TRUE_MESSAGE(risk > AG_FAILSAFE_THRESHOLD,
        "NODE_FIRMWARE risk should exceed 0.85 after repeated SIG_FAIL events");
    TEST_ASSERT_TRUE_MESSAGE(attackgraph_should_failsafe(),
        "attackgraph_should_failsafe() must return true when a node is > 0.85");
}

// Verify AUTH_PUBKEY can be driven past 0.95 (extreme failsafe case)
void test_attackgraph_auth_pubkey_extreme(void) {
    attackgraph_init();
    // EVT_AUTH_FAIL → NODE_AUTH_PUBKEY weight = 0.40
    // 3 events: 3 * 0.40 = 1.20 → clamped 1.0 > 0.95
    for (int i = 0; i < 3; ++i) {
        attackgraph_observe(NODE_AUTH_PUBKEY, EVT_AUTH_FAIL, 1.0f);
    }
    float risk = attackgraph_node_risk(NODE_AUTH_PUBKEY);
    TEST_ASSERT_TRUE_MESSAGE(risk > 0.95f,
        "NODE_AUTH_PUBKEY should exceed 0.95 after repeated EVT_AUTH_FAIL");
}

// Verify that probabilities start at 0 after init
void test_attackgraph_init_zero(void) {
    attackgraph_init();
    for (uint8_t n = 0; n < AG_NODE_COUNT; ++n) {
        TEST_ASSERT_EQUAL_FLOAT(0.0f, attackgraph_node_risk(n));
    }
    TEST_ASSERT_FALSE_MESSAGE(attackgraph_should_failsafe(),
        "should_failsafe() must be false after init");
}

// Decay should reduce non-zero risk over time
void test_attackgraph_decay_reduces_risk(void) {
    attackgraph_init();
    attackgraph_observe(NODE_MODELS, EVT_PEC_DEVIATION, 1.0f);
    float risk_before = attackgraph_node_risk(NODE_MODELS);
    TEST_ASSERT_TRUE_MESSAGE(risk_before > 0.0f, "Risk should be > 0 after observe");

    // Apply 60 seconds of decay (60 000 ms → approximately 1% reduction)
    attackgraph_decay(60000u);
    float risk_after = attackgraph_node_risk(NODE_MODELS);
    TEST_ASSERT_TRUE_MESSAGE(risk_after < risk_before,
        "Risk should decrease after decay");
}

// Invalid node/event IDs are silently ignored (no crash, no state change)
void test_attackgraph_invalid_ids_ignored(void) {
    attackgraph_init();
    attackgraph_observe(0xFF, EVT_SIG_FAIL, 1.0f);            // bad node
    attackgraph_observe(NODE_FIRMWARE, AG_EVENT_COUNT, 1.0f); // bad event
    attackgraph_observe(0xFF, AG_EVENT_COUNT, 1.0f);           // both bad
    TEST_ASSERT_FALSE_MESSAGE(attackgraph_should_failsafe(),
        "Invalid IDs must not change state");
}

// Propagation: a strong event on a parent node elevates adjacent child nodes
void test_attackgraph_propagation(void) {
    attackgraph_init();
    // EVT_OTA_REJECT on NODE_BOOTLOADER: weight 0.30, propagates to FIRMWARE (10%)
    attackgraph_observe(NODE_BOOTLOADER, EVT_OTA_REJECT, 1.0f);
    float bl_risk = attackgraph_node_risk(NODE_BOOTLOADER);
    float fw_risk = attackgraph_node_risk(NODE_FIRMWARE);
    TEST_ASSERT_TRUE_MESSAGE(bl_risk > 0.0f, "BOOTLOADER risk should be > 0");
    TEST_ASSERT_TRUE_MESSAGE(fw_risk > 0.0f,
        "FIRMWARE should have nonzero risk from propagation");
    TEST_ASSERT_TRUE_MESSAGE(bl_risk > fw_risk,
        "Primary node should have higher risk than propagated node");
}

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

    // FHSS tests
    RUN_TEST(test_fhss_3gram_converges);
    RUN_TEST(test_fhss_no_prediction_before_warmup);
    RUN_TEST(test_fhss_reset_clears_state);

    // VQC tests
    RUN_TEST(test_threat_vqc_score_zero_feats);
    RUN_TEST(test_threat_vqc_score_one_feats);
    RUN_TEST(test_threat_vqc_score_in_range);
    RUN_TEST(test_threat_vqc_clamps_inputs);
    RUN_TEST(test_threat_vqc_deterministic);

    // Attack graph tests
    RUN_TEST(test_attackgraph_init_zero);
    RUN_TEST(test_attackgraph_failsafe_trigger);
    RUN_TEST(test_attackgraph_auth_pubkey_extreme);
    RUN_TEST(test_attackgraph_decay_reduces_risk);
    RUN_TEST(test_attackgraph_invalid_ids_ignored);
    RUN_TEST(test_attackgraph_propagation);

    return UNITY_END();
}
