// ============================================================================
// Native (host) unit tests — Interop modules
//
// Covers:
//   - radio_scheduler: round-robin obeys NVS weights, lease overrides
//   - meshtastic_emit: NodeInfo + text envelope wire size + parse round-trip
//   - rnode_bridge:    KISS encode/decode round-trip, L1 framing
//   - mqtt_client:     topic formatting + detection-JSON formatting
//   - interop_router:  protocol-byte dispatch
//
// Run with: pio test -e native -f test_interop
// ============================================================================

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

// ---------------------------------------------------------------------------
// Pull implementation units directly. UNIT_TEST is set by the native env.
// We pull radio_scheduler FIRST so its weak symbols are present, then the
// protocol modules override radio_mesh_set_params / radio_rnode_set_params.
// ---------------------------------------------------------------------------
#include "../../src/radio_scheduler.cpp"
#include "../../src/meshtastic_emit.cpp"
#include "../../src/ota_signed.cpp"
#include "../../src/reticulum_identity.cpp"
#include "../../src/rnode_bridge.cpp"
#include "../../src/mqtt_client.cpp"
#include "../../src/interop_router.cpp"

// ============================================================================
// radio_scheduler tests
// ============================================================================

void test_sched_init_defaults() {
    radio_sched_test_reset();
    radio_sched_init();
    TEST_ASSERT_EQUAL(RADIO_MODE_PDL, radio_sched_current_mode());
}

void test_sched_round_robin_obeys_weights() {
    radio_sched_test_reset();
    radio_sched_init();
    // Override weights to 50/30/20 for deterministic test
    radio_sched_test_set_weights(50, 30, 20);

    // Force re-init of dwell window
    radio_sched_request_mode(RADIO_MODE_PDL, 1);
    radio_sched_test_advance_ms(2);
    radio_sched_tick();
    // Lease expired -> back to PDL round-robin
    TEST_ASSERT_EQUAL(RADIO_MODE_PDL, radio_sched_current_mode());

    // Period = 1000 ms, PDL gets 500 ms, MESHTASTIC gets 300, RNODE gets 200
    radio_sched_test_advance_ms(500);
    radio_sched_tick();
    TEST_ASSERT_EQUAL(RADIO_MODE_MESHTASTIC, radio_sched_current_mode());

    radio_sched_test_advance_ms(300);
    radio_sched_tick();
    TEST_ASSERT_EQUAL(RADIO_MODE_RNODE, radio_sched_current_mode());

    radio_sched_test_advance_ms(200);
    radio_sched_tick();
    TEST_ASSERT_EQUAL(RADIO_MODE_PDL, radio_sched_current_mode());
}

void test_sched_invalid_weights_fall_back() {
    radio_sched_test_reset();
    radio_sched_init();
    radio_sched_test_set_weights(50, 30, 30);  // sum = 110
    // After validation we expect 100/0/0
    // Force a tick into a window that should expire -> should stay at PDL
    radio_sched_test_advance_ms(1000);
    radio_sched_tick();
    TEST_ASSERT_EQUAL(RADIO_MODE_PDL, radio_sched_current_mode());
}

void test_sched_lease_grants_mode() {
    radio_sched_test_reset();
    radio_sched_init();
    radio_sched_test_set_weights(80, 15, 5);
    bool ok = radio_sched_request_mode(RADIO_MODE_RNODE, 200);
    TEST_ASSERT_TRUE(ok);
    TEST_ASSERT_EQUAL(RADIO_MODE_RNODE, radio_sched_current_mode());
    // can_tx for RNode true while lease active
    TEST_ASSERT_TRUE(radio_sched_can_tx(RADIO_MODE_RNODE));
    TEST_ASSERT_FALSE(radio_sched_can_tx(RADIO_MODE_PDL));
}

void test_sched_can_tx_only_in_active_mode() {
    radio_sched_test_reset();
    radio_sched_init();
    TEST_ASSERT_TRUE(radio_sched_can_tx(RADIO_MODE_PDL));
    TEST_ASSERT_FALSE(radio_sched_can_tx(RADIO_MODE_MESHTASTIC));
    TEST_ASSERT_FALSE(radio_sched_can_tx(RADIO_MODE_RNODE));
}

// ============================================================================
// meshtastic_emit tests
// ============================================================================

extern "C" size_t meshtastic_test_build_nodeinfo(uint8_t* out, size_t out_len);
extern "C" size_t meshtastic_test_build_text(const char* msg, uint8_t* out, size_t out_len);
extern "C" int    meshtastic_test_parse(const uint8_t* in, size_t len,
                                        uint32_t* out_src, uint8_t* out_portnum);

void test_meshtastic_nodeinfo_envelope_size() {
    uint8_t buf[256];
    size_t len = meshtastic_test_build_nodeinfo(buf, sizeof(buf));
    // Envelope = 16 + inner. Inner = portnum tag(1) + portnum varint(1)
    // + payload tag(1) + length varint(1) + "PDL-Test"(8) = 12
    TEST_ASSERT_EQUAL_size_t(16 + 12, len);
    // Channel hash byte is 0x08
    TEST_ASSERT_EQUAL_HEX8(0x08, buf[13]);
}

void test_meshtastic_text_round_trip() {
    uint8_t buf[256];
    const char* msg = "hello world";
    size_t len = meshtastic_test_build_text(msg, buf, sizeof(buf));
    TEST_ASSERT_TRUE(len > 16);

    uint32_t src = 0;
    uint8_t portnum = 0;
    int rc = meshtastic_test_parse(buf, len, &src, &portnum);
    TEST_ASSERT_EQUAL_INT(0, rc);
    TEST_ASSERT_EQUAL_HEX32(0xCB0D5033u, src);
    TEST_ASSERT_EQUAL_UINT8(MT_PORTNUM_TEXT_MSG, portnum);
}

void test_meshtastic_truncated_packet_rejected() {
    uint8_t buf[8] = {0};
    uint32_t src = 0;
    uint8_t  portnum = 0;
    int rc = meshtastic_test_parse(buf, sizeof(buf), &src, &portnum);
    TEST_ASSERT_NOT_EQUAL(0, rc);
}

// ============================================================================
// rnode_bridge tests
// ============================================================================

extern "C" size_t rnode_test_kiss_encode(const uint8_t* in, size_t in_len,
                                         uint8_t* out, size_t out_len);
extern "C" size_t rnode_test_kiss_decode(const uint8_t* in, size_t in_len,
                                         uint8_t* out, size_t out_len);
extern "C" size_t rnode_test_build_l1(uint8_t flags, uint8_t hops,
                                      const uint8_t* dest_hash, uint8_t context,
                                      const uint8_t* payload, size_t plen,
                                      uint8_t* out, size_t out_len);

void test_rnode_kiss_round_trip_simple() {
    uint8_t in[]  = { 0x01, 0x02, 0x03, 0x04 };
    uint8_t enc[64];
    uint8_t dec[64];
    size_t enc_len = rnode_test_kiss_encode(in, sizeof(in), enc, sizeof(enc));
    TEST_ASSERT_TRUE(enc_len >= sizeof(in) + 3);  // FEND CMD ... FEND
    TEST_ASSERT_EQUAL_HEX8(KISS_FEND, enc[0]);
    TEST_ASSERT_EQUAL_HEX8(KISS_FEND, enc[enc_len - 1]);
    size_t dec_len = rnode_test_kiss_decode(enc, enc_len, dec, sizeof(dec));
    TEST_ASSERT_EQUAL_size_t(sizeof(in), dec_len);
    TEST_ASSERT_EQUAL_MEMORY(in, dec, sizeof(in));
}

void test_rnode_kiss_escapes_fend_and_fesc() {
    uint8_t in[] = { KISS_FEND, KISS_FESC, 0x42 };
    uint8_t enc[64];
    uint8_t dec[64];
    size_t enc_len = rnode_test_kiss_encode(in, sizeof(in), enc, sizeof(enc));
    TEST_ASSERT_TRUE(enc_len >= 7);   // 2 fend + 1 cmd + 2 escapes (4 bytes)
    size_t dec_len = rnode_test_kiss_decode(enc, enc_len, dec, sizeof(dec));
    TEST_ASSERT_EQUAL_size_t(sizeof(in), dec_len);
    TEST_ASSERT_EQUAL_MEMORY(in, dec, sizeof(in));
}

void test_rnode_l1_header_layout() {
    uint8_t dh[16];
    for (int i = 0; i < 16; i++) dh[i] = (uint8_t)(0xA0 + i);
    uint8_t payload[] = { 'L', 'X', 'M', 'F' };
    uint8_t out[64];
    size_t l = rnode_test_build_l1(0x00, 0x00, dh, 0x01, payload, sizeof(payload), out, sizeof(out));
    TEST_ASSERT_EQUAL_size_t(RNODE_L1_HEADER_LEN + 4, l);
    TEST_ASSERT_EQUAL_HEX8(0x00, out[0]);
    TEST_ASSERT_EQUAL_HEX8(0x00, out[1]);
    TEST_ASSERT_EQUAL_MEMORY(dh, &out[2], 16);
    TEST_ASSERT_EQUAL_HEX8(0x01, out[18]);
    TEST_ASSERT_EQUAL_MEMORY(payload, &out[RNODE_L1_HEADER_LEN], 4);
}

// ============================================================================
// mqtt_client tests
// ============================================================================

void test_mqtt_topic_format_basic() {
    char buf[64];
    size_t n = mqtt_format_topic(buf, sizeof(buf), "Mark01", "detection");
    TEST_ASSERT_EQUAL_STRING("pdl/Mark01/detection", buf);
    TEST_ASSERT_EQUAL_size_t(strlen("pdl/Mark01/detection"), n);
}

void test_mqtt_topic_format_truncates() {
    char buf[8];
    size_t n = mqtt_format_topic(buf, sizeof(buf), "Mark01", "detection");
    TEST_ASSERT_EQUAL_size_t(7, n);
    TEST_ASSERT_EQUAL_CHAR(0, buf[7]);  // NUL-terminated even when truncated
}

void test_mqtt_detection_json_contains_fields() {
    DetectionEvent evt = {};
    evt.sensor_id = 0xCB0D5033u;
    evt.timestamp_utc = 1714000000;
    evt.class_label = CLASS_OCUSYNC;
    evt.confidence = 200;
    evt.rssi = -78;
    evt.noise_floor = -103;
    evt.lat_fixed = 389072000;
    evt.lon_fixed = -770369000;
    evt.battery_pct = 87;
    evt.flags = FLAG_PROMOTED;

    char buf[256];
    size_t n = mqtt_format_detection_json(evt, buf, sizeof(buf));
    TEST_ASSERT_TRUE(n > 0);
    TEST_ASSERT_NOT_NULL(strstr(buf, "\"cls\":1"));
    TEST_ASSERT_NOT_NULL(strstr(buf, "\"conf\":200"));
    TEST_ASSERT_NOT_NULL(strstr(buf, "\"rssi\":-78"));
    TEST_ASSERT_NOT_NULL(strstr(buf, "\"flags\":1"));
}

// ============================================================================
// interop_router tests
// ============================================================================

extern "C" int interop_router_test_proto_select(uint8_t first_byte);

void test_interop_proto_select() {
    TEST_ASSERT_EQUAL_INT(1,  interop_router_test_proto_select(INTEROP_PROTO_MESHTASTIC));
    TEST_ASSERT_EQUAL_INT(2,  interop_router_test_proto_select(INTEROP_PROTO_RNODE));
    TEST_ASSERT_EQUAL_INT(-1, interop_router_test_proto_select(0xFF));
}

void test_interop_router_enqueue_and_drain() {
    interop_router_init();
    uint8_t pkt[] = { MSG_TYPE_INTEROP_RX, INTEROP_PROTO_MESHTASTIC, 0xDE, 0xAD };
    interop_router_enqueue_pdl(pkt, sizeof(pkt));
    InteropStats s;
    interop_router_get_stats(&s);
    TEST_ASSERT_EQUAL_UINT32(1, s.enq_total);
    TEST_ASSERT_EQUAL_UINT32(0, s.enq_dropped_full);

    // We're in PDL by default after radio_sched_init, so a tick should drain
    radio_sched_test_reset();
    radio_sched_init();   // current = PDL
    interop_router_tick();
    interop_router_get_stats(&s);
    TEST_ASSERT_EQUAL_UINT32(1, s.flushed_to_pdl);
}

void test_interop_router_drops_when_full() {
    interop_router_init();
    uint8_t pkt[] = { MSG_TYPE_INTEROP_RX, INTEROP_PROTO_MESHTASTIC };
    for (int i = 0; i < 32; i++) {
        interop_router_enqueue_pdl(pkt, sizeof(pkt));
    }
    InteropStats s;
    interop_router_get_stats(&s);
    TEST_ASSERT_TRUE(s.enq_dropped_full > 0);
}

void test_interop_router_dispatch_unknown_proto_drops() {
    interop_router_init();
    uint8_t pkt[] = { 0xFF, 0x01, 0x02 };
    bool ok = interop_router_dispatch_tx(pkt, sizeof(pkt));
    TEST_ASSERT_FALSE(ok);
    InteropStats s;
    interop_router_get_stats(&s);
    TEST_ASSERT_TRUE(s.tx_dropped > 0);
}

// ============================================================================
// Unity runner
// ============================================================================

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

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

    RUN_TEST(test_sched_init_defaults);
    RUN_TEST(test_sched_round_robin_obeys_weights);
    RUN_TEST(test_sched_invalid_weights_fall_back);
    RUN_TEST(test_sched_lease_grants_mode);
    RUN_TEST(test_sched_can_tx_only_in_active_mode);

    RUN_TEST(test_meshtastic_nodeinfo_envelope_size);
    RUN_TEST(test_meshtastic_text_round_trip);
    RUN_TEST(test_meshtastic_truncated_packet_rejected);

    RUN_TEST(test_rnode_kiss_round_trip_simple);
    RUN_TEST(test_rnode_kiss_escapes_fend_and_fesc);
    RUN_TEST(test_rnode_l1_header_layout);

    RUN_TEST(test_mqtt_topic_format_basic);
    RUN_TEST(test_mqtt_topic_format_truncates);
    RUN_TEST(test_mqtt_detection_json_contains_fields);

    RUN_TEST(test_interop_proto_select);
    RUN_TEST(test_interop_router_enqueue_and_drain);
    RUN_TEST(test_interop_router_drops_when_full);
    RUN_TEST(test_interop_router_dispatch_unknown_proto_drops);

    return UNITY_END();
}
