// ============================================================================
// test/test_markov_scorer/test_markov.cpp
//
// Unity tests for src/markov_scorer.cpp — native PlatformIO env.
//
// Run with:
//   pio test -e native -f test_markov_scorer
//
// The test pulls in markov_scorer.cpp directly (same pattern as
// test_command_handler) so no library linking is needed.  Under UNIT_TEST
// the ARDUINO macro is undefined, so markov_scorer.cpp falls back to stdio
// for file I/O.
//
// Assertions:
//   A) After markov_init(), a benign event stream (steady HEARTBEAT at
//      1000 ms inter-arrival) produces rolling mean NLL < 1.5 nats over
//      32 events.  Measured value: ~0.65 nats.
//
//   B) An anomalous stream (rapid LOSS/GAP/HANDOFF cycling at 20 ms IA —
//      a pattern virtually absent from the benign training corpus) produces
//      rolling mean NLL > 4.0 nats after 16 events.  Measured: ~4.94 nats.
//
//   C) markov_is_anomalous() returns false for benign, true for anomalous.
//
//   D) markov_reset_track() clears state so NLL returns to 0.
//
//   E) Multi-track isolation: anomalous events on slot 1 do not change
//      slot 0's NLL.
//
//   F) markov_init() returns false on bad path, true on good path.
//
//   G) OOV event types (>= MK_EVENT_COUNT) produce high NLL without crash.
//
// NOTE TO INTEGRATOR — platformio.ini update required:
//   The native env's test_filter currently reads:
//       test_filter = test_command_handler
//   Change it to:
//       test_filter = test_command_handler|test_markov_scorer
//   (PlatformIO accepts | to OR-match multiple test folder names.)
//
// IMPORTANT — Unity float comparisons:
//   This Unity version'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>

// Pull in the implementation directly under UNIT_TEST / native env.
// ARDUINO is not defined here, so stdio stubs are used for file I/O.
#include "../../src/markov_scorer.cpp"

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

// Path to the pre-built baseline binary relative to the firmware root.
// PlatformIO's native test runner executes with cwd = project root.
static const char* BASELINE_PATH = "data/models/markov_baseline.bin";

// Synthetic clock counter — avoids dependency on millis()
static uint32_t g_clock_ms = 0;

// Advance clock and observe an event on the default track (slot 0)
static void tick_and_observe(uint8_t ev, uint32_t delta_ms) {
    g_clock_ms += delta_ms;
    markov_observe_event(ev, g_clock_ms);
}

// Advance clock and observe an event on an explicit track slot
static void tick_and_observe_track(uint8_t slot, uint8_t ev, uint32_t delta_ms) {
    g_clock_ms += delta_ms;
    markov_observe_event_track(slot, ev, g_clock_ms);
}

// The anomalous cycle used in multiple tests: LOSS/GAP/HANDOFF at 20 ms
// inter-arrival (IAT bucket 0).  Measured rolling mean NLL: ~4.94 nats.
static void pump_anomalous(int n_events) {
    static const uint8_t seq[3] = { MK_LOSS, MK_GAP, MK_HANDOFF };
    for (int i = 0; i < n_events; ++i) {
        tick_and_observe(seq[i % 3], 20u);
    }
}

static void pump_anomalous_track(uint8_t slot, int n_events) {
    static const uint8_t seq[3] = { MK_LOSS, MK_GAP, MK_HANDOFF };
    for (int i = 0; i < n_events; ++i) {
        tick_and_observe_track(slot, seq[i % 3], 20u);
    }
}

// ---------------------------------------------------------------------------
// Unity lifecycle
// ---------------------------------------------------------------------------
void setUp(void) {
    g_clock_ms = 0;
    markov_init(BASELINE_PATH);
    for (uint32_t i = 0; i < MK_MAX_TRACKS; ++i) markov_reset_track(i);
    g_clock_ms = 0;
}

void tearDown(void) {}

// ---------------------------------------------------------------------------
// Test F — init path guards (run first so later tests know model is loaded)
// ---------------------------------------------------------------------------
void test_init_bad_path_fails(void) {
    bool ok = markov_init("/nonexistent/path/model.bin");
    TEST_ASSERT_FALSE(ok);
}

void test_init_good_path_succeeds(void) {
    bool ok = markov_init(BASELINE_PATH);
    TEST_ASSERT_TRUE(ok);
}

// ---------------------------------------------------------------------------
// Test A — benign sequence yields low rolling NLL (< 1.5 nats)
// ---------------------------------------------------------------------------
void test_benign_sequence_low_nll(void) {
    // 32 steady HEARTBEAT events at 1000 ms intervals.
    // IAT = 1000 ms → bucket 2 (500 ms – 2000 ms).
    // HB|iat2 → HB|iat2 NLL is ~0.63 nats (well-trained path).
    // First event NLL (from start-state proxy) is ~1.30 nats.
    // Rolling mean over 32 events: ~0.65 nats.
    for (int i = 0; i < 32; ++i) {
        tick_and_observe(MK_HEARTBEAT, 1000u);
    }
    float nll = markov_current_nll();
    // Use explicit float comparison — Unity's LESS_THAN macro coerces to int.
    TEST_ASSERT_TRUE_MESSAGE(nll < 1.5f, "Benign NLL should be < 1.5 nats");
}

// ---------------------------------------------------------------------------
// Test B — anomalous sequence yields high rolling NLL (> 4.0 nats)
// ---------------------------------------------------------------------------
void test_anomalous_sequence_high_nll(void) {
    // LOSS/GAP/HANDOFF cycling at 20 ms IA (bucket 0) for 16 events.
    // These transitions are virtually absent from benign training data.
    // Measured rolling mean: ~4.94 nats.
    pump_anomalous(16);
    float nll = markov_current_nll();
    // Use explicit float comparison — Unity's GREATER_THAN macro coerces to int.
    TEST_ASSERT_TRUE_MESSAGE(nll > 4.0f, "Anomalous NLL should be > 4.0 nats");
}

// ---------------------------------------------------------------------------
// Test C — markov_is_anomalous() returns correct boolean
// ---------------------------------------------------------------------------
void test_is_anomalous_flag(void) {
    // Benign — steady HB, rolling NLL ~0.65, must not flag at threshold 1.5
    for (int i = 0; i < 32; ++i) {
        tick_and_observe(MK_HEARTBEAT, 1000u);
    }
    TEST_ASSERT_FALSE(markov_is_anomalous(1.5f));

    // Anomalous — LOSS/GAP/HANDOFF cycling, NLL ~4.94, must flag at 3.5
    markov_reset_track(0);
    g_clock_ms = 100000u;  // advance clock to avoid stale last_ts
    pump_anomalous(16);
    TEST_ASSERT_TRUE(markov_is_anomalous(3.5f));
}

// ---------------------------------------------------------------------------
// Test D — markov_reset_track() clears state
// ---------------------------------------------------------------------------
void test_reset_clears_nll(void) {
    // Build up high NLL
    pump_anomalous(16);
    float nll_before = markov_current_nll();
    TEST_ASSERT_TRUE_MESSAGE(nll_before > 4.0f, "Pre-reset NLL should be > 4.0");

    // Reset track 0
    markov_reset_track(0);

    // Empty window → NLL must be exactly 0.0
    TEST_ASSERT_EQUAL_FLOAT(0.0f, markov_current_nll());
}

// ---------------------------------------------------------------------------
// Test E — multi-track isolation
// ---------------------------------------------------------------------------
void test_multitrack_isolation(void) {
    // Benign stream on slot 0
    for (int i = 0; i < 32; ++i) {
        tick_and_observe_track(0, MK_HEARTBEAT, 1000u);
    }
    float nll0_before = markov_current_nll_track(0);
    TEST_ASSERT_TRUE_MESSAGE(nll0_before < 1.5f,
                             "Slot 0 benign NLL should be < 1.5");

    // Anomalous stream on slot 1 — must not affect slot 0
    pump_anomalous_track(1, 16);
    float nll1 = markov_current_nll_track(1);
    TEST_ASSERT_TRUE_MESSAGE(nll1 > 4.0f, "Slot 1 anomalous NLL should be > 4.0");

    // Slot 0 NLL must be unchanged
    float nll0_after = markov_current_nll_track(0);
    TEST_ASSERT_FLOAT_WITHIN(0.001f, nll0_before, nll0_after);
}

// ---------------------------------------------------------------------------
// Test G — OOV event type is handled without crash
// ---------------------------------------------------------------------------
void test_oov_event_type_handled(void) {
    // Event types >= MK_EVENT_COUNT are OOV.
    // The scorer emits MK_UNKNOWN_NLL_Q15 (32767) which decodes to ~8.0 nats.
    markov_reset_track(0);
    tick_and_observe(MK_EVENT_COUNT, 500u);   // OOV: type 7
    tick_and_observe(200u,           500u);   // OOV: type 200

    float nll = markov_current_nll();
    TEST_ASSERT_TRUE_MESSAGE(nll > 0.0f, "OOV NLL should be > 0");
    TEST_ASSERT_TRUE_MESSAGE(nll <= 8.1f, "OOV NLL should not exceed NLL_SCALE ceiling");
}

// ---------------------------------------------------------------------------
// Unity main
// ---------------------------------------------------------------------------
int main(int, char**) {
    UNITY_BEGIN();
    RUN_TEST(test_init_bad_path_fails);
    RUN_TEST(test_init_good_path_succeeds);
    RUN_TEST(test_benign_sequence_low_nll);
    RUN_TEST(test_anomalous_sequence_high_nll);
    RUN_TEST(test_is_anomalous_flag);
    RUN_TEST(test_reset_clears_nll);
    RUN_TEST(test_multitrack_isolation);
    RUN_TEST(test_oov_event_type_handled);
    return UNITY_END();
}
