#define MG_ENABLE_TCPIP 1
#define MG_ENABLE_TCPIP_DRIVER_INIT 0

#include "mongoose.c"
#include "driver_mock.c"

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

#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)

static const uint8_t s_mac[6] = {2, 3, 4, 5, 6, 7};
static const uint8_t s_src[6] = {8, 9, 10, 11, 12, 13};

static uint8_t s_tx[2048];

static void init_if(struct mg_tcpip_if *ifp, enum mg_l2type type) {
  memset(ifp, 0, sizeof(*ifp));
  memcpy(ifp->mac, s_mac, sizeof(s_mac));
  ifp->driver = &mg_tcpip_driver_mock;
  ifp->l2type = type;
  ifp->tx.buf = (char *) s_tx;
  ifp->tx.len = sizeof(s_tx);
  mg_l2_init(ifp);
}

static size_t mk_ppp(uint8_t *buf, uint16_t proto, const void *payload,
                     size_t len) {
  struct hdlc_ *hdlc = (struct hdlc_ *) buf;
  struct ppp *ppp = (struct ppp *) (hdlc + 1);
  hdlc->addr = MG_PPP_ADDR;
  hdlc->ctrl = MG_PPP_CTRL;
  ppp->proto = mg_htons(proto);
  memcpy(ppp + 1, payload, len);
  memset((uint8_t *) (ppp + 1) + len, 0, 2);
  return sizeof(*hdlc) + sizeof(*ppp) + len + 2;
}

static void *ppp_payload(uint8_t *frame) {
  return (struct ppp *) ((struct hdlc_ *) frame + 1) + 1;
}

static void test_fcs(void) {
  struct mg_tcpip_if ifp;
  uint8_t frame[] = {0xff, 0x03, 0x00, 0x21, 1, 2, 3, 4, 0, 0};
  uint8_t fcs[] = {0xae, 0x77};
  init_if(&ifp, MG_TCPIP_L2_PPP);
  // PPP trailer stores the FCS bytes in the proper order
  ASSERT(mg_l2_ppp_trailer(&ifp, 4, frame + sizeof(frame) - sizeof(fcs)) ==
         sizeof(frame));
  ASSERT(memcmp(frame + sizeof(frame) - sizeof(fcs), fcs, sizeof(fcs)) == 0);
}

static size_t mk_pppoe_disc(uint8_t *buf, uint8_t code, uint16_t id,
                            const void *payload, size_t len) {
  struct eth *eth = (struct eth *) buf;
  struct pppoe *pppoe = (struct pppoe *) (eth + 1);
  memcpy(eth->dst, s_mac, sizeof(eth->dst));
  memcpy(eth->src, s_src, sizeof(eth->src));
  eth->type = mg_htons(0x8863);
  pppoe->vertype = 0x11;
  pppoe->code = code;
  pppoe->id = mg_htons(id);
  pppoe->len = mg_htons((uint16_t) len);
  if (len > 0) memcpy(pppoe + 1, payload, len);
  return sizeof(*eth) + sizeof(*pppoe) + len;
}

static size_t mk_pppoe_sess(uint8_t *buf, uint16_t id, uint16_t proto,
                            const void *payload, size_t len) {
  struct eth *eth = (struct eth *) buf;
  struct pppoe *pppoe = (struct pppoe *) (eth + 1);
  struct ppp *ppp = (struct ppp *) (pppoe + 1);
  memcpy(eth->dst, s_mac, sizeof(eth->dst));
  memcpy(eth->src, s_src, sizeof(eth->src));
  eth->type = mg_htons(0x8864);
  pppoe->vertype = 0x11;
  pppoe->code = 0;
  pppoe->id = mg_htons(id);
  pppoe->len = mg_htons((uint16_t) (sizeof(*ppp) + len));
  ppp->proto = mg_htons(proto);
  memcpy(ppp + 1, payload, len);
  return sizeof(*eth) + sizeof(*pppoe) + sizeof(*ppp) + len;
}

static void test_lcp(void) {
  uint8_t frame[64];
  struct mg_tcpip_if ifp;
  struct mg_str raw, pay;
  enum mg_l2proto proto;

  {  // LCP ACK brings PPP link up
    struct lcp lcp = {MG_PPP_LCP_CFG_ACK, 1, mg_htons(sizeof(lcp))};
    init_if(&ifp, MG_TCPIP_L2_PPP);
    s_lcpup = false;
    raw = mg_str_n((char *) frame,
                   mk_ppp(frame, MG_PPP_PROTO_LCP, &lcp, sizeof(lcp)));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
    ASSERT(s_lcpup == true);
  }
  {  // Short LCP frame is discarded
    uint8_t lcp[] = {MG_PPP_LCP_CFG_ACK, 1};
    init_if(&ifp, MG_TCPIP_L2_PPP);
    s_lcpup = false;
    raw = mg_str_n((char *) frame,
                   mk_ppp(frame, MG_PPP_PROTO_LCP, lcp, sizeof(lcp)));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
    ASSERT(s_lcpup == false);
  }
}

static void test_ipcp(void) {
  uint8_t frame[64];
  struct mg_tcpip_if ifp;
  struct mg_str raw, pay;
  enum mg_l2proto proto;

  {  // IPCP request with peer IP address is ACKed
    uint8_t ipcp[] = {MG_PPP_IPCP_CFG_REQ, 1, 0, 10,
                      MG_PPP_IPCP_OPT_IPADDR, 6, 192, 0, 2, 1};
    uint32_t peer;
    init_if(&ifp, MG_TCPIP_L2_PPP);
    s_lcpup = true;
    memcpy(&peer, ipcp + 6, sizeof(peer));
    raw = mg_str_n((char *) frame,
                   mk_ppp(frame, MG_PPP_PROTO_IPCP, ipcp, sizeof(ipcp)));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
    ASSERT(ifp.gw == peer);
    ASSERT(((struct ipcp *) ppp_payload(frame))->code == MG_PPP_IPCP_CFG_ACK);
    ASSERT(ifp.nsent == 2);
  }
  {  // IPCP request without peer IP address is rejected
    uint8_t ipcp[] = {MG_PPP_IPCP_CFG_REQ, 1, 0, 4};
    init_if(&ifp, MG_TCPIP_L2_PPP);
    s_lcpup = true;
    raw = mg_str_n((char *) frame,
                   mk_ppp(frame, MG_PPP_PROTO_IPCP, ipcp, sizeof(ipcp)));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
    ASSERT(ifp.gw == 0);
    ASSERT(((struct ipcp *) ppp_payload(frame))->code ==
           MG_PPP_IPCP_CFG_REJECT);
    ASSERT(ifp.nsent == 3);
  }
}

#if MG_ENABLE_IPV6
static void test_ipv6cp(void) {
  uint8_t frame[64];
  struct mg_tcpip_if ifp;
  struct mg_str raw, pay;
  enum mg_l2proto proto;

  {  // IPv6CP request with interface ID is ACKed
    uint8_t ipv6cp[] = {MG_PPP_IPV6CP_CFG_REQ, 1, 0, 14,
                        MG_PPP_IPV6CP_OPT_IFCID, 10, 2, 3, 4, 5, 6, 7, 8, 9};
    init_if(&ifp, MG_TCPIP_L2_PPP);
    s_lcpup = true;
    raw = mg_str_n((char *) frame,
                   mk_ppp(frame, MG_PPP_PROTO_IPV6CP, ipv6cp,
                          sizeof(ipv6cp)));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
    ASSERT(memcmp(&((struct mg_l2addr *) ifp.gw6mac)->addr.ieee64, ipv6cp + 6,
                  8) == 0);
    ASSERT(((struct ipv6cp *) ppp_payload(frame))->code ==
           MG_PPP_IPV6CP_CFG_ACK);
    ASSERT(ifp.nsent == 2);
  }
  {  // IPv6CP request with zero interface ID is rejected
    uint8_t ipv6cp[] = {MG_PPP_IPV6CP_CFG_REQ, 1, 0, 14,
                        MG_PPP_IPV6CP_OPT_IFCID, 10, 0, 0, 0, 0, 0, 0, 0, 0};
    init_if(&ifp, MG_TCPIP_L2_PPP);
    s_lcpup = true;
    raw = mg_str_n((char *) frame,
                   mk_ppp(frame, MG_PPP_PROTO_IPV6CP, ipv6cp,
                          sizeof(ipv6cp)));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
    ASSERT(((struct ipv6cp *) ppp_payload(frame))->code ==
           MG_PPP_IPV6CP_CFG_REJECT);
    ASSERT(ifp.nsent == 1);
  }
}
#endif

static void test_ppp(void) {
  uint8_t frame[96];
  struct mg_tcpip_if ifp;
  struct mg_str raw, pay;
  enum mg_l2proto proto;

  {  // IPv4 packet is accepted after LCP is up
    uint8_t payload[sizeof(struct ip)];
    memset(payload, 0, sizeof(payload));
    init_if(&ifp, MG_TCPIP_L2_PPP);
    MG_DEBUG(("MTU: %u, frame size: %u", ifp.l2mtu, ifp.framesize));
    ASSERT(ifp.l2mtu != 0);
    ASSERT(ifp.framesize != 0);
    s_lcpup = true;
    raw = mg_str_n((char *) frame,
                   mk_ppp(frame, MG_PPP_PROTO_IP, payload, sizeof(payload)));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == true);
    ASSERT(proto == MG_TCPIP_L2PROTO_IPV4);
    ASSERT(pay.len == sizeof(payload));
    ASSERT(memcmp(pay.buf, payload, sizeof(payload)) == 0);
  }
  {  // IPv4 packet is discarded while LCP is down
    uint8_t payload[sizeof(struct ip)];
    memset(payload, 0, sizeof(payload));
    init_if(&ifp, MG_TCPIP_L2_PPP);
    s_lcpup = false;
    raw = mg_str_n((char *) frame,
                   mk_ppp(frame, MG_PPP_PROTO_IP, payload, sizeof(payload)));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
  }
#if MG_ENABLE_IPV6
  {  // IPv6 packet is accepted after LCP is up
    uint8_t payload[sizeof(struct ip6)];
    memset(payload, 0, sizeof(payload));
    init_if(&ifp, MG_TCPIP_L2_PPP);
    s_lcpup = true;
    raw = mg_str_n((char *) frame,
                   mk_ppp(frame, MG_PPP_PROTO_IPV6, payload,
                          sizeof(payload)));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == true);
    ASSERT(proto == MG_TCPIP_L2PROTO_IPV6);
    ASSERT(pay.len == sizeof(payload));
    ASSERT(memcmp(pay.buf, payload, sizeof(payload)) == 0);
  }
  {  // IPv6 packet is discarded while LCP is down
    uint8_t payload[sizeof(struct ip6)];
    memset(payload, 0, sizeof(payload));
    init_if(&ifp, MG_TCPIP_L2_PPP);
    s_lcpup = false;
    raw = mg_str_n((char *) frame,
                   mk_ppp(frame, MG_PPP_PROTO_IPV6, payload,
                          sizeof(payload)));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
  }
#endif
  {  // Unknown protocol is rejected after LCP is up
    uint8_t payload[] = {1, 2, 3, 4};
    init_if(&ifp, MG_TCPIP_L2_PPP);
    s_lcpup = true;
    raw = mg_str_n((char *) frame,
                   mk_ppp(frame, 0x1234, payload, sizeof(payload)));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
    ASSERT(ifp.nsent == 1);
  }
  {  // Unknown protocol is discarded while LCP is down
    uint8_t payload[] = {1, 2, 3, 4};
    init_if(&ifp, MG_TCPIP_L2_PPP);
    s_lcpup = false;
    raw = mg_str_n((char *) frame,
                   mk_ppp(frame, 0x1234, payload, sizeof(payload)));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
    ASSERT(ifp.nsent == 0);
  }
}

static void test_pppoe(void) {
  uint8_t frame[96];
  struct mg_tcpip_if ifp;
  struct mg_str raw, pay;
  enum mg_l2proto proto;

  {  // PADO moves discovery to request state
    init_if(&ifp, MG_TCPIP_L2_PPPoE);
    MG_DEBUG(("MTU: %u, frame size: %u", ifp.l2mtu, ifp.framesize));
    ASSERT(ifp.l2mtu != 0);
    ASSERT(ifp.framesize != 0);
    s_state = MG_PPPoE_ST_DISC;
    raw = mg_str_n((char *) frame,
                   mk_pppoe_disc(frame, MG_PPPoE_PADO, 0, NULL, 0));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
    ASSERT(s_state == MG_PPPoE_ST_REQ);
    ASSERT(ifp.nsent == 1);
  }
  {  // PADO is ignored outside discovery state
    init_if(&ifp, MG_TCPIP_L2_PPPoE);
    s_state = MG_PPPoE_ST_REQ;
    raw = mg_str_n((char *) frame,
                   mk_pppoe_disc(frame, MG_PPPoE_PADO, 0, NULL, 0));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
    ASSERT(s_state == MG_PPPoE_ST_REQ);
    ASSERT(ifp.nsent == 0);
  }
  {  // PADS establishes the session
    init_if(&ifp, MG_TCPIP_L2_PPPoE);
    s_state = MG_PPPoE_ST_REQ;
    raw = mg_str_n((char *) frame,
                   mk_pppoe_disc(frame, MG_PPPoE_PADS, 0x1234, NULL, 0));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
    ASSERT(s_state == MG_PPPoE_ST_SESS);
    ASSERT(s_id == mg_htons(0x1234));
  }
  {  // PADS is ignored outside request state
    init_if(&ifp, MG_TCPIP_L2_PPPoE);
    s_state = MG_PPPoE_ST_DISC;
    raw = mg_str_n((char *) frame,
                   mk_pppoe_disc(frame, MG_PPPoE_PADS, 0x1234, NULL, 0));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
    ASSERT(s_state == MG_PPPoE_ST_DISC);
  }
  {  // PADT matching the session tears it down
    init_if(&ifp, MG_TCPIP_L2_PPPoE);
    s_state = MG_PPPoE_ST_SESS;
    s_id = mg_htons(0x1234);
    s_lcpup = true;
    raw = mg_str_n((char *) frame,
                   mk_pppoe_disc(frame, MG_PPPoE_PADT, 0x1234, NULL, 0));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
    ASSERT(s_state == MG_PPPoE_ST_DISC);
    ASSERT(s_id == 0);
    ASSERT(s_lcpup == false);
  }
  {  // PADT for a different session is ignored
    init_if(&ifp, MG_TCPIP_L2_PPPoE);
    s_state = MG_PPPoE_ST_SESS;
    s_id = mg_htons(0x4321);
    s_lcpup = true;
    raw = mg_str_n((char *) frame,
                   mk_pppoe_disc(frame, MG_PPPoE_PADT, 0x1234, NULL, 0));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
    ASSERT(s_state == MG_PPPoE_ST_SESS);
    ASSERT(s_id == mg_htons(0x4321));
    ASSERT(s_lcpup == true);
  }
  {  // PPPoE session packet is accepted in session state
    uint8_t payload[sizeof(struct ip)];
    memset(payload, 0, sizeof(payload));
    init_if(&ifp, MG_TCPIP_L2_PPPoE);
    s_state = MG_PPPoE_ST_SESS;
    s_lcpup = true;
    raw = mg_str_n((char *) frame,
                   mk_pppoe_sess(frame, 0x1234, MG_PPP_PROTO_IP, payload,
                                 sizeof(payload)));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == true);
    ASSERT(proto == MG_TCPIP_L2PROTO_IPV4);
    ASSERT(pay.len == sizeof(payload));
    ASSERT(memcmp(pay.buf, payload, sizeof(payload)) == 0);
  }
  {  // PPPoE session packet is discarded outside session state
    uint8_t payload[sizeof(struct ip)];
    memset(payload, 0, sizeof(payload));
    init_if(&ifp, MG_TCPIP_L2_PPPoE);
    s_state = MG_PPPoE_ST_REQ;
    s_lcpup = true;
    raw = mg_str_n((char *) frame,
                   mk_pppoe_sess(frame, 0x1234, MG_PPP_PROTO_IP, payload,
                                 sizeof(payload)));
    proto = MG_TCPIP_L2PROTO_ARP;
    ASSERT(mg_l2_rx(&ifp, &proto, &pay, &raw) == false);
  }
}

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

int main(void) {
  s_error = false;
  test_fcs();
  DASHBOARD("fcs");

  s_error = false;
  test_lcp();
  DASHBOARD("lcp");

  s_error = false;
  test_ipcp();
  DASHBOARD("ipcp");

#if MG_ENABLE_IPV6
  s_error = false;
  test_ipv6cp();
  DASHBOARD("ipv6cp");
#endif

  s_error = false;
  test_ppp();
  DASHBOARD("ppp");

  s_error = false;
  test_pppoe();
  printf("HEALTH_DASHBOARD\t\"pppoe\": %s\n", s_error ? "false" : "true");

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

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