// ============================================================================
// test/test_csi_carrier_detector/test_csi_carrier_detector.cpp
//
// Unity tests for src/csi_carrier_detector.{h,cpp} -- CSI-based localized-
// spike continuous-carrier detection. Pure classifiers are fully covered
// with synthetic CSI amplitude arrays; the live esp_wifi_set_csi_* wiring
// (real hardware, no real continuous-carrier transmitter available to test
// against -- see the header's honest validation caveat) is not exercised
// here, same split as subghz_scanner's own test suite.
//
// Run with:
//   pio test -e native -f test_csi_carrier_detector
// ============================================================================

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

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

// ============================================================================
// csi_compute_amplitude
// ============================================================================

void test_amplitude_known_values(void) {
    // Pairs are (imag, real). |imag|+|real|.
    int8_t buf[] = { 3, 4,    -3, 4,    3, -4,   -3, -4 };
    uint16_t out[8];
    size_t n = csi_compute_amplitude(buf, sizeof(buf), out);
    TEST_ASSERT_EQUAL(4, n);
    TEST_ASSERT_EQUAL(7, out[0]);
    TEST_ASSERT_EQUAL(7, out[1]);
    TEST_ASSERT_EQUAL(7, out[2]);
    TEST_ASSERT_EQUAL(7, out[3]);
}

void test_amplitude_odd_length_truncates_last_pair(void) {
    int8_t buf[] = { 1, 2, 3 };   // 3 bytes -> 1 full pair, trailing byte dropped
    uint16_t out[4];
    size_t n = csi_compute_amplitude(buf, sizeof(buf), out);
    TEST_ASSERT_EQUAL(1, n);
}

void test_amplitude_null_safe(void) {
    uint16_t out[4];
    TEST_ASSERT_EQUAL(0, csi_compute_amplitude(nullptr, 8, out));
    int8_t buf[8] = {0};
    TEST_ASSERT_EQUAL(0, csi_compute_amplitude(buf, 8, nullptr));
}

// ============================================================================
// csi_has_localized_spike
// ============================================================================

void test_flat_array_no_spike(void) {
    uint16_t amps[60];
    for (int i = 0; i < 60; i++) amps[i] = 20;
    TEST_ASSERT_FALSE(csi_has_localized_spike(amps, 60, CSI_SPIKE_RATIO_DEFAULT,
                                              CSI_SPIKE_MAX_WIDTH_FRAC_DEFAULT));
}

void test_localized_narrow_spike_detected(void) {
    uint16_t amps[60];
    for (int i = 0; i < 60; i++) amps[i] = 20;
    // 3 of 60 subcarriers (5%) spike well above the 2.5x-median threshold.
    amps[28] = 100; amps[29] = 100; amps[30] = 100;
    TEST_ASSERT_TRUE(csi_has_localized_spike(amps, 60, CSI_SPIKE_RATIO_DEFAULT,
                                             CSI_SPIKE_MAX_WIDTH_FRAC_DEFAULT));
}

void test_broadband_elevation_not_flagged_as_localized(void) {
    uint16_t amps[60];
    for (int i = 0; i < 60; i++) amps[i] = 20;
    // Half the channel elevated -- broadband, not narrowband interference.
    for (int i = 0; i < 30; i++) amps[i] = 100;
    TEST_ASSERT_FALSE(csi_has_localized_spike(amps, 60, CSI_SPIKE_RATIO_DEFAULT,
                                              CSI_SPIKE_MAX_WIDTH_FRAC_DEFAULT));
}

void test_all_zero_no_baseline_not_flagged(void) {
    uint16_t amps[60] = {0};
    TEST_ASSERT_FALSE(csi_has_localized_spike(amps, 60, CSI_SPIKE_RATIO_DEFAULT,
                                              CSI_SPIKE_MAX_WIDTH_FRAC_DEFAULT));
}

void test_too_few_subcarriers_not_flagged(void) {
    uint16_t amps[3] = {20, 100, 20};
    TEST_ASSERT_FALSE(csi_has_localized_spike(amps, 3, CSI_SPIKE_RATIO_DEFAULT,
                                              CSI_SPIKE_MAX_WIDTH_FRAC_DEFAULT));
}

void test_mild_variation_below_ratio_not_flagged(void) {
    uint16_t amps[60];
    for (int i = 0; i < 60; i++) amps[i] = 20;
    // Realistic multipath fading -- some subcarriers a bit stronger, but
    // nowhere near the 2.5x-median spike ratio.
    amps[10] = 30; amps[11] = 32; amps[12] = 28;
    TEST_ASSERT_FALSE(csi_has_localized_spike(amps, 60, CSI_SPIKE_RATIO_DEFAULT,
                                              CSI_SPIKE_MAX_WIDTH_FRAC_DEFAULT));
}

// ============================================================================
// Integration: sustained tracking + findings
// ============================================================================

static void build_spiky_csi(int8_t* buf, size_t n_subcarriers) {
    for (size_t i = 0; i < n_subcarriers; i++) {
        buf[i * 2] = 2; buf[i * 2 + 1] = 2;         // imag,real -> amplitude 4 baseline
    }
    // Localized spike in the middle.
    size_t mid = n_subcarriers / 2;
    for (size_t i = mid; i < mid + 3 && i < n_subcarriers; i++) {
        buf[i * 2] = 20; buf[i * 2 + 1] = 20;       // amplitude 40 -- 10x baseline
    }
}

static void build_clean_csi(int8_t* buf, size_t n_subcarriers) {
    for (size_t i = 0; i < n_subcarriers; i++) {
        buf[i * 2] = 2; buf[i * 2 + 1] = 2;
    }
}

void test_sustained_spikes_on_one_channel_flagged(void) {
    int8_t buf[128];
    build_spiky_csi(buf, 60);

    for (unsigned i = 0; i < CSI_SUSTAINED_FRAMES_TO_FLAG; i++) {
        csi_carrier_detector_process_csi(buf, 120, /*channel=*/6, -50, 1000 + i);
    }

    CsiCarrierStats stats;
    csi_carrier_detector_get_stats(&stats);
    TEST_ASSERT_EQUAL(1, stats.active_suspects);

    CsiCarrierFinding findings[4];
    size_t n = csi_carrier_detector_list_findings(findings, 4);
    TEST_ASSERT_EQUAL(1, n);
    TEST_ASSERT_EQUAL(6, findings[0].channel);
}

void test_spikes_below_sustained_threshold_not_flagged(void) {
    int8_t buf[128];
    build_spiky_csi(buf, 60);

    for (unsigned i = 0; i < CSI_SUSTAINED_FRAMES_TO_FLAG - 1; i++) {
        csi_carrier_detector_process_csi(buf, 120, 6, -50, 1000 + i);
    }

    CsiCarrierStats stats;
    csi_carrier_detector_get_stats(&stats);
    TEST_ASSERT_EQUAL(0, stats.active_suspects);
}

void test_clean_reception_breaks_the_streak(void) {
    int8_t spiky[128], clean[128];
    build_spiky_csi(spiky, 60);
    build_clean_csi(clean, 60);

    // Almost enough consecutive spikes to flag...
    for (unsigned i = 0; i < CSI_SUSTAINED_FRAMES_TO_FLAG - 1; i++) {
        csi_carrier_detector_process_csi(spiky, 120, 6, -50, 1000 + i);
    }
    // ...then one clean reception breaks it.
    csi_carrier_detector_process_csi(clean, 120, 6, -50, 2000);
    // Even more spikes after the break shouldn't retroactively flag from
    // the broken streak -- need a fresh sustained run.
    for (unsigned i = 0; i < CSI_SUSTAINED_FRAMES_TO_FLAG - 2; i++) {
        csi_carrier_detector_process_csi(spiky, 120, 6, -50, 3000 + i);
    }

    CsiCarrierStats stats;
    csi_carrier_detector_get_stats(&stats);
    TEST_ASSERT_EQUAL(0, stats.active_suspects);
}

void test_different_channels_tracked_independently(void) {
    int8_t buf[128];
    build_spiky_csi(buf, 60);

    for (unsigned i = 0; i < CSI_SUSTAINED_FRAMES_TO_FLAG; i++) {
        csi_carrier_detector_process_csi(buf, 120, /*channel=*/1, -50, 1000 + i);
    }
    // Channel 11 only gets a few -- shouldn't be flagged.
    for (unsigned i = 0; i < 3; i++) {
        csi_carrier_detector_process_csi(buf, 120, /*channel=*/11, -50, 1000 + i);
    }

    CsiCarrierStats stats;
    csi_carrier_detector_get_stats(&stats);
    TEST_ASSERT_EQUAL(1, stats.active_suspects);

    CsiCarrierFinding findings[4];
    size_t n = csi_carrier_detector_list_findings(findings, 4);
    TEST_ASSERT_EQUAL(1, n);
    TEST_ASSERT_EQUAL(1, findings[0].channel);
}

void test_invalid_channel_is_noop(void) {
    int8_t buf[128];
    build_spiky_csi(buf, 60);
    csi_carrier_detector_process_csi(buf, 120, /*channel=*/0, -50, 1000);
    csi_carrier_detector_process_csi(buf, 120, /*channel=*/14, -50, 1000);

    CsiCarrierStats stats;
    csi_carrier_detector_get_stats(&stats);
    TEST_ASSERT_EQUAL(0, stats.active_suspects);
    TEST_ASSERT_EQUAL(0, stats.frames_analyzed);
}

void test_null_buffer_is_noop(void) {
    csi_carrier_detector_process_csi(nullptr, 120, 6, -50, 1000);
    CsiCarrierStats stats;
    csi_carrier_detector_get_stats(&stats);
    TEST_ASSERT_EQUAL(0, stats.frames_analyzed);
}

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

    RUN_TEST(test_amplitude_known_values);
    RUN_TEST(test_amplitude_odd_length_truncates_last_pair);
    RUN_TEST(test_amplitude_null_safe);

    RUN_TEST(test_flat_array_no_spike);
    RUN_TEST(test_localized_narrow_spike_detected);
    RUN_TEST(test_broadband_elevation_not_flagged_as_localized);
    RUN_TEST(test_all_zero_no_baseline_not_flagged);
    RUN_TEST(test_too_few_subcarriers_not_flagged);
    RUN_TEST(test_mild_variation_below_ratio_not_flagged);

    RUN_TEST(test_sustained_spikes_on_one_channel_flagged);
    RUN_TEST(test_spikes_below_sustained_threshold_not_flagged);
    RUN_TEST(test_clean_reception_breaks_the_streak);
    RUN_TEST(test_different_channels_tracked_independently);
    RUN_TEST(test_invalid_channel_is_noop);
    RUN_TEST(test_null_buffer_is_noop);

    return UNITY_END();
}
