// ============================================================================
// test/test_health_report/test_health_report.cpp
//
// Unity tests for src/health_report.{h,cpp} — periodic battery/RSSI/uptime
// health reporting. Previously zero test coverage and unconditionally
// `#include <Arduino.h>`'d, so it could not be compiled natively. Applied
// the same #ifdef ARDUINO / native-stub pattern used elsewhere in this
// sweep; analogRead() is test-overridable via STUB_ANALOG_READ_PROVIDED so
// battery-percentage tests can control the simulated raw ADC reading.
//
// Writing these tests surfaced a real bug, fixed in health_report.cpp:
// health_send_now()'s own comment declares `noise_floor = RSSI range
// (max - min)`, but the code assigned s_rssi_min directly -- and
// s_rssi_max was updated on every health_update_rssi() call yet never
// actually read anywhere, a strong signal this was an incomplete
// implementation rather than an intentional redefinition. Fixed to
// compute the actual max-min range. See test_send_now_reports_rssi_range
// below.
//
// Run with:
//   pio test -e native -f test_health_report
// ============================================================================

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

#define STUB_MILLIS_PROVIDED
static uint32_t g_stub_millis = 1000;
static inline uint32_t millis() { return g_stub_millis; }

#define STUB_ANALOG_READ_PROVIDED
static int g_stub_adc_raw = 0;
static inline int analogRead(int) { return g_stub_adc_raw; }

// health_report.cpp includes mesh_publish.h, which unconditionally pulls in
// RadioLib.h -- not native-includable. Same header-avoidance pattern used
// in test_ffps.cpp: pre-define its include guard and declare exactly the
// two functions actually called.
#define MESH_PUBLISH_H
#include "../../src/config.h"
static bool           g_publish_called = false;
static DetectionEvent g_published_evt{};
static bool           g_publish_ret = true;
bool mesh_publish_detection(const DetectionEvent& evt) {
    g_publish_called = true;
    g_published_evt = evt;
    return g_publish_ret;
}
static int g_start_receive_calls = 0;
void mesh_start_receive() { g_start_receive_calls++; }

// model_manager.h is native-safe (cstdint + config.h only) -- included
// normally, stub its three functions.
#include "../../src/model_manager.h"
static uint8_t g_model_count = 0;
uint8_t model_manager_count() { return g_model_count; }
uint32_t model_manager_psram_used() { return 0; }
bool model_manager_get_stats(uint8_t, ModelStats&) { return false; }

#include "../../src/gps_module.cpp"     // native stub: gps_has_fix() always false
#include "../../src/sensor_location.cpp"
#include "../../src/health_report.cpp"

void setUp(void) {
    g_stub_millis = 1000;
    g_stub_adc_raw = 0;
    g_publish_called = false;
    g_publish_ret = true;
    memset(&g_published_evt, 0, sizeof(g_published_evt));
    g_start_receive_calls = 0;
    g_model_count = 0;
    health_init(0x11111111u);
}
void tearDown(void) {}

// ---------------------------------------------------------------------------
// health_battery_pct_from_raw_adc — pure conversion math
// ---------------------------------------------------------------------------
void test_battery_pct_zero_raw_is_zero_percent(void) {
    TEST_ASSERT_EQUAL_UINT8(0, health_battery_pct_from_raw_adc(0));
}

void test_battery_pct_clamps_at_100(void) {
    // raw=4095 -> voltage_mv = 3300 * BATT_ADC_DIVIDER(2.0) = 6600mV, far
    // above BATT_FULL_MV(4200) -- must clamp to 100, not overflow past it.
    TEST_ASSERT_EQUAL_UINT8(100, health_battery_pct_from_raw_adc(4095));
}

void test_battery_pct_clamps_at_0_below_empty(void) {
    // A raw value low enough that voltage_mv <= BATT_EMPTY_MV(3200).
    // voltage_mv = (raw/4095)*3300*2.0 <= 3200 => raw <= ~1981.8
    TEST_ASSERT_EQUAL_UINT8(0, health_battery_pct_from_raw_adc(1000));
}

void test_battery_pct_interpolates_midrange(void) {
    // Pick a raw value whose voltage_mv sits exactly halfway between
    // BATT_EMPTY_MV(3200) and BATT_FULL_MV(4200), i.e. 3700mV.
    // raw = 3700 / (3300*2.0) * 4095
    float target_mv = (BATT_EMPTY_MV + BATT_FULL_MV) / 2.0f;
    int raw = (int)(target_mv / (3300.0f * BATT_ADC_DIVIDER) * 4095.0f);
    uint8_t pct = health_battery_pct_from_raw_adc(raw);
    TEST_ASSERT_UINT8_WITHIN(2, 50, pct);
}

// ---------------------------------------------------------------------------
// detection counter
// ---------------------------------------------------------------------------
void test_increment_detections_reflected_in_signature_hash(void) {
    health_increment_detections();
    health_increment_detections();
    health_increment_detections();

    bool sent = health_send_now();
    TEST_ASSERT_TRUE(sent);
    // detection_count packed big-endian into signature_hash[4..7]
    uint32_t packed = ((uint32_t)g_published_evt.signature_hash[4] << 24) |
                       ((uint32_t)g_published_evt.signature_hash[5] << 16) |
                       ((uint32_t)g_published_evt.signature_hash[6] << 8) |
                       (uint32_t)g_published_evt.signature_hash[7];
    TEST_ASSERT_EQUAL_UINT32(3, packed);
}

// ---------------------------------------------------------------------------
// RSSI stats
// ---------------------------------------------------------------------------
void test_send_now_reports_average_rssi(void) {
    health_update_rssi(-60);
    health_update_rssi(-80);
    health_update_rssi(-70);
    // average = (-60-80-70)/3 = -70
    health_send_now();
    TEST_ASSERT_EQUAL_INT16(-70, g_published_evt.rssi);
}

// Regression test for the real bug this pass fixed: noise_floor must be
// the RSSI range (max - min), matching the function's own documented
// design -- not just the bare minimum.
void test_send_now_reports_rssi_range(void) {
    health_update_rssi(-90);   // min
    health_update_rssi(-50);   // max
    health_update_rssi(-70);
    health_send_now();
    TEST_ASSERT_EQUAL_INT16_MESSAGE(40, g_published_evt.noise_floor,
        "noise_floor must be max-min (a 40 dB range here), not the bare minimum");
}

void test_send_now_with_no_rssi_samples_reports_zero(void) {
    // No health_update_rssi() calls this window.
    health_send_now();
    TEST_ASSERT_EQUAL_INT16(0, g_published_evt.rssi);
    TEST_ASSERT_EQUAL_INT16(0, g_published_evt.noise_floor);
}

void test_rssi_stats_reset_after_successful_send(void) {
    health_update_rssi(-60);
    health_update_rssi(-80);
    health_send_now();

    memset(&g_published_evt, 0, sizeof(g_published_evt));
    health_send_now();   // no new samples fed in between
    TEST_ASSERT_EQUAL_INT16_MESSAGE(0, g_published_evt.rssi,
        "RSSI accumulator must reset after a successful report, not carry over");
}

void test_rssi_stats_not_reset_on_failed_send(void) {
    health_update_rssi(-60);
    g_publish_ret = false;
    bool sent = health_send_now();
    TEST_ASSERT_FALSE(sent);

    g_publish_ret = true;
    health_send_now();
    // The -60 sample from before the failed send should still be reflected.
    TEST_ASSERT_EQUAL_INT16(-60, g_published_evt.rssi);
}

// ---------------------------------------------------------------------------
// health_send_now field packing
// ---------------------------------------------------------------------------
void test_send_now_mirrors_battery_pct_in_confidence(void) {
    g_stub_adc_raw = 4095;   // clamps to 100%
    health_send_now();
    TEST_ASSERT_EQUAL_UINT8(100, g_published_evt.battery_pct);
    TEST_ASSERT_EQUAL_UINT8(100, g_published_evt.confidence);
}

void test_send_now_sets_health_msg_type(void) {
    health_send_now();
    TEST_ASSERT_EQUAL_UINT8(MSG_TYPE_HEALTH, g_published_evt.msg_type);
    TEST_ASSERT_EQUAL_UINT32(0x11111111u, g_published_evt.sensor_id);
}

void test_send_now_calls_start_receive_after_tx(void) {
    health_send_now();
    TEST_ASSERT_TRUE(g_start_receive_calls > 0);
}

// ---------------------------------------------------------------------------
// health_tick interval gating
// ---------------------------------------------------------------------------
void test_tick_does_not_send_before_interval_elapsed(void) {
    g_stub_millis = 1000;
    health_init(0x11111111u);   // resets s_last_report_ms to 1000

    g_stub_millis = 1000 + HEALTH_REPORT_INTERVAL_MS - 1;
    TEST_ASSERT_FALSE(health_tick());
    TEST_ASSERT_FALSE(g_publish_called);
}

void test_tick_sends_after_interval_elapsed(void) {
    g_stub_millis = 1000;
    health_init(0x11111111u);

    g_stub_millis = 1000 + HEALTH_REPORT_INTERVAL_MS;
    TEST_ASSERT_TRUE(health_tick());
    TEST_ASSERT_TRUE(g_publish_called);
}

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

    RUN_TEST(test_battery_pct_zero_raw_is_zero_percent);
    RUN_TEST(test_battery_pct_clamps_at_100);
    RUN_TEST(test_battery_pct_clamps_at_0_below_empty);
    RUN_TEST(test_battery_pct_interpolates_midrange);

    RUN_TEST(test_increment_detections_reflected_in_signature_hash);

    RUN_TEST(test_send_now_reports_average_rssi);
    RUN_TEST(test_send_now_reports_rssi_range);
    RUN_TEST(test_send_now_with_no_rssi_samples_reports_zero);
    RUN_TEST(test_rssi_stats_reset_after_successful_send);
    RUN_TEST(test_rssi_stats_not_reset_on_failed_send);

    RUN_TEST(test_send_now_mirrors_battery_pct_in_confidence);
    RUN_TEST(test_send_now_sets_health_msg_type);
    RUN_TEST(test_send_now_calls_start_receive_after_tx);

    RUN_TEST(test_tick_does_not_send_before_interval_elapsed);
    RUN_TEST(test_tick_sends_after_interval_elapsed);

    return UNITY_END();
}
