// ============================================================================
// test/test_wifi_extended_scan/test_wifi_extended_scan.cpp
//
// Unity tests for src/wifi_extended_scan.{h,cpp} -- the 5 additional
// WiFi-space capabilities (RTT/FTM, KRACK, PMKID-burst, WPA3 downgrade,
// rogue DHCP/DNS, channel-hop). Pure classifiers are cross-checked against
// known-real IEEE 802.11 field values (same standard-compliant values
// Wireshark/aircrack-ng/hashcat use to classify real captured handshakes);
// frame-processing entry points are exercised with hand-built synthetic
// frames matching the real wire format byte-for-byte.
//
// Run with:
//   pio test -e native -f test_wifi_extended_scan
// ============================================================================

#include <unity.h>
#include <cstring>
#include "../../src/wifi_extended_scan.cpp"

void setUp(void) { wifi_extended_scan_init(); }
void tearDown(void) {}

// ============================================================================
// Pure classifiers
// ============================================================================

void test_ftm_action_frame_recognized(void) {
    TEST_ASSERT_TRUE(wifi_ext_is_ftm_action_frame(4, 32));   // FTM Request
    TEST_ASSERT_TRUE(wifi_ext_is_ftm_action_frame(4, 33));   // FTM
    TEST_ASSERT_TRUE(wifi_ext_is_ftm_action_frame(5, 0));    // Radio Measurement
}

void test_ftm_action_frame_rejects_unrelated(void) {
    TEST_ASSERT_FALSE(wifi_ext_is_ftm_action_frame(4, 0));   // unrelated public action
    TEST_ASSERT_FALSE(wifi_ext_is_ftm_action_frame(3, 32));  // wrong category
    TEST_ASSERT_FALSE(wifi_ext_is_ftm_action_frame(0, 0));
}

// Real key_info bit patterns for each of the 4 handshake messages
// (bit3=pairwise, bit6=install, bit7=ack, bit8=mic, bit9=secure).
void test_eapol_classifies_message_1(void) {
    uint16_t key_info = (1 << 3) | (1 << 7);   // pairwise, ack, no mic
    TEST_ASSERT_EQUAL(EAPOL_MSG_1, eapol_classify_key_info(key_info));
}

void test_eapol_classifies_message_2(void) {
    uint16_t key_info = (1 << 3) | (1 << 8);   // pairwise, mic, no ack, no secure
    TEST_ASSERT_EQUAL(EAPOL_MSG_2, eapol_classify_key_info(key_info));
}

void test_eapol_classifies_message_3(void) {
    uint16_t key_info = (1 << 3) | (1 << 6) | (1 << 7) | (1 << 8) | (1 << 9);
    TEST_ASSERT_EQUAL(EAPOL_MSG_3, eapol_classify_key_info(key_info));
}

void test_eapol_classifies_message_4(void) {
    uint16_t key_info = (1 << 3) | (1 << 8) | (1 << 9);   // pairwise, mic, secure, no ack
    TEST_ASSERT_EQUAL(EAPOL_MSG_4, eapol_classify_key_info(key_info));
}

void test_eapol_group_key_handshake_is_unknown(void) {
    // No pairwise bit -- group-key handshake, not the 4-way pairwise one.
    uint16_t key_info = (1 << 7);
    TEST_ASSERT_EQUAL(EAPOL_MSG_UNKNOWN, eapol_classify_key_info(key_info));
}

void test_channel_hop_wraps_13_to_1(void) {
    TEST_ASSERT_EQUAL(1, wifi_ext_next_channel(13));
}

void test_channel_hop_increments_normally(void) {
    TEST_ASSERT_EQUAL(2, wifi_ext_next_channel(1));
    TEST_ASSERT_EQUAL(7, wifi_ext_next_channel(6));
}

void test_channel_hop_recovers_from_out_of_range(void) {
    TEST_ASSERT_EQUAL(1, wifi_ext_next_channel(0));
    TEST_ASSERT_EQUAL(1, wifi_ext_next_channel(14));
}

// ============================================================================
// Frame-building helpers
// ============================================================================

static void set_mac_header(uint8_t* buf, const uint8_t* addr1,
                           const uint8_t* addr2, const uint8_t* addr3) {
    memset(buf, 0, MAC_HDR_LEN);
    memcpy(buf + 4, addr1, 6);
    memcpy(buf + 10, addr2, 6);
    if (addr3) memcpy(buf + 16, addr3, 6);
}

// ============================================================================
// 1. RTT/FTM integration
// ============================================================================

void test_rtt_action_frame_recorded(void) {
    uint8_t buf[MAC_HDR_LEN + 2] = {0};
    uint8_t bssid[6] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
    set_mac_header(buf, bssid, bssid, bssid);
    buf[MAC_HDR_LEN] = 4;       // category: Public Action
    buf[MAC_HDR_LEN + 1] = 33;  // action: FTM

    wifi_extended_scan_process_frame(buf, sizeof(buf), 0, 0x0D, 1000);

    WifiExtendedScanStats stats;
    wifi_extended_scan_get_stats(&stats);
    TEST_ASSERT_EQUAL(1, stats.rtt_devices_observed);

    RttFinding findings[4];
    size_t n = wifi_ext_list_rtt(findings, 4);
    TEST_ASSERT_EQUAL(1, n);
    TEST_ASSERT_EQUAL_UINT8_ARRAY(bssid, findings[0].bssid, 6);
}

void test_non_ftm_action_frame_not_recorded(void) {
    uint8_t buf[MAC_HDR_LEN + 2] = {0};
    uint8_t bssid[6] = {0x11, 0x22, 0x33, 0x44, 0x55, 0x66};
    set_mac_header(buf, bssid, bssid, bssid);
    buf[MAC_HDR_LEN] = 4;    // category: Public Action
    buf[MAC_HDR_LEN + 1] = 0; // unrelated action code

    wifi_extended_scan_process_frame(buf, sizeof(buf), 0, 0x0D, 1000);

    WifiExtendedScanStats stats;
    wifi_extended_scan_get_stats(&stats);
    TEST_ASSERT_EQUAL(0, stats.rtt_devices_observed);
}

// ============================================================================
// 2. KRACK / PMKID-harvest integration
// ============================================================================

static size_t build_eapol_key_frame(uint8_t* buf, const uint8_t* bssid,
                                    const uint8_t* client, uint16_t key_info,
                                    const uint8_t* nonce32) {
    set_mac_header(buf, client, bssid, bssid);   // addr1=client(rx), addr2=bssid(tx)
    size_t pos = MAC_HDR_LEN;
    memcpy(buf + pos, kEapolSnap, LLC_SNAP_LEN); pos += LLC_SNAP_LEN;
    buf[pos++] = 1;   // EAPOL version
    buf[pos++] = 3;   // EAPOL-Key
    buf[pos++] = 0;   // length hi (unused by parser)
    buf[pos++] = 0;   // length lo
    buf[pos++] = 2;   // descriptor_type
    buf[pos++] = (uint8_t)(key_info >> 8);
    buf[pos++] = (uint8_t)(key_info & 0xFF);
    buf[pos++] = 0; buf[pos++] = 0;              // key_length
    for (int i = 0; i < 8; i++) buf[pos++] = 0;  // replay_counter
    if (nonce32) { memcpy(buf + pos, nonce32, 32); }
    else         { memset(buf + pos, 0, 32); }
    pos += 32;
    // Pad remaining EAPOL-Key body fields so key_body_len >= EAPOL_KEY_MIN_LEN.
    while (pos - (MAC_HDR_LEN + LLC_SNAP_LEN + EAPOL_HDR_LEN) < EAPOL_KEY_MIN_LEN) {
        buf[pos++] = 0;
    }
    return pos;
}

void test_pmkid_burst_flagged_after_threshold(void) {
    uint8_t buf[256];
    uint8_t bssid[6] = {0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA};
    uint16_t msg1_info = (1 << 3) | (1 << 7);   // pairwise + ack, no mic

    for (unsigned i = 0; i < WIFI_EXT_PMKID_BURST_THRESHOLD; i++) {
        uint8_t client[6] = {0x00, 0x00, 0x00, 0x00, 0x00, (uint8_t)i};
        size_t len = build_eapol_key_frame(buf, bssid, client, msg1_info, nullptr);
        wifi_extended_scan_process_frame(buf, len, 2, 0, 1000 + i);
    }

    WifiExtendedScanStats stats;
    wifi_extended_scan_get_stats(&stats);
    TEST_ASSERT_EQUAL(1, stats.pmkid_burst_suspects);
}

void test_pmkid_burst_not_flagged_below_threshold(void) {
    uint8_t buf[256];
    uint8_t bssid[6] = {0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA};
    uint16_t msg1_info = (1 << 3) | (1 << 7);

    for (unsigned i = 0; i < WIFI_EXT_PMKID_BURST_THRESHOLD - 1; i++) {
        uint8_t client[6] = {0x00, 0x00, 0x00, 0x00, 0x00, (uint8_t)i};
        size_t len = build_eapol_key_frame(buf, bssid, client, msg1_info, nullptr);
        wifi_extended_scan_process_frame(buf, len, 2, 0, 1000 + i);
    }

    WifiExtendedScanStats stats;
    wifi_extended_scan_get_stats(&stats);
    TEST_ASSERT_EQUAL(0, stats.pmkid_burst_suspects);
}

void test_krack_nonce_reuse_flagged(void) {
    uint8_t buf[256];
    uint8_t bssid[6]  = {0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB};
    uint8_t client[6] = {0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC};
    uint16_t msg3_info = (1 << 3) | (1 << 6) | (1 << 7) | (1 << 8) | (1 << 9);
    uint8_t nonce[32];
    for (int i = 0; i < 32; i++) nonce[i] = (uint8_t)(i * 3 + 1);

    size_t len1 = build_eapol_key_frame(buf, bssid, client, msg3_info, nonce);
    wifi_extended_scan_process_frame(buf, len1, 2, 0, 1000);

    // Real KRACK: the SAME nonce is replayed for the same (BSSID, client).
    size_t len2 = build_eapol_key_frame(buf, bssid, client, msg3_info, nonce);
    wifi_extended_scan_process_frame(buf, len2, 2, 0, 1500);

    WifiExtendedScanStats stats;
    wifi_extended_scan_get_stats(&stats);
    TEST_ASSERT_EQUAL(1, stats.krack_suspects);
}

void test_legitimate_m3_with_different_nonce_not_flagged(void) {
    uint8_t buf[256];
    uint8_t bssid[6]  = {0xBB, 0xBB, 0xBB, 0xBB, 0xBB, 0xBB};
    uint8_t client[6] = {0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC};
    uint16_t msg3_info = (1 << 3) | (1 << 6) | (1 << 7) | (1 << 8) | (1 << 9);
    uint8_t nonce_a[32], nonce_b[32];
    for (int i = 0; i < 32; i++) { nonce_a[i] = (uint8_t)i; nonce_b[i] = (uint8_t)(i + 1); }

    size_t len1 = build_eapol_key_frame(buf, bssid, client, msg3_info, nonce_a);
    wifi_extended_scan_process_frame(buf, len1, 2, 0, 1000);
    size_t len2 = build_eapol_key_frame(buf, bssid, client, msg3_info, nonce_b);
    wifi_extended_scan_process_frame(buf, len2, 2, 0, 1500);

    WifiExtendedScanStats stats;
    wifi_extended_scan_get_stats(&stats);
    TEST_ASSERT_EQUAL(0, stats.krack_suspects);
}

// ============================================================================
// 3. WPA3 downgrade integration
// ============================================================================

static size_t build_beacon_with_rsn(uint8_t* buf, const uint8_t* bssid,
                                    const char* ssid, bool include_sae) {
    set_mac_header(buf, bssid, bssid, bssid);
    size_t pos = MAC_HDR_LEN;
    // Fixed fields: timestamp[8] + interval[2] + cap_info[2]
    memset(buf + pos, 0, 12); pos += 12;
    // SSID IE
    size_t ssid_len = strlen(ssid);
    buf[pos++] = 0; buf[pos++] = (uint8_t)ssid_len;
    memcpy(buf + pos, ssid, ssid_len); pos += ssid_len;
    // RSN IE
    size_t rsn_start = pos;
    buf[pos++] = 48;   // element id
    size_t rsn_len_pos = pos++;   // fill length after
    size_t rsn_body_start = pos;
    buf[pos++] = 1; buf[pos++] = 0;              // version = 1 (LE)
    buf[pos++] = 0x00; buf[pos++] = 0x0F; buf[pos++] = 0xAC; buf[pos++] = 4;  // group cipher CCMP
    buf[pos++] = 1; buf[pos++] = 0;               // pairwise count = 1
    buf[pos++] = 0x00; buf[pos++] = 0x0F; buf[pos++] = 0xAC; buf[pos++] = 4;  // pairwise CCMP
    uint16_t akm_count = include_sae ? 1 : 1;
    buf[pos++] = (uint8_t)akm_count; buf[pos++] = 0;
    buf[pos++] = 0x00; buf[pos++] = 0x0F; buf[pos++] = 0xAC;
    buf[pos++] = include_sae ? 8 : 2;             // 8=SAE, 2=PSK
    buf[rsn_len_pos] = (uint8_t)(pos - rsn_body_start);
    (void)rsn_start;
    return pos;
}

void test_wpa3_downgrade_detected_after_sae_disappears(void) {
    uint8_t buf[256];
    uint8_t bssid[6] = {0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD};

    size_t len1 = build_beacon_with_rsn(buf, bssid, "TestNet", /*include_sae=*/true);
    wifi_extended_scan_process_frame(buf, len1, 0, 0x08, 1000);

    size_t len2 = build_beacon_with_rsn(buf, bssid, "TestNet", /*include_sae=*/false);
    wifi_extended_scan_process_frame(buf, len2, 0, 0x08, 2000);

    WifiExtendedScanStats stats;
    wifi_extended_scan_get_stats(&stats);
    TEST_ASSERT_EQUAL(1, stats.wpa3_downgrade_suspects);

    Wpa3DowngradeFinding findings[4];
    size_t n = wifi_ext_list_wpa3_downgrades(findings, 4);
    TEST_ASSERT_EQUAL(1, n);
    TEST_ASSERT_EQUAL_STRING("TestNet", findings[0].ssid);
}

void test_wpa3_no_downgrade_when_sae_persists(void) {
    uint8_t buf[256];
    uint8_t bssid[6] = {0xEE, 0xEE, 0xEE, 0xEE, 0xEE, 0xEE};

    size_t len1 = build_beacon_with_rsn(buf, bssid, "SecureNet", true);
    wifi_extended_scan_process_frame(buf, len1, 0, 0x08, 1000);
    size_t len2 = build_beacon_with_rsn(buf, bssid, "SecureNet", true);
    wifi_extended_scan_process_frame(buf, len2, 0, 0x08, 2000);

    WifiExtendedScanStats stats;
    wifi_extended_scan_get_stats(&stats);
    TEST_ASSERT_EQUAL(0, stats.wpa3_downgrade_suspects);
}

void test_wpa3_no_false_positive_for_always_psk_network(void) {
    uint8_t buf[256];
    uint8_t bssid[6] = {0xFF, 0xEE, 0xDD, 0xCC, 0xBB, 0xAA};

    // Never advertised SAE at all -- a plain WPA2-PSK network is not a
    // "downgrade," it just never had WPA3 to begin with.
    size_t len1 = build_beacon_with_rsn(buf, bssid, "OldRouter", false);
    wifi_extended_scan_process_frame(buf, len1, 0, 0x08, 1000);
    size_t len2 = build_beacon_with_rsn(buf, bssid, "OldRouter", false);
    wifi_extended_scan_process_frame(buf, len2, 0, 0x08, 2000);

    WifiExtendedScanStats stats;
    wifi_extended_scan_get_stats(&stats);
    TEST_ASSERT_EQUAL(0, stats.wpa3_downgrade_suspects);
}

// ============================================================================
// 4. Rogue DHCP / DNS hijack integration
// ============================================================================

static size_t build_dhcp_offer(uint8_t* buf, const uint8_t* server_mac,
                               const uint8_t* client_mac, uint32_t xid) {
    set_mac_header(buf, client_mac, server_mac, nullptr);
    size_t pos = MAC_HDR_LEN;
    static const uint8_t kIpv4Snap[8] = {0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00, 0x08, 0x00};
    memcpy(buf + pos, kIpv4Snap, 8); pos += 8;
    // Minimal IPv4 header: IHL=5 (20 bytes), protocol=17 (UDP)
    memset(buf + pos, 0, 20);
    buf[pos] = 0x45;       // version=4, IHL=5
    buf[pos + 9] = 17;     // protocol = UDP
    pos += 20;
    // UDP header: src=67, dst=68
    buf[pos++] = 0; buf[pos++] = 67;
    buf[pos++] = 0; buf[pos++] = 68;
    buf[pos++] = 0; buf[pos++] = 0;   // length (unused by parser)
    buf[pos++] = 0; buf[pos++] = 0;   // checksum (unused)
    // BOOTP payload: op=2 (BOOTREPLY), then padding to xid at offset 4
    buf[pos] = 2;
    buf[pos + 1] = 1; buf[pos + 2] = 6; buf[pos + 3] = 0;
    buf[pos + 4] = (uint8_t)(xid >> 24);
    buf[pos + 5] = (uint8_t)(xid >> 16);
    buf[pos + 6] = (uint8_t)(xid >> 8);
    buf[pos + 7] = (uint8_t)(xid & 0xFF);
    pos += 8;
    return pos;
}

void test_rogue_dhcp_flagged_for_two_servers(void) {
    uint8_t buf[128];
    uint8_t client[6]   = {0x01, 0x01, 0x01, 0x01, 0x01, 0x01};
    uint8_t server_a[6] = {0x02, 0x02, 0x02, 0x02, 0x02, 0x02};
    uint8_t server_b[6] = {0x03, 0x03, 0x03, 0x03, 0x03, 0x03};
    uint32_t xid = 0x12345678;

    size_t len1 = build_dhcp_offer(buf, server_a, client, xid);
    wifi_extended_scan_process_frame(buf, len1, 2, 0, 1000);
    size_t len2 = build_dhcp_offer(buf, server_b, client, xid);
    wifi_extended_scan_process_frame(buf, len2, 2, 0, 1010);

    WifiExtendedScanStats stats;
    wifi_extended_scan_get_stats(&stats);
    TEST_ASSERT_EQUAL(1, stats.rogue_dhcp_suspects);
}

void test_single_dhcp_server_not_flagged(void) {
    uint8_t buf[128];
    uint8_t client[6]   = {0x01, 0x01, 0x01, 0x01, 0x01, 0x01};
    uint8_t server_a[6] = {0x02, 0x02, 0x02, 0x02, 0x02, 0x02};
    uint32_t xid = 0xAABBCCDD;

    size_t len1 = build_dhcp_offer(buf, server_a, client, xid);
    wifi_extended_scan_process_frame(buf, len1, 2, 0, 1000);
    // Same server replying again (e.g. a retransmit) must not double-count
    // as a second distinct server.
    size_t len2 = build_dhcp_offer(buf, server_a, client, xid);
    wifi_extended_scan_process_frame(buf, len2, 2, 0, 1010);

    WifiExtendedScanStats stats;
    wifi_extended_scan_get_stats(&stats);
    TEST_ASSERT_EQUAL(0, stats.rogue_dhcp_suspects);
}

static size_t build_dns_response(uint8_t* buf, const uint8_t* server_mac,
                                 const uint8_t* client_mac, uint16_t qid,
                                 uint8_t answer_fill) {
    set_mac_header(buf, client_mac, server_mac, nullptr);
    size_t pos = MAC_HDR_LEN;
    static const uint8_t kIpv4Snap[8] = {0xAA, 0xAA, 0x03, 0x00, 0x00, 0x00, 0x08, 0x00};
    memcpy(buf + pos, kIpv4Snap, 8); pos += 8;
    memset(buf + pos, 0, 20);
    buf[pos] = 0x45;
    buf[pos + 9] = 17;
    pos += 20;
    buf[pos++] = 0; buf[pos++] = 53;   // src port 53
    buf[pos++] = 0xC0; buf[pos++] = 0; // dst port (arbitrary high port)
    buf[pos++] = 0; buf[pos++] = 0;
    buf[pos++] = 0; buf[pos++] = 0;
    // DNS payload: qid[2] + 10 header bytes + answer tail (distinguishing fill byte)
    buf[pos++] = (uint8_t)(qid >> 8);
    buf[pos++] = (uint8_t)(qid & 0xFF);
    for (int i = 0; i < 10; i++) buf[pos++] = 0;
    for (int i = 0; i < 16; i++) buf[pos++] = answer_fill;
    return pos;
}

void test_dns_hijack_flagged_for_differing_answers(void) {
    uint8_t buf[128];
    uint8_t client[6] = {0x01, 0x01, 0x01, 0x01, 0x01, 0x01};
    uint8_t server[6] = {0x02, 0x02, 0x02, 0x02, 0x02, 0x02};
    uint16_t qid = 0xBEEF;

    size_t len1 = build_dns_response(buf, server, client, qid, 0xAA);
    wifi_extended_scan_process_frame(buf, len1, 2, 0, 1000);
    size_t len2 = build_dns_response(buf, server, client, qid, 0xBB);   // different answer
    wifi_extended_scan_process_frame(buf, len2, 2, 0, 1010);

    WifiExtendedScanStats stats;
    wifi_extended_scan_get_stats(&stats);
    TEST_ASSERT_EQUAL(1, stats.dns_hijack_suspects);
}

void test_dns_identical_retransmit_not_flagged(void) {
    uint8_t buf[128];
    uint8_t client[6] = {0x01, 0x01, 0x01, 0x01, 0x01, 0x01};
    uint8_t server[6] = {0x02, 0x02, 0x02, 0x02, 0x02, 0x02};
    uint16_t qid = 0xF00D;

    size_t len1 = build_dns_response(buf, server, client, qid, 0xCC);
    wifi_extended_scan_process_frame(buf, len1, 2, 0, 1000);
    size_t len2 = build_dns_response(buf, server, client, qid, 0xCC);   // identical answer
    wifi_extended_scan_process_frame(buf, len2, 2, 0, 1010);

    WifiExtendedScanStats stats;
    wifi_extended_scan_get_stats(&stats);
    TEST_ASSERT_EQUAL(0, stats.dns_hijack_suspects);
}

// ============================================================================
// Defensive parsing -- malformed/short buffers never crash, never process
// ============================================================================

void test_null_frame_is_noop(void) {
    wifi_extended_scan_process_frame(nullptr, 100, 0, 0x0D, 1000);
    WifiExtendedScanStats stats;
    wifi_extended_scan_get_stats(&stats);
    TEST_ASSERT_EQUAL(0, stats.rtt_devices_observed);
}

void test_truncated_action_frame_is_noop(void) {
    uint8_t buf[MAC_HDR_LEN] = {0};   // no category/action bytes at all
    wifi_extended_scan_process_frame(buf, sizeof(buf), 0, 0x0D, 1000);
    WifiExtendedScanStats stats;
    wifi_extended_scan_get_stats(&stats);
    TEST_ASSERT_EQUAL(0, stats.rtt_devices_observed);
}

void test_truncated_data_frame_is_noop(void) {
    uint8_t buf[MAC_HDR_LEN + 3] = {0};   // shorter than LLC/SNAP + EAPOL header
    wifi_extended_scan_process_frame(buf, sizeof(buf), 2, 0, 1000);
    WifiExtendedScanStats stats;
    wifi_extended_scan_get_stats(&stats);
    TEST_ASSERT_EQUAL(0, stats.krack_suspects);
    TEST_ASSERT_EQUAL(0, stats.pmkid_burst_suspects);
}

int main(int, char**) {
    UNITY_BEGIN();

    RUN_TEST(test_ftm_action_frame_recognized);
    RUN_TEST(test_ftm_action_frame_rejects_unrelated);
    RUN_TEST(test_eapol_classifies_message_1);
    RUN_TEST(test_eapol_classifies_message_2);
    RUN_TEST(test_eapol_classifies_message_3);
    RUN_TEST(test_eapol_classifies_message_4);
    RUN_TEST(test_eapol_group_key_handshake_is_unknown);
    RUN_TEST(test_channel_hop_wraps_13_to_1);
    RUN_TEST(test_channel_hop_increments_normally);
    RUN_TEST(test_channel_hop_recovers_from_out_of_range);

    RUN_TEST(test_rtt_action_frame_recorded);
    RUN_TEST(test_non_ftm_action_frame_not_recorded);

    RUN_TEST(test_pmkid_burst_flagged_after_threshold);
    RUN_TEST(test_pmkid_burst_not_flagged_below_threshold);
    RUN_TEST(test_krack_nonce_reuse_flagged);
    RUN_TEST(test_legitimate_m3_with_different_nonce_not_flagged);

    RUN_TEST(test_wpa3_downgrade_detected_after_sae_disappears);
    RUN_TEST(test_wpa3_no_downgrade_when_sae_persists);
    RUN_TEST(test_wpa3_no_false_positive_for_always_psk_network);

    RUN_TEST(test_rogue_dhcp_flagged_for_two_servers);
    RUN_TEST(test_single_dhcp_server_not_flagged);
    RUN_TEST(test_dns_hijack_flagged_for_differing_answers);
    RUN_TEST(test_dns_identical_retransmit_not_flagged);

    RUN_TEST(test_null_frame_is_noop);
    RUN_TEST(test_truncated_action_frame_is_noop);
    RUN_TEST(test_truncated_data_frame_is_noop);

    return UNITY_END();
}
