/*
 * test_steering_pid.c — unit tests for steering_pid (Issue #616).
 *
 * Host build (no HAL):
 *   gcc -I include -I src -DTEST_HOST -lm -o /tmp/test_steering test/test_steering_pid.c
 *   /tmp/test_steering
 *
 * Stubs out jlink.h to avoid the full HAL dependency chain.
 */

/* ---- Minimal stubs ---- */
#define JLINK_H  /* prevent real jlink.h from loading */

#include <stdint.h>
#include <stdbool.h>

/* Minimal jlink_tlm_steering_t and send stub */
typedef struct __attribute__((packed)) {
    int16_t  target_x10;
    int16_t  actual_x10;
    int16_t  output;
    uint8_t  enabled;
    uint8_t  _pad;
} jlink_tlm_steering_t;

static jlink_tlm_steering_t g_last_tlm;
static int g_tlm_count = 0;

void jlink_send_steering_tlm(const jlink_tlm_steering_t *tlm)
{
    g_last_tlm = *tlm;
    g_tlm_count++;
}

/* ---- Include the module under test ---- */
#include "steering_pid.h"
#include "steering_pid.c"   /* single-TU build; steering_pid.c found via -I src */

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

static int s_pass = 0;
static int s_fail = 0;

#define EXPECT(cond, msg) do { \
    if (cond) { s_pass++; } \
    else { s_fail++; printf("FAIL [%s:%d]: %s\n", __func__, __LINE__, msg); } \
} while(0)

#define EXPECT_EQ(a, b, msg)  EXPECT((a) == (b), msg)
#define EXPECT_NEAR(a, b, eps, msg) EXPECT(fabsf((a) - (b)) <= (eps), msg)
#define EXPECT_TRUE(x, msg)   EXPECT((x), msg)
#define EXPECT_FALSE(x, msg)  EXPECT(!(x), msg)

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

static void test_init(void)
{
    steering_pid_t s;
    steering_pid_init(&s);

    EXPECT_NEAR(s.target_omega_dps, 0.0f, 1e-6f, "init: target_omega 0");
    EXPECT_NEAR(s.actual_omega_dps, 0.0f, 1e-6f, "init: actual_omega 0");
    EXPECT_NEAR(s.integral,         0.0f, 1e-6f, "init: integral 0");
    EXPECT_NEAR(s.prev_error,       0.0f, 1e-6f, "init: prev_error 0");
    EXPECT_EQ  (s.output,           0,           "init: output 0");
    EXPECT_TRUE(s.enabled,                        "init: enabled true");
}

static void test_reset(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    s.target_omega_dps = 30.0f;
    s.integral  = 100.0f;
    s.prev_error = 5.0f;
    s.output     = 200;

    steering_pid_reset(&s);

    EXPECT_NEAR(s.target_omega_dps, 0.0f, 1e-6f, "reset: target 0");
    EXPECT_NEAR(s.integral,         0.0f, 1e-6f, "reset: integral 0");
    EXPECT_NEAR(s.prev_error,       0.0f, 1e-6f, "reset: prev_error 0");
    EXPECT_EQ  (s.output,           0,           "reset: output 0");
    EXPECT_TRUE(s.enabled,                        "reset: preserves enabled");
}

static void test_disabled_returns_zero(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_enabled(&s, false);
    steering_pid_set_target(&s, 50.0f);

    int16_t out = steering_pid_update(&s, 0.0f, 0.001f);

    EXPECT_EQ(out, 0, "disabled: update returns 0");
    EXPECT_EQ(steering_pid_get_output(&s), 0, "disabled: get_output 0");
    /* Target should not have been set when disabled */
    EXPECT_NEAR(s.target_omega_dps, 0.0f, 1e-6f, "disabled: target not set");
}

static void test_zero_dt_returns_zero(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, 30.0f);

    int16_t out = steering_pid_update(&s, 0.0f, 0.0f);
    EXPECT_EQ(out, 0, "zero_dt: returns 0");
}

static void test_negative_dt_returns_zero(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, 30.0f);

    int16_t out = steering_pid_update(&s, 0.0f, -0.01f);
    EXPECT_EQ(out, 0, "neg_dt: returns 0");
}

static void test_proportional_only(void)
{
    /*
     * With Ki=0 and Kd=0, output = Kp * error.
     * Set STEER_KP=2.0, error=50 deg/s → raw = 100 counts.
     * Rate limiter allows max_step = STEER_RAMP_RATE_PER_MS * dt_ms = 10 * 1 = 10 per step.
     * After 1 step (dt=0.001s): output capped at 10 by rate limiter.
     */
    steering_pid_t s;
    memset(&s, 0, sizeof(s));
    steering_pid_init(&s);
    s.integral   = 0.0f;
    s.prev_error = 0.0f;
    s.output     = 0;

    steering_pid_set_target(&s, 50.0f);
    int16_t out = steering_pid_update(&s, 0.0f, 0.001f);

    /* Rate limiter: max_step = 10*0.001*1000 = 10 → output = 10 */
    EXPECT_EQ(out, 10, "prop_only: rate-limited step");
}

static void test_converges_to_setpoint(void)
{
    /*
     * With no disturbance, run many steps until output reaches the setpoint.
     * With target=20 deg/s, Kp=2, after many steps the P term = 2*20 = 40,
     * plus I accumulation. The rate limiter will eventually allow the output
     * to grow. Run for 1s at 1kHz (1000 steps, dt=0.001s each).
     * Expected: eventually output > 0 (PID working), integral accumulates.
     */
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, 20.0f);

    int16_t out = 0;
    for (int i = 0; i < 1000; i++) {
        out = steering_pid_update(&s, 0.0f, 0.001f);
    }
    EXPECT_TRUE(out > 0, "convergence: output positive after 1s");
    EXPECT_TRUE(s.integral > 0.0f, "convergence: integral positive");
}

static void test_output_clamped_to_max(void)
{
    /*
     * Very large error should produce output clamped to STEER_OUTPUT_MAX.
     * Run many ticks so rate limiter is satisfied.
     * STEER_OUTPUT_MAX = 400; 400 ticks at 10 counts/ms should saturate.
     */
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, 500.0f);  /* 500 deg/s — way beyond robot capability */

    int16_t out = 0;
    for (int i = 0; i < 500; i++) {
        out = steering_pid_update(&s, 0.0f, 0.001f);
    }
    EXPECT_EQ(out, STEER_OUTPUT_MAX, "clamp: output saturates at STEER_OUTPUT_MAX");
}

static void test_output_clamped_negative(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, -500.0f);

    int16_t out = 0;
    for (int i = 0; i < 500; i++) {
        out = steering_pid_update(&s, 0.0f, 0.001f);
    }
    EXPECT_EQ(out, -STEER_OUTPUT_MAX, "clamp_neg: output saturates at -STEER_OUTPUT_MAX");
}

static void test_anti_windup(void)
{
    /*
     * Run with a persistent error for a long time.
     * Integral should be clamped to ±STEER_INTEGRAL_MAX.
     */
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, 50.0f);

    for (int i = 0; i < 10000; i++) {
        steering_pid_update(&s, 0.0f, 0.001f);  /* actual stays 0, error = 50 */
    }
    EXPECT_NEAR(s.integral, STEER_INTEGRAL_MAX, 0.01f, "anti-windup: integral clamped to max");
}

static void test_anti_windup_negative(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, -50.0f);

    for (int i = 0; i < 10000; i++) {
        steering_pid_update(&s, 0.0f, 0.001f);
    }
    EXPECT_NEAR(s.integral, -STEER_INTEGRAL_MAX, 0.01f, "anti-windup-neg: integral clamped");
}

static void test_rate_limiter_single_step(void)
{
    /*
     * At dt=0.001s: max_step = STEER_RAMP_RATE_PER_MS * 1 = 10.
     * Even if raw PID output = 400, first step can only reach 10.
     */
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, 500.0f);

    int16_t out = steering_pid_update(&s, 0.0f, 0.001f);
    EXPECT_EQ(out, STEER_RAMP_RATE_PER_MS * 1, "rate_lim: first step bounded");
}

static void test_rate_limiter_larger_dt(void)
{
    /*
     * At dt=0.01s (100Hz): max_step = 10 * 10 = 100.
     * First step limited to 100.
     */
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, 500.0f);

    int16_t out = steering_pid_update(&s, 0.0f, 0.01f);
    EXPECT_EQ(out, STEER_RAMP_RATE_PER_MS * 10, "rate_lim_100hz: first step bounded");
}

static void test_rate_limiter_decreasing(void)
{
    /*
     * After saturating to STEER_OUTPUT_MAX, set target to 0.
     * Output should decrease at most STEER_RAMP_RATE_PER_MS per ms.
     */
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, 500.0f);
    for (int i = 0; i < 500; i++) {
        steering_pid_update(&s, 0.0f, 0.001f);
    }
    /* Now output = STEER_OUTPUT_MAX = 400 */
    int16_t before = s.output;
    steering_pid_reset(&s);  /* reset integral and target */
    /* output is also zeroed by reset — which is fine: the test is
       really about rate limiting when driving toward 0 after a step */
    (void)before;

    /* Re-saturate, then drive to 0 with matching actual */
    steering_pid_init(&s);
    steering_pid_set_target(&s, 500.0f);
    for (int i = 0; i < 500; i++) {
        steering_pid_update(&s, 0.0f, 0.001f);
    }
    steering_pid_set_target(&s, 0.0f);
    int16_t pre  = s.output;
    int16_t post = steering_pid_update(&s, 0.0f, 0.001f);

    /* Should decrease by no more than STEER_RAMP_RATE_PER_MS per tick */
    int16_t delta = pre - post;
    EXPECT_TRUE(delta <= STEER_RAMP_RATE_PER_MS, "rate_lim_dec: decrease bounded per tick");
}

static void test_zero_error_zero_output(void)
{
    /*
     * When target == actual from the first tick, error = 0 and output stays 0.
     */
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, 30.0f);
    int16_t out = steering_pid_update(&s, 30.0f, 0.001f);
    EXPECT_EQ(out, 0, "zero_error: no output when tracking exactly");
}

static void test_enable_disable_clears_state(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, 50.0f);
    for (int i = 0; i < 100; i++) {
        steering_pid_update(&s, 0.0f, 0.001f);
    }

    /* Disable */
    steering_pid_set_enabled(&s, false);

    EXPECT_FALSE(s.enabled,                       "disable: enabled false");
    EXPECT_NEAR (s.target_omega_dps, 0.0f, 1e-6f, "disable: target zeroed");
    EXPECT_NEAR (s.integral,         0.0f, 1e-6f, "disable: integral zeroed");
    EXPECT_EQ   (s.output,           0,           "disable: output zeroed");

    /* Calls while disabled return 0 */
    int16_t out = steering_pid_update(&s, 50.0f, 0.001f);
    EXPECT_EQ(out, 0, "disable: update returns 0");
}

static void test_re_enable(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_enabled(&s, false);
    steering_pid_set_enabled(&s, true);

    EXPECT_TRUE(s.enabled, "re-enable: enabled true");

    /* After re-enable, new target and update should work */
    steering_pid_set_target(&s, 20.0f);
    EXPECT_NEAR(s.target_omega_dps, 20.0f, 1e-6f, "re-enable: set_target works");
}

static void test_set_target_ignored_when_disabled(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_enabled(&s, false);
    steering_pid_set_target(&s, 99.0f);

    EXPECT_NEAR(s.target_omega_dps, 0.0f, 1e-6f, "disabled_target: target not set");
}

static void test_actual_omega_updated(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_update(&s, 42.5f, 0.001f);
    EXPECT_NEAR(s.actual_omega_dps, 42.5f, 1e-4f, "actual_omega: stored after update");
}

static void test_get_output_matches_update(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, 20.0f);
    int16_t from_update = steering_pid_update(&s, 0.0f, 0.001f);
    EXPECT_EQ(from_update, steering_pid_get_output(&s), "get_output: matches update return");
}

static void test_omega_scale_convention(void)
{
    /*
     * Verify that setting target via STEER_OMEGA_SCALE:
     *   omega_dps = steer * STEER_OMEGA_SCALE
     *   steer=100 → omega=10 deg/s
     */
    float omega = 100.0f * STEER_OMEGA_SCALE;
    EXPECT_NEAR(omega, 10.0f, 1e-4f, "omega_scale: 100 units = 10 deg/s");

    omega = -1000.0f * STEER_OMEGA_SCALE;
    EXPECT_NEAR(omega, -100.0f, 1e-4f, "omega_scale: -1000 units = -100 deg/s");
}

static void test_tlm_first_call_fires(void)
{
    /*
     * After init, last_tlm_ms is set so that the first send_tlm fires immediately
     * at now_ms = 0.
     */
    steering_pid_t s;
    steering_pid_init(&s);
    g_tlm_count = 0;

    steering_pid_send_tlm(&s, 0);
    EXPECT_EQ(g_tlm_count, 1, "tlm_first: fires at ms=0 after init");
}

static void test_tlm_rate_limited(void)
{
    /*
     * STEER_TLM_HZ = 10 → interval = 100ms.
     * Two calls at ms=0 and ms=50 should only send once.
     */
    steering_pid_t s;
    steering_pid_init(&s);
    g_tlm_count = 0;

    steering_pid_send_tlm(&s, 0);    /* fires (first call) */
    steering_pid_send_tlm(&s, 50);   /* blocked (< 100ms) */
    EXPECT_EQ(g_tlm_count, 1, "tlm_rate: blocked within interval");

    steering_pid_send_tlm(&s, 100);  /* fires (interval elapsed) */
    EXPECT_EQ(g_tlm_count, 2, "tlm_rate: fires after interval");
}

static void test_tlm_payload_target(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    s.target_omega_dps = 25.5f;  /* → target_x10 = 255 */
    g_tlm_count = 0;

    steering_pid_send_tlm(&s, 0);
    EXPECT_EQ(g_last_tlm.target_x10, 255, "tlm_payload: target_x10 correct");
}

static void test_tlm_payload_actual(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, 10.0f);
    steering_pid_update(&s, 12.3f, 0.001f);
    g_tlm_count = 0;

    steering_pid_send_tlm(&s, 0);
    /* actual = 12.3 deg/s → actual_x10 = 123 */
    EXPECT_EQ(g_last_tlm.actual_x10, 123, "tlm_payload: actual_x10 correct");
}

static void test_tlm_payload_output(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    s.output = 150;
    g_tlm_count = 0;

    steering_pid_send_tlm(&s, 0);
    EXPECT_EQ(g_last_tlm.output, 150, "tlm_payload: output correct");
}

static void test_tlm_payload_enabled_flag(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_enabled(&s, true);
    g_tlm_count = 0;
    steering_pid_send_tlm(&s, 0);
    EXPECT_EQ(g_last_tlm.enabled, 1u, "tlm_payload: enabled=1 when on");

    steering_pid_init(&s);
    steering_pid_set_enabled(&s, false);
    steering_pid_send_tlm(&s, 0);
    EXPECT_EQ(g_last_tlm.enabled, 0u, "tlm_payload: enabled=0 when off");
}

static void test_tlm_payload_pad_zero(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    g_tlm_count = 0;
    steering_pid_send_tlm(&s, 0);
    EXPECT_EQ(g_last_tlm._pad, 0u, "tlm_payload: _pad = 0");
}

static void test_tlm_target_x10_negative(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    s.target_omega_dps = -30.7f;  /* → target_x10 = -307 */
    g_tlm_count = 0;
    steering_pid_send_tlm(&s, 0);
    EXPECT_EQ(g_last_tlm.target_x10, -307, "tlm_payload: negative target_x10");
}

static void test_tlm_int16_clamp_positive(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    s.target_omega_dps = 4000.0f;  /* × 10 = 40000 > INT16_MAX */
    g_tlm_count = 0;
    steering_pid_send_tlm(&s, 0);
    EXPECT_EQ(g_last_tlm.target_x10, 32767, "tlm_clamp: target_x10 clamped to INT16_MAX");
}

static void test_tlm_int16_clamp_negative(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    s.target_omega_dps = -4000.0f;  /* × 10 = -40000 < INT16_MIN */
    g_tlm_count = 0;
    steering_pid_send_tlm(&s, 0);
    EXPECT_EQ(g_last_tlm.target_x10, (int16_t)-32768, "tlm_clamp: target_x10 clamped to INT16_MIN");
}

static void test_balance_not_disturbed_by_small_step(void)
{
    /*
     * A balance bot is most sensitive to sudden changes in steer_cmd.
     * Verify that a large target step at t=0 only increments output by
     * STEER_RAMP_RATE_PER_MS per millisecond — not a step change.
     * This confirms the rate limiter protects the balance PID.
     */
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, 100.0f);   /* large yaw demand */

    /* 10ms at 1kHz */
    for (int i = 0; i < 10; i++) {
        int16_t pre  = s.output;
        int16_t post = steering_pid_update(&s, 0.0f, 0.001f);
        int16_t step = post - pre;
        EXPECT_TRUE(step <= STEER_RAMP_RATE_PER_MS, "balance_protect: step bounded each tick");
        EXPECT_TRUE(step >= -STEER_RAMP_RATE_PER_MS, "balance_protect: neg step bounded each tick");
    }
}

static void test_derivative_on_error_change(void)
{
    /*
     * When error changes suddenly (e.g. actual_omega jumps), the derivative
     * term should add a braking contribution.
     * With STEER_KD=0.05 and a 10 deg/s²-equivalent error rate change:
     *   d_term = 0.05 * (error1 - error0) / dt
     * We just verify output changes when error changes, indicating D is active.
     */
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, 20.0f);

    /* Step 1: actual = 0, error = 20 */
    int16_t out1 = steering_pid_update(&s, 0.0f, 0.001f);

    /* Step 2: actual jumps to 20, error = 0. D will oppose. */
    int16_t out2 = steering_pid_update(&s, 20.0f, 0.001f);

    /* out2 should be less than out1 (derivative braking as error drops) */
    EXPECT_TRUE(out2 <= out1, "derivative: braking when error shrinks");
}

static void test_reset_then_restart(void)
{
    steering_pid_t s;
    steering_pid_init(&s);
    steering_pid_set_target(&s, 50.0f);
    for (int i = 0; i < 100; i++) {
        steering_pid_update(&s, 0.0f, 0.001f);
    }

    steering_pid_reset(&s);
    EXPECT_EQ  (s.output,            0,    "reset_restart: output 0");
    EXPECT_NEAR(s.integral,  0.0f, 1e-6f,  "reset_restart: integral 0");
    EXPECT_NEAR(s.target_omega_dps, 0.0f, 1e-6f, "reset_restart: target 0");

    /* After reset, new target should work cleanly */
    steering_pid_set_target(&s, 10.0f);
    int16_t out = steering_pid_update(&s, 0.0f, 0.001f);
    EXPECT_TRUE(out > 0, "reset_restart: positive output after fresh start");
}

static void test_tlm_hz_constant(void)
{
    EXPECT_TRUE(STEER_TLM_HZ == 10u, "tlm_hz: STEER_TLM_HZ is 10");
}

static void test_output_max_constant(void)
{
    EXPECT_TRUE(STEER_OUTPUT_MAX == 400, "output_max: STEER_OUTPUT_MAX is 400");
}

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

    test_init();
    test_reset();
    test_disabled_returns_zero();
    test_zero_dt_returns_zero();
    test_negative_dt_returns_zero();
    test_proportional_only();
    test_converges_to_setpoint();
    test_output_clamped_to_max();
    test_output_clamped_negative();
    test_anti_windup();
    test_anti_windup_negative();
    test_rate_limiter_single_step();
    test_rate_limiter_larger_dt();
    test_rate_limiter_decreasing();
    test_zero_error_zero_output();
    test_enable_disable_clears_state();
    test_re_enable();
    test_set_target_ignored_when_disabled();
    test_actual_omega_updated();
    test_get_output_matches_update();
    test_omega_scale_convention();
    test_tlm_first_call_fires();
    test_tlm_rate_limited();
    test_tlm_payload_target();
    test_tlm_payload_actual();
    test_tlm_payload_output();
    test_tlm_payload_enabled_flag();
    test_tlm_payload_pad_zero();
    test_tlm_target_x10_negative();
    test_tlm_int16_clamp_positive();
    test_tlm_int16_clamp_negative();
    test_balance_not_disturbed_by_small_step();
    test_derivative_on_error_change();
    test_reset_then_restart();
    test_tlm_hz_constant();
    test_output_max_constant();

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