// ============================================================================
// Native Unity tests — wifi_odid_scanner.cpp (pure parsing section only)
//
//   pio test -e native -f test_wifi_odid
//
// Section 2 (ESP32 integration) is #ifdef ARDUINO-gated and not compiled in
// the native env, same pattern as test_ble_odid.
// ============================================================================

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

// wifi_odid_scanner.cpp calls odid_extract_basic_id_serial/odid_decode_location,
// which are defined in ble_odid_scanner.cpp's pure-parsing section (its
// Section 2, NimBLE-only, is #ifdef PDL_BLE_COMPANION_ENABLED-gated out here,
// same as in test_ble_odid).
#include "../../src/ble_odid_scanner.cpp"
#include "../../src/wifi_odid_scanner.cpp"

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

static uint8_t odid_header_byte(uint8_t msg_type, uint8_t proto_version) {
    return (uint8_t)((proto_version << 4) | (msg_type & 0x0F));
}

static void build_basic_id_message(uint8_t* buf, const char* serial) {
    memset(buf, 0, 25);
    buf[0] = odid_header_byte(0x0, 0x00);
    buf[1] = (0x01 << 4) | 0x02;
    strncpy((char*)&buf[2], serial, 20);
}

static void build_location_message(uint8_t* buf, double lat, double lon,
                                    float alt_m, float speed_ms, uint16_t heading) {
    memset(buf, 0, 25);
    buf[0] = odid_header_byte(0x1, 0x00);
    buf[1] = 0x02;
    buf[2] = heading & 0xFF;
    buf[3] = (heading >> 8) & 0x01;
    buf[4] = (uint8_t)(speed_ms * 4.0f);
    buf[5] = 1;

    int32_t lat_raw = (int32_t)(lat * 1e7);
    buf[6] = lat_raw & 0xFF; buf[7] = (lat_raw >> 8) & 0xFF;
    buf[8] = (lat_raw >> 16) & 0xFF; buf[9] = (lat_raw >> 24) & 0xFF;

    int32_t lon_raw = (int32_t)(lon * 1e7);
    buf[10] = lon_raw & 0xFF; buf[11] = (lon_raw >> 8) & 0xFF;
    buf[12] = (lon_raw >> 16) & 0xFF; buf[13] = (lon_raw >> 24) & 0xFF;

    uint16_t geo_alt = (uint16_t)((alt_m + 1000.0f) * 2.0f);
    buf[16] = geo_alt & 0xFF; buf[17] = (geo_alt >> 8) & 0xFF;
}

// Build a minimal synthetic beacon frame: 36-byte fixed header (content
// doesn't matter beyond frame[0]=0x80) + a vendor-specific IE (tag 0xDD)
// carrying the given OUI, followed immediately by an ODID message body.
// Returns total frame length.
static size_t build_beacon_with_vendor_ie(uint8_t* out,
                                           uint8_t oui0, uint8_t oui1, uint8_t oui2,
                                           const uint8_t* odid_msg, size_t odid_len) {
    memset(out, 0, WIFI_BEACON_IE_START);
    out[0] = WIFI_FRAME_CTRL_BEACON;

    size_t i = WIFI_BEACON_IE_START;
    out[i++] = WIFI_IE_TAG_VENDOR;
    size_t ie_payload_len = 3 /*OUI*/ + 1 /*type byte*/ + odid_len;
    out[i++] = (uint8_t)ie_payload_len;
    out[i++] = oui0; out[i++] = oui1; out[i++] = oui2;
    // Vendor "type" byte. Low nibble deliberately != 0x0/0x1 so it can't be
    // mistaken by the brute-force header scan for a Basic ID / Location
    // message header (0x00's low nibble is 0x0, which collides with
    // Basic ID type — found the hard way when this was 0x00).
    out[i++] = 0xC5;
    memcpy(&out[i], odid_msg, odid_len);
    i += odid_len;
    return i;
}

// ---------------------------------------------------------------------------
// wifi_odid_classify_oui
// ---------------------------------------------------------------------------

static void test_classify_dji_oui() {
    uint8_t basic_id[25];
    build_basic_id_message(basic_id, "1581F0000000000001");
    uint8_t frame[80];
    size_t len = build_beacon_with_vendor_ie(frame, 0x60, 0x60, 0x1F, basic_id, sizeof(basic_id));

    TEST_ASSERT_EQUAL_UINT8(ODID_VENDOR_DJI, wifi_odid_classify_oui(frame, len));
}

static void test_classify_skydio_oui() {
    uint8_t basic_id[25];
    build_basic_id_message(basic_id, "SK-TESTUNIT1234567");
    uint8_t frame[80];
    size_t len = build_beacon_with_vendor_ie(frame, 0x04, 0xE5, 0x48, basic_id, sizeof(basic_id));

    TEST_ASSERT_EQUAL_UINT8(ODID_VENDOR_SKYDIO, wifi_odid_classify_oui(frame, len));
}

static void test_classify_unknown_oui_returns_unknown() {
    uint8_t basic_id[25];
    build_basic_id_message(basic_id, "NOTADRONE00000000000");
    uint8_t frame[80];
    size_t len = build_beacon_with_vendor_ie(frame, 0x11, 0x22, 0x33, basic_id, sizeof(basic_id));

    TEST_ASSERT_EQUAL_UINT8(ODID_VENDOR_UNKNOWN, wifi_odid_classify_oui(frame, len));
}

static void test_classify_non_beacon_frame_ignored() {
    uint8_t basic_id[25];
    build_basic_id_message(basic_id, "1581F0000000000001");
    uint8_t frame[80];
    size_t len = build_beacon_with_vendor_ie(frame, 0x60, 0x60, 0x1F, basic_id, sizeof(basic_id));
    frame[0] = 0x40;  // Probe Request, not a Beacon

    TEST_ASSERT_EQUAL_UINT8(ODID_VENDOR_UNKNOWN, wifi_odid_classify_oui(frame, len));
}

// ---------------------------------------------------------------------------
// wifi_odid_parse_beacon
// ---------------------------------------------------------------------------

static void test_parse_beacon_basic_id() {
    uint8_t basic_id[25];
    build_basic_id_message(basic_id, "1581F0000000000001");
    uint8_t frame[80];
    size_t len = build_beacon_with_vendor_ie(frame, 0x60, 0x60, 0x1F, basic_id, sizeof(basic_id));

    WifiOdidParseResult r = wifi_odid_parse_beacon(frame, len);

    TEST_ASSERT_TRUE(r.hit);
    TEST_ASSERT_EQUAL_UINT8(ODID_VENDOR_DJI, r.vendor);
    TEST_ASSERT_TRUE(r.has_serial);
    TEST_ASSERT_EQUAL_STRING("1581F0000000000001", r.serial);
    TEST_ASSERT_FALSE(r.location.valid);
}

static void test_parse_beacon_location() {
    uint8_t loc_msg[25];
    build_location_message(loc_msg, 37.4602, 13.2264, 620.0f, 12.0f, 180);
    uint8_t frame[80];
    size_t len = build_beacon_with_vendor_ie(frame, 0x70, 0x38, 0x69, loc_msg, sizeof(loc_msg));

    WifiOdidParseResult r = wifi_odid_parse_beacon(frame, len);

    TEST_ASSERT_TRUE(r.hit);
    TEST_ASSERT_EQUAL_UINT8(ODID_VENDOR_AUTEL, r.vendor);
    TEST_ASSERT_FALSE(r.has_serial);
    TEST_ASSERT_TRUE(r.location.valid);
    TEST_ASSERT_FLOAT_WITHIN(1e-4f, 37.4602f, (float)r.location.lat_deg);
    TEST_ASSERT_FLOAT_WITHIN(1e-4f, 13.2264f, (float)r.location.lon_deg);
    TEST_ASSERT_FLOAT_WITHIN(0.5f, 620.0f, r.location.alt_m);
}

static void test_parse_beacon_no_vendor_ie_no_hit() {
    uint8_t frame[40];
    memset(frame, 0, sizeof(frame));
    frame[0] = WIFI_FRAME_CTRL_BEACON;
    // No IE at all.

    WifiOdidParseResult r = wifi_odid_parse_beacon(frame, sizeof(frame));
    TEST_ASSERT_FALSE(r.hit);
}

static void test_parse_beacon_truncated_frame_safe() {
    uint8_t frame[10] = {0};
    frame[0] = WIFI_FRAME_CTRL_BEACON;

    WifiOdidParseResult r = wifi_odid_parse_beacon(frame, sizeof(frame));
    TEST_ASSERT_FALSE(r.hit);
}

static void test_parse_beacon_null_frame_safe() {
    WifiOdidParseResult r = wifi_odid_parse_beacon(nullptr, 0);
    TEST_ASSERT_FALSE(r.hit);
}

// ---------------------------------------------------------------------------
// Unity entrypoints
// ---------------------------------------------------------------------------

void setUp() {}
void tearDown() {}

int main(int argc, char** argv) {
    UNITY_BEGIN();
    RUN_TEST(test_classify_dji_oui);
    RUN_TEST(test_classify_skydio_oui);
    RUN_TEST(test_classify_unknown_oui_returns_unknown);
    RUN_TEST(test_classify_non_beacon_frame_ignored);
    RUN_TEST(test_parse_beacon_basic_id);
    RUN_TEST(test_parse_beacon_location);
    RUN_TEST(test_parse_beacon_no_vendor_ie_no_hit);
    RUN_TEST(test_parse_beacon_truncated_frame_safe);
    RUN_TEST(test_parse_beacon_null_frame_safe);
    return UNITY_END();
}
