/*
 * test_baro.c — Unit tests for the baro module (Issue #672).
 *
 * Build (host, no hardware):
 *   gcc -I include -I test/stubs -DTEST_HOST -lm \
 *       -o /tmp/test_baro src/baro.c test/test_baro.c
 *
 * All tests are self-contained; no HAL, no I2C, no UART required.
 * bmp280_init/read/humidity/alt and jlink_send_baro_tlm are stubbed below.
 */

/* ---- Stubs ---- */

/* Prevent jlink.h from pulling in HAL / pid_flash types */
#define JLINK_H
#include <stdint.h>
#include <stdbool.h>

/* Minimal jlink_tlm_baro_t matching the real definition */
typedef struct __attribute__((packed)) {
    int32_t  pressure_pa;
    int16_t  temp_x10;
    int32_t  alt_cm;
    int16_t  humidity_pct_x10;
} jlink_tlm_baro_t;

/* Capture last transmitted baro tlm for inspection */
static jlink_tlm_baro_t g_last_baro_tlm;
static int g_baro_tlm_count = 0;

void jlink_send_baro_tlm(const jlink_tlm_baro_t *tlm)
{
    g_last_baro_tlm = *tlm;
    g_baro_tlm_count++;
}

/* Stub bmp280_read() — returns configurable values */
static int32_t  g_stub_pressure_pa = 101325;
static int16_t  g_stub_temp_x10    = 230;  /* 23.0 °C */

void bmp280_read(int32_t *pressure_pa, int16_t *temp_x10)
{
    *pressure_pa = g_stub_pressure_pa;
    *temp_x10    = g_stub_temp_x10;
}

/* Stub bmp280_read_humidity() */
static int16_t g_stub_humidity = 500;  /* 50.0 %RH */

int16_t bmp280_read_humidity(void)
{
    return g_stub_humidity;
}

/* Stub bmp280_pressure_to_alt_cm() — proportional approx for testing */
int32_t bmp280_pressure_to_alt_cm(int32_t pressure_pa)
{
    /* Simplified: sea level = 101325 Pa = 0 cm; decrease of 12 Pa ≈ 100 cm */
    return (int32_t)((101325 - pressure_pa) * 100 / 12);
}

/* ---- Include implementation directly ---- */
#include "../src/baro.c"

/* ---- Test framework ---- */
#include <stdio.h>
#include <math.h>
#include <string.h>

static int g_pass = 0;
static int g_fail = 0;

#define ASSERT(cond, msg) do { \
    if (cond) { g_pass++; } \
    else { g_fail++; printf("FAIL [%s:%d] %s\n", __FILE__, __LINE__, msg); } \
} while(0)

/* ---- Helper ---- */
static void reset_stubs(void)
{
    g_stub_pressure_pa  = 101325;
    g_stub_temp_x10     = 230;
    g_stub_humidity     = 500;
    g_baro_tlm_count    = 0;
    memset(&g_last_baro_tlm, 0, sizeof(g_last_baro_tlm));
}

/* ---- Tests ---- */

static void test_init_absent_chip_no_read(void)
{
    reset_stubs();
    baro_init(0);  /* chip absent */

    /* baro_tick must be a no-op */
    baro_tick(1000u);
    ASSERT(g_baro_tlm_count == 0, "absent chip: no telemetry sent");

    baro_data_t d;
    ASSERT(!baro_get(&d), "absent chip: baro_get returns false");
    ASSERT(baro_get_alt_cm() == 0, "absent chip: baro_get_alt_cm returns 0");
}

static void test_first_tick_fires_immediately(void)
{
    /*
     * After baro_init() timestamps are pre-wound so that the very first
     * baro_tick() triggers a read and a telemetry send.
     */
    reset_stubs();
    baro_init(0x60);  /* BME280 */

    baro_tick(1000u);
    ASSERT(g_baro_tlm_count == 1, "first tick sends telemetry immediately");

    baro_data_t d;
    ASSERT(baro_get(&d), "first tick: data valid");
}

static void test_data_populated_after_tick(void)
{
    reset_stubs();
    g_stub_pressure_pa = 100000;
    g_stub_temp_x10    = 150;   /* 15.0 °C */
    g_stub_humidity    = 400;   /* 40.0 %RH */

    baro_init(0x60);
    baro_tick(0u);

    baro_data_t d;
    ASSERT(baro_get(&d),           "data valid after tick");
    ASSERT(d.pressure_pa == 100000, "pressure_pa correct");
    ASSERT(d.temp_x10 == 150,       "temp_x10 correct");
    ASSERT(d.humidity_pct_x10 == 400, "humidity correct (BME280)");
    ASSERT(d.valid,                 "valid flag set");
}

static void test_altitude_computed(void)
{
    reset_stubs();
    g_stub_pressure_pa = 101325 - 120;  /* 120 Pa below sea level → ~1000 cm */
    baro_init(0x58);  /* BMP280 */
    baro_tick(0u);

    baro_data_t d;
    baro_get(&d);
    /* stub: alt_cm = (101325 - 101205)*100/12 = 12000/12 = 1000 cm */
    ASSERT(d.alt_cm == 1000, "altitude computed correctly");
    ASSERT(baro_get_alt_cm() == 1000, "baro_get_alt_cm matches");
}

static void test_bmp280_no_humidity(void)
{
    reset_stubs();
    baro_init(0x58);  /* BMP280 — no humidity */
    baro_tick(0u);

    baro_data_t d;
    baro_get(&d);
    ASSERT(d.humidity_pct_x10 == (int16_t)-1,
           "BMP280: humidity_pct_x10 == -1");
}

static void test_bme280_has_humidity(void)
{
    reset_stubs();
    g_stub_humidity = 650;
    baro_init(0x60);  /* BME280 */
    baro_tick(0u);

    baro_data_t d;
    baro_get(&d);
    ASSERT(d.humidity_pct_x10 == 650,
           "BME280: humidity_pct_x10 populated");
}

static void test_rate_limited_1hz(void)
{
    /*
     * At BARO_READ_HZ = 1, only one read per second.
     * Call baro_tick() every ms for 3000 ms → expect 3 sends.
     */
    reset_stubs();
    baro_init(0x60);

    for (uint32_t ms = 0; ms < 3000u; ms++) {
        baro_tick(ms);
    }
    ASSERT(g_baro_tlm_count == 3,
           "1 Hz rate limit: 3 sends in 3000 ms");
}

static void test_no_tlm_before_interval(void)
{
    reset_stubs();
    baro_init(0x60);

    /* First tick at ms=0 fires (pre-wound). Next must not fire until ms=1000. */
    baro_tick(0u);
    ASSERT(g_baro_tlm_count == 1, "first tick sends");

    baro_tick(500u);  /* only 500 ms later — must not send again */
    ASSERT(g_baro_tlm_count == 1, "second tick before interval: no extra send");
}

static void test_tlm_payload_encoding(void)
{
    reset_stubs();
    g_stub_pressure_pa = 95000;
    g_stub_temp_x10    = -50;  /* -5.0 °C */
    g_stub_humidity    = 900;
    baro_init(0x60);
    baro_tick(0u);

    ASSERT(g_baro_tlm_count == 1, "TLM sent");
    ASSERT(g_last_baro_tlm.pressure_pa == 95000,
           "TLM: pressure_pa encoded");
    ASSERT(g_last_baro_tlm.temp_x10 == (int16_t)-50,
           "TLM: negative temp encoded");
    ASSERT(g_last_baro_tlm.humidity_pct_x10 == 900,
           "TLM: humidity encoded");
    /* alt_cm via stub: (101325-95000)*100/12 = 632500/12 ≈ 52708 */
    ASSERT(g_last_baro_tlm.alt_cm == g_last_baro_tlm.alt_cm, /* sanity */
           "TLM: alt_cm field present");
    /* verify alt matches baro_get */
    baro_data_t d;
    baro_get(&d);
    ASSERT(g_last_baro_tlm.alt_cm == d.alt_cm,
           "TLM: alt_cm matches baro_get");
}

static void test_get_returns_false_before_first_tick(void)
{
    reset_stubs();
    baro_init(0x60);

    /* Trick: call with a time that won't fire yet by advancing only 1 ms
     * BUT init pre-winds timestamps so tick at 0 fires. Test the initial
     * state by checking valid flag before any tick. */
    /* We can observe this by checking the internal s_data.valid = false
     * by attempting a get on a freshly reinitialised module.
     * We simulate by re-init'ing with chip=0 so tick is no-op. */
    baro_init(0);  /* chip absent — tick will never fire */
    baro_data_t d;
    ASSERT(!baro_get(&d), "no tick yet: baro_get returns false");
}

static void test_get_alt_zero_before_valid(void)
{
    reset_stubs();
    baro_init(0);  /* no chip → never valid */
    ASSERT(baro_get_alt_cm() == 0, "alt_cm == 0 when no valid data");
}

static void test_data_updates_each_interval(void)
{
    /*
     * Change stub pressure between ticks.
     * Verify baro_get() reflects the latest reading.
     */
    reset_stubs();
    baro_init(0x58);

    g_stub_pressure_pa = 101325;
    baro_tick(0u);
    baro_data_t d1;
    baro_get(&d1);

    g_stub_pressure_pa = 100000;
    baro_tick(1000u);  /* one interval later */
    baro_data_t d2;
    baro_get(&d2);

    ASSERT(d1.pressure_pa == 101325, "first reading correct");
    ASSERT(d2.pressure_pa == 100000, "second reading updated");
    ASSERT(d1.alt_cm != d2.alt_cm,   "alt_cm updates between reads");
}

static void test_reinit_clears_valid(void)
{
    reset_stubs();
    baro_init(0x60);
    baro_tick(0u);

    baro_data_t d;
    ASSERT(baro_get(&d), "data valid after tick");

    /* Re-init with absent chip — valid flag cleared */
    baro_init(0);
    ASSERT(!baro_get(&d), "after re-init (absent): valid cleared");
    ASSERT(baro_get_alt_cm() == 0, "after re-init (absent): alt == 0");
}

/* ---- main ---- */
int main(void)
{
    printf("=== test_baro ===\n");

    test_init_absent_chip_no_read();
    test_first_tick_fires_immediately();
    test_data_populated_after_tick();
    test_altitude_computed();
    test_bmp280_no_humidity();
    test_bme280_has_humidity();
    test_rate_limited_1hz();
    test_no_tlm_before_interval();
    test_tlm_payload_encoding();
    test_get_returns_false_before_first_tick();
    test_get_alt_zero_before_valid();
    test_data_updates_each_interval();
    test_reinit_clears_valid();

    printf("\nResults: %d passed, %d failed\n", g_pass, g_fail);
    return g_fail ? 1 : 0;
}
