#include "lamp_guard.h"
#include <assert.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>

static const lg_inputs good = {true, true, true, true, true};
static unsigned checks;
static lg_config config(void) {
    lg_config c = {0};
    c.channels = 61;
    c.max_run_ms = 150000; /* The mode-specific limits must not be configurable upward. */
    c.ticket_lifetime_ms = 200;
    c.max_poll_gap_ms = 20;
    c.calibration_id = 17;
    c.fixed_input_mw = 4000;
    c.input_budget_mw = 20400;
    for (unsigned i = 0; i < LG_MAX_CHANNELS; ++i) c.channel_input_mw[i] = 300;
    return c;
}
static lg_guard fresh(uint32_t now) {
    lg_guard g;
    lg_config c = config();
    assert(lg_init(&g, &c, 0x1020304050607080ULL, now));
    lg_poll(&g, now, good);
    assert(!g.output_enable);
    return g;
}
static void dark(const lg_guard *g) {
    assert(!g->output_enable);
    for (unsigned i = 0; i < LG_MAX_CHANNELS; ++i) assert(g->pwm[i] == 0);
    ++checks;
}
static lg_frame ticket(lg_guard *g, uint32_t now) {
    lg_frame f;
    assert(lg_issue_ticket(g, now, &f));
    f.source_frame_seq = g->ticket_counter; /* Host fake of a fresh sensor frame. */
    f.capture_baseline_seq = f.source_frame_seq - 1u;
    f.hand_present = true;
    f.pwm[0] = 255;
    return f;
}
static lg_frame presence_ticket(lg_guard *g, uint32_t now) {
    lg_frame f;
    assert(lg_issue_ticket(g, now, &f));
    f.source_frame_seq = g->ticket_counter;
    f.capture_baseline_seq = f.source_frame_seq - 1u;
    f.hand_present = true;
    return f;
}
static void advance(lg_guard *g, uint32_t from, uint32_t ms) {
    for (uint32_t i = 1; i <= ms; ++i) lg_poll(g, from + i, good);
}

static void remaining_time_tests(void) {
    assert(lg_remaining_ms(NULL, 0u) == 0u);
    lg_guard g = fresh(100u);
    assert(lg_remaining_ms(&g, 100u) == 0u);
    assert(lg_arm(&g, LG_AI, LG_AI_MAX_RUN_MS, 100u));
    assert(lg_remaining_ms(&g, 100u) == 90000u);
    assert(lg_remaining_ms(&g, 45100u) == 45000u);
    assert(lg_remaining_ms(&g, 90099u) == 1u);
    assert(lg_remaining_ms(&g, 90100u) == 0u);

    uint32_t start = UINT32_MAX - 15u;
    g = fresh(start);
    assert(lg_arm(&g, LG_MANUAL, LG_MANUAL_MAX_RUN_MS, start));
    assert(lg_remaining_ms(&g, 0u) == LG_MANUAL_MAX_RUN_MS - 16u);
    ++checks;
}

int main(void) {
    remaining_time_tests();
    lg_guard g = fresh(0);
    lg_guard manual_limit = fresh(0);
    lg_frame f = {0};
    assert(!lg_arm(&manual_limit, LG_MANUAL, 120001, 0));
    assert(lg_arm(&manual_limit, LG_MANUAL, 120000, 0));
    assert(!lg_accept_ai(&g, &f, 0)); dark(&g);
    assert(!lg_arm(&g, LG_AI, 90001, 0));
    assert(lg_arm(&g, LG_AI, 90000, 0));
    f = ticket(&g, 0);
    assert(lg_accept_ai(&g, &f, 0)); assert(g.output_enable);
    assert(!lg_accept_ai(&g, &f, 0)); dark(&g); /* replay */

    /* A fresh negative vision result switches off without ending automatic monitoring. */
    f = ticket(&g, 0);
    f.hand_present = false;
    memset(f.pwm, 0, sizeof(f.pwm));
    assert(lg_accept_ai(&g, &f, 0));
    assert(g.armed && g.mode == LG_AI && !g.hand_present); dark(&g);
    f = ticket(&g, 0);
    f.hand_present = true;
    f.pwm[0] = 100;
    assert(lg_accept_ai(&g, &f, 0));
    assert(g.output_enable && g.hand_present);

    /* A new ticket cannot relabel a previously consumed camera frame. */
    uint32_t accepted_source_seq = f.source_frame_seq;
    f = ticket(&g, 0);
    f.source_frame_seq = accepted_source_seq;
    assert(!lg_accept_ai(&g, &f, 0)); dark(&g);
    f = ticket(&g, 0);
    f.source_frame_seq = accepted_source_seq + 1u;
    f.capture_baseline_seq = f.source_frame_seq; /* Claimed result predates exposure baseline. */
    assert(!lg_accept_ai(&g, &f, 0)); dark(&g);
    f = ticket(&g, 0);
    assert(lg_accept_ai(&g, &f, 0)); assert(g.output_enable);

    /* Deadline anchored to issue time; receipt must not renew it. */
    f = ticket(&g, 0);
    advance(&g, 0, 190);
    assert(lg_accept_ai(&g, &f, 190)); assert(g.output_enable);
    advance(&g, 190, 9); assert(g.output_enable);
    lg_poll(&g, 200, good); dark(&g);
    f = ticket(&g, 200);
    advance(&g, 200, 200);
    assert(!lg_accept_ai(&g, &f, 400)); dark(&g);

    /* Wrong boot, calibration, channel count, or replaced ticket. */
    g = fresh(0); assert(lg_arm(&g, LG_AI, 500, 0));
    f = ticket(&g, 0); ++f.session_id;
    assert(!lg_accept_ai(&g, &f, 0)); dark(&g);
    f = ticket(&g, 0); ++f.calibration_id;
    assert(!lg_accept_ai(&g, &f, 0)); dark(&g);
    f = ticket(&g, 0); --f.channels;
    assert(!lg_accept_ai(&g, &f, 0)); dark(&g);
    f = ticket(&g, 0); (void)ticket(&g, 0);
    assert(!lg_accept_ai(&g, &f, 0)); dark(&g);

    /* A new request cannot extend the previous active frame. */
    f = ticket(&g, 0); assert(lg_accept_ai(&g, &f, 0));
    advance(&g, 0, 190); f = ticket(&g, 190);
    advance(&g, 190, 10); dark(&g);
    assert(lg_accept_ai(&g, &f, 200)); assert(g.output_enable);
    advance(&g, 200, 190); dark(&g);

    /* All independent hardware interlock branches latch off. */
    for (unsigned i = 0; i < 4; ++i) {
        g = fresh(0); assert(lg_arm(&g, LG_AI, 1000, 0));
        f = ticket(&g, 0); assert(lg_accept_ai(&g, &f, 0));
        lg_inputs in = good;
        if (i == 0) in.temperature_ok = false;
        if (i == 1) in.supply_ok = false;
        if (i == 2) in.led_driver_ok = false;
        if (i == 3) in.hardware_interlock_ok = false;
        lg_poll(&g, 1, in); dark(&g);
        lg_poll(&g, 2, good); dark(&g);
        assert(!lg_accept_ai(&g, &f, 2));
        assert(g.mode == LG_FAULT);
        lg_stop(&g); assert(g.mode == LG_FAULT);
        assert(!lg_arm(&g, LG_AI, 100, 2));
    }

    g = fresh(0); assert(lg_arm(&g, LG_AI, 1000, 0));
    f = ticket(&g, 0); assert(lg_accept_ai(&g, &f, 0));
    lg_poll(&g, 21, good); assert(g.mode == LG_FAULT); dark(&g);

    /* Power budget must reject; it must not silently dim the requested cure. */
    g = fresh(0); assert(lg_arm(&g, LG_AI, 1000, 0));
    f = ticket(&g, 0); memset(f.pwm, 255, sizeof(f.pwm));
    assert(!lg_accept_ai(&g, &f, 0)); dark(&g);
    f = ticket(&g, 0); memset(f.pwm, 128, sizeof(f.pwm));
    assert(lg_accept_ai(&g, &f, 0)); assert(g.output_enable);
    for (unsigned i = 61; i < LG_MAX_CHANNELS; ++i) assert(g.pwm[i] == 0);

    /* Manual operation is explicit and requires a fresh, ticketed presence result. */
    uint8_t pwm[LG_MAX_CHANNELS] = {255};
    assert(!lg_set_manual(&g, pwm, 61, 0));
    lg_stop(&g); dark(&g);
    assert(lg_arm(&g, LG_MANUAL, 300, 0));
    f = presence_ticket(&g, 0);
    advance(&g, 0, 199);
    assert(lg_accept_ai(&g, &f, 199));
    assert(g.hand_present && !g.output_enable);
    assert(lg_set_manual(&g, pwm, 61, 199));
    lg_poll(&g, 200, good); dark(&g);
    assert(g.armed && !g.hand_present);
    assert(!lg_set_manual(&g, pwm, 61, 200));
    f = presence_ticket(&g, 200);
    assert(lg_accept_ai(&g, &f, 200));
    assert(lg_set_manual(&g, pwm, 61, 200));
    advance(&g, 200, 99); assert(g.output_enable);
    lg_poll(&g, 300, good); dark(&g); assert(!g.armed && !g.hand_present);

    /* Clock wrap and finite ticket lifetime. */
    uint32_t start = UINT32_MAX - 100;
    g = fresh(start); assert(lg_arm(&g, LG_AI, 1000, start));
    f = ticket(&g, start); assert(lg_accept_ai(&g, &f, start));
    advance(&g, start, 199); assert(g.output_enable);
    lg_poll(&g, start + 200u, good); dark(&g);
    g.ticket_counter = UINT32_MAX;
    assert(!lg_issue_ticket(&g, start + 200u, &f));
    assert(g.mode == LG_FAULT); dark(&g);

    /* Config errors cannot leave previous outputs enabled. */
    lg_config c = config(); c.channel_input_mw[3] = 0;
    assert(!lg_init(&g, &c, 1, 0)); dark(&g);
    c = config(); c.channels = 123;
    assert(!lg_init(&g, &c, 1, 0)); dark(&g);
    c = config(); assert(!lg_init(&g, &c, 0, 0)); dark(&g);
    c = config(); c.channels = 122;
    assert(lg_init(&g, &c, 9, 0)); lg_poll(&g, 0, good);
    assert(lg_arm(&g, LG_AI, 1000, 0)); f = ticket(&g, 0);
    f.pwm[121] = 255; assert(lg_accept_ai(&g, &f, 0));
    assert(g.pwm[121] == 255);
    lg_stop(&g); dark(&g);

    printf("PASS: %u zero-output invariants plus replay, deadline, budget, mode, sensor, rollover and config checks.\n", checks);
    return 0;
}
