/*
 * midi2_proc unit tests
 * Compile: gcc -std=c99 -Wall -I../src test_midi2_proc.c ../src/midi2_proc.c -o test_midi2_proc
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "midi2_proc.h"

static int passed = 0;
static int failed = 0;

#define TEST(name) printf("  %-50s ", name)
#define PASS() do { printf("PASS\n"); passed++; } while(0)
#define FAIL(msg) do { printf("FAIL: %s\n", msg); failed++; } while(0)
#define CHECK(cond, msg) do { if (!(cond)) { FAIL(msg); return; } } while(0)

/* --- Test helpers --- */

static uint32_t last_ump[4];
static uint8_t  last_ump_wc;
static int      ump_cb_count;

static void test_ump_cb(const uint32_t *words, uint8_t wc, void *ctx) {
  (void)ctx;
  memcpy(last_ump, words, wc * 4);
  last_ump_wc = wc;
  ump_cb_count++;
}

static uint8_t  last_sysex_group;
static uint8_t  last_sysex_data[256];
static uint16_t last_sysex_len;
static int      sysex_cb_count;

static void test_sysex_cb(uint8_t group, const uint8_t *data, uint16_t len, void *ctx) {
  (void)ctx;
  last_sysex_group = group;
  memcpy(last_sysex_data, data, len);
  last_sysex_len = len;
  sysex_cb_count++;
}

static uint32_t write_buf[64];
static uint32_t write_buf_pos;

static uint32_t test_write_fn(const uint32_t *words, uint32_t count, void *ctx) {
  (void)ctx;
  uint32_t i;
  for (i = 0; i < count && write_buf_pos < 64; i++) {
    write_buf[write_buf_pos++] = words[i];
  }
  return count;
}

static uint8_t proc_sysex_buf[128];
static uint8_t proc_sysex8_buf[256];

static void reset_state(void) {
  memset(last_ump, 0, sizeof(last_ump));
  last_ump_wc = 0;
  ump_cb_count = 0;
  last_sysex_group = 0xFF;
  memset(last_sysex_data, 0, sizeof(last_sysex_data));
  last_sysex_len = 0;
  sysex_cb_count = 0;
  memset(write_buf, 0, sizeof(write_buf));
  write_buf_pos = 0;
}

/* --- Init tests --- */

void test_init_defaults(void) {
  TEST("init: group_mask = 0xFFFF");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  CHECK(s.group_mask == 0xFFFF, "expected all groups enabled");
  CHECK(s.group_map[0] == 0, "map[0] == 0");
  CHECK(s.group_map[15] == 15, "map[15] == 15");
  CHECK(s.sysex_group == 0xFF, "no active sysex");
  CHECK(s.on_ump == NULL, "no ump callback");
  CHECK(s.on_sysex7 == NULL, "no sysex callback");
  PASS();
}

/* --- Feed tests --- */

void test_feed_note_on_delivered(void) {
  TEST("feed: Note On delivered via on_ump");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  s.on_ump = test_ump_cb;
  reset_state();

  uint32_t w[2];
  midi2_msg_note_on(w, 0, 0, 60, 0xC000, 0, 0);
  midi2_proc_feed(&s, w, 2);

  CHECK(ump_cb_count == 1, "callback called once");
  CHECK(last_ump_wc == 2, "2 words");
  CHECK(midi2_msg_get_note(last_ump) == 60, "note 60");
  PASS();
}

void test_feed_group_filtered(void) {
  TEST("feed: group 1 filtered out when mask = 0x0001");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  s.on_ump = test_ump_cb;
  s.group_mask = 0x0001;  /* only group 0 */
  reset_state();

  uint32_t w[2];
  midi2_msg_note_on(w, 1, 0, 60, 0xC000, 0, 0);  /* group 1 */
  midi2_proc_feed(&s, w, 2);

  CHECK(ump_cb_count == 0, "callback NOT called");
  PASS();
}

void test_feed_group_0_passes(void) {
  TEST("feed: group 0 passes when mask = 0x0001");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  s.on_ump = test_ump_cb;
  s.group_mask = 0x0001;
  reset_state();

  uint32_t w[2];
  midi2_msg_note_on(w, 0, 0, 60, 0xC000, 0, 0);  /* group 0 */
  midi2_proc_feed(&s, w, 2);

  CHECK(ump_cb_count == 1, "callback called");
  PASS();
}

void test_feed_utility_bypasses_filter(void) {
  TEST("feed: Utility (MT 0x0) bypasses group filter");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  s.on_ump = test_ump_cb;
  s.group_mask = 0x0000;  /* no groups */
  reset_state();

  /* MT 0x0, group 0 */
  uint32_t w = 0x00000000;
  midi2_proc_feed(&s, &w, 1);

  CHECK(ump_cb_count == 1, "utility delivered despite mask=0");
  PASS();
}

void test_feed_stream_bypasses_filter(void) {
  TEST("feed: Stream (MT 0xF) bypasses group filter");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  s.on_ump = test_ump_cb;
  s.group_mask = 0x0000;
  reset_state();

  /* MT 0xF, 4 words */
  uint32_t w[4] = { 0xF0000000, 0, 0, 0 };
  midi2_proc_feed(&s, w, 4);

  CHECK(ump_cb_count == 1, "stream delivered despite mask=0");
  PASS();
}

/* --- SysEx7 reassembly --- */

void test_sysex7_complete(void) {
  TEST("sysex7: complete single-packet");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  s.on_sysex7 = test_sysex_cb;
  reset_state();

  uint32_t w[2];
  uint8_t data[] = {0x7E, 0x7F, 0x09};
  midi2_msg_sysex7_packet(w, 0, MIDI2_SYSEX7_COMPLETE, data, 3);
  midi2_proc_feed(&s, w, 2);

  CHECK(sysex_cb_count == 1, "callback called");
  CHECK(last_sysex_len == 3, "3 bytes");
  CHECK(last_sysex_data[0] == 0x7E, "data[0]");
  CHECK(last_sysex_data[1] == 0x7F, "data[1]");
  CHECK(last_sysex_data[2] == 0x09, "data[2]");
  PASS();
}

void test_sysex7_multi_packet(void) {
  TEST("sysex7: multi-packet reassembly (12 bytes)");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  s.on_sysex7 = test_sysex_cb;
  reset_state();

  uint32_t w[2];
  uint8_t part1[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
  uint8_t part2[] = {0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C};

  midi2_msg_sysex7_packet(w, 0, MIDI2_SYSEX7_START, part1, 6);
  midi2_proc_feed(&s, w, 2);
  CHECK(sysex_cb_count == 0, "not delivered yet");

  midi2_msg_sysex7_packet(w, 0, MIDI2_SYSEX7_END, part2, 6);
  midi2_proc_feed(&s, w, 2);
  CHECK(sysex_cb_count == 1, "delivered after END");
  CHECK(last_sysex_len == 12, "12 bytes total");
  CHECK(last_sysex_data[0] == 0x01, "first byte");
  CHECK(last_sysex_data[11] == 0x0C, "last byte");
  PASS();
}

/* Track 1: MT5 (Data128, needs 4 words) fed with word_count=1 must not
 * over-read. Buffer holds exactly one word so ASan traps any read of w[1..3]. */
void test_proc_guard_short_data128(void) {
  TEST("guard: MT5 with word_count=1 does not over-read");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  reset_state();
  uint32_t *w = malloc(sizeof(uint32_t));   /* exactly ONE word */
  w[0] = 0x50030000u;                        /* MT5, num_bytes=3 -> sysex8_process reads w[1] */
  midi2_proc_feed(&s, w, 1);                 /* must early-return before sysex8_process */
  free(w);
  PASS();                                    /* ASan-clean == pass */
}

/* Track 1 caveat: START -> truncated CONTINUE (guard-dropped) -> valid END.
 * Specified behavior: the dropped fragment's bytes are missing from the
 * delivered SysEx, but the buffer is not corrupted and END still delivers. */
void test_proc_guard_sysex_reassembly(void) {
  TEST("guard: truncated SysEx7 CONTINUE drops cleanly, no corruption");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  s.on_sysex7 = test_sysex_cb;
  reset_state();

  uint32_t w[2];
  uint8_t part1[] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06};
  uint8_t mid[]   = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
  uint8_t part2[] = {0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C};

  midi2_msg_sysex7_packet(w, 0, MIDI2_SYSEX7_START, part1, 6);
  midi2_proc_feed(&s, w, 2);

  midi2_msg_sysex7_packet(w, 0, MIDI2_SYSEX7_CONTINUE, mid, 6);
  midi2_proc_feed(&s, w, 1);                 /* truncated: guard drops it */
  CHECK(sysex_cb_count == 0, "still not delivered");

  midi2_msg_sysex7_packet(w, 0, MIDI2_SYSEX7_END, part2, 6);
  midi2_proc_feed(&s, w, 2);
  CHECK(sysex_cb_count == 1, "delivered after END");
  CHECK(last_sysex_len == 12, "12 bytes: START + END, dropped CONTINUE missing");
  CHECK(last_sysex_data[0] == 0x01, "first byte from START");
  CHECK(last_sysex_data[6] == 0x07, "byte 6 is END's first byte, not the dropped CONTINUE");
  CHECK(last_sysex_data[11] == 0x0C, "last byte from END");
  PASS();
}

/* Track 2: the reentrancy guard works by holding state->in_feed true for the
 * duration of feed. We verify that mechanism directly (deterministic, no abort):
 * a callback observes the flag while feed is running, and a second state's flag
 * stays independent. The MIDI2_ASSERT(!in_feed) in the wrapper therefore fires
 * on a same-state re-entry. */
static bool g_in_feed_during_cb;
static const midi2_proc_state *g_observe_self;
static const midi2_proc_state *g_observe_other;
static bool g_other_in_feed_during_cb;

static void observe_ump_cb(const uint32_t *w, uint8_t n, void *ctx) {
  (void)w; (void)n; (void)ctx;
  if (g_observe_self)  g_in_feed_during_cb       = g_observe_self->in_feed;
  if (g_observe_other) g_other_in_feed_during_cb = g_observe_other->in_feed;
}

void test_proc_in_feed_set_during_feed(void) {
  TEST("Track 2: in_feed is true during feed, clear after");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  s.on_ump = observe_ump_cb;
  g_observe_self = &s;
  g_observe_other = NULL;
  g_in_feed_during_cb = false;
  uint32_t msg = 0x20903C7Fu;             /* MT2 -> reaches on_ump */
  midi2_proc_feed(&s, &msg, 1);
  CHECK(g_in_feed_during_cb == true, "in_feed set during feed (re-entry detectable)");
  CHECK(s.in_feed == false, "in_feed cleared after feed returns");
  PASS();
}

void test_proc_in_feed_independent_states(void) {
  TEST("Track 2: a second state's in_feed stays independent");
  midi2_proc_state a, b;
  midi2_proc_init(&a, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  midi2_proc_init(&b, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  a.on_ump = observe_ump_cb;
  g_observe_self = &a;
  g_observe_other = &b;                    /* b is not being fed */
  g_other_in_feed_during_cb = true;
  uint32_t msg = 0x20903C7Fu;
  midi2_proc_feed(&a, &msg, 1);
  CHECK(g_other_in_feed_during_cb == false, "feeding a must not set b->in_feed");
  PASS();
}

void test_sysex7_group_interleave_discards(void) {
  TEST("sysex7: different group mid-stream discards");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  s.on_sysex7 = test_sysex_cb;
  reset_state();

  uint32_t w[2];
  uint8_t data[] = {0x01, 0x02, 0x03};

  /* Start on group 0 */
  midi2_msg_sysex7_packet(w, 0, MIDI2_SYSEX7_START, data, 3);
  midi2_proc_feed(&s, w, 2);

  /* Start on group 1 (different group, discards group 0 in-progress) */
  midi2_msg_sysex7_packet(w, 1, MIDI2_SYSEX7_START, data, 3);
  midi2_proc_feed(&s, w, 2);

  /* End on group 1 */
  midi2_msg_sysex7_packet(w, 1, MIDI2_SYSEX7_END, data, 3);
  midi2_proc_feed(&s, w, 2);

  CHECK(sysex_cb_count == 1, "only group 1 delivered");
  CHECK(last_sysex_group == 1, "from group 1");
  CHECK(last_sysex_len == 6, "6 bytes (start + end)");
  PASS();
}

/* --- Group remap --- */

void test_remap_identity(void) {
  TEST("remap: identity map does not change group");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));

  uint32_t w[2];
  midi2_msg_note_on(w, 3, 0, 60, 0xC000, 0, 0);
  midi2_proc_remap_group(&s, w);
  CHECK(midi2_msg_get_group(w) == 3, "group unchanged");
  PASS();
}

void test_remap_changes_group(void) {
  TEST("remap: map[0] = 5 changes group 0 to 5");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  s.group_map[0] = 5;

  uint32_t w[2];
  midi2_msg_note_on(w, 0, 0, 60, 0xC000, 0, 0);
  midi2_proc_remap_group(&s, w);
  CHECK(midi2_msg_get_group(w) == 5, "group remapped to 5");
  PASS();
}

void test_remap_skips_utility(void) {
  TEST("remap: Utility (MT 0x0) not remapped");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  s.group_map[0] = 5;

  uint32_t w = 0x00000000;  /* MT 0x0, group 0 */
  midi2_proc_remap_group(&s, &w);
  CHECK(((w >> 24) & 0x0F) == 0, "group unchanged");
  PASS();
}

/* --- SysEx7 send (fragmentation) --- */

void test_send_sysex7_short(void) {
  TEST("send sysex7: 3 bytes = 1 complete packet");
  reset_state();
  uint8_t data[] = {0x7E, 0x7F, 0x09};
  midi2_proc_send_sysex7(0, data, 3, test_write_fn, NULL);
  CHECK(write_buf_pos == 2, "1 packet (2 words)");
  CHECK(midi2_msg_get_mt(write_buf) == MIDI2_MT_SYSEX7, "MT 0x3");
  CHECK(((write_buf[0] >> 20) & 0x0F) == 0x00, "status=complete");
  PASS();
}

void test_send_sysex7_long(void) {
  TEST("send sysex7: 15 bytes = 3 packets (start+cont+end)");
  reset_state();
  uint8_t data[15];
  int i;
  for (i = 0; i < 15; i++) data[i] = (uint8_t)(i + 1);

  midi2_proc_send_sysex7(0, data, 15, test_write_fn, NULL);
  CHECK(write_buf_pos == 6, "3 packets (6 words)");

  /* Check statuses: start, continue, end */
  CHECK(((write_buf[0] >> 20) & 0x0F) == 0x01, "packet 1 = start");
  CHECK(((write_buf[2] >> 20) & 0x0F) == 0x02, "packet 2 = continue");
  CHECK(((write_buf[4] >> 20) & 0x0F) == 0x03, "packet 3 = end");
  PASS();
}

void test_send_sysex7_exact_6(void) {
  TEST("send sysex7: 6 bytes = 1 complete packet");
  reset_state();
  uint8_t data[] = {1, 2, 3, 4, 5, 6};
  midi2_proc_send_sysex7(0, data, 6, test_write_fn, NULL);
  CHECK(write_buf_pos == 2, "1 packet");
  CHECK(((write_buf[0] >> 20) & 0x0F) == 0x00, "status=complete");
  PASS();
}

void test_send_sysex7_exact_12(void) {
  TEST("send sysex7: 12 bytes = 2 packets (start+end)");
  reset_state();
  uint8_t data[12];
  int i;
  for (i = 0; i < 12; i++) data[i] = (uint8_t)(i + 1);

  midi2_proc_send_sysex7(0, data, 12, test_write_fn, NULL);
  CHECK(write_buf_pos == 4, "2 packets (4 words)");
  CHECK(((write_buf[0] >> 20) & 0x0F) == 0x01, "packet 1 = start");
  CHECK(((write_buf[2] >> 20) & 0x0F) == 0x03, "packet 2 = end");
  PASS();
}

/* --- FB Name Notification (v0.2.4+) --- */

void test_send_fb_name_short(void) {
  TEST("send fb_name: 8 bytes = 1 complete packet (4 words)");
  reset_state();
  midi2_proc_send_fb_name(0, "gingo.p4", test_write_fn, NULL);
  CHECK(write_buf_pos == 4, "1 packet = 4 words");
  uint32_t w0 = write_buf[0];
  CHECK(((w0 >> 28) & 0x0F) == 0x0F, "MT == Stream");
  CHECK(((w0 >> 26) & 0x03) == 0x00, "Form == Complete");
  CHECK(((w0 >> 16) & 0x3FF) == 0x012, "status == FB Name");
  CHECK(((w0 >> 8) & 0xFF) == 0x00, "FB# == 0");
  CHECK((w0 & 0xFF) == 'g', "first name byte at bits 0-7");
  PASS();
}

void test_send_fb_name_long(void) {
  TEST("send fb_name: 15 bytes = 2 packets (start+end)");
  reset_state();
  midi2_proc_send_fb_name(1, "gingo.p4 Router", test_write_fn, NULL);
  CHECK(write_buf_pos == 8, "2 packets = 8 words");
  CHECK(((write_buf[0] >> 26) & 0x03) == 0x01, "packet 1 == Start");
  CHECK(((write_buf[4] >> 26) & 0x03) == 0x03, "packet 2 == End");
  CHECK(((write_buf[0] >> 8) & 0xFF) == 0x01, "FB# == 1 on packet 1");
  CHECK(((write_buf[4] >> 8) & 0xFF) == 0x01, "FB# == 1 on packet 2");
  PASS();
}

void test_send_fb_name_exact_13(void) {
  TEST("send fb_name: 13 bytes = 1 complete packet");
  reset_state();
  midi2_proc_send_fb_name(2, "0123456789abc", test_write_fn, NULL);
  CHECK(write_buf_pos == 4, "1 packet");
  CHECK(((write_buf[0] >> 26) & 0x03) == 0x00, "Form == Complete");
  PASS();
}

void test_send_fb_name_empty_noop(void) {
  TEST("send fb_name: empty string sends nothing");
  reset_state();
  midi2_proc_send_fb_name(0, "", test_write_fn, NULL);
  CHECK(write_buf_pos == 0, "no packets");
  PASS();
}

/* --- Endpoint Name / Product Instance ID senders (v0.3.0) --- */

void test_send_endpoint_name_short(void) {
  TEST("send endpoint_name: 2 bytes = 1 complete packet");
  reset_state();
  midi2_proc_send_endpoint_name("Hi", test_write_fn, NULL);
  CHECK(write_buf_pos == 4, "1 packet = 4 words");
  uint32_t w0 = write_buf[0];
  CHECK(((w0 >> 28) & 0x0F) == 0x0F, "MT == Stream");
  CHECK(((w0 >> 26) & 0x03) == 0x00, "Form == Complete");
  CHECK(((w0 >> 16) & 0x3FF) == 0x003, "status == Endpoint Name");
  CHECK(((w0 >> 8) & 0xFF) == 'H', "byte 0 in w0[15:8]");
  CHECK((w0 & 0xFF) == 'i', "byte 1 in w0[7:0]");
  PASS();
}

void test_send_endpoint_name_long(void) {
  TEST("send endpoint_name: >14 bytes fragments Start/End");
  reset_state();
  midi2_proc_send_endpoint_name("gingo.p4 Endpoint Name",
                                test_write_fn, NULL);
  CHECK(write_buf_pos == 8, "2 packets = 8 words");
  CHECK(((write_buf[0] >> 26) & 0x03) == 0x01, "packet 1 == Start");
  CHECK(((write_buf[4] >> 26) & 0x03) == 0x03, "packet 2 == End");
  CHECK(((write_buf[0] >> 16) & 0x3FF) == 0x003, "status 0x003 on p1");
  CHECK(((write_buf[4] >> 16) & 0x3FF) == 0x003, "status 0x003 on p2");
  PASS();
}

void test_send_endpoint_name_exact_14(void) {
  TEST("send endpoint_name: exactly 14 bytes = 1 complete packet");
  reset_state();
  midi2_proc_send_endpoint_name("0123456789abcd", test_write_fn, NULL);
  CHECK(write_buf_pos == 4, "1 packet");
  CHECK(((write_buf[0] >> 26) & 0x03) == 0x00, "Form == Complete");
  PASS();
}

void test_send_endpoint_name_empty_noop(void) {
  TEST("send endpoint_name: empty sends nothing");
  reset_state();
  midi2_proc_send_endpoint_name("", test_write_fn, NULL);
  CHECK(write_buf_pos == 0, "no packets");
  PASS();
}

void test_send_product_id_short(void) {
  TEST("send product_id: 7 bytes = 1 complete packet, status 0x004");
  reset_state();
  midi2_proc_send_product_id("PID-123", test_write_fn, NULL);
  CHECK(write_buf_pos == 4, "1 packet");
  CHECK(((write_buf[0] >> 26) & 0x03) == 0x00, "Form == Complete");
  CHECK(((write_buf[0] >> 16) & 0x3FF) == 0x004, "status == Product Instance ID");
  PASS();
}

/* --- Device Identity sender (v0.3.0) --- */

void test_send_device_identity(void) {
  TEST("send device_identity: single 4-word UMP with status 0x002");
  reset_state();
  midi2_proc_send_device_identity(0x00123456u, 0x0123, 0x04FE,
                                    0x12345678u, test_write_fn, NULL);
  CHECK(write_buf_pos == 4, "always 1 packet = 4 words");
  uint32_t w0 = write_buf[0];
  CHECK(((w0 >> 28) & 0x0F) == 0x0F, "MT == Stream");
  CHECK(((w0 >> 16) & 0x3FF) == 0x002, "status == Device Identity");
  CHECK((write_buf[1] & 0xFFFFFF00u) == (0x00123456u << 8),
        "w[1] carries manufacturer << 8");
  CHECK(write_buf[2] == (((uint32_t)0x0123 << 16) | (uint32_t)0x04FE),
        "w[2] = family<<16 | model");
  CHECK(write_buf[3] == 0x12345678u, "w[3] = version");
  PASS();
}

/* --- SysEx8 sender (v0.3.0) --- */

void test_send_sysex8_short(void) {
  TEST("send sysex8: 5 bytes = 1 complete packet");
  reset_state();
  uint8_t data[] = {0xAA, 0xBB, 0xCC, 0xDD, 0xEE};
  midi2_proc_send_sysex8(0, 0x01, data, 5, test_write_fn, NULL);
  CHECK(write_buf_pos == 4, "1 packet");
  uint32_t w0 = write_buf[0];
  CHECK(((w0 >> 28) & 0x0F) == 0x05, "MT == SysEx8");
  CHECK(((w0 >> 24) & 0x0F) == 0x00, "group == 0");
  CHECK(((w0 >> 20) & 0x0F) == 0x00, "status nibble == Complete");
  CHECK(((w0 >> 16) & 0x0F) == 6, "num_bytes == stream_id(1) + data(5)");
  CHECK(((w0 >> 8) & 0xFF) == 0x01, "stream_id == 0x01");
  CHECK((w0 & 0xFF) == 0xAA, "data[0] in w0[7:0]");
  PASS();
}

void test_send_sysex8_long(void) {
  TEST("send sysex8: 40 bytes = 4 packets (start/continue/continue/end)");
  reset_state();
  uint8_t data[40];
  int i;
  for (i = 0; i < 40; i++) data[i] = (uint8_t)i;
  midi2_proc_send_sysex8(3, 0x02, data, 40, test_write_fn, NULL);
  CHECK(write_buf_pos == 16, "4 packets * 4 words");
  CHECK(((write_buf[0]  >> 20) & 0x0F) == 0x01, "p1 == Start");
  CHECK(((write_buf[4]  >> 20) & 0x0F) == 0x02, "p2 == Continue");
  CHECK(((write_buf[8]  >> 20) & 0x0F) == 0x02, "p3 == Continue");
  CHECK(((write_buf[12] >> 20) & 0x0F) == 0x03, "p4 == End");
  CHECK(((write_buf[0] >> 24) & 0x0F) == 3, "group preserved on all packets");
  CHECK(((write_buf[12] >> 24) & 0x0F) == 3, "group preserved on last packet");
  PASS();
}

void test_send_sysex8_exact_13(void) {
  TEST("send sysex8: 13 bytes = 1 complete packet");
  reset_state();
  uint8_t data[13];
  int i;
  for (i = 0; i < 13; i++) data[i] = (uint8_t)(i + 1);
  midi2_proc_send_sysex8(0, 0x00, data, 13, test_write_fn, NULL);
  CHECK(write_buf_pos == 4, "1 packet");
  CHECK(((write_buf[0] >> 20) & 0x0F) == 0x00, "Complete");
  PASS();
}

void test_send_sysex8_empty_noop(void) {
  TEST("send sysex8: zero-length sends nothing");
  reset_state();
  midi2_proc_send_sysex8(0, 0x00, NULL, 0, test_write_fn, NULL);
  CHECK(write_buf_pos == 0, "no packets");
  PASS();
}

/* --- SysEx8 reassembly --- */

static uint8_t  last_sysex8_stream_id;
static uint8_t  last_sysex8_data[256];
static uint16_t last_sysex8_len;
static int      sysex8_cb_count;

static void test_sysex8_cb(uint8_t group, uint8_t stream_id,
                             const uint8_t *data, uint16_t len, void *ctx) {
  (void)group; (void)ctx;
  last_sysex8_stream_id = stream_id;
  memcpy(last_sysex8_data, data, len);
  last_sysex8_len = len;
  sysex8_cb_count++;
}

void test_sysex8_complete(void) {
  TEST("sysex8: complete single-packet");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  s.on_sysex8 = test_sysex8_cb;
  sysex8_cb_count = 0;
  last_sysex8_len = 0;

  uint32_t w[4];
  uint8_t data[] = {0xAA, 0xBB, 0xCC};
  midi2_msg_sysex8_packet(w, 0, MIDI2_SYSEX8_COMPLETE, 1, data, 3);
  midi2_proc_feed(&s, w, 4);

  CHECK(sysex8_cb_count == 1, "callback called");
  CHECK(last_sysex8_stream_id == 1, "stream_id=1");
  CHECK(last_sysex8_len == 3, "3 bytes");
  CHECK(last_sysex8_data[0] == 0xAA, "data[0]");
  PASS();
}

void test_sysex8_multi_packet(void) {
  TEST("sysex8: multi-packet reassembly");
  midi2_proc_state s;
  midi2_proc_init(&s, proc_sysex_buf, sizeof(proc_sysex_buf), proc_sysex8_buf, sizeof(proc_sysex8_buf));
  s.on_sysex8 = test_sysex8_cb;
  sysex8_cb_count = 0;
  last_sysex8_len = 0;

  uint32_t w[4];
  uint8_t part1[] = {0x01, 0x02, 0x03, 0x04, 0x05};
  uint8_t part2[] = {0x06, 0x07, 0x08};

  midi2_msg_sysex8_packet(w, 0, MIDI2_SYSEX8_START, 2, part1, 5);
  midi2_proc_feed(&s, w, 4);
  CHECK(sysex8_cb_count == 0, "not delivered yet");

  midi2_msg_sysex8_packet(w, 0, MIDI2_SYSEX8_END, 2, part2, 3);
  midi2_proc_feed(&s, w, 4);
  CHECK(sysex8_cb_count == 1, "delivered after END");
  CHECK(last_sysex8_len == 8, "8 bytes total");
  CHECK(last_sysex8_stream_id == 2, "stream_id=2");
  CHECK(last_sysex8_data[0] == 0x01, "first byte");
  CHECK(last_sysex8_data[7] == 0x08, "last byte");
  PASS();
}

/* --- NULL paths (boundary defensive checks) --- */

void test_init_null_safe(void) {
  TEST("proc_init: NULL state is no-op (no crash)");
  midi2_proc_init(NULL, NULL, 0, NULL, 0);
  /* If we reach here, no segfault occurred */
  PASS();
}

void test_feed_null_safe(void) {
  TEST("proc_feed: NULL state or NULL words returns no-op");
  midi2_proc_state s;
  uint8_t sx7[16], sx8[16];
  midi2_proc_init(&s, sx7, sizeof(sx7), sx8, sizeof(sx8));
  uint32_t w = 0x20903C7Fu;

  midi2_proc_feed(NULL, &w, 1);  /* NULL state */
  midi2_proc_feed(&s, NULL, 1);  /* NULL words */
  /* No crash, no callbacks invoked */
  PASS();
}

void test_remap_group_null_safe(void) {
  TEST("proc_remap_group: NULL state or NULL words is no-op");
  midi2_proc_state s;
  uint8_t sx7[16], sx8[16];
  midi2_proc_init(&s, sx7, sizeof(sx7), sx8, sizeof(sx8));
  uint32_t w = 0x40903C7Fu;
  uint32_t w_orig = w;

  midi2_proc_remap_group(NULL, &w);   /* NULL state */
  CHECK(w == w_orig, "w unchanged with NULL state");

  midi2_proc_remap_group(&s, NULL);   /* NULL words */
  /* No crash */
  PASS();
}

static uint32_t null_safe_writes;
static uint32_t null_safe_write_count(const uint32_t *w, uint32_t c, void *ctx) {
  (void)w; (void)ctx;
  null_safe_writes += c;
  return c;
}

void test_send_sysex7_null_safe(void) {
  TEST("proc_send_sysex7: NULL write_fn or NULL data with length>0 are no-op");
  uint8_t data[6] = {1, 2, 3, 4, 5, 6};
  null_safe_writes = 0;

  midi2_proc_send_sysex7(0, data, 6, NULL, NULL);          /* NULL write_fn */
  CHECK(null_safe_writes == 0, "no writes with NULL write_fn");

  midi2_proc_send_sysex7(0, NULL, 6, null_safe_write_count, NULL); /* NULL data + len>0 */
  CHECK(null_safe_writes == 0, "no writes with NULL data");

  midi2_proc_send_sysex7(0, data, 0, null_safe_write_count, NULL); /* len=0 */
  CHECK(null_safe_writes == 0, "no writes with length=0");
  PASS();
}

/* --- Main --- */

int main(void) {
  printf("\n=== midi2_proc Unit Tests ===\n\n");

  printf("[Init]\n");
  test_init_defaults();

  printf("\n[Feed & Group Filter]\n");
  test_feed_note_on_delivered();
  test_feed_group_filtered();
  test_feed_group_0_passes();
  test_feed_utility_bypasses_filter();
  test_feed_stream_bypasses_filter();

  printf("\n[SysEx7 Reassembly]\n");
  test_sysex7_complete();
  test_sysex7_multi_packet();

  printf("\n[Track 1 guard]\n");
  test_proc_guard_short_data128();
  test_proc_guard_sysex_reassembly();

  printf("\n[Track 2 reentrancy]\n");
  test_proc_in_feed_set_during_feed();
  test_proc_in_feed_independent_states();
  test_sysex7_group_interleave_discards();

  printf("\n[Group Remap]\n");
  test_remap_identity();
  test_remap_changes_group();
  test_remap_skips_utility();

  printf("\n[SysEx7 Send]\n");
  test_send_sysex7_short();
  test_send_sysex7_long();
  test_send_sysex7_exact_6();
  test_send_sysex7_exact_12();
  test_send_fb_name_short();
  test_send_fb_name_long();
  test_send_fb_name_exact_13();
  test_send_fb_name_empty_noop();

  printf("\n[Stream Text Notifications (v0.3.0)]\n");
  test_send_endpoint_name_short();
  test_send_endpoint_name_long();
  test_send_endpoint_name_exact_14();
  test_send_endpoint_name_empty_noop();
  test_send_product_id_short();

  printf("\n[Device Identity (v0.3.0)]\n");
  test_send_device_identity();

  printf("\n[SysEx8 Send (v0.3.0)]\n");
  test_send_sysex8_short();
  test_send_sysex8_long();
  test_send_sysex8_exact_13();
  test_send_sysex8_empty_noop();

  printf("\n[SysEx8 Reassembly]\n");
  test_sysex8_complete();
  test_sysex8_multi_packet();

  printf("\n[NULL Paths (boundary defensive)]\n");
  test_init_null_safe();
  test_feed_null_safe();
  test_remap_group_null_safe();
  test_send_sysex7_null_safe();

  printf("\n=== Results: %d passed, %d failed ===\n\n", passed, failed);
  return failed > 0 ? 1 : 0;
}
