#define MG_ENABLE_TCPIP 1
#define MG_ENABLE_TCPIP_DRIVER_INIT 0

#include "mongoose.c"  // order is important, this one first

#include "driver_mock.c"

static int s_num_tests = 0;
static bool s_error = false;
static int s_sent_fragment = 0;
static int s_seg_sent = 0;

#ifdef NO_ABORT
static int s_abort = 0;
#define ABORT() ++s_abort, s_error = true
#else
#ifdef NO_SLEEP_ABORT
#define ABORT() abort()
#else
#define ABORT()                       \
  sleep(2); /* 2s, GH print reason */ \
  abort();
#endif
#endif

#define ASSERT(expr)                                            \
  do {                                                          \
    s_num_tests++;                                              \
    if (!(expr)) {                                              \
      printf("FAILURE %s:%d: %s\n", __FILE__, __LINE__, #expr); \
      fflush(stdout);                                           \
      ABORT();                                                  \
    }                                                           \
  } while (0)

struct ipp {
  struct ip *ip4;
  struct ip6 *ip6;
};

static void test_csum(void) {
  uint8_t ip[20] = {0x45, 0x00, 0x00, 0x1c, 0x00, 0x00, 0x00, 0x00, 0x28, 0x11,
                    0x94, 0xcf, 0x7f, 0x00, 0x00, 0x01, 0x7f, 0x00, 0x00, 0x01};
  uint8_t pseudo_udp_odd[53] = {
      0xc0, 0xa8, 0x45, 0x58, 0x08, 0x08, 0x08, 0x08, 0x00, 0x11, 0x00,
      0x29, 0xad, 0x67, 0x00, 0x35, 0x00, 0x29, 0xaf, 0xf8, 0x00, 0x01,
      0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04,
      0x74, 0x69, 0x6d, 0x65, 0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
      0x03, 0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01};

  ASSERT(ipcsum(ip, 20) == 0);
  ASSERT(ipcsum(pseudo_udp_odd, 53) == 0);
  // UDP and TCP checksum calc funcions use the same basic calls as ipcsum()
}

#if !MG_ENABLE_IPV6
#define udp6csum_ok(d, u) true
#define tcp6csum_ok(d, t) true
#endif

static bool executed = false;

static void mif_fn(struct mg_tcpip_if *ifp, int ev, void *ev_data) {
  if (ev == MG_TCPIP_EV_ST_CHG) {
    ASSERT(*(uint8_t *) ev_data == MG_TCPIP_STATE_READY);
    executed = true;
  }
  (void) ifp;
}

static void test_statechange(void) {
  struct mg_tcpip_if iface;
  memset(&iface, 0, sizeof(iface));
  iface.ip = mg_htonl(0x01020304);
  iface.state = MG_TCPIP_STATE_READY;
  iface.driver = &mg_tcpip_driver_mock;
  iface.fn = mif_fn;
  onstatechange(&iface);
  ASSERT(executed == true);
  executed = false;
}
#if MG_ENABLE_IPV6
static void mif6_fn(struct mg_tcpip_if *ifp, int ev, void *ev_data) {
  if (ev == MG_TCPIP_EV_ST6_CHG) {
    ASSERT(*(uint8_t *) ev_data == MG_TCPIP_STATE_READY);
    executed = true;
  }
  (void) ifp;
}

static void test_state6change(void) {
  struct mg_tcpip_if iface;
  memset(&iface, 0, sizeof(iface));
  iface.ip6[0] = mg_htonll(0x01020304);
  iface.ip6[1] = mg_htonll(0x05060708);
  iface.state6 = MG_TCPIP_STATE_READY;
  iface.driver = &mg_tcpip_driver_mock;
  iface.fn = mif6_fn;
  onstate6change(&iface);
  ASSERT(executed == true);
  executed = false;
}
#endif

static void ph(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_POLL) ++(*(int *) c->fn_data);
  (void) c, (void) ev_data;
}

static void fn(struct mg_connection *c, int ev, void *ev_data) {
  (void) c, (void) ev, (void) ev_data;
}

static void tcpclosure_fn(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_ACCEPT) c->is_draining = 1;
  (void) c, (void) ev_data;
}

static void client_fn(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_ERROR || ev == MG_EV_CONNECT) (*(int *) c->fn_data) = ev;
  (void) c, (void) ev_data;
}

static void frag_recv_fn(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_ERROR) {
    if (s_sent_fragment > 0) {
      ASSERT(s_sent_fragment == 1);
      ASSERT(strcmp((char *) ev_data, "Received fragmented packet") == 0);
      s_sent_fragment = 2;
    }
  }
  (void) c, (void) ev_data;
}

// mock send to a non-existent peer using the listener connection
static void frag_send_fn(struct mg_connection *c, int ev, void *ev_data) {
  static bool s_sent;
  static int s_seg_sizes[] = {416, 416, 368};  // based on len=1200 and MTU=500
  if (ev == MG_EV_POLL) {
    if (!s_sent) {
      struct connstate *s = (struct connstate *) (c + 1);
      s->dmss = 1500;      // mock set some destination MSS way larger
      c->send.len = 1200;  // setting TCP payload size
      s_sent = true;
    }
  } else if (ev == MG_EV_WRITE) {
    // Checking TCP segment sizes (ev_data points to the TCP payload length)
    ASSERT(*(int *) ev_data == s_seg_sizes[s_seg_sent++]);
  }
  (void) c, (void) ev_data;
}

static void test_poll(void) {
  int count = 0, i;
  struct mg_tcpip_if mif;
  struct mg_mgr mgr;
  mg_mgr_init(&mgr);
  memset(&mif, 0, sizeof(mif));
  mif.driver = &mg_tcpip_driver_mock;
  mg_tcpip_init(&mgr, &mif);
  mg_http_listen(&mgr, "http://0.0.0.0:12346", ph, &count);
  for (i = 0; i < 10; i++) mg_mgr_poll(&mgr, 0);
  ASSERT(count == 10);
  mg_mgr_free(&mgr);
}

#define DRIVER_BUF_SIZE 1540

struct driver_data {
  char buf[DRIVER_BUF_SIZE];
  size_t len;
  bool tx_ready;  // data can be read from tx
};

static struct driver_data s_driver_data;

static size_t if_tx(const void *buf, size_t len, struct mg_tcpip_if *ifp) {
  struct driver_data *driver_data = (struct driver_data *) ifp->driver_data;
  if (len > DRIVER_BUF_SIZE) len = DRIVER_BUF_SIZE;
  driver_data->len = len;
  memcpy(driver_data->buf, buf, len);
  driver_data->tx_ready = true;
  return len;
}

static bool if_poll(struct mg_tcpip_if *ifp, bool s1) {
  return s1 && ifp->driver_data ? true : false;
}

static size_t if_rx(void *buf, size_t len, struct mg_tcpip_if *ifp) {
  struct driver_data *driver_data = (struct driver_data *) ifp->driver_data;
  if (driver_data->len == 0) return 0;
  if (len > driver_data->len) len = driver_data->len;
  memcpy(buf, driver_data->buf, len);
  driver_data->len = 0;  // cleaning up the buffer
  driver_data->tx_ready = false;
  return len;
}

static bool received_response(struct driver_data *driver) {
  bool was_ready = driver->tx_ready;
  driver->tx_ready = false;
  return was_ready;
}

static void create_tcp_seg(struct eth *e, struct ipp *ipp, uint32_t seq,
                           uint32_t ack, uint8_t flags, uint16_t sport,
                           uint16_t dport, size_t payload_len, void *opts,
                           unsigned int opts_len) {
  struct tcp t;
  memset(&t, 0, sizeof(t));
  t.flags = flags;
  t.seq = mg_htonl(seq);
  t.ack = mg_htonl(ack);
  t.sport = mg_htons(sport);
  t.dport = mg_htons(dport);
  t.off = (uint8_t) ((sizeof(t) / 4) << 4) + (uint8_t) ((opts_len / 4) << 4);
  memcpy(s_driver_data.buf, e, sizeof(*e));
#if MG_ENABLE_IPV6
  if (ipp->ip6 != NULL) {
    struct ip6 *ip = ipp->ip6;
    struct tcp *tcp =
        (struct tcp *) (s_driver_data.buf + sizeof(*e) + sizeof(*ip));
    ip->plen = mg_htons((uint16_t) (4 * (t.off >> 4) + payload_len));
    memcpy(s_driver_data.buf + sizeof(*e), ip, sizeof(*ip));
    memcpy(tcp, &t, sizeof(t));
    if (opts != NULL && opts_len)
      memcpy(s_driver_data.buf + sizeof(*e) + sizeof(*ip) + sizeof(t), opts,
             opts_len);
    tcp->csum = p6csum(ip, tcp, (4 * (t.off >> 4) + payload_len));
    s_driver_data.len =
        sizeof(*e) + sizeof(*ip) + sizeof(t) + payload_len + opts_len;
  } else
#endif
  {
    struct ip *ip = ipp->ip4;
    struct tcp *tcp =
        (struct tcp *) (s_driver_data.buf + sizeof(*e) + sizeof(*ip));
    ip->len =
        mg_htons((uint16_t) (sizeof(*ip) + 4 * (t.off >> 4) + payload_len));
    ip->csum = ipcsum(ip, sizeof(*ip));  // no options
    memcpy(s_driver_data.buf + sizeof(*e), ip, sizeof(*ip));
    ip->csum = 0;
    memcpy(tcp, &t, sizeof(t));
    if (opts != NULL && opts_len)
      memcpy(s_driver_data.buf + sizeof(*e) + sizeof(*ip) + sizeof(t), opts,
             opts_len);
    tcp->csum = pcsum(ip, tcp, (4 * (t.off >> 4) + payload_len));
    s_driver_data.len =
        sizeof(*e) + sizeof(*ip) + sizeof(t) + payload_len + opts_len;
  }
  if (s_driver_data.len < 64) s_driver_data.len = 64;  // add padding if needed
}

static void create_tcp_simpleseg(struct eth *e, struct ipp *ipp, uint32_t seq,
                                 uint32_t ack, uint8_t flags,
                                 size_t payload_len) {
  // use sport=1 to ease seqno stuff, dport=80 due to init_tcp_tests() below
  create_tcp_seg(e, ipp, seq, ack, flags, 1, 80, payload_len, NULL, 0);
}

static void init_tests(struct mg_mgr *mgr, struct eth *e, struct ipp *ipp,
                       struct mg_tcpip_driver *driver, struct mg_tcpip_if *mif,
                       uint8_t proto) {
  mg_mgr_init(mgr);
  memset(mif, 0, sizeof(*mif));
  memset(&s_driver_data, 0, sizeof(struct driver_data));
  driver->init = NULL, driver->tx = if_tx, driver->poll = if_poll,
  driver->rx = if_rx;
  mif->driver = driver;
  mif->driver_data = &s_driver_data;
#if MG_ENABLE_IPV6
  if (ipp->ip6 != NULL) {
    mif->ip6[0] = mg_htonll(0x100000000000000);
    mif->prefix[0] = 1;
    mif->prefix_len = 64;
    mif->gw6[0] = mg_htonll(0x100000000000000);
    mif->gw6_ready = true;
    mif->state = MG_TCPIP_STATE_READY;   // so DHCP stops
    mif->state6 = MG_TCPIP_STATE_READY;  // so mg_send() works and RS stops
  } else
#endif
  {
    mif->ip = mg_htonl(0x1000000);
    mif->gw = mg_htonl(0x1000000);
    mif->gw_ready = true;
    mif->mask = 255;  // use router, to avoid firing an ARP request
    mif->state = MG_TCPIP_STATE_READY;  // so mg_send() works and DHCP stops
  }
  mg_tcpip_init(mgr, mif);

  // setting the Ethernet header
  memset(e, 0, sizeof(*e));
  memcpy(e->dst, mif->mac, 6 * sizeof(uint8_t));
  e->type = mg_htons(ipp->ip4 != NULL ? 0x800 : 0x86dd);

  // setting the IP header
#if MG_ENABLE_IPV6
  if (ipp->ip6 != NULL) {
    struct ip6 *ip = ipp->ip6;
    memset(ip, 0, sizeof(*ip));
    ip->ver = 0x60;
    ip->next = proto;
    // must be outside of Mongoose network to avoid firing NS requests
    ip->src[0] = mg_htonll(0x200000000000000);
    ip->dst[0] = mif->ip6[0];
  } else
#endif
  {
    struct ip *ip = ipp->ip4;
    memset(ip, 0, sizeof(*ip));
    ip->ver = (4 << 4) | 5;
    ip->proto = proto;
    // must be outside of Mongoose network to avoid firing ARP requests
    ip->src = mg_htonl(0x2000000);
    ip->dst = mif->ip;
  }
}

static void init_tcp_tests(struct mg_mgr *mgr, struct eth *e, struct ipp *ipp,
                           struct mg_tcpip_driver *driver,
                           struct mg_tcpip_if *mif, mg_event_handler_t f) {
  init_tests(mgr, e, ipp, driver, mif, 6);  // 6 -> TCP
#if MG_ENABLE_IPV6
  if (ipp->ip6 != NULL) {
    mg_http_listen(mgr, "http://[::]:80", f, NULL);
  } else
#endif
  {
    mg_http_listen(mgr, "http://0.0.0.0:80", f, NULL);
  }
  mgr->conns->pfn = NULL;  // HTTP handler not needed
  mg_mgr_poll(mgr, 0);
}

static void init_tcp_handshake(struct eth *e, struct ipp *ipp,
                               struct mg_mgr *mgr) {
  struct tcp *t =
      (struct tcp *) (s_driver_data.buf + sizeof(*e) +
                      (ipp->ip4 ? sizeof(struct ip) : sizeof(struct ip6)));

  // SYN
  create_tcp_simpleseg(e, ipp, 1000, 0, TH_SYN, 0);
  MG_VERBOSE(("SYN     -->"));
  mg_mgr_poll(mgr, 0);  // make sure we clean former stuff in buffer

  // SYN-ACK
  while (!received_response(&s_driver_data)) mg_mgr_poll(mgr, 0);
  ASSERT((t->flags == (TH_SYN | TH_ACK)));
  ASSERT((t->ack == mg_htonl(1001)));
  MG_VERBOSE(("SYN+ACK <--"));

  // ACK
  create_tcp_simpleseg(e, ipp, 1001, 2, TH_ACK, 0);
  MG_VERBOSE(("ACK     -->"));
  mg_mgr_poll(mgr, 0);  // this may have data on return !
}

// DHCP discovery works as a 1 second timeout, we take advantage of it
// (something is received within 1s) and we mask it when doing longer waits
// (verify received data is TCP by checking IP's protocol field)
static void test_tcp_basics(bool ipv6) {
  struct mg_mgr mgr;
  struct eth e;
  struct ip ip;
  struct ip6 ip6;
  struct ipp ipp;
  struct tcp *t = (struct tcp *) (s_driver_data.buf + sizeof(e) +
                                  (!ipv6 ? sizeof(ip) : sizeof(ip6)));
  struct ip *i = (struct ip *) (s_driver_data.buf + sizeof(e));
  struct ip6 *i6 = (struct ip6 *) (s_driver_data.buf + sizeof(e));
  uint64_t start, now;
  struct mg_tcpip_driver driver;
  struct mg_tcpip_if mif;

  ipp.ip4 = !ipv6 ? &ip : NULL;
  ipp.ip6 = ipv6 ? &ip6 : NULL;

  init_tcp_tests(&mgr, &e, &ipp, &driver, &mif, fn);

  // https://datatracker.ietf.org/doc/html/rfc9293#section-3.5.2	Reset
  // Generation non-used port. Group 1 in RFC send SYN, expect RST + ACK
  create_tcp_seg(&e, &ipp, 1234, 4321, TH_SYN, 1, 69, 0, NULL, 0);
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT(t->flags == (TH_RST | TH_ACK));
  ASSERT(t->seq == mg_htonl(0));
  ASSERT(t->ack == mg_htonl(1235));
  if (ipv6) {
    ASSERT(i6->src[0] == mg_htonll(0x100000000000000) && i6->src[1] == 0 &&
           i6->dst[0] == mg_htonll(0x200000000000000) && i6->dst[1] == 0);
    ASSERT(tcp6csum_ok(i6, t));
  } else {
    ASSERT(i->src == mg_htonl(0x1000000) && i->dst == mg_htonl(0x2000000));
    ASSERT(tcpcsum_ok(i, t));
  }

  // send SYN+ACK, expect RST
  create_tcp_seg(&e, &ipp, 1234, 4321, TH_SYN | TH_ACK, 1, 69, 0, NULL, 0);
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT(t->flags == TH_RST);
  ASSERT(t->seq == mg_htonl(4321));
  // send data, expect RST + ACK
  create_tcp_seg(&e, &ipp, 1234, 4321, TH_PUSH, 1, 69, 2, NULL, 0);
  mg_mgr_poll(&mgr, 0);
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT(t->flags == (TH_RST | TH_ACK));
  ASSERT(t->seq == mg_htonl(0));
  ASSERT(t->ack == mg_htonl(1236));

  // send ACK, expect RST
  create_tcp_seg(&e, &ipp, 1234, 4321, TH_ACK, 1, 69, 0, NULL, 0);
  mg_mgr_poll(&mgr, 0);
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT(t->flags == TH_RST);
  ASSERT(t->seq == mg_htonl(4321));

  // send FIN, expect RST + ACK
  create_tcp_seg(&e, &ipp, 1234, 4321, TH_FIN, 1, 69, 0, NULL, 0);
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT(t->flags == (TH_RST | TH_ACK));  // Linux answers RST only
  ASSERT(t->seq == mg_htonl(0));
  ASSERT(t->ack == mg_htonl(1235));

  // send FIN+ACK, expect RST
  create_tcp_seg(&e, &ipp, 1234, 4321, TH_FIN | TH_ACK, 1, 69, 0, NULL, 0);
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT(t->flags == TH_RST);
  ASSERT(t->seq == mg_htonl(4321));

  // listening, non-connected port. Group 2 in RFC
  // send data, expect no response
  create_tcp_seg(&e, &ipp, 1234, 4321, TH_PUSH, 1, 80, 2, NULL, 0);
  mg_mgr_poll(&mgr, 0);
  ASSERT(!received_response(&s_driver_data));

  // send ACK, expect RST
  create_tcp_seg(&e, &ipp, 1234, 4321, TH_ACK, 1, 80, 0, NULL, 0);
  mg_mgr_poll(&mgr, 0);
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT(t->flags == TH_RST);
  ASSERT(t->seq == mg_htonl(4321));
  if (ipv6) {
    ASSERT(i6->src[0] == mg_htonll(0x100000000000000) && i6->src[1] == 0 &&
           i6->dst[0] == mg_htonll(0x200000000000000) && i6->dst[1] == 0);
  } else {
    ASSERT(i->src == mg_htonl(0x1000000) && i->dst == mg_htonl(0x2000000));
  }

  // send SYN+ACK, expect RST
  create_tcp_seg(&e, &ipp, 1234, 4321, TH_SYN | TH_ACK, 1, 80, 0, NULL, 0);
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT(t->flags == TH_RST);
  ASSERT(t->seq == mg_htonl(4321));

  // send FIN, expect no response
  create_tcp_seg(&e, &ipp, 1234, 4321, TH_FIN, 1, 80, 0, NULL, 0);
  mg_mgr_poll(&mgr, 0);
  ASSERT(!received_response(&s_driver_data));

  // send FIN+ACK, expect RST
  create_tcp_seg(&e, &ipp, 1234, 4321, TH_FIN | TH_ACK, 1, 80, 0, NULL, 0);
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT(t->flags == TH_RST);
  ASSERT(t->seq == mg_htonl(4321));

  // we currently don't validate checksum, no silently discarded segment test

  init_tcp_handshake(&e, &ipp, &mgr);  // starts with seq_no=1000, ackno=2

  // no MSS sent, so it must default to 536/1220 (RFC-9293 3.7.1)
  ASSERT(((struct connstate *) (mgr.conns + 1))->dmss == (ipv6 ? 1220 : 536));

  // segment with seq_no within window
  create_tcp_simpleseg(&e, &ipp, 1010, 2, TH_PUSH, 2);
  mg_mgr_poll(&mgr, 0);
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT((t->flags == TH_ACK));
  ASSERT((t->ack == mg_htonl(1001)));  // expecting 1001, dude
  if (ipv6) {
    ASSERT(i6->src[0] == mg_htonll(0x100000000000000) && i6->src[1] == 0 &&
           i6->dst[0] == mg_htonll(0x200000000000000) && i6->dst[1] == 0);
  } else {
    ASSERT(i->src == mg_htonl(0x1000000) && i->dst == mg_htonl(0x2000000));
  }

  // segment with seq_no way out of window
  create_tcp_simpleseg(&e, &ipp, 1000000, 2, TH_PUSH, 2);
  mg_mgr_poll(&mgr, 0);
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT((t->flags == TH_ACK));
  ASSERT((t->ack == mg_htonl(1001)));  // expecting 1001, dude

  // Initiate closure, send FIN (test client-initiated closure)
  // https://datatracker.ietf.org/doc/html/rfc9293#section-3.6
  // We are case 1, Mongoose is case 2
  create_tcp_simpleseg(&e, &ipp, 1001, 2, TH_FIN, 0);
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  // Mongoose does a fast reduced ("3-way instead of 4-way" closure)
  ASSERT((t->flags == (TH_FIN | TH_ACK)));  // Mongoose ACKs our FIN, sends FIN
  ASSERT((t->seq == mg_htonl(2)));
  ASSERT((t->ack == mg_htonl(1002)));
  // make sure it is still open
  ASSERT(mgr.conns->next !=
         NULL);  // more than one connection: the listener + us
  create_tcp_simpleseg(&e, &ipp, 1002, 3, TH_ACK, 0);  // ACK Mongoose FIN
  mg_mgr_poll(&mgr, 0);
  ASSERT(!received_response(&s_driver_data));
  // make sure it is closed
  ASSERT(mgr.conns->next == NULL);  // only one connection: the listener

  s_driver_data.len = 0;
  mg_mgr_free(&mgr);

  // Test client-initiated closure timeout, do not ACK
  init_tcp_tests(&mgr, &e, &ipp, &driver, &mif, fn);
  init_tcp_handshake(&e, &ipp, &mgr);  // starts with seq_no=1000, ackno=2
  create_tcp_simpleseg(&e, &ipp, 1001, 2, TH_FIN, 0);
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  // Mongoose does a fast reduced ("3-way instead of 4-way" closure)
  ASSERT((t->flags == (TH_FIN | TH_ACK)));  // Mongoose ACKs our FIN, sends FIN
  ASSERT((t->seq == mg_htonl(2)));
  ASSERT((t->ack == mg_htonl(1002)));
  // make sure it is still open
  ASSERT(mgr.conns->next !=
         NULL);           // more than one connection: the listener + us
  s_driver_data.len = 0;  // avoid Mongoose "receiving itself"
  start = mg_millis();
  now = 0;
  do {
    mg_mgr_poll(&mgr, 0);
    if (received_response(&s_driver_data) && (ipv6 ? i6->next : i->proto) == 6)
      break;  // check first
    now = mg_millis() - start;
  } while (now < (12 * MG_TCPIP_FIN_MS) / 10);
  ASSERT(now > MG_TCPIP_FIN_MS);
  // make sure it is closed
  ASSERT(mgr.conns->next == NULL);  // only one connection: the listener

  s_driver_data.len = 0;
  mg_mgr_free(&mgr);

  // Test server-initiated closure, abbreviated 3-way: respond FIN+ACK
  // https://datatracker.ietf.org/doc/html/rfc9293#section-3.6
  // We are case 2, Mongoose is case 1
  init_tcp_tests(&mgr, &e, &ipp, &driver, &mif, tcpclosure_fn);
  init_tcp_handshake(&e, &ipp, &mgr);  // starts with seq_no=1000, ackno=2
  // we should have already received the FIN due to the call above
  start = mg_millis();
  while (!received_response(&s_driver_data)) {
    mg_mgr_poll(&mgr, 0);
    now = mg_millis() - start;
    if (now > 2 * MG_TCPIP_ACK_MS)
      ASSERT(0);  // response should have been received by now
  }
  ASSERT((t->seq == mg_htonl(2)));
  ASSERT((t->ack == mg_htonl(1001)));
  ASSERT(t->flags == (TH_FIN | TH_ACK));  // Mongoose ACKs last data, sends FIN
  // send FIN + ACK
  create_tcp_simpleseg(&e, &ipp, 1001, 3, TH_FIN | TH_ACK,
                       0);  // ACK FIN, send FIN
  mg_mgr_poll(&mgr, 0);     // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT((t->flags == TH_ACK));  // Mongoose ACKs our FIN
  ASSERT((t->seq == mg_htonl(3)));
  ASSERT((t->ack == mg_htonl(1002)));
  // make sure it is closed
  ASSERT(mgr.conns->next == NULL);  // only one connection: the listener

  s_driver_data.len = 0;
  mg_mgr_free(&mgr);

  // Test server-initiated closure, long 4-way closure: respond ACK
  init_tcp_tests(&mgr, &e, &ipp, &driver, &mif, tcpclosure_fn);
  init_tcp_handshake(&e, &ipp, &mgr);  // starts with seq_no=1000, ackno=2
  // we should have already received the FIN, tested in above tst
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT((t->seq == mg_htonl(2)));
  ASSERT((t->ack == mg_htonl(1001)));
  ASSERT(t->flags == (TH_FIN | TH_ACK));  // Mongoose ACKs last data, sends FIN
  // ACK Mongoose FIN, do *not* send FIN yet
  create_tcp_simpleseg(&e, &ipp, 1001, 3, TH_ACK, 0);  // ACK FIN
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  start = mg_millis();
  now = 0;
  do {
    if (received_response(&s_driver_data)) break;  // check first
    mg_mgr_poll(&mgr, 0);
    now = mg_millis() - start;
  } while (now < 2 * MG_TCPIP_ACK_MS);  // keep timeout below 1s (DHCP discover)
  ASSERT(now >= 2 * MG_TCPIP_ACK_MS);
  // make sure it is still open
  ASSERT(mgr.conns->next !=
         NULL);  // more than one connection: the listener + us
  create_tcp_simpleseg(&e, &ipp, 1001, 3, TH_FIN, 0);  // send FIN
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT((t->flags == TH_ACK));  // Mongoose ACKs our FIN
  ASSERT((t->seq == mg_htonl(3)));
  ASSERT((t->ack == mg_htonl(1002)));
  // make sure it is closed
  ASSERT(mgr.conns->next == NULL);  // only one connection: the listener

  s_driver_data.len = 0;
  mg_mgr_free(&mgr);

  // Test server-initiated closure, FIN retransmission: do not ACK FIN
  // Actual data retransmission is tested on another unit test
  init_tcp_tests(&mgr, &e, &ipp, &driver, &mif, tcpclosure_fn);
  init_tcp_handshake(&e, &ipp, &mgr);  // starts with seq_no=1000, ackno=2
  // we should have already received the FIN, tested in some tst above
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT((t->seq == mg_htonl(2)));
  ASSERT((t->ack == mg_htonl(1001)));
  ASSERT(t->flags == (TH_FIN | TH_ACK));  // Mongoose ACKs last data, sends FIN
  s_driver_data.len = 0;                  // avoid Mongoose "receiving itself"
  start = mg_millis();
  now = 0;
  do {
    if (received_response(&s_driver_data)) break;  // check first
    mg_mgr_poll(&mgr, 0);
    now = mg_millis() - start;
  } while (now < 2 * MG_TCPIP_ACK_MS);  // keep timeout below 1s (DHCP discover)
  //  ASSERT(now < 2 * MG_TCPIP_ACK_MS); ******** WE FAIL THIS, Mongoose does
  //  not retransmit, FIN is not an additional element in the stream
  //  ASSERT((t->seq
  //  == mg_htonl(2))); ASSERT((t->ack == mg_htonl(1001))); ASSERT(t->flags ==
  //  (TH_FIN | TH_ACK)); // Mongoose retransmits FIN
  // send FIN + ACK
  create_tcp_simpleseg(&e, &ipp, 1001, 3, TH_FIN | TH_ACK,
                       0);  // ACK FIN, send FIN
  mg_mgr_poll(&mgr, 0);     // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT((t->flags == TH_ACK));  // Mongoose ACKs our FIN
  ASSERT((t->seq == mg_htonl(3)));
  ASSERT((t->ack == mg_htonl(1002)));
  // make sure it is closed
  ASSERT(mgr.conns->next == NULL);  // only one connection: the listener

  s_driver_data.len = 0;
  mg_mgr_free(&mgr);

  // Test simultaneous closure
  // https://datatracker.ietf.org/doc/html/rfc9293#section-3.6 case 3
  init_tcp_tests(&mgr, &e, &ipp, &driver, &mif, tcpclosure_fn);
  init_tcp_handshake(&e, &ipp, &mgr);  // starts with seq_no=1000, ackno=2
  // we should have already received the FIN due to the call above
  start = mg_millis();
  while (!received_response(&s_driver_data)) {
    mg_mgr_poll(&mgr, 0);
    now = mg_millis() - start;
    if (now > 2 * MG_TCPIP_ACK_MS)
      ASSERT(0);  // response should have been received by now
  }
  ASSERT((t->seq == mg_htonl(2)));
  ASSERT((t->ack == mg_htonl(1001)));
  ASSERT(t->flags == (TH_FIN | TH_ACK));  // Mongoose ACKs last data, sends FIN
  // Also initiate closure, send FIN, do *not* ACK Mongoose FIN
  create_tcp_simpleseg(&e, &ipp, 1001, 2, TH_FIN, 0);
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT((t->flags == TH_ACK));  // Mongoose ACKs our FIN
  ASSERT((t->seq == mg_htonl(3)));
  ASSERT((t->ack == mg_htonl(1002)));
  // make sure it is still open   ******** WE FAIL THIS, Mongoose closes
  // immediately, does not wait to retransmit its ACK nor to get the other end
  // ACK
  //  ASSERT(mgr.conns->next != NULL);  // more than one connection: the
  //  listener + us create_tcp_simpleseg(&e, &ipp, 1002, 3, TH_ACK, 0); // ACK
  //  FIN mg_mgr_poll(&mgr, 0);
  // make sure it is closed
  ASSERT(mgr.conns->next == NULL);  // only one connection: the listener

  s_driver_data.len = 0;
  mg_mgr_free(&mgr);

  // Test responses to a connecting client
  // https://datatracker.ietf.org/doc/html/rfc9293#section-3.5
  // NOTE: Mongoose ignores any data until connection is actually established
  // NOTE: Mongoose does not support the concept of "simultaneous open",
  // Mongoose is either client or server
  {
    struct mg_connection *c;
    int event = 255;
    uint32_t ackno;
    // this creates a listener we won't use
    init_tcp_tests(&mgr, &e, &ipp, &driver, &mif, tcpclosure_fn);

    // must be outside of our network to avoid firing ARP requests
    if (ipv6) {
      c = mg_connect(&mgr, "tcp://[200::]:1234/", client_fn, &event);
    } else {
      c = mg_connect(&mgr, "tcp://2.0.0.0:1234/", client_fn, &event);
    }
    ASSERT(c != NULL);
    ASSERT(received_response(&s_driver_data));
    ASSERT((t->flags == TH_SYN));
    ASSERT(event == 255);
    if (ipv6) {
      ASSERT(i6->src[0] == mg_htonll(0x100000000000000) && i6->src[1] == 0 &&
             i6->dst[0] == mg_htonll(0x200000000000000) && i6->dst[1] == 0);
      ASSERT(tcp6csum_ok(i6, t));
    } else {
      ASSERT(i->src == mg_htonl(0x1000000) && i->dst == mg_htonl(0x2000000));
      ASSERT(tcpcsum_ok(i, t));
    }

    // invalid SYN + ACK to connecting client (after SYN...), send ACK != seq
    ackno = mg_ntohl(t->seq) + 1000;
    //  create_tcp_seg(&e, &ipp, 4321, ackno, TH_SYN | TH_ACK, 1234,
    //  mg_ntohs(c->loc.port), 0, NULL, 0); mg_mgr_poll(&mgr, 0);
    //  while(!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
    //  ASSERT((t->flags == (TH_RST | TH_ACK)));
    // ******** WE FAIL THIS, Mongoose does not validate the ACK number
    //  ASSERT((t->seq == mg_htonl(ackno)));
    //  ASSERT((t->ack == mg_htonl(4322)));

    // connect
    ackno = mg_ntohl(t->seq) + 1;
    create_tcp_seg(&e, &ipp, 4321, ackno, TH_SYN | TH_ACK, 1234,
                   mg_ntohs(c->loc.port), 0, NULL, 0);
    mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
    while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
    ASSERT(t->flags == TH_ACK);
    ASSERT(t->seq == mg_htonl(ackno));
    ASSERT((t->ack == mg_htonl(4322)));
    ASSERT(event == MG_EV_CONNECT);

    event = 255;
    s_driver_data.len = 0;
    mg_mgr_free(&mgr);

    // test connection failure, send RST+ACK
    // this creates a listener we won't use
    init_tcp_tests(&mgr, &e, &ipp, &driver, &mif, tcpclosure_fn);
    if (ipv6) {
      c = mg_connect(&mgr, "tcp://[200::]:1234/", client_fn, &event);
    } else {
      c = mg_connect(&mgr, "tcp://2.0.0.0:1234/", client_fn, &event);
    }
    received_response(&s_driver_data);  // get the SYN
    ackno = mg_ntohl(t->seq) + 1;
    create_tcp_seg(&e, &ipp, 4321, ackno, TH_RST + TH_ACK, 1234,
                   mg_ntohs(c->loc.port), 0, NULL, 0);
    mg_mgr_poll(&mgr, 0);
    MG_DEBUG(("event: %d", event));
    ASSERT(event == MG_EV_ERROR);
    ASSERT(!received_response(&s_driver_data));
  }

  s_driver_data.len = 0;
  mg_mgr_free(&mgr);
}

// NOTE: a 1-byte payload could be an erroneous Keep-Alive, keep length > 1 in
// this operation, we're testing retransmissions and having len=1 won't work

static void test_tcp_retransmit(void) {
  struct mg_mgr mgr;
  struct eth e;
  struct ip ip;
  struct ipp ipp;
  struct tcp *t = (struct tcp *) (s_driver_data.buf + sizeof(e) + sizeof(ip));
  uint64_t start, now;
  bool response_recv = true;
  struct mg_tcpip_driver driver;
  struct mg_tcpip_if mif;

  ipp.ip4 = &ip;
  ipp.ip6 = NULL;

  init_tcp_tests(&mgr, &e, &ipp, &driver, &mif, fn);

  init_tcp_handshake(&e, &ipp, &mgr);  // starts with seq_no=1000, ackno=2

  // packet with seq_no = 1001
  create_tcp_simpleseg(&e, &ipp, 1001, 2, TH_PUSH | TH_ACK, 2);
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT((t->flags == TH_ACK));
  ASSERT((t->ack == mg_htonl(1003)));  // OK

  // resend packet with seq_no = 1001 (e.g.: MIP ACK lost)
  create_tcp_simpleseg(&e, &ipp, 1001, 2, TH_PUSH | TH_ACK, 2);
  mg_mgr_poll(&mgr, 0);
  start = mg_millis();
  while (!received_response(&s_driver_data)) {
    mg_mgr_poll(&mgr, 0);
    now = mg_millis() - start;
    // we wait enough time for a reply
    if (now > 2 * MG_TCPIP_ACK_MS) {
      response_recv = false;
      break;
    }
  }
  ASSERT((!response_recv));  // replies should not be sent for duplicate packets

  // packet with seq_no = 1003 got lost/delayed, send seq_no = 1005
  create_tcp_simpleseg(&e, &ipp, 1005, 2, TH_PUSH | TH_ACK, 2);
  mg_mgr_poll(&mgr, 0);
  start = mg_millis();
  while (!received_response(&s_driver_data)) {
    mg_mgr_poll(&mgr, 0);
    now = mg_millis() - start;
    if (now > 2 * MG_TCPIP_ACK_MS)
      ASSERT(0);  // response should have been received by now
  }
  ASSERT((t->flags == TH_ACK));
  ASSERT((t->ack == mg_htonl(1003)));  // dup ACK

  // retransmitting packet with seq_no = 1003
  create_tcp_simpleseg(&e, &ipp, 1003, 2, TH_PUSH | TH_ACK, 2);
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT((t->flags == TH_ACK));
  ASSERT((t->ack == mg_htonl(1005)));  // OK

  // packet with seq_no = 1005 got delayed, send FIN with seq_no = 1007
  create_tcp_simpleseg(&e, &ipp, 1007, 2, TH_FIN, 0);
  mg_mgr_poll(&mgr, 0);
  start = mg_millis();
  while (!received_response(&s_driver_data)) {
    mg_mgr_poll(&mgr, 0);
    now = mg_millis() - start;
    if (now > 2 * MG_TCPIP_ACK_MS)
      ASSERT(0);  // response should have been received by now
  }
  ASSERT((t->flags == TH_ACK));
  ASSERT((t->ack == mg_htonl(1005)));  // dup ACK

  // retransmitting packet with seq_no = 1005
  create_tcp_simpleseg(&e, &ipp, 1005, 2, TH_PUSH | TH_ACK, 2);
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT((t->flags == TH_ACK));
  ASSERT((t->ack == mg_htonl(1007)));  // OK

  // retransmitting FIN packet with seq_no = 1007
  create_tcp_simpleseg(&e, &ipp, 1007, 2, TH_FIN | TH_ACK, 0);
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT((t->flags == (TH_FIN | TH_ACK)));  // check we respond with FIN ACK
  ASSERT((t->ack == mg_htonl(1008)));       // OK

  s_driver_data.len = 0;
  mg_mgr_free(&mgr);
}

static void test_frag_recv_path(void) {
  struct mg_mgr mgr;
  struct eth e;
  struct ip ip;
  struct ipp ipp;
  struct mg_tcpip_driver driver;
  struct mg_tcpip_if mif;

  ipp.ip4 = &ip;
  ipp.ip6 = NULL;

  init_tcp_tests(&mgr, &e, &ipp, &driver, &mif, frag_recv_fn);

  init_tcp_handshake(&e, &ipp, &mgr);  // starts with seq_no=1000, ackno=2

  // send fragmented TCP packet
  ip.frag |= IP_MORE_FRAGS_MSK;  // setting More Fragments bit to 1
  create_tcp_simpleseg(&e, &ipp, 1001, 2, TH_PUSH | TH_ACK, 1000);
  s_sent_fragment = 1;           // "enable" fn
  mg_mgr_poll(&mgr, 0);          // call it (process fake frag IP)
  ASSERT(s_sent_fragment == 2);  // check it followed the right path

  s_driver_data.len = 0;
  mg_mgr_free(&mgr);
}

static void test_frag_send_path(void) {
  struct mg_mgr mgr;
  struct mg_tcpip_driver driver;
  struct mg_tcpip_if mif;
  unsigned int i;

  mg_mgr_init(&mgr);
  memset(&mif, 0, sizeof(mif));
  memset(&s_driver_data, 0, sizeof(struct driver_data));
  driver.init = NULL, driver.tx = if_tx, driver.poll = if_poll,
  driver.rx = if_rx;
  mif.driver = &driver;
  mif.driver_data = &s_driver_data;
  mg_tcpip_init(&mgr, &mif);
  mif.mtu = 500;  // force ad hoc small MTU to fragment IP
  mg_http_listen(&mgr, "http://0.0.0.0:80", frag_send_fn, NULL);
  mgr.conns->pfn = NULL;
  for (i = 0; i < 10; i++) mg_mgr_poll(&mgr, 0);
  ASSERT(s_seg_sent == 3);
  s_driver_data.len = 0;
  mg_mgr_free(&mgr);
}

static void test_fragmentation(void) {
  test_frag_recv_path();
  test_frag_send_path();
}

static void test_tcp_backlog(void) {
  struct mg_mgr mgr;
  struct eth e;
  struct ip ip;
  struct ipp ipp;
  struct tcp *t = (struct tcp *) (s_driver_data.buf + sizeof(e) + sizeof(ip));
  struct ip *i = (struct ip *) (s_driver_data.buf + sizeof(e));
  uint64_t start, now;
  struct mg_tcpip_driver driver;
  struct mg_tcpip_if mif;
  uint16_t opts[4 / 2];  // Send MSS, RFC-9293 3.7.1
  struct mg_connection *c;
  unsigned int j;
#define LOGSZ (sizeof(c->data) / sizeof(struct mg_backlog))
  uint32_t seqnos[LOGSZ];

  ipp.ip4 = &ip;
  ipp.ip6 = NULL;

  init_tcp_tests(&mgr, &e, &ipp, &driver, &mif, fn);

  // test expired connection attempts cleanup
  create_tcp_seg(&e, &ipp, 1234, 0, TH_SYN, 1, 80, 0, NULL, 0);
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT(t->flags == (TH_SYN | TH_ACK));
  // delay ACK so conn attempt is removed from the backlog
  s_driver_data.len = 0;  // avoid Mongoose "receiving itself"
  start = mg_millis();
  do {
    mg_mgr_poll(&mgr, 0);
    now = mg_millis() - start;
  } while (now < 2100);
  // check backlog is empty
  c = mgr.conns;
  ASSERT(c->next == NULL);
  for (j = 0; j < LOGSZ; j++) {
    struct mg_backlog *b = (struct mg_backlog *) (c->data) + j;
    ASSERT(b->port == 0);
  }
  // Mongoose may have retransmitted SYN + ACK, and DHCP sent discover
  received_response(&s_driver_data);  // make sure we clean buffer

  opts[0] = mg_htons(0x0204);  // RFC-9293 3.2
  // fill the backlog
  for (j = 0; j < LOGSZ; j++) {
    // assign one MSS for each connection
    opts[1] = mg_htons((uint16_t) (1010 + j));
    create_tcp_seg(&e, &ipp, 100 + j, 0, TH_SYN, (uint16_t) (j + 1), 80, 0,
                   opts, sizeof(opts));
    while (!received_response(&s_driver_data) || i->proto != 6)
      mg_mgr_poll(&mgr, 0);
    ASSERT(t->flags == (TH_SYN | TH_ACK));
    seqnos[j] = mg_ntohl(t->seq);
    MG_VERBOSE(("SEQ: %p", seqnos[j]));
  }
  // check backlog is full and MSS are there
  c = mgr.conns;
  for (j = 0; j < LOGSZ; j++) {
    struct mg_backlog *b = (struct mg_backlog *) (c->data) + j;
    ASSERT(b->port != 0);
    MG_DEBUG(("SEQ: %p, MSS: %u", seqnos[j], (unsigned int) b->mss));
    ASSERT(b->mss == (1010 + j));
  }
  // one more attempt, it must fail
  opts[1] = mg_htons((uint16_t) (1010 + j));
  create_tcp_seg(&e, &ipp, 100 + j, 0, TH_SYN, (uint16_t) (j + 1), 80, 0, opts,
                 sizeof(opts));
  mg_mgr_poll(&mgr, 0);
  ASSERT(!received_response(&s_driver_data) || i->proto != 6);
  // a late response for this attempt would break what follows
  // establish all connections
  for (j = 0; j < LOGSZ; j++) {
    create_tcp_seg(&e, &ipp, 100 + j + 1, seqnos[j] + 1, TH_ACK,
                   (uint16_t) (j + 1), 80, 0, NULL, 0);
    mg_mgr_poll(&mgr, 0);
    ASSERT(!received_response(&s_driver_data) || i->proto != 6);
  }
  // check backlog is now empty
  c = mgr.conns;  // last one is the listener
  for (; c->next != NULL; c = c->next)
    ;
  for (j = 0; j < LOGSZ; j++) {
    struct mg_backlog *b = (struct mg_backlog *) (c->data) + j;
    ASSERT(b->port == 0);
  }
  c = mgr.conns;  // first one is more recent
  // check MSS is what we sent, everything's fine
  for (j = LOGSZ; j > 0; j--, c = c->next) {
    struct connstate *s = (struct connstate *) (c + 1);
    ASSERT(c != NULL);
    MG_DEBUG(("MSS: %u", (unsigned int) s->dmss));
    ASSERT(s->dmss == (1010 + j - 1));
  }
  ASSERT(c != NULL);  // last one is the listener
  ASSERT(c->next == NULL);

  s_driver_data.len = 0;
  mg_mgr_free(&mgr);
}

static void test_tcp(bool ipv6) {
  test_tcp_basics(ipv6);
  if (!ipv6) {
    test_tcp_backlog();
    test_tcp_retransmit();
  }
}

static void udp_fn(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_READ && c->recv.len == 2 && c->recv.buf[0] == 'p')
    mg_send(c, "P90", 3);
  (void) ev_data;
}

static void create_udp_dat(struct eth *e, struct ipp *ipp, uint16_t sport,
                           uint16_t dport, size_t payload_len) {
  struct udp u;
  memset(&u, 0, sizeof(u));
  u.sport = mg_htons(sport);
  u.dport = mg_htons(dport);
  u.len = mg_htons((uint16_t) (sizeof(u) + payload_len));
  memcpy(s_driver_data.buf, e, sizeof(*e));
#if MG_ENABLE_IPV6
  if (ipp->ip6 != NULL) {
    struct ip6 *ip = ipp->ip6;
    struct udp *udp =
        (struct udp *) (s_driver_data.buf + sizeof(*e) + sizeof(*ip));
    ip->plen = mg_htons((uint16_t) (sizeof(u) + payload_len));
    memcpy(s_driver_data.buf + sizeof(*e), ip, sizeof(*ip));
    memcpy(udp, &u, sizeof(u));
    *(s_driver_data.buf + sizeof(*e) + sizeof(*ip) + sizeof(u)) = 'p';
    udp->csum = p6csum(ip, udp, (sizeof(u) + payload_len));
    s_driver_data.len = sizeof(*e) + sizeof(*ip) + sizeof(u) + payload_len;
  } else
#endif
  {
    struct ip *ip = ipp->ip4;
    struct udp *udp =
        (struct udp *) (s_driver_data.buf + sizeof(*e) + sizeof(*ip));
    ip->len = mg_htons((uint16_t) (sizeof(*ip) + sizeof(u) + payload_len));
    ip->csum = ipcsum(ip, sizeof(*ip));  // no options
    memcpy(s_driver_data.buf + sizeof(*e), ip, sizeof(*ip));
    ip->csum = 0;
    memcpy(udp, &u, sizeof(u));
    *(s_driver_data.buf + sizeof(*e) + sizeof(*ip) + sizeof(u)) = 'p';
    udp->csum = pcsum(ip, udp, (sizeof(u) + payload_len));
    s_driver_data.len = sizeof(*e) + sizeof(*ip) + sizeof(u) + payload_len;
  }
  if (s_driver_data.len < 64) s_driver_data.len = 64;  // add padding if needed
}

static void init_udp_tests(struct mg_mgr *mgr, struct eth *e, struct ipp *ipp,
                           struct mg_tcpip_driver *driver,
                           struct mg_tcpip_if *mif, mg_event_handler_t f) {
  init_tests(mgr, e, ipp, driver, mif, 17);  // 17 -> UDP
#if MG_ENABLE_IPV6
  if (ipp->ip6 != NULL) {
    mif->state = MG_TCPIP_STATE_READY;   // so DHCP stops
    mif->state6 = MG_TCPIP_STATE_READY;  // so mg_send() works and RS stops
    mg_listen(mgr, "udp://[::]:888", f, NULL);
  } else
#endif
  {
    mif->state = MG_TCPIP_STATE_READY;  // so mg_send() works and DHCP stops
    mg_listen(mgr, "udp://0.0.0.0:888", f, NULL);
  }
  mg_mgr_poll(mgr, 0);
}

static void test_udp(bool ipv6) {
  struct mg_mgr mgr;
  struct eth e;
  struct ip ip;
  struct ip6 ip6;
  struct ipp ipp;
  struct udp *u = (struct udp *) (s_driver_data.buf + sizeof(e) +
                                  (!ipv6 ? sizeof(ip) : sizeof(ip6)));
  struct ip *i = (struct ip *) (s_driver_data.buf + sizeof(e));
  struct ip6 *i6 = (struct ip6 *) (s_driver_data.buf + sizeof(e));
  struct mg_tcpip_driver driver;
  struct mg_tcpip_if mif;

  ipp.ip4 = !ipv6 ? &ip : NULL;
  ipp.ip6 = ipv6 ? &ip6 : NULL;

  init_udp_tests(&mgr, &e, &ipp, &driver, &mif, udp_fn);
  received_response(&s_driver_data);
  s_driver_data.len = 0;

  // send data to a non-open port, expect no response (we don't send Destination
  // Unreachable)
  create_udp_dat(&e, &ipp, 1, 800, 2);
  mg_mgr_poll(&mgr, 0);
  ASSERT(!received_response(&s_driver_data));

  // send data to an open port, expect response
  create_udp_dat(&e, &ipp, 1, 888, 2);
  mg_mgr_poll(&mgr, 0);
  ASSERT(received_response(&s_driver_data));
  ASSERT(u->sport == mg_htons(888));
  ASSERT(u->len == mg_htons(sizeof(*u) + 3));
  ASSERT(*((char *) (u + 1)) == 'P');
  if (ipv6) {
    ASSERT(i6->src[0] == mg_htonll(0x100000000000000) && i6->src[1] == 0 &&
           i6->dst[0] == mg_htonll(0x200000000000000) && i6->dst[1] == 0);
    ASSERT(udp6csum_ok(i6, u));
  } else {
    ASSERT(i->src == mg_htonl(0x1000000) && i->dst == mg_htonl(0x2000000));
    ASSERT(udpcsum_ok(i, u));
  }

  s_driver_data.len = 0;
  mg_mgr_free(&mgr);
}

static void create_icmp_dat(struct eth *e, struct ipp *ipp, uint8_t type,
                            uint8_t code, size_t payload_len) {
  struct ip *ip = ipp->ip4;
  struct icmp i,
      *icmp = (struct icmp *) (s_driver_data.buf + sizeof(*e) + sizeof(*ip));
  memset(&i, 0, sizeof(i));
  i.type = type;
  i.code = code;
  memcpy(s_driver_data.buf, e, sizeof(*e));
  ip->len = mg_htons((uint16_t) (sizeof(*ip) + sizeof(*icmp) + payload_len));
  ip->csum = ipcsum(ip, sizeof(*ip));
  memcpy(s_driver_data.buf + sizeof(*e), ip, sizeof(*ip));
  ip->csum = 0;
  memcpy(icmp, &i, sizeof(i));
  icmp->csum = ipcsum(icmp, sizeof(*icmp) + payload_len);
  s_driver_data.len = sizeof(*e) + sizeof(*ip) + sizeof(*icmp) + payload_len;
  if (s_driver_data.len < 64) s_driver_data.len = 64;  // add padding if needed
}

static void init_icmp_tests(struct mg_mgr *mgr, struct eth *e, struct ipp *ipp,
                            struct mg_tcpip_driver *driver,
                            struct mg_tcpip_if *mif) {
  init_tests(mgr, e, ipp, driver, mif, 1);  // 1 -> ICMP
#if MG_ENABLE_IPV6
  mif->state6 = MG_TCPIP_STATE_READY;  // so RS stops
  mg_mgr_poll(mgr, 0);
#endif
}

static void test_icmp_basics(void) {
  struct mg_mgr mgr;
  struct eth e;
  struct ip ip;
  struct ipp ipp;
  struct icmp *icmp =
      (struct icmp *) (s_driver_data.buf + sizeof(e) + sizeof(ip));
  struct ip *i = (struct ip *) (s_driver_data.buf + sizeof(e));
  struct mg_tcpip_driver driver;
  struct mg_tcpip_if mif;

  ipp.ip4 = &ip;
  ipp.ip6 = NULL;
  init_icmp_tests(&mgr, &e, &ipp, &driver, &mif);

  create_icmp_dat(&e, &ipp, 8, 0, 0);  // Echo Request
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT(i->src == mg_htonl(0x1000000) && i->dst == mg_htonl(0x2000000));
  ASSERT(i->proto == 1);
  ASSERT(i->len == mg_htons(sizeof(*i) + sizeof(*icmp) + 0));
  ASSERT(ipcsum_ok(i));     // Bonus, not tested elsewhere
  ASSERT(icmp->type == 0);  // Echo Reply
  ASSERT(icmp->code == 0);
  ASSERT(icmpcsum_ok(icmp, sizeof(*icmp) + 0));

  create_icmp_dat(&e, &ipp, 8, 0, 69);  // Echo Request
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT(i->src == mg_htonl(0x1000000) && i->dst == mg_htonl(0x2000000));
  ASSERT(i->proto == 1);
  ASSERT(i->len == mg_htons(sizeof(*i) + sizeof(*icmp) + 69));
  ASSERT(icmp->type == 0);  // Echo Reply
  ASSERT(icmp->code == 0);
  ASSERT(ipcsum(icmp, sizeof(*icmp) + 69) == 0);

  s_driver_data.len = 0;
  mg_mgr_free(&mgr);
}

static void test_icmp(void) {
  test_icmp_basics();
}

#if MG_ENABLE_IPV6
static void create_icmp6_dat(struct eth *e, struct ipp *ipp, uint8_t type,
                             uint8_t code, uint8_t *payload,
                             size_t payload_len) {
  struct ip6 *ip6 = ipp->ip6;
  struct icmp6 i6,
      *icmp6 = (struct icmp6 *) (s_driver_data.buf + sizeof(*e) + sizeof(*ip6));
  memset(&i6, 0, sizeof(i6));
  i6.type = type;
  i6.code = code;
  if (payload != NULL)
    memcpy(s_driver_data.buf + sizeof(*e) + sizeof(*ip6) + sizeof(*icmp6),
           payload, payload_len);
  ip6->plen = mg_htons((uint16_t) (sizeof(*icmp6) + payload_len));
  memcpy(s_driver_data.buf, e, sizeof(*e));
  memcpy(s_driver_data.buf + sizeof(*e), ip6, sizeof(*ip6));
  memcpy(icmp6, &i6, sizeof(i6));
  icmp6->csum = p6csum(ip6, icmp6, sizeof(*icmp6) + payload_len);
  s_driver_data.len = sizeof(*e) + sizeof(*ip6) + sizeof(*icmp6) + payload_len;
  if (s_driver_data.len < 64) s_driver_data.len = 64;  // add padding if needed
}

static void init_icmp6_tests(struct mg_mgr *mgr, struct eth *e, struct ipp *ipp,
                             struct mg_tcpip_driver *driver,
                             struct mg_tcpip_if *mif) {
  init_tests(mgr, e, ipp, driver, mif, 58);  // 58 -> ICMPv6
}

static void test_icmp6_basics(void) {
  struct mg_mgr mgr;
  struct eth e;
  struct ip6 ip6;
  struct ipp ipp;
  struct icmp6 *icmp6 =
      (struct icmp6 *) (s_driver_data.buf + sizeof(e) + sizeof(ip6));
  struct ip6 *i = (struct ip6 *) (s_driver_data.buf + sizeof(e));
  struct mg_tcpip_driver driver;
  struct mg_tcpip_if mif;
  uint8_t payload[28], *p = (uint8_t *) (icmp6 + 1);

  ipp.ip4 = NULL;
  ipp.ip6 = &ip6;
  init_icmp6_tests(&mgr, &e, &ipp, &driver, &mif);

  create_icmp6_dat(&e, &ipp, 128, 0, NULL, 0);  // Echo Request
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT(i->src[0] == mg_htonll(0x100000000000000) && i->src[1] == 0 &&
         i->dst[0] == mg_htonll(0x200000000000000) && i->dst[1] == 0);
  ASSERT(i->next == 58);
  ASSERT(i->plen == mg_htons(sizeof(*icmp6) + 0));
  ASSERT(icmp6->type == 129);  // Echo Reply
  ASSERT(icmp6->code == 0);
  ASSERT(icmp6csum_ok(i, icmp6));

  create_icmp6_dat(&e, &ipp, 128, 0, NULL, 69);  // Echo Request
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT(i->src[0] == mg_htonll(0x100000000000000) && i->src[1] == 0 &&
         i->dst[0] == mg_htonll(0x200000000000000) && i->dst[1] == 0);
  ASSERT(i->next == 58);
  ASSERT(i->plen == mg_htons(sizeof(*icmp6) + 69));
  ASSERT(icmp6->type == 129);  // Echo Reply
  ASSERT(icmp6->code == 0);
  ASSERT(icmp6csum_ok(i, icmp6));

  // Neighbor Solicitation
  memset(payload, 0, sizeof(payload));
  memcpy(payload + 4, mif.ip6, 16);
  payload[20] = 1;  // source hwaddr
  payload[21] = 1;  // hwaddr len
  create_icmp6_dat(&e, &ipp, 135, 0, payload, 28);
  mg_mgr_poll(&mgr, 0);  // make sure we clean former stuff in buffer
  while (!received_response(&s_driver_data)) mg_mgr_poll(&mgr, 0);
  ASSERT(i->src[0] == mg_htonll(0x100000000000000) && i->src[1] == 0 &&
         i->dst[0] == mg_htonll(0x200000000000000) && i->dst[1] == 0);
  ASSERT(i->next == 58);
  ASSERT(i->plen == mg_htons(sizeof(*icmp6) + 28));
  ASSERT(icmp6->type == 136);  // Neighbor Advertisement
  ASSERT(icmp6->code == 0);
  ASSERT(p[0] == 0x60);                     // solicited + override
  ASSERT(memcmp(p + 4, mif.ip6, 16) == 0);  // target address
  ASSERT(p[20] == 2);                       // target hwaddr
  ASSERT(p[21] == 1);                       // hwaddr len
  ASSERT(memcmp(p + 22, mif.mac, 6) == 0);

  s_driver_data.len = 0;
  mg_mgr_free(&mgr);
}

static void test_icmp6(void) {
  test_icmp6_basics();
}
#endif

#define DASHBOARD(x) \
  printf("HEALTH_DASHBOARD\t\"%s\": %s,\n", x, s_error ? "false" : "true");

int main(void) {
  s_error = false;
  test_csum();
  DASHBOARD("checksum");

  s_error = false;
  test_statechange();
  DASHBOARD("statechange");

  s_error = false;
  test_poll();
  DASHBOARD("poll");

  s_error = false;
  test_icmp();
  DASHBOARD("icmp");

  s_error = false;
  test_tcp(false);
  DASHBOARD("tcp");

  s_error = false;
  test_udp(false);
  DASHBOARD("udp");

#if MG_ENABLE_IPV6
  s_error = false;
  test_icmp6();
  DASHBOARD("icmp6");

  s_error = false;
  test_state6change();
  DASHBOARD("state6change");

  s_error = false;
  test_tcp(true);
  DASHBOARD("tcp_ipv6");

  s_error = false;
  test_udp(true);
  DASHBOARD("udp_ipv6");

#endif

  s_error = false;
  test_fragmentation();
  printf("HEALTH_DASHBOARD\t\"ipfrag\": %s\n", s_error ? "false" : "true");
  // last entry with no comma

#ifdef NO_ABORT
  if (s_abort != 0) return EXIT_FAILURE;
#endif

  printf("SUCCESS. Total tests: %d\n", s_num_tests);
  return EXIT_SUCCESS;
}
