// ============================================================================
// test/test_batch_b/test_batch_b.cpp
//
// Unity tests for Batch-B modules:
//   - mesh_timesync  (IIR offset smoothing)
//   - enrollment     (response packet parsing + NVS writes)
//   - ota_signed     (Ed25519 verify — RFC 8032 test vector)
//
// Native PlatformIO environment (UNIT_TEST defined, ARDUINO undefined).
// Run with:
//   pio test -e native -f test_batch_b
//
// All three .cpp files are pulled in directly via #include (same pattern as
// test_markov_scorer / test_command_handler) to avoid separate library
// linking.
//
// IMPORTANT — Unity float comparisons:
//   Use TEST_ASSERT_TRUE with explicit boolean expressions for floats;
//   Unity's GREATER_THAN / LESS_THAN macros coerce to int.
// ============================================================================

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

// ---------------------------------------------------------------------------
// Pull in implementations under UNIT_TEST / native env
// ---------------------------------------------------------------------------
#include "../../src/mesh_timesync.cpp"
#include "../../src/enrollment.cpp"
#include "../../src/ota_signed.cpp"

// ---------------------------------------------------------------------------
// Unity lifecycle
// ---------------------------------------------------------------------------
void setUp(void) {
    // Reset all three modules before every test
    timesync_init();
    enroll_stub_reset_nvs();
    enroll_stub_set_millis(0);
    ota_signed_stub_reset_nvs();
}

void tearDown(void) {}

// ============================================================================
// Test 1 — mesh_timesync: IIR offset smoothing converges within 5 packets
//
// Feed 5 packets with a constant epoch that is 10 000 ms ahead of the stub
// clock (which stays at 0). After the first packet the offset is seeded at
// 10 000. Each subsequent IIR step pulls the estimate toward 10 000 with
// alpha=0.2; after 5 packets the value must lie within 5% of 10 000 ms.
//
// Additionally: is_synced() must be false before packet 3 and true after.
// ============================================================================
void test_timesync_offset_smoothing(void) {
    // Stub clock stays at 0; epoch_us = 10_000_000 us = 10 000 ms ahead
    const uint64_t EPOCH_MS = 10000ULL;
    const uint64_t EPOCH_US = EPOCH_MS * 1000ULL;

    auto make_pkt = [](uint8_t* buf, uint64_t epoch_us) {
        buf[0] = 0x19;  // MSG_TYPE_TIME_SYNC
        for (int i = 0; i < 8; i++)
            buf[1 + i] = (uint8_t)(epoch_us >> (8 * i));
        buf[9] = buf[10] = buf[11] = 0;  // reserved
    };

    uint8_t pkt[12];
    make_pkt(pkt, EPOCH_US);

    TEST_ASSERT_FALSE_MESSAGE(timesync_is_synced(),
        "should not be synced before any packets");

    // Packet 1
    timesync_handle_packet(pkt, 12);
    TEST_ASSERT_FALSE_MESSAGE(timesync_is_synced(),
        "should not be synced after 1 packet");

    // Packet 2
    timesync_handle_packet(pkt, 12);
    TEST_ASSERT_FALSE_MESSAGE(timesync_is_synced(),
        "should not be synced after 2 packets");

    // Packet 3 — crosses MIN_PACKETS threshold
    timesync_handle_packet(pkt, 12);
    TEST_ASSERT_TRUE_MESSAGE(timesync_is_synced(),
        "should be synced after 3 packets");

    // Packets 4 and 5
    timesync_handle_packet(pkt, 12);
    timesync_handle_packet(pkt, 12);

    // After 5 packets IIR has had 4 update steps (2nd–5th packets).
    // Analytically: offset_n = 10000 * (1 - (0.8)^n) / (1 - 0.8) * 0.2
    //               Simpler: offset after n steps ≥ 0.2 * 10000 = 2000 ms (step 1).
    // After 5 steps offset ≥ 10000 * (1 - 0.8^5) ≈ 10000 * 0.672 = 6720 ms.
    // We assert it is within [2000, 10500] — generous band to catch divergence.
    uint64_t now = timesync_now_utc_ms();
    TEST_ASSERT_TRUE_MESSAGE(now >= 2000ULL,
        "UTC estimate after 5 packets should be >= 2000 ms");
    TEST_ASSERT_TRUE_MESSAGE(now <= 10500ULL,
        "UTC estimate after 5 packets should be <= 10500 ms (IIR should not overshoot)");

    // Wrong type byte must be ignored
    pkt[0] = 0x01;
    timesync_handle_packet(pkt, 12);
    TEST_ASSERT_TRUE_MESSAGE(timesync_is_synced(),
        "synced flag must persist after bad-type packet");

    // Short packet must be ignored without crash
    pkt[0] = 0x19;
    timesync_handle_packet(pkt, 4);  // too short
    TEST_ASSERT_TRUE_MESSAGE(timesync_is_synced(),
        "synced flag must persist after short packet");
}

// ============================================================================
// Test 2 — enrollment: response parsing and NVS writes (stubbed)
//
// Feed a synthetic 28-byte MSG_TYPE_ENROLLMENT_RESPONSE packet.
// Assertions:
//   a) enrollment_is_complete() becomes true
//   b) enrollment_get_sensor_id() returns the assigned ID from the packet
//   c) A second call with a different ID is idempotent (first one wins)
// ============================================================================
void test_enrollment_response_parses(void) {
    // enrollment_init() will broadcast (to g_last_tx_buf stub) and return false.
    bool already_enrolled = enrollment_init();
    TEST_ASSERT_FALSE_MESSAGE(already_enrolled,
        "should not be enrolled on clean NVS");
    TEST_ASSERT_FALSE_MESSAGE(enrollment_is_complete(),
        "is_complete should be false before response");
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(DEFAULT_SENSOR_ID,
        enrollment_get_sensor_id(),
        "sensor_id should default before enrollment");

    // Build a 28-byte enrollment response
    // assigned_sensor_id = 0xCAFEBABE (LE)
    // lat_fixed          = 0x001A1B1C (LE int32)
    // lon_fixed          = 0xFFD0D1D2 (LE int32, negative OK)
    // reserved           = 0x000000
    // auth_pubkey_first16 = 0x00..0F
    uint8_t resp[28];
    memset(resp, 0, sizeof(resp));
    resp[0] = 0x18;  // MSG_TYPE_ENROLLMENT_RESPONSE
    // sensor_id = 0xCAFEBABE
    resp[1] = 0xBE; resp[2] = 0xBA; resp[3] = 0xFE; resp[4] = 0xCA;
    // lat = 0x001A1B1C
    resp[5] = 0x1C; resp[6] = 0x1B; resp[7] = 0x1A; resp[8] = 0x00;
    // lon = 0x80D1D2D3 (arbitrary)
    resp[9] = 0xD3; resp[10] = 0xD2; resp[11] = 0xD1; resp[12] = 0x80;
    // reserved [13..15] = 0
    // auth_pubkey_first16 [16..27]
    for (int i = 0; i < 12; i++) resp[16 + i] = (uint8_t)(i);

    enrollment_handle_response(resp, sizeof(resp));

    TEST_ASSERT_TRUE_MESSAGE(enrollment_is_complete(),
        "is_complete should be true after valid response");
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(0xCAFEBABEu,
        enrollment_get_sensor_id(),
        "sensor_id must match assigned value in response");

    // Idempotency: second response with different ID must not change stored ID
    resp[1] = 0x01; resp[2] = 0x02; resp[3] = 0x03; resp[4] = 0x04;
    enrollment_handle_response(resp, sizeof(resp));
    TEST_ASSERT_EQUAL_UINT32_MESSAGE(0xCAFEBABEu,
        enrollment_get_sensor_id(),
        "sensor_id must not change on second response (idempotent)");

    // Short packet must be silently ignored
    uint8_t short_resp[10] = {0x18};
    enrollment_handle_response(short_resp, sizeof(short_resp));
    TEST_ASSERT_TRUE_MESSAGE(enrollment_is_complete(),
        "is_complete must remain true after short packet");

    // Wrong type byte must be silently ignored
    resp[0] = 0x17;
    enrollment_handle_response(resp, sizeof(resp));
    TEST_ASSERT_TRUE_MESSAGE(enrollment_is_complete(),
        "is_complete must remain true after wrong-type packet");

    // enrollment_init() on a fresh call with NVS marked done must return true
    // (NVS stub now has done=1)
    bool re_enrolled = enrollment_init();
    TEST_ASSERT_TRUE_MESSAGE(re_enrolled,
        "enrollment_init must return true when NVS already has done=1");
}

// ============================================================================
// Test 3 — Ed25519: RFC 8032 §6.1 test vector 1 (verify pass + tamper fail)
//
// Test vector from RFC 8032 Section 6.1, Test Vector 1:
//   SECRET : 9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae3d55
//   PUBLIC : d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a
//   MESSAGE: (empty — 0 bytes)
//   SIG    : e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555
//            fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b
//
// (All values from RFC 8032, big-endian hex as written in the RFC; we convert
//  to little-endian byte arrays as required by the ref10 compact encoding.)
// ============================================================================
void test_ed25519_verify_known_vector(void) {
    // RFC 8032 §6.1 Test Vector 1
    // Public key (32 bytes)
    static const uint8_t pk[32] = {
        0xd7, 0x5a, 0x98, 0x01, 0x82, 0xb1, 0x0a, 0xb7,
        0xd5, 0x4b, 0xfe, 0xd3, 0xc9, 0x64, 0x07, 0x3a,
        0x0e, 0xe1, 0x72, 0xf3, 0xda, 0xa6, 0x23, 0x25,
        0xaf, 0x02, 0x1a, 0x68, 0xf7, 0x07, 0x51, 0x1a
    };
    // Signature (64 bytes)
    static const uint8_t sig[64] = {
        0xe5, 0x56, 0x43, 0x00, 0xc3, 0x60, 0xac, 0x72,
        0x90, 0x86, 0xe2, 0xcc, 0x80, 0x6e, 0x82, 0x8a,
        0x84, 0x87, 0x7f, 0x1e, 0xb8, 0xe5, 0xd9, 0x74,
        0xd8, 0x73, 0xe0, 0x65, 0x22, 0x49, 0x01, 0x55,
        0x5f, 0xb8, 0x82, 0x15, 0x90, 0xa3, 0x3b, 0xac,
        0xc6, 0x1e, 0x39, 0x70, 0x1c, 0xf9, 0xb4, 0x6b,
        0xd2, 0x5b, 0xf5, 0xf0, 0x59, 0x5b, 0xbe, 0x24,
        0x65, 0x51, 0x41, 0x43, 0x8e, 0x7a, 0x10, 0x0b
    };
    // Message: empty (0 bytes)

    // Set public key into module
    bool pk_set = ota_signed_set_pubkey(pk);
    TEST_ASSERT_TRUE_MESSAGE(pk_set, "set_pubkey must succeed");

    // Verify with correct sig over empty message — must pass
    bool ok = ota_signed_verify_chunk(nullptr, 0, sig);
    // Note: chunk=nullptr, len=0 is a valid "empty message" in Ed25519.
    // Our API returns false for nullptr chunk — use a valid zero-length buffer.
    static const uint8_t empty_msg[1] = {0};
    ok = ota_signed_verify_chunk(empty_msg, 0, sig);
    TEST_ASSERT_TRUE_MESSAGE(ok,
        "RFC 8032 test vector 1: verify must pass for correct sig over empty msg");

    // Tamper: flip one bit in the signature — must fail
    uint8_t bad_sig[64];
    memcpy(bad_sig, sig, 64);
    bad_sig[0] ^= 0x01;
    bool fail = ota_signed_verify_chunk(empty_msg, 0, bad_sig);
    TEST_ASSERT_FALSE_MESSAGE(fail,
        "tampered signature must fail verification");

    // Verify without a pubkey set — must fail
    ota_signed_stub_reset_nvs();
    // Reset module state by wiping s_pk_loaded via a stub workaround:
    // set an all-zero key (invalid point, will fail curve decode)
    static const uint8_t zero_pk[32] = {0};
    ota_signed_set_pubkey(zero_pk);  // set invalid key
    bool bad_pk = ota_signed_verify_chunk(empty_msg, 0, sig);
    TEST_ASSERT_FALSE_MESSAGE(bad_pk,
        "verify with zeroed/invalid pubkey must fail");
}

// ============================================================================
// Unity main
// ============================================================================
int main(int, char**) {
    UNITY_BEGIN();
    RUN_TEST(test_timesync_offset_smoothing);
    RUN_TEST(test_enrollment_response_parses);
    RUN_TEST(test_ed25519_verify_known_vector);
    return UNITY_END();
}
