// ============================================================================
// test/test_mythos_bridge/test_mythos_bridge.cpp
//
// Unity tests for src/mythos_bridge.{h,cpp} — the Mythos federated-
// retraining gateway's HTTP correction-count parser. This module had zero
// native test coverage before this file: mythos_bridge_count_corrections()
// previously lived as a static function taking Arduino's String type
// inside the #ifdef ARDUINO block, making it untestable natively. Extracted
// to a pure const-char*-based function (no radio/HTTP/Arduino dependency),
// same pattern already used for subghz_is_plausible_rate() and
// rf_cataloger_classify().
//
// Run with:
//   pio test -e native -f test_mythos_bridge
// ============================================================================

#include <unity.h>
#include "../../src/mythos_bridge.cpp"

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

void test_zero_corrections_in_empty_array(void) {
    TEST_ASSERT_EQUAL_UINT32(0, mythos_bridge_count_corrections("{\"corrections\":[]}"));
}

void test_single_correction(void) {
    const char* body = "{\"corrections\":[{\"id\":42,\"label_from\":1,\"label_to\":2}]}";
    TEST_ASSERT_EQUAL_UINT32(1, mythos_bridge_count_corrections(body));
}

void test_multiple_corrections(void) {
    const char* body =
        "{\"corrections\":["
        "{\"id\":1,\"label_from\":0,\"label_to\":3},"
        "{\"id\":2,\"label_from\":1,\"label_to\":4},"
        "{\"id\":3,\"label_from\":2,\"label_to\":5}"
        "]}";
    TEST_ASSERT_EQUAL_UINT32(3, mythos_bridge_count_corrections(body));
}

void test_null_body_returns_zero(void) {
    TEST_ASSERT_EQUAL_UINT32(0, mythos_bridge_count_corrections(nullptr));
}

void test_empty_string_returns_zero(void) {
    TEST_ASSERT_EQUAL_UINT32(0, mythos_bridge_count_corrections(""));
}

void test_malformed_json_still_counts_id_markers(void) {
    // Deliberately honest-scope: this is a marker count, not a real JSON
    // parser (see design note) -- it must count "id": occurrences wherever
    // they appear, even in malformed/truncated JSON, since that's the
    // documented, accepted tradeoff for staying dependency-free on-device.
    const char* body = "{\"id\":1,\"id\":2, garbage not valid json \"id\":";
    TEST_ASSERT_EQUAL_UINT32(3, mythos_bridge_count_corrections(body));
}

void test_does_not_match_similar_keys(void) {
    // Must not false-positive on keys that merely contain "id" as a
    // substring without the exact `"id":` marker (e.g. "valid":, "grid":).
    const char* body = "{\"valid\":true,\"grid\":5,\"width\":10}";
    TEST_ASSERT_EQUAL_UINT32(0, mythos_bridge_count_corrections(body));
}

void test_mixed_real_and_similar_keys(void) {
    const char* body = "{\"valid\":true,\"id\":7,\"grid\":5,\"id\":8}";
    TEST_ASSERT_EQUAL_UINT32(2, mythos_bridge_count_corrections(body));
}

// ---------------------------------------------------------------------------
// mythos_bridge_parse_and_apply_corrections / mythos_bridge_apply_correction
//
// Real gap closed 2026-07-30: this module used to only COUNT corrections
// (see tests above) and threw label_from/label_to away entirely -- meaning
// every correction a human operator ever queued was fetched once (the
// gateway's get_pending_corrections() is consume-once) and then lost
// forever. These tests cover the actual apply path against the real
// gateway response shape (gateway/fed_gateway.py's add_correction(), in
// the sibling mythos-fedretrain repo: id/label_from/label_to/features/
// signature/source/created_at, in that field order).
// ---------------------------------------------------------------------------

void test_apply_correction_is_identity_before_any_correction(void) {
    for (uint8_t i = 0; i < NUM_CLASSES; i++) {
        TEST_ASSERT_EQUAL_UINT8(i, mythos_bridge_apply_correction(i));
    }
}

void test_apply_correction_identity_for_direct_decode_classes(void) {
    // CLASS_RID_BLE_DECODED (7) / CLASS_RID_WIFI_DECODED (8) are direct
    // message content, not classifier output -- must always pass through
    // untouched, correction or not.
    TEST_ASSERT_EQUAL_UINT8(7, mythos_bridge_apply_correction(7));
    TEST_ASSERT_EQUAL_UINT8(8, mythos_bridge_apply_correction(8));
    TEST_ASSERT_EQUAL_UINT8(200, mythos_bridge_apply_correction(200));
}

void test_parse_and_apply_single_correction(void) {
    const char* body =
        "{\"ok\":true,\"corrections\":["
        "{\"id\":\"corr_1\",\"label_from\":1,\"label_to\":2,\"features\":[],"
        "\"signature\":\"abc\",\"source\":\"c2\",\"created_at\":123.4}"
        "]}";
    TEST_ASSERT_EQUAL_UINT32(1, mythos_bridge_parse_and_apply_corrections(body));
    TEST_ASSERT_EQUAL_UINT8(2, mythos_bridge_apply_correction(1));
    // Everything else stays identity.
    TEST_ASSERT_EQUAL_UINT8(0, mythos_bridge_apply_correction(0));
    TEST_ASSERT_EQUAL_UINT8(2, mythos_bridge_apply_correction(2));
}

void test_parse_and_apply_multiple_corrections(void) {
    const char* body =
        "{\"corrections\":["
        "{\"id\":\"a\",\"label_from\":0,\"label_to\":3,\"features\":[],\"signature\":\"\",\"source\":\"c2\",\"created_at\":1},"
        "{\"id\":\"b\",\"label_from\":5,\"label_to\":6,\"features\":[],\"signature\":\"\",\"source\":\"c2\",\"created_at\":2}"
        "]}";
    TEST_ASSERT_EQUAL_UINT32(2, mythos_bridge_parse_and_apply_corrections(body));
    TEST_ASSERT_EQUAL_UINT8(3, mythos_bridge_apply_correction(0));
    TEST_ASSERT_EQUAL_UINT8(6, mythos_bridge_apply_correction(5));
}

void test_parse_and_apply_ignores_out_of_range_labels(void) {
    // label_to=9 is out of the 7-class classifier space (>= NUM_CLASSES) --
    // must be skipped, not silently clamped or applied anyway.
    const char* body = "{\"corrections\":[{\"id\":\"x\",\"label_from\":2,\"label_to\":9}]}";
    TEST_ASSERT_EQUAL_UINT32(0, mythos_bridge_parse_and_apply_corrections(body));
    TEST_ASSERT_EQUAL_UINT8(2, mythos_bridge_apply_correction(2));  // untouched
}

void test_parse_and_apply_later_correction_overwrites_earlier_one(void) {
    TEST_ASSERT_EQUAL_UINT32(1, mythos_bridge_parse_and_apply_corrections(
        "{\"corrections\":[{\"label_from\":1,\"label_to\":2}]}"));
    TEST_ASSERT_EQUAL_UINT8(2, mythos_bridge_apply_correction(1));

    TEST_ASSERT_EQUAL_UINT32(1, mythos_bridge_parse_and_apply_corrections(
        "{\"corrections\":[{\"label_from\":1,\"label_to\":4}]}"));
    TEST_ASSERT_EQUAL_UINT8_MESSAGE(4, mythos_bridge_apply_correction(1),
        "a newer correction for the same label_from must replace the older one");
}

void test_parse_and_apply_null_and_empty_body(void) {
    TEST_ASSERT_EQUAL_UINT32(0, mythos_bridge_parse_and_apply_corrections(nullptr));
    TEST_ASSERT_EQUAL_UINT32(0, mythos_bridge_parse_and_apply_corrections(""));
    TEST_ASSERT_EQUAL_UINT32(0, mythos_bridge_parse_and_apply_corrections("{\"corrections\":[]}"));
}

void test_parse_and_apply_malformed_missing_label_to_does_not_crash(void) {
    // label_from present with no matching label_to anywhere after it --
    // must bail out cleanly, not read past the end of the string.
    const char* body = "{\"corrections\":[{\"id\":\"z\",\"label_from\":3}]}";
    TEST_ASSERT_EQUAL_UINT32(0, mythos_bridge_parse_and_apply_corrections(body));
}

void test_reset_restores_identity_mapping(void) {
    mythos_bridge_parse_and_apply_corrections("{\"corrections\":[{\"label_from\":1,\"label_to\":2}]}");
    TEST_ASSERT_EQUAL_UINT8(2, mythos_bridge_apply_correction(1));

    mythos_bridge_test_reset();
    TEST_ASSERT_EQUAL_UINT8_MESSAGE(1, mythos_bridge_apply_correction(1),
        "test_reset() must restore the identity mapping for isolation between tests");
}

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

    RUN_TEST(test_zero_corrections_in_empty_array);
    RUN_TEST(test_single_correction);
    RUN_TEST(test_multiple_corrections);
    RUN_TEST(test_null_body_returns_zero);
    RUN_TEST(test_empty_string_returns_zero);
    RUN_TEST(test_malformed_json_still_counts_id_markers);
    RUN_TEST(test_does_not_match_similar_keys);
    RUN_TEST(test_mixed_real_and_similar_keys);

    RUN_TEST(test_apply_correction_is_identity_before_any_correction);
    RUN_TEST(test_apply_correction_identity_for_direct_decode_classes);
    RUN_TEST(test_parse_and_apply_single_correction);
    RUN_TEST(test_parse_and_apply_multiple_corrections);
    RUN_TEST(test_parse_and_apply_ignores_out_of_range_labels);
    RUN_TEST(test_parse_and_apply_later_correction_overwrites_earlier_one);
    RUN_TEST(test_parse_and_apply_null_and_empty_body);
    RUN_TEST(test_parse_and_apply_malformed_missing_label_to_does_not_crash);
    RUN_TEST(test_reset_restores_identity_mapping);

    return UNITY_END();
}
