/*
 * Copyright (c) 2014 Cesanta Software Limited
 * All rights reserved
 * This software is dual-licensed: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation. For the terms of this
 * license, see <http://www.gnu.org/licenses/>.
 *
 * You are free to use this software under the terms of the GNU General
 * Public License, but WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details.
 *
 * Alternatively, you can license this software under a commercial
 * license, as set out in <https://www.cesanta.com/license>.
 */

#include "mongoose/mongoose.h"
#include "mongoose/src/internal.h"
#include "unit_test.h"
#include "common/test_util.h"
#include "common/cs_md5.h"
#include "tuna/dispatcher.h"

#if defined(_MSC_VER) && _MSC_VER >= 1900
#include <crtdbg.h>
#endif

#if defined(__STDC_VERSION__) && __STDC_VERSION__ < 199901L && !defined(WIN32)
#define __func__ ""
#endif

#define FETCH_BUF_SIZE (1024 * 16)

#define HTTP_PORT "45772"
#define LOOPBACK_IP "127.0.0.1"
#define LISTENING_ADDR LOOPBACK_IP ":" HTTP_PORT

static const char *s_argv_0 = NULL;
static struct mg_serve_http_opts s_http_server_opts;
static int s_listening_port = 23456;

#define TEST_MG_MALLOC malloc
#define TEST_MG_CALLOC calloc

#ifndef intptr_t
#define intptr_t long
#endif

void *(*test_malloc)(size_t) = TEST_MG_MALLOC;
void *(*test_calloc)(size_t, size_t) = TEST_MG_CALLOC;

void *failing_malloc(size_t size) {
  (void) size;
  return NULL;
}

void *failing_calloc(size_t count, size_t size) {
  (void) count;
  (void) size;
  return NULL;
}

static char *read_file(const char *path, size_t *size) {
  FILE *fp;
  struct stat st;
  char *data = NULL;
  if ((fp = mg_fopen(path, "rb")) != NULL && !fstat(fileno(fp), &st)) {
    *size = st.st_size;
    data = (char *) malloc(*size);
    fread(data, 1, *size, fp);
    fclose(fp);
  }
  return data;
}

void msleep(int millis) {
#if defined(_WIN32)
  Sleep(millis);
#else
  usleep(millis * 1000);
#endif
}

static struct mg_iface *test_iface = NULL;

static void init_test_connection(struct mg_connection *nc) {
  memset(nc, 0, sizeof(*nc));
  nc->iface = test_iface;
}

static struct mg_connection *create_test_connection() {
  struct mg_connection *nc = (struct mg_connection *) calloc(1, sizeof(*nc));
  init_test_connection(nc);
  return nc;
}

void mg_destroy_conn(struct mg_connection *conn, int destroy_if);
static void destroy_test_connection(struct mg_connection *nc) {
  mg_destroy_conn(nc, 0 /* destroy_if */);
}

static const char *test_mbuf(void) {
  struct mbuf io;
  const char *data = "TEST";
  const char *prefix = "MY";
  const char *big_prefix = "Some long prefix: ";
  size_t old_size;

  mbuf_init(&io, 0);
  ASSERT(io.buf == NULL);
  ASSERT_EQ(io.len, 0);
  ASSERT_EQ(io.size, 0);
  mbuf_free(&io);
  ASSERT(io.buf == NULL);
  ASSERT_EQ(io.len, 0);
  ASSERT_EQ(io.size, 0);

  mbuf_init(&io, 10);
  ASSERT(io.buf != NULL);
  ASSERT_EQ(io.len, 0);
  ASSERT_EQ(io.size, 10);
  mbuf_free(&io);
  ASSERT(io.buf == NULL);
  ASSERT_EQ(io.len, 0);
  ASSERT_EQ(io.size, 0);

  mbuf_init(&io, 10);
  ASSERT_EQ(mbuf_append(&io, NULL, 0), 0);
  /* test allocation failure */
  ASSERT_EQ(mbuf_append(&io, NULL, 1125899906842624), 0);

  ASSERT_EQ(mbuf_append(&io, data, strlen(data)), strlen(data));

  mbuf_resize(&io, 2);
  ASSERT_EQ(io.size, 10);
  ASSERT_EQ(io.len, strlen(data));

  ASSERT_EQ(mbuf_insert(&io, 0, prefix, strlen(prefix)), strlen(prefix));
  ASSERT_EQ(io.size, 10);
  ASSERT_EQ(io.len, strlen(data) + strlen(prefix));

  ASSERT_EQ(mbuf_insert(&io, 0, big_prefix, strlen(big_prefix)),
            strlen(big_prefix));
  ASSERT_EQ(io.size, MBUF_SIZE_MULTIPLIER *
                         (strlen(big_prefix) + strlen(prefix) + strlen(data)));
  ASSERT_STREQ_NZ(io.buf, "Some long prefix: MYTEST");

  old_size = io.size;
  ASSERT_EQ(mbuf_insert(&io, strlen(big_prefix), data, strlen(data)),
            strlen(data));
  ASSERT_EQ(io.size, old_size);
  ASSERT_STREQ_NZ(io.buf, "Some long prefix: TESTMYTEST");

  /* test allocation failure */
  ASSERT_EQ(mbuf_insert(&io, 0, NULL, 1125899906842624), 0);

  /* test overflow */
  ASSERT_EQ(mbuf_insert(&io, 0, NULL, -1), 0);
  mbuf_free(&io);
  return NULL;
}

static void eh1(struct mg_connection *nc, int ev, void *ev_data) {
  struct mbuf *io = &nc->recv_mbuf;

  switch (ev) {
    case MG_EV_CONNECT: {
      int res = *((int *) ev_data);
      if (res == 0) {
        mg_printf(nc, "%d %s there", *(int *) ev_data, "hi");
      } else {
        sprintf((char *) nc->user_data, "connect failed! %d", res);
      }
      break;
    }
    case MG_EV_RECV: {
      if (nc->listener != NULL) {
        mg_printf(nc, "%d", (int) io->len);
        mbuf_remove(io, io->len);
      } else if (io->len > 0) {
        sprintf((char *) nc->user_data, "%sok!",
                (io->len == 2 && memcmp(io->buf, "10", 2) == 0) ? "" : "NOT ");
        DBG(("%s", (const char *) nc->user_data));
        nc->flags |= MG_F_CLOSE_IMMEDIATELY;
      }
      break;
    }
    default:
      break;
  }
}

#define S_PEM "server.pem"
#define C_PEM "client.pem"
#define CA_PEM "ca.pem"

static const char *test_mgr_with_ssl(int use_ssl) {
  char addr[100] = "127.0.0.1:0", ip[sizeof(addr)], buf[100] = "",
       short_addr[5];
  struct mg_mgr mgr;
  struct mg_connection *nc;
  struct mg_bind_opts bopts;
  int port, port2;
#if !MG_ENABLE_SSL
  (void) use_ssl;
#endif

  mg_mgr_init(&mgr, NULL);
  /* mgr.hexdump_file = "-"; */

  memset(&bopts, 0, sizeof(bopts));
#if MG_ENABLE_SSL
  if (use_ssl) {
    bopts.ssl_cert = S_PEM;
    bopts.ssl_ca_cert = CA_PEM;
  }
#endif
  ASSERT((nc = mg_bind_opt(&mgr, addr, eh1, bopts)) != NULL);
  port2 = htons(nc->sa.sin.sin_port);
  ASSERT(port2 > 0);

  mg_sock_to_str(nc->sock, addr, sizeof(addr), 3);
  ASSERT_EQ(sscanf(addr, "%[^:]:%d", ip, &port), 2);
  ASSERT_STREQ(ip, "127.0.0.1");
  ASSERT_EQ(port, port2);

  mg_sock_to_str(nc->sock, short_addr, sizeof(short_addr), 3);
#ifndef _WIN32
  ASSERT_STREQ(short_addr, "");
#else
  /* We use `inet_ntoa` in Windows, so, can get partial result */
  ASSERT_STREQ(short_addr, "127.");
#endif

  ASSERT((nc = mg_connect(&mgr, addr, eh1)) != NULL);
#if MG_ENABLE_SSL
  if (use_ssl) {
    ASSERT(mg_set_ssl(nc, C_PEM, CA_PEM) == NULL);
  }
#endif
  nc->user_data = buf;
  poll_until(&mgr, 1, c_str_ne, buf, (void *) "");
  ASSERT_STREQ(buf, "ok!");
  buf[0] = '\0';

#if MG_ENABLE_SSL
  if (use_ssl) {
    struct mg_connect_opts opts;
    memset(&opts, 0, sizeof(opts));
    opts.user_data = buf;
    opts.ssl_cert = C_PEM;
    opts.ssl_ca_cert = CA_PEM;
    opts.ssl_server_name = "*"; /* TODO(rojer): Test this too. */
    ASSERT((nc = mg_connect_opt(&mgr, addr, eh1, opts)) != NULL);
    poll_until(&mgr, 1, c_str_ne, buf, (void *) "");
    ASSERT_STREQ(buf, "ok!");
    buf[0] = '\0';
  }
#endif

  mg_mgr_free(&mgr);
  return NULL;
}

static const char *test_mgr(void) {
  return test_mgr_with_ssl(0);
}

#if MG_ENABLE_SSL
void *run_manager(void *param) {
  struct mg_mgr *mgr = (struct mg_mgr *) param;
  while (((intptr_t) mgr->user_data) == 0) {
    mg_mgr_poll(mgr, 1000);
  }
  mg_mgr_free(mgr);
  free(mgr);
  return NULL;
}

static const char *test_ssl(void) {
  return test_mgr_with_ssl(1);
}

/*
 * unfortunately krypton has no BIO so it's hard to test this way
 * We still set MG_ENABLE_SSL so that vc6 has a binary to build
 */
#ifdef OPENSSL_VERSION_NUMBER
static void eh_hello_server(struct mg_connection *nc, int ev, void *ev_data) {
  (void) ev_data;
  if (ev == MG_EV_ACCEPT) mg_printf(nc, "hello");
}

static const char *test_modern_crypto(void) {
  char addr[100] = "127.0.0.1:8000";
  struct mg_mgr *mgr = (struct mg_mgr *) malloc(sizeof(*mgr));
  struct mg_connection *nc;
  int port;

  mg_mgr_init(mgr, NULL);

  ASSERT((nc = mg_bind(mgr, addr, eh_hello_server)) != NULL);
  port = htons(nc->sa.sin.sin_port);
  ASSERT(port > 0);
  ASSERT(mg_set_ssl(nc, S_PEM, NULL /* no client certs */) == NULL);
  mg_sock_to_str(nc->sock, addr, sizeof(addr),
                 MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT);
  mg_start_thread(run_manager, mgr);

  {
    char buf[100];
/* For older OpenSSL version we have to allow older digests. Then it still tests
 * DH. */
#if OPENSSL_VERSION_NUMBER < 0x10000000
    const char *ciphers = "DH:!ADH:AES:MD5:SHA1";
#else
    const char *ciphers = "DH:!ADH:AES:!MD5:!SHA1";
#endif

    SSL_CTX *ctx = NULL;
    BIO *bio = NULL;
    SSL *ssl = NULL;

    ctx = SSL_CTX_new(SSLv23_client_method());
    ASSERT(ctx != NULL);
    SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
    SSL_CTX_set_verify_depth(ctx, 1);
    /*
     * These are pretty restrictive settings and should satisfy e.g.
     * Chrome's "modern cryptography" requirements.
     */
    ASSERT_EQ(SSL_CTX_set_cipher_list(nc->ssl_ctx, ciphers), 1);
    ASSERT_EQ(SSL_CTX_load_verify_locations(ctx, CA_PEM, NULL), 1);
    bio = BIO_new_ssl_connect(ctx);
    ASSERT(bio != NULL);
    BIO_get_ssl(bio, &ssl);
    ASSERT(ssl != NULL);
    SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);
    BIO_set_conn_hostname(bio, addr);
    ASSERT_EQ(BIO_do_connect(bio), 1);
    ASSERT_EQ(BIO_do_handshake(bio), 1);
    ASSERT_EQ(SSL_get_verify_result(ssl), X509_V_OK);
    ASSERT_EQ(BIO_read(bio, buf, 5), 5);
    ASSERT_STREQ_NZ(buf, "hello");
    BIO_free_all(bio);
    SSL_CTX_free(ctx);
  }

  mgr->user_data = (void *) 1;
  return NULL;
}
#endif /* OPENSSL_VERSION_NUMBER */
#endif /* MG_ENABLE_SSL */

static const char *test_to64(void) {
  ASSERT_EQ(to64("0"), 0);
  ASSERT_EQ(to64(""), 0);
  ASSERT_EQ(to64("123"), 123);
  ASSERT_EQ(to64("-34"), -34);
  ASSERT_EQ(to64("3566626116"), 3566626116U);
  return NULL;
}

static const char *test_check_ip_acl(void) {
  uint32_t ip = 0x01020304;
  ASSERT_EQ(mg_check_ip_acl(NULL, ip), 1);
  ASSERT_EQ(mg_check_ip_acl("", ip), 1);
  ASSERT_EQ(mg_check_ip_acl("invalid", ip), -1);
  ASSERT_EQ(mg_check_ip_acl("-0.0.0.0/0", ip), 0);
  ASSERT_EQ(mg_check_ip_acl("-0.0.0.0/0,+1.0.0.0/8", ip), 1);
  ASSERT_EQ(mg_check_ip_acl("-0.0.0.0/0,+1.2.3.4", ip), 1);
  ASSERT_EQ(mg_check_ip_acl("-0.0.0.0/0,+1.0.0.0/16", ip), 0);
  return NULL;
}

static const char *test_parse_uri(void) {
  struct mg_str uri_out;
  struct mg_str scheme, user_info, host, path, query, fragment;
  unsigned int port;
  {
    struct mg_str uri = MG_MK_STR("foo");
    ASSERT_EQ(mg_parse_uri(uri, NULL, NULL, NULL, NULL, NULL, NULL, NULL), 0);
  }

  {
    struct mg_str uri = MG_MK_STR("http://user:pw@host:80/foo?bar#baz");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              0);

    ASSERT_MG_STREQ(scheme, "http");
    ASSERT_MG_STREQ(user_info, "user:pw");
    ASSERT_MG_STREQ(host, "host");
    ASSERT_EQ(port, 80);
    ASSERT_MG_STREQ(path, "/foo");
    ASSERT_MG_STREQ(query, "bar");
    ASSERT_MG_STREQ(fragment, "baz");

    ASSERT_EQ(mg_assemble_uri(&scheme, &user_info, &host, port, &path, &query,
                              &fragment, 0, &uri_out),
              0);
    ASSERT_MG_STREQ(uri_out, uri.p);
    free((void *) uri_out.p);
  }

  {
    struct mg_str uri = MG_MK_STR("http://host:80/foo?bar");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              0);

    ASSERT_MG_STREQ(scheme, "http");
    ASSERT_MG_STREQ(user_info, "");
    ASSERT_MG_STREQ(host, "host");
    ASSERT_EQ(port, 80);
    ASSERT_MG_STREQ(path, "/foo");
    ASSERT_MG_STREQ(query, "bar");
    ASSERT_MG_STREQ(fragment, "");

    ASSERT_EQ(mg_assemble_uri(&scheme, &user_info, &host, port, &path, &query,
                              &fragment, 0, &uri_out),
              0);
    ASSERT_MG_STREQ(uri_out, uri.p);
    free((void *) uri_out.p);
  }

  {
    struct mg_str uri = MG_MK_STR("http://host:80/foo#baz");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              0);

    ASSERT_MG_STREQ(scheme, "http");
    ASSERT_MG_STREQ(user_info, "");
    ASSERT_MG_STREQ(host, "host");
    ASSERT_EQ(port, 80);
    ASSERT_MG_STREQ(path, "/foo");
    ASSERT_MG_STREQ(query, "");
    ASSERT_MG_STREQ(fragment, "baz");

    ASSERT_EQ(mg_assemble_uri(&scheme, &user_info, &host, port, &path, &query,
                              &fragment, 0, &uri_out),
              0);
    ASSERT_MG_STREQ(uri_out, uri.p);
    free((void *) uri_out.p);
  }

  {
    struct mg_str uri = MG_MK_STR("http://host:80/foo");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              0);

    ASSERT_MG_STREQ(scheme, "http");
    ASSERT_MG_STREQ(user_info, "");
    ASSERT_MG_STREQ(host, "host");
    ASSERT_EQ(port, 80);
    ASSERT_MG_STREQ(path, "/foo");
    ASSERT_MG_STREQ(query, "");
    ASSERT_MG_STREQ(fragment, "");

    ASSERT_EQ(mg_assemble_uri(&scheme, &user_info, &host, port, &path, &query,
                              &fragment, 0, &uri_out),
              0);
    ASSERT_MG_STREQ(uri_out, uri.p);
    free((void *) uri_out.p);
  }

  {
    struct mg_str uri = MG_MK_STR("http://host/foo");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              0);

    ASSERT_MG_STREQ(scheme, "http");
    ASSERT_MG_STREQ(user_info, "");
    ASSERT_MG_STREQ(host, "host");
    ASSERT_EQ(port, 0);
    ASSERT_MG_STREQ(path, "/foo");
    ASSERT_MG_STREQ(query, "");
    ASSERT_MG_STREQ(fragment, "");

    ASSERT_EQ(mg_assemble_uri(&scheme, &user_info, &host, port, &path, &query,
                              &fragment, 0, &uri_out),
              0);
    ASSERT_MG_STREQ(uri_out, uri.p);
    free((void *) uri_out.p);
  }

  {
    struct mg_str uri = MG_MK_STR("http://host");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              0);
    ASSERT_MG_STREQ(scheme, "http");
    ASSERT_MG_STREQ(user_info, "");
    ASSERT_MG_STREQ(host, "host");
    ASSERT_EQ(port, 0);
    ASSERT_MG_STREQ(path, "");
    ASSERT_MG_STREQ(query, "");
    ASSERT_MG_STREQ(fragment, "");

    ASSERT_EQ(mg_assemble_uri(&scheme, &user_info, &host, port, &path, &query,
                              &fragment, 0, &uri_out),
              0);
    ASSERT_MG_STREQ(uri_out, uri.p);
    free((void *) uri_out.p);
  }

  {
    struct mg_str uri = MG_MK_STR("http://host:80");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              0);
    ASSERT_MG_STREQ(scheme, "http");
    ASSERT_MG_STREQ(user_info, "");
    ASSERT_MG_STREQ(host, "host");
    ASSERT_EQ(port, 80);
    ASSERT_MG_STREQ(path, "");
    ASSERT_MG_STREQ(query, "");
    ASSERT_MG_STREQ(fragment, "");

    ASSERT_EQ(mg_assemble_uri(&scheme, &user_info, &host, port, &path, &query,
                              &fragment, 0, &uri_out),
              0);
    ASSERT_MG_STREQ(uri_out, uri.p);
    free((void *) uri_out.p);
  }

  {
    struct mg_str uri = MG_MK_STR("tcp://1.2.3.4:5678");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              0);
    ASSERT_MG_STREQ(scheme, "tcp");
    ASSERT_MG_STREQ(user_info, "");
    ASSERT_MG_STREQ(host, "1.2.3.4");
    ASSERT_EQ(port, 5678);
    ASSERT_MG_STREQ(path, "");
    ASSERT_MG_STREQ(query, "");
    ASSERT_MG_STREQ(fragment, "");

    ASSERT_EQ(mg_assemble_uri(&scheme, &user_info, &host, port, &path, &query,
                              &fragment, 0, &uri_out),
              0);
    ASSERT_MG_STREQ(uri_out, uri.p);
    free((void *) uri_out.p);
  }

  {
    const struct mg_str uri = MG_MK_STR("tcp://[::1]:234");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              0);
    ASSERT_MG_STREQ(scheme, "tcp");
    ASSERT_MG_STREQ(user_info, "");
    ASSERT_MG_STREQ(host, "[::1]");
    ASSERT_EQ(port, 234);
    ASSERT_EQ(path.len, 0);
    ASSERT_EQ(query.len, 0);
    ASSERT_EQ(fragment.len, 0);
    ASSERT_MG_STREQ(path, "");
    ASSERT_MG_STREQ(query, "");
    ASSERT_MG_STREQ(fragment, "");

    ASSERT_EQ(mg_assemble_uri(&scheme, &user_info, &host, port, &path, &query,
                              &fragment, 0, &uri_out),
              0);
    ASSERT_MG_STREQ(uri_out, uri.p);
    free((void *) uri_out.p);
  }

  {
    const struct mg_str uri = MG_MK_STR("tcp://[::1]");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              0);
    ASSERT_MG_STREQ(scheme, "tcp");
    ASSERT_MG_STREQ(user_info, "");
    ASSERT_MG_STREQ(host, "[::1]");
    ASSERT_EQ(port, 0);
    ASSERT_MG_STREQ(path, "");
    ASSERT_MG_STREQ(query, "");
    ASSERT_MG_STREQ(fragment, "");

    ASSERT_EQ(mg_assemble_uri(&scheme, &user_info, &host, port, &path, &query,
                              &fragment, 0, &uri_out),
              0);
    ASSERT_MG_STREQ(uri_out, uri.p);
    free((void *) uri_out.p);
  }

  {
    const struct mg_str uri = MG_MK_STR("tcp://[::1");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              -1);
  }

  {
    struct mg_str uri = MG_MK_STR("host:80");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              0);
    ASSERT_MG_STREQ(scheme, "");
    ASSERT_MG_STREQ(user_info, "");
    ASSERT_MG_STREQ(host, "host");
    ASSERT_EQ(port, 80);
    ASSERT_MG_STREQ(path, "");
    ASSERT_MG_STREQ(query, "");
    ASSERT_MG_STREQ(fragment, "");

    ASSERT_EQ(mg_assemble_uri(&scheme, &user_info, &host, port, &path, &query,
                              &fragment, 0, &uri_out),
              0);
    ASSERT_MG_STREQ(uri_out, uri.p);
    free((void *) uri_out.p);
  }

  {
    const struct mg_str uri = MG_MK_STR("1.2.3.4:56789");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              0);
    ASSERT_MG_STREQ(scheme, "");
    ASSERT_MG_STREQ(user_info, "");
    ASSERT_MG_STREQ(host, "1.2.3.4");
    ASSERT_EQ(port, 56789);
    ASSERT_MG_STREQ(path, "");
    ASSERT_MG_STREQ(query, "");
    ASSERT_MG_STREQ(fragment, "");

    ASSERT_EQ(mg_assemble_uri(&scheme, &user_info, &host, port, &path, &query,
                              &fragment, 0, &uri_out),
              0);
    ASSERT_MG_STREQ(uri_out, uri.p);
    free((void *) uri_out.p);
  }

  {
    const struct mg_str uri = MG_MK_STR("[::1]:2345");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              0);
    ASSERT_MG_STREQ(scheme, "");
    ASSERT_MG_STREQ(user_info, "");
    ASSERT_MG_STREQ(host, "[::1]");
    ASSERT_EQ(port, 2345);
    ASSERT_MG_STREQ(path, "");
    ASSERT_MG_STREQ(query, "");
    ASSERT_MG_STREQ(fragment, "");

    ASSERT_EQ(mg_assemble_uri(&scheme, &user_info, &host, port, &path, &query,
                              &fragment, 0, &uri_out),
              0);
    ASSERT_MG_STREQ(uri_out, uri.p);
    free((void *) uri_out.p);
  }

  {
    const struct mg_str uri = MG_MK_STR("[::1]");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              0);
    ASSERT_MG_STREQ(scheme, "");
    ASSERT_MG_STREQ(user_info, "");
    ASSERT_MG_STREQ(host, "[::1]");
    ASSERT_EQ(port, 0);
    ASSERT_MG_STREQ(path, "");
    ASSERT_MG_STREQ(query, "");
    ASSERT_MG_STREQ(fragment, "");

    ASSERT_EQ(mg_assemble_uri(&scheme, &user_info, &host, port, &path, &query,
                              &fragment, 0, &uri_out),
              0);
    ASSERT_MG_STREQ(uri_out, uri.p);
    free((void *) uri_out.p);
  }

  {
    struct mg_str uri = MG_MK_STR("host/foo");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              0);
    ASSERT_MG_STREQ(scheme, "");
    ASSERT_MG_STREQ(user_info, "");
    ASSERT_MG_STREQ(host, "host");
    ASSERT_EQ(port, 0);
    ASSERT_MG_STREQ(path, "/foo");
    ASSERT_EQ(query.len, 0);
    ASSERT_EQ(fragment.len, 0);

    ASSERT_EQ(mg_assemble_uri(&scheme, &user_info, &host, port, &path, &query,
                              &fragment, 0, &uri_out),
              0);
    ASSERT_MG_STREQ(uri_out, uri.p);
    free((void *) uri_out.p);
  }

  {
    struct mg_str uri = MG_MK_STR("https://a.com/foo/x@y.z/fw.zip");
    ASSERT_EQ(mg_parse_uri(uri, &scheme, &user_info, &host, &port, &path,
                           &query, &fragment),
              0);
    ASSERT_MG_STREQ(scheme, "https");
    ASSERT_MG_STREQ(user_info, "");
    ASSERT_MG_STREQ(host, "a.com");
    ASSERT_EQ(port, 0);
    ASSERT_MG_STREQ(path, "/foo/x@y.z/fw.zip");
    ASSERT_EQ(query.len, 0);
    ASSERT_EQ(fragment.len, 0);

    ASSERT_EQ(mg_assemble_uri(&scheme, &user_info, &host, port, &path, &query,
                              &fragment, 0, &uri_out),
              0);
    ASSERT_MG_STREQ(uri_out, uri.p);
    free((void *) uri_out.p);
  }

  return NULL;
}

static const char *test_assemble_uri(void) {
  struct mg_str scheme, path, uri;
  {
    ASSERT_EQ(mg_assemble_uri(NULL, NULL, NULL, 0, NULL, NULL, NULL, 0, &uri),
              0);
    ASSERT_MG_STREQ(uri, "");
  }

  {
    scheme = mg_mk_str("file");
    path = mg_mk_str("/foo/bar");
    ASSERT_EQ(
        mg_assemble_uri(&scheme, NULL, NULL, 0, &path, NULL, NULL, 0, &uri), 0);
    ASSERT_MG_STREQ(uri, "file:///foo/bar");
    free((void *) uri.p);
  }

  {
    scheme = mg_mk_str("file");
    path = mg_mk_str("/foo/.././bar/baz");
    ASSERT_EQ(
        mg_assemble_uri(&scheme, NULL, NULL, 0, &path, NULL, NULL, 0, &uri), 0);
    ASSERT_MG_STREQ(uri, "file:///foo/.././bar/baz");
    free((void *) uri.p);
    ASSERT_EQ(
        mg_assemble_uri(&scheme, NULL, NULL, 0, &path, NULL, NULL, 1, &uri), 0);
    ASSERT_MG_STREQ(uri, "file:///bar/baz");
    free((void *) uri.p);
  }

  return NULL;
}

/* TODO(mkm) port these test cases to the new async parse_address */
static const char *test_parse_address(void) {
  static const char *valid[] = {
    "1",
    "1.2.3.4:1",
    "tcp://123",
    "udp://0.0.0.0:99",
#ifndef _WIN32 /* No /etc/hosts on Windows. */
    "tcp://localhost:99",
#endif
    ":8080",
#if MG_ENABLE_IPV6
    "udp://[::1]:123",
    "[3ffe:2a00:100:7031::1]:900",
#endif
    NULL
  };
  static const int protos[] = {
    SOCK_STREAM,
    SOCK_STREAM,
    SOCK_STREAM,
    SOCK_DGRAM,
    SOCK_STREAM,
    SOCK_STREAM
#if MG_ENABLE_IPV6
    ,
    SOCK_DGRAM,
    SOCK_STREAM
#endif
  };
  static const char *need_lookup[] = {"udp://a.com:53", "locl_host:12", NULL};
  static const char *invalid[] = {
      "99999", "1k", "1.2.3", "1.2.3.4:", "1.2.3.4:2p", "blah://12", ":123x",
      "veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeery.long:12345",
      "udp://missingport", NULL};
  char host[50];
  union socket_address sa;
  int i, proto;

  for (i = 0; valid[i] != NULL; i++) {
    ASSERT(mg_parse_address(valid[i], &sa, &proto, host, sizeof(host)) > 0);
    ASSERT_EQ(proto, protos[i]);
  }

  for (i = 0; invalid[i] != NULL; i++) {
    ASSERT_EQ(mg_parse_address(invalid[i], &sa, &proto, host, sizeof(host)),
              -1);
  }

  for (i = 0; need_lookup[i] != NULL; i++) {
    ASSERT_EQ(mg_parse_address(need_lookup[i], &sa, &proto, host, sizeof(host)),
              0);
  }

  return NULL;
}

#define CHECK_MGNP(in, exp_rv, exp_out)     \
  do {                                      \
    int rv;                                 \
    struct mg_str mgs;                      \
    char *in_copy = strdup(in);             \
    mgs.p = in_copy;                        \
    mgs.len = strlen(in);                   \
    rv = mg_normalize_uri_path(&mgs, &mgs); \
    ASSERT_EQ(rv, exp_rv);                  \
    if (rv > 0) {                           \
      ASSERT_MG_STREQ(mgs, exp_out);        \
    }                                       \
    MG_FREE(in_copy);                       \
  } while (0)

static const char *test_mg_normalize_uri_path(void) {
  CHECK_MGNP("", 0, "");
  CHECK_MGNP("x", 0, "");
  CHECK_MGNP("/", 1, "/");
  CHECK_MGNP("//", 1, "//");
  CHECK_MGNP("/.", 1, "/");
  CHECK_MGNP("//.", 1, "//");
  CHECK_MGNP("/..", 1, "/");
  CHECK_MGNP("//..", 1, "/");
  CHECK_MGNP("///..", 1, "//");
  CHECK_MGNP("/./", 1, "/");
  CHECK_MGNP("/.//", 1, "//");
  CHECK_MGNP("/./.", 1, "/");
  CHECK_MGNP("/.//.", 1, "//");
  CHECK_MGNP("/foo", 1, "/foo");
  CHECK_MGNP("/foo/", 1, "/foo/");
  CHECK_MGNP("/foo/.", 1, "/foo/");
  CHECK_MGNP("/foo/./", 1, "/foo/");
  CHECK_MGNP("/foo/..", 1, "/");
  CHECK_MGNP("/foo/../", 1, "/");
  CHECK_MGNP("/foo/../bar", 1, "/bar");
  CHECK_MGNP("/foo/../bar/", 1, "/bar/");
  CHECK_MGNP("/foo/../../bar", 1, "/bar");
  CHECK_MGNP("/foo////bar/", 1, "/foo////bar/");
  /* No percent-decoding. */
  CHECK_MGNP("/fo%2fo/%2e%2e/%2e/bar", 1, "/fo%2fo/%2e%2e/%2e/bar");
  return NULL;
}

#define CHECK_U2LP(u, exp_rv, exp_lp, exp_rem)      \
  do {                                              \
    int rv;                                         \
    char *lp = NULL;                                \
    struct mg_str r;                                \
    hm.uri.p = u;                                   \
    hm.uri.len = strlen(u);                         \
    rv = mg_uri_to_local_path(&hm, &opts, &lp, &r); \
    ASSERT_EQ(rv, exp_rv);                          \
    if (rv == 1) {                                  \
      ASSERT_STREQ(lp, exp_lp);                     \
      ASSERT_MG_STREQ(r, exp_rem);                  \
      MG_FREE(lp);                                  \
    }                                               \
  } while (0)

#ifdef _WIN32
#define DS "\\"
#else
#define DS "/"
#endif

static const char *test_mg_uri_to_local_path(void) {
  struct http_message hm;
  struct mg_serve_http_opts opts;
  memset(&hm, 0, sizeof(hm));
  memset(&opts, 0, sizeof(opts));
  opts.document_root = ".";
  CHECK_U2LP("/", 1, ".", "");
  CHECK_U2LP("//", 1, ".", "");
  CHECK_U2LP("/.", 1, ".", "");
  CHECK_U2LP("/%2e", 1, ".", "");
  CHECK_U2LP("//.", 1, ".", "");
  CHECK_U2LP("/..", 1, ".", "");
  CHECK_U2LP("//..", 1, ".", "");
  CHECK_U2LP("/./", 1, ".", "");
  CHECK_U2LP("/%2E/", 1, ".", "");
  CHECK_U2LP("/.//", 1, ".", "");
  CHECK_U2LP("/./.", 1, ".", "");
  CHECK_U2LP("/.//.", 1, ".", "");
  CHECK_U2LP("/data", 1, "." DS "data", "");
  CHECK_U2LP("/data/", 1, "." DS "data", "");
  CHECK_U2LP("/data/.", 1, "." DS "data", "");
  CHECK_U2LP("/data/./", 1, "." DS "data", "");
  CHECK_U2LP("/data/..", 1, ".", "");
  CHECK_U2LP("/data/../", 1, ".", "");
  CHECK_U2LP("/data/../data", 1, "." DS "data", "");
  CHECK_U2LP("/data/../../data", 1, "." DS "data", "");
  CHECK_U2LP("/no/such/file", 1, "." DS "no" DS "such" DS "file", "");
  CHECK_U2LP("/data/no/such/file", 1, "." DS "data" DS "no" DS "such" DS "file",
             "");
  CHECK_U2LP("/data/dummy.xml/", 1, "." DS "data" DS "dummy.xml", "/");
  CHECK_U2LP("/./data/./%2E/dummy.xml", 1, "." DS "data" DS "dummy.xml", "");
  /* No DIRSEPs in path. */
  CHECK_U2LP("/data%2Fdummy%2Exml/in%2Ffo/", 0, "", "");
  /* Ok in PATH_INFO. */
  CHECK_U2LP("/data/dummy%2Exml/in%2Ffo/", 1, "." DS "data" DS "dummy.xml",
             "/in%2Ffo/");
  /* Test rewrites */
  opts.url_rewrites = "/foo=/bar,/=/xxx";
  CHECK_U2LP("/", 1, "/xxx", "");
  CHECK_U2LP("/yyy", 1, "/xxx" DS "yyy", "");
  CHECK_U2LP("/foo/baz", 1, "/bar" DS "baz", "");
  /* /foo does not match - must fall on the boundary, / matches. */
  CHECK_U2LP("/foobario/baz", 1, "/xxx" DS "foobario" DS "baz", "");
  opts.url_rewrites = "**=/xxx";
  CHECK_U2LP("/", 1, "/xxx", "");
  CHECK_U2LP("/yyy", 1, "/xxx", "");
  hm.header_names[0] = mg_mk_str("Host");
  hm.header_values[0] = mg_mk_str("example.org");
  opts.url_rewrites = "@EXAMPLE.org=/test";
  CHECK_U2LP("/hello", 1, "/test" DS "hello", "");
  return NULL;
}

static const char *test_mg_url_encode(void) {
  const struct mg_str encode_me =
      MG_MK_STR("I'm a.little_tea-pot,here's$my;spout~oink(oink)oink/!");
  struct mg_str encoded = mg_url_encode(encode_me);
  ASSERT_MG_STREQ(
      encoded, "I%27m%20a.little_tea-pot,here%27s$my;spout~oink(oink)oink/%21");
  free((void *) encoded.p);
  return NULL;
}

static void connect_fail_cb(struct mg_connection *nc, int ev, void *p) {
  switch (ev) {
    case MG_EV_CONNECT:
      /* On connection success, set flag 1, else set 4 */
      *(int *) nc->user_data |= *(int *) p == 0 ? 1 : 4;
      break;
    case MG_EV_CLOSE:
      *(int *) nc->user_data |= 2;
      break;
  }
}

static void ev_handler_empty(struct mg_connection *nc, int ev, void *p) {
  (void) nc;
  (void) ev;
  (void) p;
}

static const char *test_connection_errors(void) {
  struct mg_mgr mgr;
  struct mg_bind_opts bopts;
  struct mg_connect_opts copts;
  struct mg_connection *nc;
  const char *error_string;
  int data = 0;

  mg_mgr_init(&mgr, NULL);

  memset(&bopts, 0, sizeof(bopts));
  bopts.error_string = &error_string;

  ASSERT(mg_bind_opt(&mgr, "blah://12", NULL, bopts) == NULL);
  ASSERT_STREQ(error_string, "handler is required");

  ASSERT(mg_bind_opt(&mgr, "blah://12", ev_handler_empty, bopts) == NULL);
  ASSERT_STREQ(error_string, "cannot parse address");

  ASSERT(mg_bind_opt(&mgr, "tcp://8.8.8.8:88", ev_handler_empty, bopts) ==
         NULL);
  ASSERT_STREQ(error_string, "failed to open listener");

#if MG_ENABLE_SSL
  bopts.ssl_cert = S_PEM;
  ASSERT(mg_bind_opt(&mgr, "udp://:0", ev_handler_empty, bopts) == NULL);
  ASSERT_STREQ(error_string, "SSL for UDP is not supported");
  bopts.ssl_cert = "no_such_file";
  ASSERT(mg_bind_opt(&mgr, "tcp://:0", ev_handler_empty, bopts) == NULL);
  ASSERT_STREQ(error_string, "Invalid SSL cert");
  bopts.ssl_cert = NULL;
  bopts.ssl_ca_cert = "no_such_file";
  ASSERT(mg_bind_opt(&mgr, "tcp://:0", ev_handler_empty, bopts) == NULL);
  ASSERT_STREQ(error_string, "Invalid SSL CA cert");
#endif

  memset(&copts, 0, sizeof(copts));
  copts.error_string = &error_string;

#if 0
  copts.error_string = &error_string;
  ASSERT(mg_connect_opt(&mgr, "tcp://255.255.255.255:0", NULL, copts) == NULL);
  ASSERT_STREQ(error_string, "cannot connect to socket");

  copts.user_data = &data;
  ASSERT(mg_connect_opt(&mgr, "tcp://255.255.255.255:0", connect_fail_cb,
                        copts) == NULL);
  ASSERT_STREQ(error_string, "cannot connect to socket");
  /* handler isn't invoked when it fails synchronously */
  ASSERT_EQ(data, 0);
#endif

  data = 0;
  copts.user_data = &data;
  ASSERT((nc = mg_connect_opt(&mgr, "tcp://does.not.exist:8080",
                              connect_fail_cb, copts)) != NULL);

  /* handler is invoked when it fails asynchronously */
  poll_until(&mgr, 5, c_int_eq, &data, (void *) 6);
  ASSERT_EQ(data, 6);

  /* mg_bind() does not use MG_CALLOC, but async resolver does */
  test_calloc = failing_calloc;
#ifndef _WIN32
  ASSERT(mg_connect(&mgr, "some.domain.needs.async.resolv:777", NULL) == NULL);
  ASSERT(mg_bind(&mgr, ":4321", NULL) == NULL);
#endif
  test_calloc = TEST_MG_CALLOC;

#if MG_ENABLE_SSL
  copts.ssl_cert = "no_such_file";
  ASSERT((nc = mg_connect_opt(&mgr, "tcp://google.com:80", connect_fail_cb,
                              copts)) == NULL);
  ASSERT_STREQ(error_string, "Invalid SSL cert");
  copts.ssl_cert = NULL;
  copts.ssl_ca_cert = "no_such_file";
  ASSERT((nc = mg_connect_opt(&mgr, "tcp://google.com:80", connect_fail_cb,
                              copts)) == NULL);
  ASSERT_STREQ(error_string, "Invalid SSL CA cert");
#endif

  mg_mgr_free(&mgr);
  return NULL;
}

static int avt(char **buf, size_t buf_size, const char *fmt, ...) {
  int result;
  va_list ap;
  va_start(ap, fmt);
  result = mg_avprintf(buf, buf_size, fmt, ap);
  va_end(ap);
  return result;
}

static const char *test_alloc_vprintf(void) {
  char buf[5], *p = buf;

  ASSERT_EQ(avt(&p, sizeof(buf), "%d", 123), 3);
  ASSERT(p == buf);
  ASSERT_STREQ(p, "123");

  ASSERT_EQ(avt(&p, sizeof(buf), "%d", 123456789), 9);
  ASSERT(p != buf);
  ASSERT_STREQ(p, "123456789");
  free(p);

  return NULL;
}

static const char *test_socketpair(void) {
  sock_t sp[2];
  static const char foo[] = "hi there";
  char buf[20];

  ASSERT_EQ(mg_socketpair(sp, SOCK_DGRAM), 1);
  ASSERT(sizeof(foo) < sizeof(buf));

  /* Send string in one direction */
  ASSERT_EQ(send(sp[0], foo, sizeof(foo), 0), sizeof(foo));
  ASSERT_EQ(recv(sp[1], buf, sizeof(buf), 0), sizeof(foo));
  ASSERT_STREQ(buf, "hi there");

  /* Now in opposite direction */
  ASSERT_EQ(send(sp[1], foo, sizeof(foo), 0), sizeof(foo));
  ASSERT_EQ(recv(sp[0], buf, sizeof(buf), 0), sizeof(foo));
  ASSERT_STREQ(buf, "hi there");

  closesocket(sp[0]);
  closesocket(sp[1]);

  return NULL;
}

static void ev_timer_handler(struct mg_connection *c, int ev, void *ev_data) {
  (void) ev_data;
  switch (ev) {
    case MG_EV_TIMER:
      (*(int *) c->user_data)++;
      break;
    case MG_EV_CLOSE:
      /* Make sure we're alive. Make the ASSERTs fail if we're here. */
      (*(int *) c->user_data) += 100;
      break;
  }
}

static const char *test_timer(void) {
  struct mg_mgr m;
  struct mg_connection *c;
  double begin, end;
  int n = 0, i;

  mg_mgr_init(&m, NULL);
  ASSERT((c = mg_add_sock(&m, INVALID_SOCKET, ev_timer_handler)) != NULL);
  c->user_data = &n;

  /* MG_EV_TIMER should not fire - we did not set it up */
  mg_mgr_poll(&m, 1);
  ASSERT_EQ(n, 0);

  /*
   * Now, set it up. Make sure MG_EV_TIMER event fires.
   * Also, it brings forward the poll timeout.
   */
  ASSERT_EQ(mg_set_timer(c, mg_time() + 0.1), 0.0);
  begin = mg_time();
  /*
   * Windows is a bit sloppy about select() timeouts, so it may take
   * a couple iterations for the timer to actually fire.
   */
  for (i = 0; n != 1 && i < 5; i++) {
    mg_mgr_poll(&m, 1000);
  }
  end = mg_time();
  ASSERT_EQ(n, 1);
  ASSERT_LT(end - begin, 0.9);
  ASSERT_GT(end - begin, 0.09);

  /* Make sure that timer is reset - second time it does not fire */
  ASSERT_EQ(c->ev_timer_time, 0.0);
  mg_mgr_poll(&m, 1);
  ASSERT_EQ(n, 1);

  c->flags |= MG_F_CLOSE_IMMEDIATELY;
  mg_mgr_poll(&m, 1);
  ASSERT_EQ(n, 101);

  /* Async resolver codepath */
  n = 0;
  ASSERT((c = mg_connect(&m, "awful.sad:1234", ev_timer_handler)) != NULL);
  c->user_data = &n;
  mg_set_timer(c, 1);
  mg_mgr_poll(&m, 1);
  ASSERT_EQ(n, 101);

  mg_mgr_free(&m);

  return NULL;
}

struct simple_data {
  int num_accept;
  int num_connect;
  int num_recv;
  int num_send;
  int num_close;
  int num_timer;
  char to_send[100];
  char data_rcvd[100];
  char fail[200];
  struct simple_data *sclient_data;
  struct mg_connection *sclient_nc;
};

#ifndef __APPLE__
static void count_events(struct simple_data *d, int ev) {
  switch (ev) {
    case MG_EV_POLL:
      break;
    case MG_EV_ACCEPT:
      d->num_accept++;
      break;
    case MG_EV_CONNECT:
      d->num_connect++;
      break;
    case MG_EV_RECV:
      d->num_recv++;
      break;
    case MG_EV_SEND:
      d->num_send++;
      break;
    case MG_EV_CLOSE:
      d->num_close++;
      break;
    case MG_EV_TIMER:
      d->num_timer++;
      break;
    default: {
      char msg[100];
      sprintf(msg, "(unexpected event: %d)", ev);
      strcat(d->fail, msg);
    }
  }
}

static void do_send(struct simple_data *d, struct mg_connection *nc) {
  if (d->to_send[0] != '\0') {
    mg_printf(nc, "%s", d->to_send);
  }
}

static void do_recv(struct simple_data *d, struct mg_connection *nc,
                    void *ev_data) {
  if (*((int *) ev_data) != (int) nc->recv_mbuf.len) {
    char msg[100];
    sprintf(msg, "(num recv wrong: %d vs %d)", *((int *) ev_data),
            (int) strlen(d->to_send));
    strcat(d->fail, msg);
    return;
  }
  strncat(d->data_rcvd, nc->recv_mbuf.buf, nc->recv_mbuf.len);
  mbuf_remove(&nc->recv_mbuf, nc->recv_mbuf.len);
}

static void check_sent(struct simple_data *d, struct mg_connection *nc,
                       void *ev_data) {
  if (*((int *) ev_data) != (int) strlen(d->to_send)) {
    char msg[100];
    sprintf(msg, "(num sent wrong: %d vs %d)", *((int *) ev_data),
            (int) strlen(d->to_send));
    strcat(d->fail, msg);
  } else {
    d->to_send[0] = '\0';
  }
  if (nc->send_mbuf.len != 0) strcat(d->fail, "(send buf not empty)");
}

static void cb_client(struct mg_connection *nc, int ev, void *ev_data) {
  struct simple_data *d = (struct simple_data *) nc->user_data;
  count_events(d, ev);
  switch (ev) {
    case MG_EV_CONNECT:
      do_send(d, nc);
      break;
    case MG_EV_SEND:
      check_sent(d, nc, ev_data);
      break;
    case MG_EV_RECV:
      do_recv(d, nc, ev_data);
      break;
  }
}

static void cb_sclient(struct mg_connection *nc, int ev, void *ev_data) {
  struct simple_data *d = (struct simple_data *) nc->user_data;
  count_events(d, ev);
  switch (ev) {
    case MG_EV_RECV: {
      do_recv(d, nc, ev_data);
      do_send(d, nc);
      nc->flags |= MG_F_SEND_AND_CLOSE;
      break;
    }
    case MG_EV_SEND: {
      check_sent(d, nc, ev_data);
      break;
    }
  }
}

static void cb_server(struct mg_connection *nc, int ev, void *ev_data) {
  struct simple_data *d = (struct simple_data *) nc->user_data;
  (void) ev_data;
  if (ev == MG_EV_ACCEPT) {
    d->sclient_nc = nc;
    nc->user_data = d = d->sclient_data;
    nc->handler = cb_sclient;
  }
  count_events(d, ev);
}
#endif

/* The following test is only reliable on Linux. */
#ifdef __linux__
static const char *test_simple(void) {
  struct mg_mgr mgr;
  struct mg_connection *nc_server, *nc_client, *nc_sclient;
  const char *address = "tcp://127.0.0.1:8910";
  struct simple_data client_data, server_data, sclient_data;

  (void) nc_sclient;
  mg_mgr_init(&mgr, NULL);

  ASSERT((nc_server = mg_bind(&mgr, address, cb_server)) != NULL);
  nc_server->user_data = &server_data;
  memset(&server_data, 0, sizeof(server_data));
  server_data.sclient_data = &sclient_data;
  memset(&sclient_data, 0, sizeof(sclient_data));

  mg_mgr_poll(&mgr, 1); /* 1 - nothing */

  ASSERT((nc_client = mg_connect(&mgr, address, cb_client)) != NULL);
  nc_client->user_data = &client_data;
  memset(&client_data, 0, sizeof(client_data));
  strcpy(client_data.to_send, "hi");

  mg_mgr_poll(&mgr, 1); /* 2 - client connects and sends, server accepts */
  ASSERT_EQ(server_data.num_accept, 0);
  ASSERT_EQ(sclient_data.num_accept, 1);
  ASSERT_EQ(client_data.num_connect, 1);
  ASSERT_EQ(client_data.num_send, 1);
  ASSERT_STREQ(client_data.fail, "");
  ASSERT_STREQ(server_data.fail, "");

  ASSERT(server_data.sclient_nc != NULL);
  nc_sclient = server_data.sclient_nc;
  ASSERT_EQ(sclient_data.num_send, 0);
  ASSERT_EQ(sclient_data.num_recv, 0);

  strcpy(sclient_data.to_send, "hello");
  mg_mgr_poll(&mgr, 1); /* 3 - server receives, buffers response, closes */
  ASSERT_STREQ(sclient_data.fail, "");
  ASSERT_EQ(sclient_data.num_recv, 1);
  ASSERT_EQ(sclient_data.num_send, 0);
  ASSERT_EQ(client_data.num_recv, 0);
  ASSERT_STREQ(sclient_data.data_rcvd, "hi");

  mg_mgr_poll(&mgr, 1); /* 4 - server sends */
  ASSERT_STREQ(sclient_data.fail, "");
  ASSERT_EQ(sclient_data.num_send, 1);

  mg_mgr_poll(&mgr, 1); /* 5 - client receives */
  ASSERT_STREQ(client_data.fail, "");
  ASSERT_EQ(client_data.num_recv, 1);
  ASSERT_STREQ(client_data.data_rcvd, "hello");

  mg_mgr_free(&mgr);

  ASSERT_STREQ(client_data.fail, "");
  ASSERT_STREQ(server_data.fail, "");
  ASSERT_STREQ(sclient_data.fail, "");

  ASSERT_EQ(server_data.num_accept, 0);
  ASSERT_EQ(server_data.num_connect, 0);
  ASSERT_EQ(server_data.num_recv, 0);
  ASSERT_EQ(server_data.num_send, 0);
  ASSERT_EQ(server_data.num_close, 1);

  ASSERT_EQ(client_data.num_accept, 0);
  ASSERT_EQ(client_data.num_connect, 1);
  ASSERT_EQ(client_data.num_recv, 1);
  ASSERT_EQ(client_data.num_send, 1);
  ASSERT_EQ(client_data.num_close, 1);

  ASSERT_EQ(sclient_data.num_accept, 1);
  ASSERT_EQ(sclient_data.num_connect, 0);
  ASSERT_EQ(sclient_data.num_recv, 1);
  ASSERT_EQ(sclient_data.num_send, 1);
  ASSERT_EQ(sclient_data.num_close, 1);

  return NULL;
}
#endif

#if MG_ENABLE_THREADS
static void eh2(struct mg_connection *nc, int ev, void *p) {
  (void) p;
  switch (ev) {
    case MG_EV_RECV:
      strcpy((char *) nc->user_data, nc->recv_mbuf.buf);
      break;
    default:
      break;
  }
}

static void *thread_func(void *param) {
  sock_t sock = *(sock_t *) param;
  send(sock, ":-)", 4, 0);
  return NULL;
}

static const char *test_thread(void) {
  struct mg_mgr mgr;
  struct mg_connection *nc;
  sock_t sp[2];
  char buf[20] = "";

  ASSERT_EQ(mg_socketpair(sp, SOCK_STREAM), 1);
  mg_start_thread(thread_func, &sp[1]);

  mg_mgr_init(&mgr, NULL);
  ASSERT((nc = mg_add_sock(&mgr, sp[0], eh2)) != NULL);
  nc->user_data = buf;
  poll_until(&mgr, 1, c_str_ne, buf, (void *) "");
  ASSERT_STREQ(buf, ":-)");
  mg_mgr_free(&mgr);
  closesocket(sp[1]);

  return NULL;
}
#endif /* MG_ENABLE_THREADS */

struct udp_res {
  struct mbuf buf_srv;
  struct mg_connection *srv_recv_conn;
  struct mbuf buf_clnt;
  struct mg_connection *clt_recv_conn;
};

static void eh3_srv(struct mg_connection *nc, int ev, void *p) {
  struct udp_res *ur = (struct udp_res *) nc->mgr->user_data;
  struct mbuf *io = &nc->recv_mbuf;
  (void) p;

  if (nc->flags & MG_F_LISTENING) {
    if (ev != MG_EV_POLL && ev != MG_EV_CLOSE) {
      mbuf_append(&ur->buf_srv, "BUG", 3);
    }
    return;
  }

  if (ev == MG_EV_ACCEPT) {
    mbuf_append(&ur->buf_srv, "ACCEPT", 6);
  } else if (ev == MG_EV_RECV) {
    mbuf_append(&ur->buf_srv, (nc->flags & MG_F_SEND_AND_CLOSE ? "+" : "-"), 1);
    mbuf_append(&ur->buf_srv, " RECV ", 6);
    mbuf_append(&ur->buf_srv, io->buf, io->len);
    mg_send(nc, io->buf, io->len);
    mbuf_remove(io, io->len);
  } else if (ev == MG_EV_CLOSE) {
    mbuf_append(&ur->buf_srv, " CLOSE", 6);
  }
}

static void eh3_clnt(struct mg_connection *nc, int ev, void *p) {
  struct udp_res *ur = (struct udp_res *) nc->mgr->user_data;
  struct mbuf *io = &nc->recv_mbuf;
  (void) p;

  if (ev == MG_EV_CONNECT) {
    mbuf_append(&ur->buf_clnt, "CONNECT", 7);
    mg_printf(nc, "%s", "boo!");
  } else if (ev == MG_EV_RECV) {
    mbuf_append(&ur->buf_clnt, " RECV ", 6);
    mbuf_append(&ur->buf_clnt, io->buf, io->len);
    mbuf_remove(io, io->len);
    nc->flags |= MG_F_CLOSE_IMMEDIATELY;
  } else if (ev == MG_EV_CLOSE) {
    mbuf_append(&ur->buf_clnt, " CLOSE", 6);
  }
}

static const char *test_udp(void) {
  struct mg_mgr mgr;
  struct mg_connection *nc1, *nc2;
  const char *address = "udp://127.0.0.1:7878";
  struct udp_res res;
  memset(&res, 0, sizeof(res));
  mbuf_init(&res.buf_srv, 100);
  mbuf_init(&res.buf_clnt, 100);

  mg_mgr_init(&mgr, &res);
  ASSERT((nc1 = mg_bind(&mgr, address, eh3_srv)) != NULL);
  ASSERT((nc2 = mg_connect(&mgr, address, eh3_clnt)) != NULL);

  poll_until(&mgr, 1, c_int_eq, &res.buf_clnt.len, (void *) 23);
  ASSERT_STREQ_NZ(res.buf_srv.buf, "ACCEPT+ RECV boo! CLOSE");
  ASSERT_STREQ_NZ(res.buf_clnt.buf, "CONNECT RECV boo! CLOSE");

  mbuf_free(&res.buf_srv);
  mbuf_free(&res.buf_clnt);
  mg_mgr_free(&mgr);

  return NULL;
}

static const char *test_parse_http_message(void) {
  static const char *a = "GET / HTTP/1.0\n\n";
  static const char *b = "GET /blah HTTP/1.0\r\nFoo:  bar  \r\n\r\n";
  static const char *c = "get b c\nz:  k \nb: t\nvvv\n\n xx";
  static const char *d = "a b c\nContent-Length: 21 \nb: t\nvvv\n\n";
  static const char *e = "GET /foo?a=b&c=d HTTP/1.0\n\n";
  static const char *f = "POST /x HTTP/1.0\n\n";
  static const char *g = "WOHOO /x HTTP/1.0\n\n";
  static const char *h = "HTTP/1.0 200 OK\n\n";
  static const char *i = "HTTP/1.0 999 OMGWTFBBQ\n\n";
  static const char *j =
      "GET / HTTP/1.0\r\nHost: 127.0.0.1:18888\r\nCookie:\r\nX-PlayID: "
      "45455\r\nRange: 0-1\r\n\r\n";
  int idx, len;
  struct mg_str *v;
  struct http_message req;

  len = strlen(b);
  for (idx = 0; idx < len; idx++) {
    ASSERT_EQ(mg_parse_http(b, idx, &req, 1), 0);
  }

  ASSERT_EQ(mg_parse_http("\b23", 3, &req, 1), -1);
  ASSERT_EQ(mg_parse_http("\b23", 3, &req, 0), -1);
  ASSERT_EQ(mg_parse_http("get\n\n", 5, &req, 1), -1);
  ASSERT_EQ(mg_parse_http("get\n\n", 5, &req, 0), -1);
  ASSERT_EQ(mg_parse_http(a, strlen(a) - 1, &req, 1), 0);
  ASSERT_EQ(mg_parse_http(a, strlen(a), &req, 0), -1);

  ASSERT_EQ(mg_parse_http(a, strlen(a), &req, 1), (int) strlen(a));
  ASSERT_EQ(req.message.len, strlen(a));
  ASSERT_EQ(req.body.len, 0);

  ASSERT_EQ(mg_parse_http(b, strlen(b), &req, 0), -1);
  ASSERT_EQ(mg_parse_http(b, strlen(b), &req, 1), (int) strlen(b));
  ASSERT_EQ(req.header_names[0].len, 3);
  ASSERT_EQ(req.header_values[0].len, 3);
  ASSERT(req.header_names[1].p == NULL);
  ASSERT_EQ(req.query_string.len, 0);
  ASSERT_EQ(req.message.len, strlen(b));
  ASSERT_EQ(req.body.len, 0);

  ASSERT_EQ(mg_parse_http(c, strlen(c), &req, 1), (int) strlen(c) - 3);
  ASSERT(req.header_names[2].p == NULL);
  ASSERT(req.header_names[0].p != NULL);
  ASSERT(req.header_names[1].p != NULL);
  ASSERT_EQ(memcmp(req.header_values[1].p, "t", 1), 0);
  ASSERT_EQ(req.header_names[1].len, 1);
  ASSERT_EQ(req.body.len, 0);

  ASSERT_EQ(mg_parse_http(d, strlen(d), &req, 1), (int) strlen(d));
  ASSERT_EQ(req.body.len, 21);
  ASSERT_EQ(req.message.len, 21 + strlen(d));
  ASSERT(mg_get_http_header(&req, "foo") == NULL);
  ASSERT((v = mg_get_http_header(&req, "contENT-Length")) != NULL);
  ASSERT_EQ(v->len, 2);
  ASSERT_STREQ_NZ(v->p, "21");

  ASSERT_EQ(mg_parse_http(e, strlen(e), &req, 1), (int) strlen(e));
  ASSERT_EQ(mg_vcmp(&req.uri, "/foo"), 0);
  ASSERT_EQ(mg_vcmp(&req.query_string, "a=b&c=d"), 0);

  ASSERT_EQ(mg_parse_http(f, strlen(f), &req, 1), (int) strlen(f));
  ASSERT_EQ(req.body.len, (size_t) ~0);

  ASSERT_EQ(mg_parse_http(g, strlen(g), &req, 1), (int) strlen(g));
  ASSERT_EQ(req.body.len, 0);

  ASSERT_EQ(mg_parse_http(h, strlen(h), &req, 0), (int) strlen(h));
  ASSERT_EQ(mg_vcmp(&req.proto, "HTTP/1.0"), 0);
  ASSERT_EQ(req.resp_code, 200);
  ASSERT_EQ(mg_vcmp(&req.resp_status_msg, "OK"), 0);
  ASSERT_EQ(req.body.len, (size_t) ~0);

  ASSERT_EQ(mg_parse_http(i, strlen(i), &req, 0), -1);

  ASSERT_EQ(mg_parse_http(j, strlen(j), &req, 1), (int) strlen(j));
  ASSERT(mg_get_http_header(&req, "Host") != NULL);
  ASSERT(mg_get_http_header(&req, "Cookie") == NULL);
  ASSERT(mg_get_http_header(&req, "Range") != NULL);

  return NULL;
}

static const char *test_get_http_var(void) {
  char buf[256];
  struct mg_str body;
  body.p = "key1=value1&key2=value2&key3=value%203&key4=value+4";
  body.len = strlen(body.p);

  ASSERT(mg_get_http_var(&body, "key1", buf, sizeof(buf)) > 0);
  ASSERT_STREQ(buf, "value1");
  ASSERT(mg_get_http_var(&body, "KEY1", buf, sizeof(buf)) > 0);
  ASSERT_STREQ(buf, "value1");
  ASSERT(mg_get_http_var(&body, "key2", buf, sizeof(buf)) > 0);
  ASSERT_STREQ(buf, "value2");
  ASSERT(mg_get_http_var(&body, "key3", buf, sizeof(buf)) > 0);
  ASSERT_STREQ(buf, "value 3");
  ASSERT(mg_get_http_var(&body, "key4", buf, sizeof(buf)) > 0);
  ASSERT_STREQ(buf, "value 4");

  ASSERT_EQ(mg_get_http_var(&body, "key1", NULL, sizeof(buf)), -2);
  ASSERT_EQ(mg_get_http_var(&body, "key1", buf, 0), -2);
  ASSERT_EQ(mg_get_http_var(&body, NULL, buf, sizeof(buf)), -1);
  ASSERT_EQ(mg_get_http_var(&body, "key1", buf, 1), -3);

  body.p = "key=broken%2";
  body.len = strlen(body.p);
  ASSERT(mg_get_http_var(&body, "key", buf, sizeof(buf)) < 0);

  body.p = "key=broken%2x";
  body.len = strlen(body.p);
  ASSERT(mg_get_http_var(&body, "key", buf, sizeof(buf)) < 0);

  ASSERT_EQ(mg_get_http_var(&body, "inexistent", buf, sizeof(buf)), -4);

  return NULL;
}

static void cb1(struct mg_connection *nc, int ev, void *ev_data) {
  if (ev == MG_EV_HTTP_REQUEST) {
    struct http_message *hm = (struct http_message *) ev_data;
    if (mg_vcmp(&hm->uri, "/foo") == 0) {
      mg_printf(nc, "HTTP/1.0 200 OK\r\n\r\n[%.*s %d]", (int) hm->uri.len,
                hm->uri.p, (int) hm->body.len);
      nc->flags |= MG_F_SEND_AND_CLOSE;
    } else {
      s_http_server_opts.document_root = ".";
      s_http_server_opts.per_directory_auth_file = "passwords.txt";
      s_http_server_opts.auth_domain = "foo.com";
      s_http_server_opts.dav_document_root = "./data/dav";
      s_http_server_opts.hidden_file_pattern = "hidden_file.*$";
      s_http_server_opts.custom_mime_types =
          ".txt=text/plain; charset=windows-1251,"
          ".c=text/plain; charset=utf-8";
      mg_serve_http(nc, hm, s_http_server_opts);
    }
  } else if (ev == MG_EV_SSI_CALL) {
    mg_printf_html_escape(nc, "[ssi call: %s]", (char *) ev_data);
  } else if (ev == MG_EV_SSI_CALL_CTX) {
    struct mg_ssi_call_ctx *cctx = (struct mg_ssi_call_ctx *) ev_data;
    mg_printf_html_escape(nc, "[ssi call w/ ctx: %.*s %.*s %.*s]",
                          (int) cctx->req->uri.len, cctx->req->uri.p,
                          (int) cctx->file.len, cctx->file.p,
                          (int) cctx->arg.len, cctx->arg.p);
  }
}

static void secret_endpoint_cb(struct mg_connection *nc, int ev,
                               void *ev_data) {
  if (ev != MG_EV_HTTP_REQUEST) return;
  mg_send_response_line(nc, 200, "Content-Type: text/plain\r\n");
  mg_printf(nc, "%s", "Secret!");
  nc->flags |= MG_F_SEND_AND_CLOSE;
  (void) ev_data;
}

static void cb2(struct mg_connection *nc, int ev, void *ev_data) {
  struct http_message *hm = (struct http_message *) ev_data;

  if (ev == MG_EV_HTTP_REPLY) {
    sprintf((char *) nc->user_data, "%.*s %lu", (int) hm->body.len, hm->body.p,
            (unsigned long) hm->message.len);
    nc->flags |= MG_F_CLOSE_IMMEDIATELY;
  }
}

static void cb7(struct mg_connection *nc, int ev, void *ev_data) {
  struct http_message *hm = (struct http_message *) ev_data;
  size_t size;
  char *data, *user_data = (char *) nc->user_data;

  if (ev == MG_EV_HTTP_REPLY) {
    /* Make sure that we've downloaded this executable, byte-to-byte */
    data = read_file(s_argv_0, &size);
    DBG(("file %s, size %d; got %d", s_argv_0, (int) size, (int) hm->body.len));
    if (data != NULL && size == hm->body.len &&
        memcmp(hm->body.p, data, size) == 0) {
      strcpy(user_data, "success");
    } else {
      strcpy(user_data, "fail");
    }
    free(data);
    nc->flags |= MG_F_CLOSE_IMMEDIATELY;
  } else if (ev == MG_EV_RECV) {
#if 0
    LOG(LL_INFO, ("%d", (int) nc->recv_mbuf.len));
#endif
  }
}

static void cb8(struct mg_connection *nc, int ev, void *ev_data) {
  struct http_message *hm = (struct http_message *) ev_data;
  DBG(("%p ev=%d", (void *) nc, ev));

  if (ev == MG_EV_HTTP_REPLY) {
    snprintf((char *) nc->user_data, FETCH_BUF_SIZE, "%.*s",
             (int) hm->message.len, hm->message.p);
    nc->flags |= MG_F_CLOSE_IMMEDIATELY;
  }
}

static void cb10(struct mg_connection *nc, int ev, void *ev_data) {
  struct http_message *hm = (struct http_message *) ev_data;
  struct mg_str *s;

  if (ev == MG_EV_HTTP_REPLY &&
      (s = mg_get_http_header(hm, "Content-Type")) != NULL) {
    sprintf((char *) nc->user_data, "%.*s", (int) s->len, s->p);
  }
}

static void endpoint_handler(struct mg_connection *nc, int ev, void *ev_data) {
  struct http_message *hm = (struct http_message *) ev_data;
  (void) ev_data;

  if (ev == MG_EV_HTTP_REQUEST) {
    if (mg_vcmp(&hm->uri, "/") == 0 || mg_vcmp(&hm->uri, "/foo") == 0) {
      mg_printf(nc, "HTTP/1.0 200 OK\r\n\r\n[%.*s%s%.*s %d]", (int) hm->uri.len,
                hm->uri.p, hm->query_string.len > 0 ? "?" : "",
                (int) hm->query_string.len, hm->query_string.p,
                (int) hm->body.len);
      nc->flags |= MG_F_SEND_AND_CLOSE;
    }
  } else if (ev == MG_EV_CLOSE) {
    if (nc->listener != NULL) {
      (*(int *) nc->listener->user_data) += 100;
    }
  }
}

static void handle_hello1(struct mg_connection *nc, int ev, void *ev_data) {
  (void) ev_data;
  switch (ev) {
    case MG_EV_HTTP_REQUEST:
      mg_printf(nc, "HTTP/1.0 200 OK\r\n\r\n[I am Hello1]");
      nc->flags |= MG_F_SEND_AND_CLOSE;
      break;
    case MG_EV_CLOSE:
      (*(int *) nc->listener->user_data)++;
      break;
  }
}

static void handle_hello2(struct mg_connection *nc, int ev, void *ev_data) {
  (void) ev_data;
  switch (ev) {
    case MG_EV_HTTP_REQUEST:
      mg_printf(nc, "HTTP/1.0 200 OK\r\n\r\n[I am Hello two]");
      nc->flags |= MG_F_SEND_AND_CLOSE;
      break;
    case MG_EV_CLOSE:
      (*(int *) nc->listener->user_data)++;
      break;
  }
}

static void handle_hello5(struct mg_connection *nc, int ev, void *ev_data) {
  (void) ev_data;
  switch (ev) {
    case MG_EV_HTTP_REQUEST:
      mg_printf(nc, "HTTP/1.0 200 OK\r\n\r\n[I am Hello again]");
      nc->flags |= MG_F_SEND_AND_CLOSE;
      break;
    case MG_EV_CLOSE:
      (*(int *) nc->listener->user_data)++;
      break;
  }
}

static void vfetch_http(char *buf, const char *auth_file,
                        const char *request_fmt, va_list ap) {
  struct mg_mgr mgr;
  struct mg_connection *nc;
  struct mg_http_endpoint_opts ep_opts;
  char local_addr[50];

  /* Setup server. Use different local port for the next invocation. */
  mg_mgr_init(&mgr, NULL);
  /* mgr.hexdump_file = "-"; */
  snprintf(local_addr, sizeof(local_addr), "127.0.0.1:%d", s_listening_port++);
  nc = mg_bind(&mgr, local_addr, cb1);
  mg_set_protocol_http_websocket(nc);
  memset(&ep_opts, 0, sizeof(ep_opts));
  ep_opts.auth_file = auth_file;
  ep_opts.auth_domain = "foo.com";
  mg_register_http_endpoint_opt(nc, "/secret", secret_endpoint_cb, ep_opts);

  /* Setup client */
  nc = mg_connect(&mgr, local_addr, cb8);
  mg_set_protocol_http_websocket(nc);
  nc->user_data = buf;
  mg_vprintf(nc, request_fmt, ap);

  /* Run event loop, destroy server */
  buf[0] = '\0';
  poll_until(&mgr, 5, c_str_ne, buf, (void *) "");

  mg_mgr_free(&mgr);
}

static void fetch_http(char *buf, const char *request_fmt, ...) {
  va_list ap;
  va_start(ap, request_fmt);
  vfetch_http(buf, "data/auth/passwords.txt", request_fmt, ap);
  va_end(ap);
}

static void fetch_http2(char *buf, const char *auth_file,
                        const char *request_fmt, ...) {
  va_list ap;
  va_start(ap, request_fmt);
  vfetch_http(buf, auth_file, request_fmt, ap);
  va_end(ap);
}

static const char *test_http_endpoints(void) {
  struct mg_mgr mgr;
  struct mg_connection *nc;
  const char *local_addr = "127.0.0.1:7778";
  char buf[50];
  int close_count = 0;

  mg_mgr_init(&mgr, NULL);
  /* mgr.hexdump_file = "-"; */
  ASSERT((nc = mg_bind(&mgr, local_addr, endpoint_handler)) != NULL);
  mg_register_http_endpoint(nc, "/hello1", handle_hello1 MG_UD_ARG(NULL));
  mg_register_http_endpoint(nc, "/hello1/hello2",
                            handle_hello2 MG_UD_ARG(NULL));
  mg_register_http_endpoint(nc, "/hello5/*", handle_hello5 MG_UD_ARG(NULL));
  nc->user_data = &close_count;

  mg_set_protocol_http_websocket(nc);
  /* Valid HTTP request. Pass test buffer to the callback. */
  ASSERT((nc = mg_connect_http(&mgr, cb2, "127.0.0.1:7778", NULL,
                               "0123456789")) != NULL);
  memset(buf, 0, sizeof(buf));
  nc->user_data = buf;
  DBG(("== WAIT =="));
  poll_until(&mgr, 1, c_str_ne, buf, (void *) "");
  ASSERT_STREQ(buf, "[/ 10] 25");

  ASSERT((nc = mg_connect_http(&mgr, cb2, "http://127.0.0.1:7778/", NULL,
                               "0123456789")) != NULL);
  memset(buf, 0, sizeof(buf));
  nc->user_data = buf;
  DBG(("== WAIT =="));
  poll_until(&mgr, 1, c_str_ne, buf, (void *) "");
  ASSERT_STREQ(buf, "[/ 10] 25");

  ASSERT((nc = mg_connect_http(&mgr, cb2, "http://127.0.0.1:7778/foo?a=b", NULL,
                               "foo")) != NULL);
  memset(buf, 0, sizeof(buf));
  nc->user_data = buf;
  DBG(("== WAIT =="));
  poll_until(&mgr, 1, c_str_ne, buf, (void *) "");
  ASSERT_STREQ(buf, "[/foo?a=b 3] 31");

  ASSERT((nc = mg_connect_http(&mgr, cb2, "http://127.0.0.1:7778/foo", NULL,
                               "0123456789")) != NULL);
  memset(buf, 0, sizeof(buf));
  nc->user_data = buf;
  DBG(("== WAIT =="));
  poll_until(&mgr, 1, c_str_ne, buf, (void *) "");
  ASSERT_STREQ(buf, "[/foo 10] 28");

  ASSERT((nc = mg_connect_http(&mgr, cb2, "http://127.0.0.1:7778/hello1", NULL,
                               "0123456789")) != NULL);
  memset(buf, 0, sizeof(buf));
  nc->user_data = buf;
  DBG(("== WAIT =="));
  poll_until(&mgr, 1, c_str_ne, buf, (void *) "");
  ASSERT_STREQ(buf, "[I am Hello1] 32");

  ASSERT((nc = mg_connect_http(&mgr, cb2, "http://127.0.0.1:7778/hello1/hello2",
                               NULL, "0123456789")) != NULL);
  memset(buf, 0, sizeof(buf));
  nc->user_data = buf;
  DBG(("== WAIT =="));
  poll_until(&mgr, 1, c_str_ne, buf, (void *) "");
  ASSERT_STREQ(buf, "[I am Hello two] 35");

  ASSERT((nc = mg_connect_http(&mgr, cb2, "http://127.0.0.1:7778/hello5/dummy",
                               NULL, "0123456789")) != NULL);
  memset(buf, 0, sizeof(buf));
  nc->user_data = buf;

  poll_until(&mgr, 1, c_str_ne, buf, (void *) "");
  ASSERT_STREQ(buf, "[I am Hello again] 37");

  ASSERT_EQ(close_count, 700);

  mg_mgr_free(&mgr);

  return NULL;
}

static const char *serve_file_verify(struct http_message *hm) {
  size_t size;
  char *data = read_file("unit_test.c", &size);
  if (data == NULL || size != hm->body.len ||
      memcmp(hm->body.p, data, size) != 0) {
    return "wrong data";
  }
  free(data);

  {
    char buf[20];
    struct mg_str *h = mg_get_http_header(hm, "Content-Length");
    ASSERT(h != NULL);
    snprintf(buf, sizeof(buf), "%d", (int) hm->body.len);
    ASSERT_MG_STREQ(*h, buf);
  }
  {
    struct mg_str *h = mg_get_http_header(hm, "Connection");
    ASSERT(h != NULL);
    ASSERT_MG_STREQ(*h, "keep-alive");
  }
  return "success";
}

static void serve_file_cb(struct mg_connection *nc, int ev, void *ev_data) {
  struct http_message *hm = (struct http_message *) ev_data;
  char *user_data = (char *) nc->user_data;

  if (ev == MG_EV_HTTP_REQUEST) {
    mg_http_serve_file(nc, hm, "unit_test.c", mg_mk_str("text/plain"),
                       mg_mk_str(""));
  } else if (ev == MG_EV_HTTP_REPLY) {
    strcpy(user_data, serve_file_verify(hm));
    nc->flags |= MG_F_CLOSE_IMMEDIATELY;
  }
}

static const char *test_http_serve_file(void) {
  struct mg_mgr mgr;
  struct mg_connection *lc, *nc;
  const char *local_addr = "127.0.0.1:7777";
  char status[100];
  mg_mgr_init(&mgr, NULL);
  /* mgr.hexdump_file = "-"; */
  ASSERT((lc = mg_bind(&mgr, local_addr, serve_file_cb)) != NULL);
  mg_set_protocol_http_websocket(lc);
  ASSERT((nc = mg_connect(&mgr, local_addr, serve_file_cb)) != NULL);
  mg_set_protocol_http_websocket(nc);
  status[0] = '\0';
  nc->user_data = status;
  mg_printf(nc, "GET / HTTP/1.1\r\n\r\n");
  poll_until(&mgr, 5, c_str_ne, status, (void *) "");
  ASSERT_STREQ(status, "success");
  mg_mgr_free(&mgr);
  return NULL;
}

static void srv1(struct mg_connection *c, int ev, void *ev_data) {
  if (ev == MG_EV_HTTP_REQUEST) {
    mg_http_serve_file(c, (struct http_message *) ev_data, "unit_test.c",
                       mg_mk_str("text/plain"), mg_mk_str(""));
  }
}

static void srv2(struct mg_connection *c, int ev, void *ev_data) {
  static cs_md5_ctx c1, c2;
  struct http_message *hm = (struct http_message *) ev_data;

  switch (ev) {
    case MG_EV_CONNECT:
      cs_md5_init(&c1);
      cs_md5_init(&c2);
      break;
    case MG_EV_HTTP_CHUNK:
      cs_md5_update(&c1, (const unsigned char *) hm->body.p, hm->body.len);
      c->flags |= MG_F_DELETE_CHUNK;
      break;
    case MG_EV_HTTP_REPLY: {
      unsigned char sig1[16], sig2[sizeof(sig1)];
      size_t size;
      char *data = read_file("unit_test.c", &size);
      if (data != NULL) cs_md5_update(&c2, (const unsigned char *) data, size);
      free(data);
      cs_md5_final(sig1, &c1);
      cs_md5_final(sig2, &c2);
      *(int *) c->user_data = (memcmp(sig1, sig2, sizeof(sig1)) == 0) ? 1 : 2;
      break;
    }
  }
}

static const char *test_http_serve_file_streaming(void) {
  struct mg_mgr mgr;
  struct mg_connection *lc, *nc;
  const char *local_addr = "127.0.0.1:7732";
  int status = 0;
  mg_mgr_init(&mgr, NULL);
  ASSERT((lc = mg_bind(&mgr, local_addr, srv1)) != NULL);
  mg_set_protocol_http_websocket(lc);
  ASSERT((nc = mg_connect(&mgr, local_addr, srv2)) != NULL);
  mg_set_protocol_http_websocket(nc);
  nc->user_data = &status;
  mg_printf(nc, "GET / HTTP/1.1\r\n\r\n");
  poll_until(&mgr, 30, c_int_ne, &status, (void *) 0);
  ASSERT_EQ(status, 1);
  mg_mgr_free(&mgr);
  return NULL;
}

static const char *test_http(void) {
  struct mg_mgr mgr;
  struct mg_connection *nc;
  const char *this_binary, *local_addr = "127.0.0.1:7777";
  char buf[50] = "", status[100] = "", mime1[20] = "", mime2[100] = "";
  char opt_buf[1024] = "";
  const char *opt_answer =
      "HTTP/1.1 200 OK\r\nAllow: GET, POST, HEAD, CONNECT, OPTIONS, MKCOL, "
      "PUT, DELETE, PROPFIND, MOVE";
  char url[1000];

  mg_mgr_init(&mgr, NULL);
  /* mgr.hexdump_file = "-"; */
  ASSERT((nc = mg_bind(&mgr, local_addr, cb1)) != NULL);

  mg_set_protocol_http_websocket(nc);
  /* Valid HTTP request. Pass test buffer to the callback. */
  ASSERT((nc = mg_connect_http(&mgr, cb2, "http://127.0.0.1:7777/foo", NULL,
                               "0123456789")) != NULL);
  nc->user_data = buf;
  DBG(("== WAIT =="));
  poll_until(&mgr, 1, c_str_ne, buf, (void *) "");
  ASSERT_STREQ(buf, "[/foo 10] 28");

  /* OPTIONS request */
  ASSERT((nc = mg_connect(&mgr, local_addr, cb8)) != NULL);
  mg_set_protocol_http_websocket(nc);
  nc->user_data = opt_buf;

  mg_printf(nc, "%s", "OPTIONS / HTTP 1.0\n\n");

  poll_until(&mgr, 1, c_str_ne, opt_buf, (void *) "");
  ASSERT_STREQ_NZ(opt_buf, opt_answer);

  /* Invalid HTTP request */
  ASSERT((nc = mg_connect(&mgr, local_addr, cb2)) != NULL);
  mg_set_protocol_http_websocket(nc);

  mg_printf(nc, "%s", "bl\x03\n\n");
  /* Test static file download by downloading this executable, argv[0] */
  ASSERT((nc = mg_connect(&mgr, local_addr, cb7)) != NULL);
  mg_set_protocol_http_websocket(nc);
  nc->user_data = status;

  /* Wine and GDB set argv0 to full path: strip the dir component */
  if ((this_binary = strrchr(s_argv_0, '\\')) != NULL) {
    this_binary++;
  } else if ((this_binary = strrchr(s_argv_0, '/')) != NULL) {
    this_binary++;
  } else {
    this_binary = s_argv_0;
  }
  mg_printf(nc, "GET /%s HTTP/1.0\n\n", this_binary);
  /* Test mime type for static file */
  snprintf(url, sizeof(url), "http://%s/data/dummy.xml", local_addr);
  ASSERT((nc = mg_connect_http(&mgr, cb10, url, NULL, NULL)) != NULL);
  nc->user_data = mime1;

  /* Test custom mime type for static file */
  snprintf(url, sizeof(url), "http://%s/data/range.txt", local_addr);
  ASSERT((nc = mg_connect_http(&mgr, cb10, url, NULL, NULL)) != NULL);
  nc->user_data = mime2;

  /* Run event loop. Use more cycles to let file download complete. */
  poll_until(&mgr, 5, c_str_ne, status, (void *) "");
  mg_mgr_free(&mgr);

  /* Check that test buffer has been filled by the callback properly. */
  ASSERT_STREQ(status, "success");
  ASSERT_STREQ(mime1, "text/xml");
  ASSERT_STREQ(mime2, "text/plain; charset=windows-1251");

  return NULL;
}

static const char *test_http_send_redirect(void) {
  struct mg_connection nc;
  init_test_connection(&nc);

  mg_http_send_redirect(&nc, 301, mg_mk_str("http://foo"), mg_mk_str(NULL));
  mg_send(&nc, "", 1); /* NUL */
  ASSERT_STREQ_NZ(nc.send_mbuf.buf,
                  "HTTP/1.1 301 Moved\r\n"
                  "Server: Mongoose/" MG_VERSION
                  "\r\n"
                  "Location: http://foo\r\n"
                  "Content-Type: text/html\r\n"
                  "Content-Length: 41\r\n"
                  "Cache-Control: no-cache\r\n"
                  "\r\n"
                  "<p>Moved <a href='http://foo'>here</a>.\r\n");
  ASSERT_EQ(nc.send_mbuf.len, 179);
  mbuf_remove(&nc.send_mbuf, nc.send_mbuf.len);

  mg_http_send_redirect(&nc, 302, mg_mk_str("/bar"), mg_mk_str("O-M-G: WTF?!"));
  mg_send(&nc, "", 1); /* NUL */
  ASSERT_STREQ_NZ(nc.send_mbuf.buf,
                  "HTTP/1.1 302 Found\r\n"
                  "Server: Mongoose/" MG_VERSION
                  "\r\n"
                  "Location: /bar\r\n"
                  "Content-Type: text/html\r\n"
                  "Content-Length: 35\r\n"
                  "Cache-Control: no-cache\r\n"
                  "O-M-G: WTF?!\r\n"
                  "\r\n"
                  "<p>Moved <a href='/bar'>here</a>.\r\n");
  ASSERT_EQ(nc.send_mbuf.len, 181);
  mbuf_remove(&nc.send_mbuf, nc.send_mbuf.len);

  mbuf_free(&nc.send_mbuf);
  return NULL;
}

static const char *test_http_digest_auth(void) {
  char buf[FETCH_BUF_SIZE], auth_hdr[200];
  char nonce[40];
  struct mg_str bufstr;

  /* Test digest authorization popup - per-directory auth file */
  fetch_http(buf, "%s", "GET /data/auth/a.txt?a=b HTTP/1.0\r\n\r\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 401");
  bufstr = mg_mk_str(buf);
  ASSERT_GT(mg_http_parse_header(&bufstr, "nonce", nonce, sizeof(nonce)), 0);
  buf[0] = '\0';
  /* Per-endpoint auth config */
  fetch_http(buf, "%s", "GET /secret HTTP/1.0\r\n\r\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 401");
#if MG_ENABLE_HTTP_STREAMING_MULTIPART
  fetch_http(buf, "%s",
             "POST /secret/upload HTTP/1.0\r\n"
             "Content-Type: multipart/form-data;boundary=omgwtf\r\n"
             "\r\n--omgwtf\r\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 401");
#endif

  /* Test digest authorization success */
  mg_http_create_digest_auth_header(auth_hdr, sizeof(auth_hdr), "GET",
                                    "/data/auth/a.txt?a=b", "foo.com", "joe",
                                    "doe", nonce);
  fetch_http(buf, "GET /data/auth/a.txt?a=b HTTP/1.0\r\n%s\r\n\r\n", auth_hdr);
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 200");
  ASSERT_STREQ(buf + strlen(buf) - 7, "\r\n\r\nhi\n");
  mg_http_create_digest_auth_header(auth_hdr, sizeof(auth_hdr), "GET",
                                    "/secret", "foo.com", "joe", "doe", nonce);
  fetch_http(buf, "GET /secret HTTP/1.0\r\n%s\r\n\r\n", auth_hdr);
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 200");
  ASSERT_STREQ(buf + strlen(buf) - 7, "Secret!");

  /* Test digest authorization failure with non-existing passwords file */
  mg_http_create_digest_auth_header(auth_hdr, sizeof(auth_hdr), "GET",
                                    "/data/auth/a.txt?a=b", "foo.com", "joe",
                                    "doe", nonce);
  fetch_http2(buf, "data/auth/non-existing-passwords.txt",
              "GET /data/auth/a.txt?a=b HTTP/1.0\r\n%s\r\n\r\n", auth_hdr);
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 200");
  ASSERT_STREQ(buf + strlen(buf) - 7, "\r\n\r\nhi\n");
  mg_http_create_digest_auth_header(auth_hdr, sizeof(auth_hdr), "GET",
                                    "/secret", "foo.com", "joe", "doe", nonce);
  fetch_http2(buf, "data/auth/non-existing-passwords.txt",
              "GET /secret HTTP/1.0\r\n%s\r\n\r\n", auth_hdr);
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 401");

  /* Test digest authorization failure with old nonce */
  {
    /*
     * Add 10 seconds to the nonce value, so that it'll be in the future
     * and mg_check_nonce() should fail.
     */
    unsigned long nonce_val = strtoul(nonce, NULL, 16);
    sprintf(nonce, "%lx", nonce_val + 10);

    mg_http_create_digest_auth_header(auth_hdr, sizeof(auth_hdr), "GET",
                                      "/data/auth/a.txt?a=b", "foo.com", "joe",
                                      "doe", nonce);
    fetch_http(buf, "GET /data/auth/a.txt?a=b HTTP/1.0\r\n%s\r\n\r\n",
               auth_hdr);
    ASSERT_STREQ_NZ(buf, "HTTP/1.1 401");

    mg_http_create_digest_auth_header(auth_hdr, sizeof(auth_hdr), "GET",
                                      "/secret", "foo.com", "joe", "doe",
                                      nonce);

    fetch_http(buf, "GET /secret HTTP/1.0\r\n%s\r\n\r\n", auth_hdr);
    ASSERT_STREQ_NZ(buf, "HTTP/1.1 401");

    /* Make nonce 1-hour-old, so mg_check_nonce() should fail again. */
    sprintf(nonce, "%lx", nonce_val - 60 * 60);

    mg_http_create_digest_auth_header(auth_hdr, sizeof(auth_hdr), "GET",
                                      "/data/auth/a.txt?a=b", "foo.com", "joe",
                                      "doe", nonce);
    fetch_http(buf, "GET /data/auth/a.txt?a=b HTTP/1.0\r\n%s\r\n\r\n",
               auth_hdr);
    ASSERT_STREQ_NZ(buf, "HTTP/1.1 401");

    /* Renew nonce to represent the actual nonce from the server */
    bufstr = mg_mk_str(buf);
    ASSERT_GT(mg_http_parse_header(&bufstr, "nonce", nonce, sizeof(nonce)), 0);
  }

  /* Test that passwords file is hidden from view */
  mg_http_create_digest_auth_header(auth_hdr, sizeof(auth_hdr), "GET",
                                    "/data/auth/p%61sswords%2etxt", "foo.com",
                                    "joe", "doe", nonce);
  fetch_http(buf,
             "GET /data/auth/p%%61sswords%%2etxt HTTP/1.0\r\n"
             "%s\r\n\r\n",
             auth_hdr);
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 404");
  /* On case-insensitive filesystems too. */
  mg_http_create_digest_auth_header(auth_hdr, sizeof(auth_hdr), "GET",
                                    "/data/auth/Passwords.txt", "foo.com",
                                    "joe", "doe", nonce);
  fetch_http(buf,
             "GET /data/auth/Passwords.txt HTTP/1.0\r\n"
             "%s\r\n\r\n",
             auth_hdr);
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 404");

  return NULL;
}

static const char *test_http_errors(void) {
  struct mg_mgr mgr;
  struct mg_connection *nc;
  const char *local_addr = "127.0.0.1:7777";
  char status[1000] = "";

  mg_mgr_init(&mgr, NULL);
  s_http_server_opts.enable_directory_listing = NULL;
  ASSERT((nc = mg_bind(&mgr, local_addr, cb1)) != NULL);
  mg_set_protocol_http_websocket(nc);

#if !defined(TEST_UNDER_VIRTUALBOX) && !defined(_WIN32)
  /* Test file which exists but cannot be opened */
  ASSERT((nc = mg_connect(&mgr, local_addr, cb8)) != NULL);
  mg_set_protocol_http_websocket(nc);
  nc->user_data = status;
  system("touch test_unreadable; chmod 000 test_unreadable");
  mg_printf(nc, "GET /%s HTTP/1.0\n\n", "../test_unreadable");

  /* Run event loop. Use more cycles to let file download complete. */
  poll_until(&mgr, 1, c_str_ne, status, (void *) "");
  system("rm -f test_unreadable");

  /* Check that it failed */
  ASSERT_STREQ_NZ(status, "HTTP/1.1 403");
#endif

  /* Test non existing file */
  ASSERT((nc = mg_connect(&mgr, local_addr, cb8)) != NULL);
  mg_set_protocol_http_websocket(nc);
  status[0] = '\0';
  nc->user_data = status;
  mg_printf(nc, "GET /%s HTTP/1.0\n\n", "/please_dont_create_this_file_srsly");

  poll_until(&mgr, 1, c_str_ne, status, (void *) "");

  /* Check that it failed */
  ASSERT_STREQ_NZ(status, "HTTP/1.1 404");

  /* Cleanup */
  mg_mgr_free(&mgr);

  return NULL;
}

static const char *test_http_index(void) {
  char buf[FETCH_BUF_SIZE];
  fetch_http(buf, "%s", "GET /data/dir_with_index/ HTTP/1.0\r\n\r\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 200");
  ASSERT(strstr(buf, "Content-Length: 3\r\n") != 0);
  ASSERT_STREQ(buf + strlen(buf) - 5, "\r\nfoo");
  /* Directory listing: allowed by default. */
  s_http_server_opts.enable_directory_listing = NULL;
  fetch_http(buf, "%s", "GET /data/dir_no_index/ HTTP/1.0\r\n\r\n");
  ASSERT_STREQ_NZ(buf,
                  "HTTP/1.1 200 OK\r\n"
                  "Server: Mongoose/" MG_VERSION
                  "\r\n"
                  "Transfer-Encoding: chunked\r\n");
  ASSERT(strstr(buf, "<html><head><title>") != NULL);
  /* Allowed explicitly. */
  s_http_server_opts.enable_directory_listing = "yes";
  fetch_http(buf, "%s", "GET /data/dir_no_index/ HTTP/1.0\r\n\r\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 200");
  /* Disallowed. */
  s_http_server_opts.enable_directory_listing = "no";
  fetch_http(buf, "%s", "GET /data/dir_no_index/ HTTP/1.0\r\n\r\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 403");
  return NULL;
}

static const char *test_ssi(void) {
  char buf[FETCH_BUF_SIZE];
  fetch_http(buf, "%s", "GET /data/ssi/ HTTP/1.0\n\n");
  ASSERT_STREQ(buf,
               "HTTP/1.1 200 OK\r\n"
               "Server: Mongoose/" MG_VERSION
               "\r\n"
               "Content-Type: text/html\r\n"
               "Connection: close\r\n\r\na\n\n"
               "<b>[ssi call: foo(bar)]"
               "[ssi call w/ ctx: /data/ssi/ ." DS "data" DS "ssi" DS
               "nested.shtml foo(bar)]"
               "</b>\nb\n\n\n");
  return NULL;
}

#ifndef NO_CGI_TEST
static const char *test_cgi(void) {
  char buf[FETCH_BUF_SIZE];
  char expected_server_port[100];
  const char *post_data = "aa=1234&bb=hi there";

  fetch_http(buf,
             "POST /data/cgi/ HTTP/1.0\r\n"
             "Content-Type: application/x-www-form-urlencoded\r\n"
             "Content-Length: %d\n\n%s",
             (int) strlen(post_data), post_data);

  /* fprintf(stderr, "%s", buf); */

  snprintf(expected_server_port, sizeof(expected_server_port),
           "\nE: SERVER_PORT=%d\r\n", s_listening_port - 1);
  ASSERT(strstr(buf, expected_server_port) != NULL);

  ASSERT_STREQ_NZ(buf, "HTTP/1.1 201 Created\r\n");
  ASSERT(strstr(buf, "\nE: CONTENT_LENGTH=19\r\n") != NULL);
  ASSERT(
      strstr(buf, "\nE: CONTENT_TYPE=application/x-www-form-urlencoded\r\n") !=
      NULL);
  ASSERT(strstr(buf, "\nE: DOCUMENT_ROOT=.\r\n") != NULL);
  ASSERT(strstr(buf, "\nE: HTTPS=off\r\n") != NULL);
  ASSERT(strstr(buf, "\nE: HTTP_CONTENT_LENGTH=19\r\n") != NULL);
  ASSERT(
      strstr(buf,
             "\nE: HTTP_CONTENT_TYPE=application/x-www-form-urlencoded\r\n") !=
      NULL);
  ASSERT(strstr(buf, "\nE: REMOTE_ADDR=127.0.0.1\r\n") != NULL);
  ASSERT(strstr(buf, "\nE: REQUEST_METHOD=POST\r\n") != NULL);
  ASSERT(strstr(buf, "\nE: REQUEST_URI=/data/cgi/\r\n") != NULL);
  ASSERT(strstr(buf, "\nE: SCRIPT_NAME=/data/cgi/index.cgi\r\n") != NULL);
  ASSERT(strstr(buf, "\nE: SCRIPT_FILENAME=." DS "data" DS "cgi" DS
                     "index.cgi\r\n") != NULL);
  ASSERT(strstr(buf, "\nE: PATH_INFO=") == NULL);
  ASSERT(strstr(buf, "\nE: PATH_TRANSLATED=") == NULL);
  ASSERT(strstr(buf, "\nE: SERVER_SOFTWARE=Mongoose/" MG_VERSION "\r\n") !=
         NULL);
  ASSERT(strstr(buf, "\nP: aa=1234\r\n") != NULL);
  ASSERT(strstr(buf, "\nP: bb=hi there\r\n") != NULL);

  fetch_http(buf,
             "GET /data/cgi/index%%2ecgi/foo%%2fbar/baz/?x=y HTTP/1.0\r\n\r\n");

  ASSERT_STREQ_NZ(buf, "HTTP/1.1 201 Created\r\n");
  ASSERT(strstr(buf, "\nE: CONTENT_LENGTH=") == NULL);
  ASSERT(strstr(buf, "\nE: CONTENT_TYPE=") == NULL);
  ASSERT(strstr(buf, "\nE: DOCUMENT_ROOT=.\r\n") != NULL);
  ASSERT(strstr(buf, "\nE: HTTPS=off\r\n") != NULL);
  ASSERT(strstr(buf, "\nE: HTTP_CONTENT_LENGTH=") == NULL);
  ASSERT(strstr(buf, "\nE: HTTP_CONTENT_TYPE=") == NULL);
  ASSERT(strstr(buf, "\nE: REQUEST_METHOD=GET\r\n") != NULL);
  ASSERT(
      strstr(buf,
             "\nE: REQUEST_URI=/data/cgi/index%2ecgi/foo%2fbar/baz/?x=y\r\n") !=
      NULL);
  ASSERT(strstr(buf, "\nE: SCRIPT_NAME=/data/cgi/index%2ecgi\r\n") != NULL);
  ASSERT(strstr(buf, "\nE: SCRIPT_FILENAME=." DS "data" DS "cgi" DS
                     "index.cgi\r\n") != NULL);
  ASSERT(strstr(buf, "\nE: PATH_INFO=/foo%2fbar/baz/") != NULL);
  ASSERT(strstr(buf, "\nE: PATH_TRANSLATED=/foo%2fbar/baz/") != NULL);
  ASSERT(strstr(buf, "\nE: SERVER_SOFTWARE=Mongoose/" MG_VERSION "\r\n") !=
         NULL);
  ASSERT(strstr(buf, "\nQ: x=y\r\n") != NULL);

  return NULL;
}
#endif

static const char *test_http_rewrites(void) {
  char buf[FETCH_BUF_SIZE];

  s_http_server_opts.url_rewrites =
      "/~joe=./data/rewrites,"
      "@foo.com=./data/rewrites/foo.com";

  /* Test rewrite */
  fetch_http(buf, "%s", "GET /~joe/msg.txt HTTP/1.0\nHost: foo.co\n\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 200 OK");
  ASSERT(strstr(buf, "Content-Length: 6\r\n") != 0);
  ASSERT_STREQ(buf + strlen(buf) - 8, "\r\nworks\n");

  /* Test rewrite that points to directory, expect redirect */
  fetch_http(buf, "%s", "GET /~joe HTTP/1.0\n\n");
  ASSERT_STREQ(buf,
               "HTTP/1.1 301 Moved\r\nLocation: /~joe/\r\n"
               "Content-Length: 0\r\n\r\n");

  /* Test domain-based rewrite */
  fetch_http(buf, "%s", "GET / HTTP/1.0\nHost: foo.com\n\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 200 OK");
  ASSERT(strstr(buf, "Content-Length: 9\r\n") != 0);
  ASSERT_STREQ(buf + strlen(buf) - 11, "\r\nfoo_root\n");

  /* Test port-based rewrites */
  s_listening_port = 25555;
  s_http_server_opts.url_rewrites = "%25555=https://foo";
  fetch_http(buf, "%s", "GET / HTTP/1.0\n\n");
  ASSERT_STREQ(buf,
               "HTTP/1.1 301 Moved\r\n"
               "Server: Mongoose/" MG_VERSION
               "\r\n"
               "Content-Length: 0\r\n"
               "Location: https://foo/\r\n\r\n");

  return NULL;
}

static const char *test_http_dav(void) {
  char buf[FETCH_BUF_SIZE];
  cs_stat_t st;

  remove("./data/dav/b.txt");
  rmdir("./data/dav/d");

  /* Test PROPFIND  */
  s_http_server_opts.enable_directory_listing = "yes";
  fetch_http(buf, "%s", "PROPFIND / HTTP/1.0\n\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 207");
  ASSERT(strstr(buf, "a.txt") != NULL);
  ASSERT(strstr(buf, "hidden_file.txt") == NULL);

  /* Test MKCOL */
  fetch_http(buf, "%s", "MKCOL /d HTTP/1.0\nContent-Length:5\n\n12345");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 415");
  fetch_http(buf, "%s", "MKCOL /d HTTP/1.0\n\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 201");
  fetch_http(buf, "%s", "MKCOL /d HTTP/1.0\n\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 405");
  fetch_http(buf, "%s", "MKCOL /x/d HTTP/1.0\n\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 409");

  /* Test PUT */
  fetch_http(buf, "%s", "PUT /b.txt HTTP/1.0\nContent-Length: 5\n\n12345");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 201");
  fetch_http(buf, "%s", "GET /data/dav/b.txt HTTP/1.0\n\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 200");
  ASSERT(strstr(buf, "Content-Length: 5\r\n") != 0);
  ASSERT_STREQ(buf + strlen(buf) - 7, "\r\n12345");

  /* Test DELETE */
  fetch_http(buf, "%s", "DELETE /b.txt HTTP/1.0\n\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 204");
  ASSERT(mg_stat("./data/dav/b.txt", &st) != 0);
  fetch_http(buf, "%s", "DELETE /d HTTP/1.0\n\n");
  ASSERT(mg_stat("./data/dav/d", &st) != 0);

  /* Non-existing file */
  fetch_http(buf, "%s", "PROPFIND /__blah.txt HTTP/1.0\n\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 404");

  return NULL;
}

static const char *test_http_range(void) {
  char buf[FETCH_BUF_SIZE];

  fetch_http(buf, "%s", "GET /data/range.txt HTTP/1.0\n\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 200 OK");
  ASSERT(strstr(buf, "Content-Length: 312\r\n") != 0);

  /* Fetch a piece from the middle of the file */
  fetch_http(buf, "%s", "GET /data/range.txt HTTP/1.0\nRange: bytes=5-10\n\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 206 Partial Content");
  ASSERT(strstr(buf, "Content-Length: 6\r\n") != 0);
  ASSERT(strstr(buf, "Content-Range: bytes 5-10/312\r\n") != 0);
  ASSERT_STREQ(buf + strlen(buf) - 8, "\r\n of co");

  /* Fetch till EOF */
  fetch_http(buf, "%s", "GET /data/range.txt HTTP/1.0\nRange: bytes=300-\n\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 206 Partial Content");
  ASSERT(strstr(buf, "Content-Length: 12\r\n") != 0);
  ASSERT(strstr(buf, "Content-Range: bytes 300-311/312\r\n") != 0);
  ASSERT_STREQ(buf + strlen(buf) - 14, "\r\nis disease.\n");

  /* Fetch past EOF, must trigger 416 response */
  fetch_http(buf, "%s", "GET /data/range.txt HTTP/1.0\nRange: bytes=1000-\n\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 416");
  ASSERT(strstr(buf, "Content-Length: 0\r\n") != 0);
  ASSERT(strstr(buf, "Content-Range: bytes */312\r\n") != 0);

  /* Request range past EOF, must trigger 416 response */
  fetch_http(buf, "%s", "GET /data/range.txt HTTP/1.0\nRange: bytes=0-312\n\n");
  ASSERT_STREQ_NZ(buf, "HTTP/1.1 416");

  return NULL;
}

static void cb3(struct mg_connection *nc, int ev, void *ev_data) {
  struct websocket_message *wm = (struct websocket_message *) ev_data;

  if (ev == MG_EV_WEBSOCKET_FRAME) {
    const char *reply = wm->size == 2 && !memcmp(wm->data, "hi", 2) ? "A" : "B";
    mg_printf_websocket_frame(nc, WEBSOCKET_OP_TEXT, "%s", reply);
  }
}

static void cb4(struct mg_connection *nc, int ev, void *ev_data) {
  struct websocket_message *wm = (struct websocket_message *) ev_data;

  if (ev == MG_EV_WEBSOCKET_FRAME) {
    memcpy(nc->user_data, wm->data, wm->size);
    mg_send_websocket_frame(nc, WEBSOCKET_OP_CLOSE, NULL, 0);
  } else if (ev == MG_EV_WEBSOCKET_HANDSHAKE_DONE) {
    /* Send "hi" to server. server must reply "A". */
    struct mg_str h[2];
    h[0].p = "h";
    h[0].len = 1;
    h[1].p = "i";
    h[1].len = 1;
    mg_send_websocket_framev(nc, WEBSOCKET_OP_TEXT, h, 2);
  }
}

static void cb_ws_server(struct mg_connection *nc, int ev, void *ev_data) {
  struct websocket_message *wm = (struct websocket_message *) ev_data;

  if (ev == MG_EV_WEBSOCKET_FRAME || ev == MG_EV_WEBSOCKET_CONTROL_FRAME) {
    mg_printf_websocket_frame(
        nc, WEBSOCKET_OP_TEXT, "%s%.2x:{%.*s}",
        (ev == MG_EV_WEBSOCKET_CONTROL_FRAME ? "CONTROL:" : ""), wm->flags,
        (int) wm->size, wm->data);
  }
}

static int s_ws_client1_connected = false;
static void cb_ws_client1(struct mg_connection *nc, int ev, void *ev_data) {
  struct websocket_message *wm = (struct websocket_message *) ev_data;

  if (ev == MG_EV_WEBSOCKET_FRAME || ev == MG_EV_WEBSOCKET_CONTROL_FRAME) {
    char *tmp = NULL;
    int tmplen;
    tmplen =
        mg_asprintf(&tmp, 0, "%s%.2x:[%.*s]",
                    (ev == MG_EV_WEBSOCKET_CONTROL_FRAME ? "CONTROL:" : ""),
                    wm->flags, (int) wm->size, wm->data);
    mbuf_append((struct mbuf *) nc->user_data, tmp, tmplen);
    free(tmp);
  } else if (ev == MG_EV_CLOSE) {
    s_ws_client1_connected = 0;
  } else if (ev == MG_EV_WEBSOCKET_HANDSHAKE_DONE) {
    s_ws_client1_connected = 1;
  }
}

static const char *test_websocket(void) {
  struct mg_mgr mgr;
  struct mg_connection *nc;
  const char *local_addr = "127.0.0.1:7778";
  struct mbuf mb;
  mbuf_init(&mb, 100);

  mg_mgr_init(&mgr, NULL);
  /* mgr.hexdump_file = "-"; */
  ASSERT((nc = mg_bind(&mgr, local_addr, cb_ws_server)) != NULL);
  mg_set_protocol_http_websocket(nc);

  /*
   * Websocket request "hi" via mg_send_websocket_framev()
   */
  mb.len = 0;
  s_ws_client1_connected = 0;
  ASSERT((nc = mg_connect(&mgr, local_addr, cb_ws_client1)) != NULL);
  mg_set_protocol_http_websocket(nc);
  nc->user_data = &mb;
  mg_send_websocket_handshake(nc, "/ws", NULL);
  poll_until(&mgr, 1, c_int_ne, &s_ws_client1_connected, (void *) 1);
  {
    /* Send "hi" to server. server must reply "A". */
    struct mg_str h[2];
    h[0].p = "h";
    h[0].len = 1;
    h[1].p = "i";
    h[1].len = 1;
    mg_send_websocket_framev(nc, WEBSOCKET_OP_TEXT, h, 2);
  }
  poll_until(&mgr, 1, c_int_ne, &mb.len, (void *) 0);
  mbuf_append(&mb, "\0", 1);
  /* Check that test buffer has been filled by the callback properly. */
  ASSERT_STREQ(mb.buf, "81:[81:{hi}]");

  /*
   * Websocket request "hallloo", composed from two fragments
   */
  mb.len = 0;
  s_ws_client1_connected = 0;
  ASSERT((nc = mg_connect(&mgr, local_addr, cb_ws_client1)) != NULL);
  mg_set_protocol_http_websocket(nc);
  nc->user_data = &mb;
  mg_send_websocket_handshake(nc, "/ws", NULL);
  poll_until(&mgr, 1, c_int_ne, &s_ws_client1_connected, (void *) 1);
  ASSERT_EQ(s_ws_client1_connected, 1);

  mg_send_websocket_frame(nc, WEBSOCKET_OP_TEXT | WEBSOCKET_DONT_FIN, "hall",
                          4);
  mg_send_websocket_frame(nc, WEBSOCKET_OP_CONTINUE | WEBSOCKET_DONT_FIN, "--",
                          2);

  /* Poll 0.5 seconds, we don't expect c_str_ne predicate to return true */
  poll_until(&mgr, 0.5, c_int_ne, &mb.len, (void *) 0);
  ASSERT_EQ(mb.len, 0);

  mg_send_websocket_frame(nc, WEBSOCKET_OP_CONTINUE, "loo", 3);

  poll_until(&mgr, 1, c_int_ne, &mb.len, (void *) 0);
  mbuf_append(&mb, "\0", 1);
  /* Check that test buffer has been filled by the callback properly. */
  ASSERT_STREQ(mb.buf, "81:[81:{hall--loo}]");

  /*
   * Ping request
   */
  mb.len = 0;
  s_ws_client1_connected = 0;
  ASSERT((nc = mg_connect(&mgr, local_addr, cb_ws_client1)) != NULL);
  mg_set_protocol_http_websocket(nc);
  nc->user_data = &mb;
  mg_send_websocket_handshake(nc, "/ws", NULL);
  poll_until(&mgr, 1, c_int_ne, &s_ws_client1_connected, (void *) 1);
  ASSERT_EQ(s_ws_client1_connected, 1);

  mg_send_websocket_frame(nc, WEBSOCKET_OP_PING, "myping", 6);

  {
    /*
     * As a response to Ping, we should first receive Pong (0x8a),
     * and then text message (0x81) with our Ping (0x89) echo.
     */
    const char *expected = "CONTROL:8a:[myping]81:[CONTROL:89:{myping}]";
    poll_until(&mgr, 1, c_int_eq, &mb.len, (void *) strlen(expected));
    mbuf_append(&mb, "\0", 1);
    ASSERT_STREQ(mb.buf, expected);
  }

  /*
   * Ping request injected in between of a fragmented message
   */
  mb.len = 0;
  s_ws_client1_connected = 0;
  ASSERT((nc = mg_connect(&mgr, local_addr, cb_ws_client1)) != NULL);
  mg_set_protocol_http_websocket(nc);
  nc->user_data = &mb;
  mg_send_websocket_handshake(nc, "/ws", NULL);
  poll_until(&mgr, 1, c_int_ne, &s_ws_client1_connected, (void *) 1);
  ASSERT_EQ(s_ws_client1_connected, 1);

  mg_send_websocket_frame(nc, WEBSOCKET_OP_TEXT | WEBSOCKET_DONT_FIN, "abc", 3);
  mg_send_websocket_frame(nc, WEBSOCKET_OP_CONTINUE | WEBSOCKET_DONT_FIN, "def",
                          3);

  mg_send_websocket_frame(nc, WEBSOCKET_OP_PING, "0123", 4);

  mg_send_websocket_frame(nc, WEBSOCKET_OP_CONTINUE, "ghi", 3);

  {
    /*
     * As a response to Ping, we should first receive Pong (0x8a),
     * and then text message (0x81) with our Ping (0x89) echo.
     * And then, our fragmented text message: abcdefghi.
     */
    const char *expected =
        "CONTROL:8a:[0123]81:[CONTROL:89:{0123}]81:[81:{abcdefghi}]";
    poll_until(&mgr, 1, c_int_eq, &mb.len, (void *) strlen(expected));
    mbuf_append(&mb, "\0", 1);
    ASSERT_STREQ(mb.buf, expected);
  }

  /*
   * Test illegal text frame in the middle of a fragmented message
   */
  mb.len = 0;
  s_ws_client1_connected = 0;
  ASSERT((nc = mg_connect(&mgr, local_addr, cb_ws_client1)) != NULL);
  mg_set_protocol_http_websocket(nc);
  nc->user_data = &mb;
  mg_send_websocket_handshake(nc, "/ws", NULL);
  poll_until(&mgr, 1, c_int_ne, &s_ws_client1_connected, (void *) 1);
  ASSERT_EQ(s_ws_client1_connected, 1);

  /* Send a few parts of a fragmented message */
  mg_send_websocket_frame(nc, WEBSOCKET_OP_TEXT | WEBSOCKET_DONT_FIN, "abc", 3);
  mg_send_websocket_frame(nc, WEBSOCKET_OP_CONTINUE | WEBSOCKET_DONT_FIN, "def",
                          3);

  /* Send (illegal) text frame in the middle of a fragmented message */
  mg_send_websocket_frame(nc, WEBSOCKET_OP_TEXT, "ghi", 3);

  /* Wait until connection is closed by the server */
  poll_until(&mgr, 1, c_int_ne, &s_ws_client1_connected, (void *) 0);
  ASSERT_EQ(s_ws_client1_connected, 0);

  /* Verify the error message */
  mbuf_append(&mb, "\0", 1);
  ASSERT_STREQ(
      mb.buf,
      "CONTROL:88:[non-continuation in the middle of a fragmented message]");

  /*
   * Test closing by the client
   */
  mb.len = 0;
  s_ws_client1_connected = 0;
  ASSERT((nc = mg_connect(&mgr, local_addr, cb_ws_client1)) != NULL);
  mg_set_protocol_http_websocket(nc);
  nc->user_data = &mb;
  mg_send_websocket_handshake(nc, "/ws", NULL);
  poll_until(&mgr, 1, c_int_ne, &s_ws_client1_connected, (void *) 1);
  ASSERT_EQ(s_ws_client1_connected, 1);

  /* Send a few parts of a fragmented message */
  mg_send_websocket_frame(nc, WEBSOCKET_OP_CLOSE, "bye", 3);

  /* Wait until connection is closed by the server */
  poll_until(&mgr, 1, c_int_ne, &s_ws_client1_connected, (void *) 0);
  ASSERT_EQ(s_ws_client1_connected, 0);
  /*
   * TODO(dfrank): mongoose closes the connection automatically when
   * mg_send_websocket_frame() is called with a WEBSOCKET_OP_CLOSE op, so we
   * can't hear anything from the server. Server is actually obliged to send
   * the control frame in response, but we can't test it here.
   */

  mg_mgr_free(&mgr);
  mbuf_free(&mb);

  return NULL;
}

static void cbwep(struct mg_connection *c, int ev, void *ev_data) {
  struct websocket_message *wm = (struct websocket_message *) ev_data;
  char *buf = (char *) c->user_data;

  switch (ev) {
    case MG_EV_WEBSOCKET_HANDSHAKE_REQUEST:
      strcat(buf, "R");
      break;
    case MG_EV_WEBSOCKET_HANDSHAKE_DONE:
      strcat(buf, "D");
      break;
    case MG_EV_WEBSOCKET_FRAME:
      strcat(buf, "F");
      mg_printf_websocket_frame(c, WEBSOCKET_OP_TEXT, "%s|%.*s", buf,
                                (int) wm->size, wm->data);
      break;
  }
}

static const char *test_websocket_endpoint(void) {
  struct mg_mgr mgr;
  struct mg_connection *nc;
  const char *local_addr = "127.0.0.1:7798";
  char buf[20] = "", buf2[20] = "";

  mg_mgr_init(&mgr, NULL);
  /* mgr.hexdump_file = "-"; */
  ASSERT((nc = mg_bind(&mgr, local_addr, cb3)) != NULL);
  mg_set_protocol_http_websocket(nc);
  nc->user_data = buf2;
  mg_register_http_endpoint(nc, "/boo", cbwep MG_UD_ARG(NULL));

  /* Websocket request */
  ASSERT((nc = mg_connect(&mgr, local_addr, cb4)) != NULL);
  mg_set_protocol_http_websocket(nc);
  nc->user_data = buf;
  mg_send_websocket_handshake(nc, "/boo", NULL);
  poll_until(&mgr, 1, c_str_ne, buf, (void *) "");
  mg_mgr_free(&mgr);

  /* Check that test buffer has been filled by the callback properly. */
  ASSERT_STREQ(buf, "RDF|hi");

  return NULL;
}

static const char *test_connect_ws(void) {
  struct mg_mgr mgr;
  struct mg_connection *nc;
  const char *urls[] = {"ws://127.0.0.1:7778/ws", "http://127.0.0.1:7778/ws",
                        "127.0.0.1:7778/ws", NULL};
  const char **url;

  mg_mgr_init(&mgr, NULL);
  /* mgr.hexdump_file = "-"; */
  ASSERT((nc = mg_bind(&mgr, "127.0.0.1:7778", cb3)) != NULL);
  mg_set_protocol_http_websocket(nc);

  for (url = urls; *url != NULL; url++) {
    char buf[20] = "";
    ASSERT((nc = mg_connect_ws(&mgr, cb4, "ws://127.0.0.1:7778/ws", NULL,
                               NULL)) != NULL);
    nc->user_data = buf;
    poll_until(&mgr, 1, c_str_ne, buf, (void *) "");
    ASSERT_STREQ(buf, "A");
  }

  mg_mgr_free(&mgr);

  return NULL;
}

struct big_payload_params {
  size_t size;
  char *buf;
};

static void cb3_big(struct mg_connection *nc, int ev, void *ev_data) {
  struct websocket_message *wm = (struct websocket_message *) ev_data;

  if (ev == MG_EV_WEBSOCKET_FRAME) {
    int success = 1;
    size_t i;
    for (i = 0; i < wm->size; i++) {
      if (wm->data[i] != 'x') {
        success = 0;
        break;
      }
    }
    mg_printf_websocket_frame(nc, WEBSOCKET_OP_TEXT, "%s",
                              success ? "success" : "fail");
  }
}

static void cb4_big(struct mg_connection *nc, int ev, void *ev_data) {
  struct websocket_message *wm = (struct websocket_message *) ev_data;
  struct big_payload_params *params =
      (struct big_payload_params *) nc->user_data;

  if (ev == MG_EV_WEBSOCKET_FRAME) {
    memcpy(params->buf, wm->data, wm->size);
    mg_send_websocket_frame(nc, WEBSOCKET_OP_CLOSE, NULL, 0);
  } else if (ev == MG_EV_WEBSOCKET_HANDSHAKE_DONE) {
    /* Send large payload to server. server must reply "success". */
    char *payload = (char *) malloc(params->size);
    memset(payload, 'x', params->size);
    mg_printf_websocket_frame(nc, WEBSOCKET_OP_TEXT, "%.*s", params->size,
                              payload);
    free(payload);
  }
}

/* Big payloads follow a different code path because it will use the extended
 * length field and possibly mg_avprintf will need to reallocate the buffer. */
static const char *test_websocket_big(void) {
  struct mg_mgr mgr;
  struct mg_connection *nc;
  const char *local_addr = "127.0.0.1:7778";
  char buf[20] = "";
  struct big_payload_params params;
  params.buf = buf;

  mg_mgr_init(&mgr, NULL);
  /* mgr.hexdump_file = "-"; */
  ASSERT((nc = mg_bind(&mgr, local_addr, cb3_big)) != NULL);
  mg_set_protocol_http_websocket(nc);

  /* Websocket request */
  ASSERT((nc = mg_connect(&mgr, local_addr, cb4_big)) != NULL);
  mg_set_protocol_http_websocket(nc);
  params.size = 8192;
  nc->user_data = &params;
  params.buf[0] = '\0';
  mg_send_websocket_handshake(nc, "/ws", NULL);
  poll_until(&mgr, 1, c_str_ne, params.buf, (void *) "");

  /* Check that test buffer has been filled by the callback properly. */
  ASSERT_STREQ(buf, "success");

  /* Websocket request */
  ASSERT((nc = mg_connect(&mgr, local_addr, cb4_big)) != NULL);
  mg_set_protocol_http_websocket(nc);
  params.size = 65535;
  nc->user_data = &params;
  params.buf[0] = '\0';
  mg_send_websocket_handshake(nc, "/ws", NULL);
  poll_until(&mgr, 1, c_str_ne, params.buf, (void *) "");
  mg_mgr_free(&mgr);

  /* Check that test buffer has been filled by the callback properly. */
  ASSERT_STREQ(buf, "success");

  return NULL;
}

static const char *test_mqtt_handshake(void) {
  struct mg_send_mqtt_handshake_opts opts;
  struct mg_connection *nc = create_test_connection();
  const char *client_id = "testclient";
  const char *user_name = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
  const char *password = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
  const char *will_topic = "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC";
  const char *will_message = "DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD";
  double before = mg_time();
  const char *got;

  msleep(2);
  mg_set_protocol_mqtt(nc);
  mg_send_mqtt_handshake(nc, client_id);
  got = nc->send_mbuf.buf;
  /* handshake header + keepalive + client id len + client id */
  ASSERT_EQ(nc->send_mbuf.len,
            1 + 1 + 1 + 5 + 1 + 1 + 2 + 2 + strlen(client_id));
  ASSERT_EQ(got[0], 0x10);
  ASSERT_EQ(got[1], (int) nc->send_mbuf.len - 2);
  ASSERT_EQ(got[2], 0);
  ASSERT_EQ(got[3], 4);
  ASSERT_STREQ_NZ(&got[4], "MQTT");
  ASSERT_EQ(got[8], 4);
  ASSERT_EQ(got[9], 0); /* connect flags */
  ASSERT_EQ(got[10], 0);
  ASSERT_EQ(got[11], 60);
  ASSERT_EQ(got[12], 0);
  ASSERT_EQ(got[13], (char) strlen(client_id));
  ASSERT_EQ(strncmp(&got[14], client_id, strlen(client_id)), 0);
  ASSERT_EQ(((struct mg_mqtt_proto_data *) nc->proto_data)->keep_alive, 60);
  ASSERT_GT(((struct mg_mqtt_proto_data *) nc->proto_data)->last_control_time,
            before);
  mbuf_remove(&nc->send_mbuf, nc->send_mbuf.len);

  memset(&opts, 0, sizeof(opts));
  before = mg_time();
  msleep(2);
  opts.will_topic = will_topic;
  opts.will_message = will_message;
  opts.user_name = user_name;
  opts.password = password;
  opts.keep_alive = 0x1234;
  mg_send_mqtt_handshake_opt(nc, client_id, opts);
  got = nc->send_mbuf.buf;
  /* handshake header + keepalive + client id len + client id */
  ASSERT_EQ(nc->send_mbuf.len, 1 + 2 + 1 + 5 + 1 + 1 + 2 + 2 +
                                   strlen(client_id) + 2 + strlen(will_topic) +
                                   2 + strlen(will_message) + 2 +
                                   strlen(user_name) + 2 + strlen(password));
  ASSERT_EQ(got[0], 0x10);
  ASSERT_EQ(((unsigned char *) got)[1], 223);
  ASSERT_EQ(got[2], 1);
  ASSERT_EQ(got[3], 0);
  ASSERT_EQ(got[4], 4);
  ASSERT_STREQ_NZ(&got[5], "MQTT");
  ASSERT_EQ(got[9], 4);
  ASSERT_EQ(((unsigned char *) got)[10], 0xc4); /* connect flags */
  ASSERT_EQ(got[11], 0x12);
  ASSERT_EQ(got[12], 0x34);
  ASSERT_EQ(got[13], 0);
  ASSERT_EQ(got[14], (char) strlen(client_id));
  ASSERT_EQ(strncmp(&got[15], client_id, strlen(client_id)), 0);
  ASSERT_EQ(((struct mg_mqtt_proto_data *) nc->proto_data)->keep_alive, 0x1234);
  ASSERT_GT(((struct mg_mqtt_proto_data *) nc->proto_data)->last_control_time,
            before);
  mbuf_remove(&nc->send_mbuf, nc->send_mbuf.len);

  destroy_test_connection(nc);
  return NULL;
}

static const char *test_mqtt_publish(void) {
  struct mg_connection *nc = create_test_connection();
  const double before = mg_time();
  char data[] = "dummy";
  const char *got;

  mg_set_protocol_mqtt(nc);
  msleep(2);
  mg_mqtt_publish(nc, "/test", 42, MG_MQTT_QOS(1) | MG_MQTT_RETAIN, data,
                  sizeof(data));
  got = nc->send_mbuf.buf;
  ASSERT_EQ(nc->send_mbuf.len, 17);

  ASSERT(got[0] & MG_MQTT_RETAIN);
  ASSERT_EQ((got[0] & 0xf0), (MG_MQTT_CMD_PUBLISH << 4));
  ASSERT_EQ(MG_MQTT_GET_QOS(got[0]), 1);
  ASSERT_EQ((size_t) got[1], (nc->send_mbuf.len - 2));

  ASSERT_EQ(got[2], 0);
  ASSERT_EQ(got[3], 5);
  ASSERT_STREQ_NZ(&got[4], "/test");

  ASSERT_EQ(got[9], 0);
  ASSERT_EQ(got[10], 42);

  ASSERT_EQ(strncmp(&got[11], data, sizeof(data)), 0);

  ASSERT_GT(((struct mg_mqtt_proto_data *) nc->proto_data)->last_control_time,
            before);

  destroy_test_connection(nc);
  return NULL;
}

static const char *test_mqtt_subscribe(void) {
  struct mg_connection *nc = create_test_connection();
  const double before = mg_time();
  const int qos = 1;
  const char *got;
  struct mg_mqtt_topic_expression topic_expressions[] = {{"/stuff", qos}};
  mg_set_protocol_mqtt(nc);
  msleep(2);

  mg_mqtt_subscribe(nc, topic_expressions, 1, 42);
  got = nc->send_mbuf.buf;
  ASSERT_EQ(nc->send_mbuf.len, 13);
  ASSERT_EQ((got[0] & 0xf0), (MG_MQTT_CMD_SUBSCRIBE << 4));
  ASSERT_EQ((size_t) got[1], (nc->send_mbuf.len - 2));
  ASSERT_EQ(got[2], 0);
  ASSERT_EQ(got[3], 42);

  ASSERT_EQ(got[4], 0);
  ASSERT_EQ(got[5], 6);
  ASSERT_STREQ_NZ(&got[6], "/stuff");
  ASSERT_EQ(got[12], qos);

  ASSERT_GT(((struct mg_mqtt_proto_data *) nc->proto_data)->last_control_time,
            before);

  destroy_test_connection(nc);
  return NULL;
}

static const char *test_mqtt_unsubscribe(void) {
  struct mg_connection *nc = create_test_connection();
  char *topics[] = {(char *) "/stuff"};
  const double before = mg_time();
  const char *got;
  mg_set_protocol_mqtt(nc);
  msleep(2);

  mg_mqtt_unsubscribe(nc, topics, 1, 42);
  got = nc->send_mbuf.buf;
  ASSERT_EQ(nc->send_mbuf.len, 12);
  ASSERT_EQ((got[0] & 0xf0), (MG_MQTT_CMD_UNSUBSCRIBE << 4));
  ASSERT_EQ((size_t) got[1], (nc->send_mbuf.len - 2));
  ASSERT_EQ(got[2], 0);
  ASSERT_EQ(got[3], 42);

  ASSERT_EQ(got[4], 0);
  ASSERT_EQ(got[5], 6);
  ASSERT_STREQ_NZ(&got[6], "/stuff");

  ASSERT_GT(((struct mg_mqtt_proto_data *) nc->proto_data)->last_control_time,
            before);

  destroy_test_connection(nc);
  return NULL;
}

static const char *test_mqtt_connack(void) {
  struct mg_connection *nc = create_test_connection();
  const char *got;
  mg_set_protocol_mqtt(nc);

  mg_mqtt_connack(nc, 42);
  got = nc->send_mbuf.buf;
  ASSERT(nc->send_mbuf.len > 0);
  ASSERT_EQ((got[0] & 0xf0), (MG_MQTT_CMD_CONNACK << 4));
  ASSERT_EQ((size_t) got[1], (nc->send_mbuf.len - 2));
  ASSERT_EQ(got[3], 42);

  destroy_test_connection(nc);
  return NULL;
}

static const char *test_mqtt_suback(void) {
  struct mg_connection *nc = create_test_connection();
  uint8_t qoss[] = {1};
  const char *got;
  mg_set_protocol_mqtt(nc);

  mg_mqtt_suback(nc, qoss, 1, 42);

  got = nc->send_mbuf.buf;
  ASSERT_EQ(nc->send_mbuf.len, 5);
  ASSERT_EQ((got[0] & 0xf0), (MG_MQTT_CMD_SUBACK << 4));
  ASSERT_EQ(MG_MQTT_GET_QOS(got[0]), 1);
  ASSERT_EQ((size_t) got[1], (nc->send_mbuf.len - 2));
  ASSERT_EQ(got[2], 0);
  ASSERT_EQ(got[3], 42);
  ASSERT_EQ(got[4], 1);

  destroy_test_connection(nc);
  return NULL;
}

static const char *test_mqtt_simple_acks(void) {
  unsigned long i;
  struct {
    uint8_t cmd;
    void (*f)(struct mg_connection *, uint16_t);
  } cases[] = {
      {MG_MQTT_CMD_PUBACK, mg_mqtt_puback},
      {MG_MQTT_CMD_PUBREC, mg_mqtt_pubrec},
      {MG_MQTT_CMD_PUBREL, mg_mqtt_pubrel},
      {MG_MQTT_CMD_PUBCOMP, mg_mqtt_pubcomp},
      {MG_MQTT_CMD_UNSUBACK, mg_mqtt_unsuback},
  };

  for (i = 0; i < ARRAY_SIZE(cases); i++) {
    struct mg_connection *nc = create_test_connection();
    const double before = mg_time();
    const char *got;
    mg_set_protocol_mqtt(nc);
    msleep(2);

    cases[i].f(nc, 42);

    got = nc->send_mbuf.buf;
    ASSERT_EQ(nc->send_mbuf.len, 4);
    ASSERT_EQ((got[0] & 0xf0), (cases[i].cmd << 4));
    if (cases[i].cmd == MG_MQTT_CMD_PUBREL) {
      ASSERT_EQ(got[0] & 0xf, 2);
    } else {
      ASSERT_EQ(got[0] & 0xf, 0);
    }
    ASSERT_EQ((size_t) got[1], (nc->send_mbuf.len - 2));
    ASSERT_EQ(got[2], 0);
    ASSERT_EQ(got[3], 42);
    ASSERT_GT(((struct mg_mqtt_proto_data *) nc->proto_data)->last_control_time,
              before);

    destroy_test_connection(nc);
  }
  return NULL;
}

static const char *test_mqtt_nullary(void) {
  unsigned long i;
  struct {
    uint8_t cmd;
    void (*f)(struct mg_connection *);
  } cases[] = {
      {MG_MQTT_CMD_PINGREQ, mg_mqtt_ping},
      {MG_MQTT_CMD_PINGRESP, mg_mqtt_pong},
      {MG_MQTT_CMD_DISCONNECT, mg_mqtt_disconnect},
  };

  for (i = 0; i < ARRAY_SIZE(cases); i++) {
    struct mg_connection *nc = create_test_connection();
    const double before = mg_time();
    struct mg_mqtt_message msg;
    const char *got;
    mg_set_protocol_mqtt(nc);
    msleep(2);

    cases[i].f(nc);

    got = nc->send_mbuf.buf;
    ASSERT_EQ(nc->send_mbuf.len, 2);
    ASSERT_EQ((got[0] & 0xf0), (cases[i].cmd << 4));
    ASSERT_EQ((size_t) got[1], (nc->send_mbuf.len - 2));
    ASSERT_EQ(parse_mqtt(&nc->send_mbuf, &msg), (int) nc->send_mbuf.len);
    ASSERT_GT(((struct mg_mqtt_proto_data *) nc->proto_data)->last_control_time,
              before);

    destroy_test_connection(nc);
  }
  return NULL;
}

static const size_t mqtt_long_payload_len = 200;
static const size_t mqtt_very_long_payload_len = 20000;

static void mqtt_eh(struct mg_connection *nc, int ev, void *ev_data) {
  int *check = (int *) nc->user_data;
  struct mg_mqtt_message *mm = (struct mg_mqtt_message *) ev_data;
  size_t i;
  (void) nc;
  (void) ev_data;

  switch (ev) {
    case MG_EV_MQTT_SUBACK:
      *check = 1;
      break;
    case MG_EV_MQTT_PUBLISH:
      *check = 0;
      if (mg_vcmp(&mm->topic, "/topic")) {
        *check = -1;
        break;
      }

      for (i = 0; i < mm->payload.len; i++) {
        if (nc->recv_mbuf.buf[10 + i] != 'A') {
          *check = -1;
          break;
        }
      }

      if (mm->payload.len == mqtt_long_payload_len) {
        *check = 2;
      } else if (mm->payload.len == mqtt_very_long_payload_len) {
        *check = 3;
      }
      break;
    case MG_EV_MQTT_CONNACK:
      *check = 4;
      break;
  }
}

static const char *test_mqtt_parse_mqtt(void) {
  struct mg_connection *nc = create_test_connection();
  char msg[] = {(char) (MG_MQTT_CMD_SUBACK << 4), 2, 1, 1};
  char *long_msg;
  int check = 0;
  int num_bytes = sizeof(msg), rest_len, i;

  nc->user_data = &check;
  nc->handler = mqtt_eh;
  mg_set_protocol_mqtt(nc);

  mbuf_append(&nc->recv_mbuf, msg, num_bytes);
  nc->proto_handler(nc, MG_EV_RECV, &num_bytes);

  ASSERT_EQ(check, 1);
  mbuf_free(&nc->recv_mbuf);

  /* test a payload whose length encodes as two bytes */
  rest_len = 2 + 6 + mqtt_long_payload_len;
  long_msg = (char *) malloc(512);
  long_msg[0] = (char) (MG_MQTT_CMD_PUBLISH << 4);
  long_msg[1] = (rest_len & 0x7f) | 0x80;
  long_msg[2] = rest_len >> 7;
  memcpy(&long_msg[3], "\0\006/topic", 8);
  memset(&long_msg[11], 'A', mqtt_long_payload_len);

  num_bytes = 3 + rest_len;
  mbuf_append(&nc->recv_mbuf, long_msg, num_bytes);

  /* Short read: handler is not run and data is not consumed */
  for (i = 0; i < num_bytes; i++) {
    check = 123;
    nc->recv_mbuf.len = i;
    nc->proto_handler(nc, MG_EV_RECV, &i);
    ASSERT_EQ((int) nc->recv_mbuf.len, i);
    ASSERT_EQ(check, 123);
  }

  nc->recv_mbuf.len = num_bytes;
  nc->proto_handler(nc, MG_EV_RECV, &num_bytes);
  ASSERT_EQ(check, 2);
  mbuf_free(&nc->recv_mbuf);
  free(long_msg);

  /* test a payload whose length encodes as two bytes */
  rest_len = 8 + mqtt_very_long_payload_len;
  long_msg = (char *) malloc(20100);
  long_msg[0] = (char) (MG_MQTT_CMD_PUBLISH << 4);
  long_msg[1] = (rest_len & 127) | 0x80;
  long_msg[2] = ((rest_len >> 7) & 127) | 0x80;
  long_msg[3] = (rest_len >> 14);
  memcpy(&long_msg[4], "\0\006/topic", 8);
  memset(&long_msg[12], 'A', mqtt_very_long_payload_len);

  num_bytes = 4 + rest_len;
  mbuf_append(&nc->recv_mbuf, long_msg, num_bytes);
  nc->proto_handler(nc, MG_EV_RECV, &num_bytes);

  ASSERT_EQ(check, 3);
  mbuf_remove(&nc->recv_mbuf, nc->recv_mbuf.len);

  /* Message length encodings > 4 bytes are not allowed by the standard,
   * connections should be closed immediately. */
  long_msg[0] = (char) (MG_MQTT_CMD_PUBLISH << 4);
  long_msg[1] = 0xff;
  long_msg[2] = 0xff;
  long_msg[3] = 0xff;
  long_msg[4] = 0xff;
  long_msg[5] = 0xff;
  mbuf_append(&nc->recv_mbuf, long_msg, 10);
  nc->proto_handler(nc, MG_EV_RECV, &num_bytes);
  ASSERT(nc->flags & MG_F_CLOSE_IMMEDIATELY);

  mbuf_free(&nc->recv_mbuf);
  free(long_msg);

  /* test encoding a large payload */
  long_msg = (char *) malloc(mqtt_very_long_payload_len);
  memset(long_msg, 'A', mqtt_very_long_payload_len);
  mg_mqtt_publish(nc, "/topic", 0, 0, long_msg, mqtt_very_long_payload_len);
  nc->recv_mbuf = nc->send_mbuf;
  mbuf_init(&nc->send_mbuf, 0);
  num_bytes = nc->recv_mbuf.len;
  nc->proto_handler(nc, MG_EV_RECV, &num_bytes);

  ASSERT_EQ(check, 3);
  mbuf_free(&nc->recv_mbuf);
  free(long_msg);

  /* test connack parsing */
  mg_mqtt_connack(nc, 0);
  nc->recv_mbuf = nc->send_mbuf;
  mbuf_init(&nc->send_mbuf, 0);
  num_bytes = 4;
  nc->proto_handler(nc, MG_EV_RECV, &num_bytes);

  ASSERT_EQ(check, 4);
  mbuf_free(&nc->recv_mbuf);

  destroy_test_connection(nc);
  return NULL;
}

static const char *test_mqtt_parse_mqtt_qos1(void) {
  /* clang-format off */
  struct mg_mqtt_message mm;
  char msg_qos1[] = {
      ((MG_MQTT_CMD_PUBLISH << 4) | (1 << 1)),
      2 + 6 + 2 + 7,
      0, 6, '/', 't', 'o', 'p', 'i', 'c',
      0x12, 0x34,
      'p', 'a', 'y', 'l', 'o', 'a', 'd',
  };
  /* clang-format on */
  struct mbuf mb;
  memset(&mm, 0, sizeof(mm));
  mbuf_init(&mb, 0);
  mbuf_append(&mb, msg_qos1, sizeof(msg_qos1));
  ASSERT_EQ(parse_mqtt(&mb, &mm), (int) sizeof(msg_qos1));
  ASSERT_EQ(mm.cmd, MG_MQTT_CMD_PUBLISH);
  ASSERT_EQ(mm.qos, 1);
  ASSERT_EQ(mm.message_id, 0x1234);
  ASSERT_EQ(mm.topic.len, 6);
  ASSERT_STREQ_NZ(mm.topic.p, "/topic");
  ASSERT_EQ(mm.payload.len, 7);
  ASSERT_STREQ_NZ(mm.payload.p, "payload");
  mbuf_free(&mb);
  return NULL;
}

static const char *test_mqtt_match_topic_expression(void) {
  ASSERT_EQ(mg_mqtt_vmatch_topic_expression("foo", mg_mk_str("foo")), 1);
  ASSERT_EQ(mg_mqtt_vmatch_topic_expression("foo", mg_mk_str("foo/")), 0);
  ASSERT_EQ(mg_mqtt_vmatch_topic_expression("foo", mg_mk_str("foo/bar")), 0);

  ASSERT_EQ(mg_mqtt_vmatch_topic_expression("foo/#", mg_mk_str("foo")), 0);
  ASSERT_EQ(mg_mqtt_vmatch_topic_expression("foo/#", mg_mk_str("foo/")), 0);
  ASSERT_EQ(mg_mqtt_vmatch_topic_expression("foo/#", mg_mk_str("foo/bar")), 1);
  ASSERT_EQ(mg_mqtt_vmatch_topic_expression("foo/#", mg_mk_str("foo/bar/baz")),
            1);

  return NULL;
}

#if MG_ENABLE_MQTT_BROKER
struct mg_mqtt_topic_expression brk_test_te[] = {{"/dummy", 0}, {"/unit/#", 0}};

static void brk_cln_cb1(struct mg_connection *nc, int ev, void *p) {
  struct mg_mqtt_message *msg = (struct mg_mqtt_message *) p;

  switch (ev) {
    case MG_EV_CONNECT:
      mg_set_protocol_mqtt(nc);
      mg_send_mqtt_handshake(nc, "dummy");
      break;
    case MG_EV_MQTT_CONNACK:
      mg_mqtt_subscribe(nc, brk_test_te, ARRAY_SIZE(brk_test_te), 42);
      break;
    case MG_EV_MQTT_SUBACK:
      mg_mqtt_publish(nc, "/unit/test", 0, MG_MQTT_QOS(0), "payload", 7);
      break;
    case MG_EV_MQTT_PUBLISH:
      if (mg_vcmp(&msg->topic, "/unit/test") == 0 && msg->payload.len == 7 &&
          mg_vcmp(&msg->payload, "payload") == 0) {
        *(int *) nc->user_data = 1;
      }
      break;
  }
}

static void mqtt_dummy_eh(struct mg_connection *nc, int ev, void *ev_data) {
  (void) nc;
  (void) ev;
  (void) ev_data;
}

static const char *test_mqtt_client_keep_alive(void) {
  struct mg_send_mqtt_handshake_opts opts;
  struct mg_connection *nc = create_test_connection();
  struct mg_mqtt_proto_data *pd;
  double before = mg_time();
  const char *got;
  mg_set_protocol_mqtt(nc);
  pd = (struct mg_mqtt_proto_data *) nc->proto_data;
  nc->handler = mqtt_dummy_eh;

  memset(&opts, 0, sizeof(opts));
  before = mg_time();
  msleep(2);
  opts.keep_alive = 2;
  mg_send_mqtt_handshake_opt(nc, "testclient", opts);
  ASSERT_EQ(pd->keep_alive, 2);
  ASSERT_GT(pd->last_control_time, before);
  mbuf_remove(&nc->send_mbuf, nc->send_mbuf.len); /* Remove the CONNECT. */

  before = pd->last_control_time;
  msleep(2);
  nc->proto_handler(nc, MG_EV_POLL, NULL);
  ASSERT_EQ(nc->send_mbuf.len, 0);
  ASSERT_EQ(pd->last_control_time, before);
  sleep(1);
  nc->proto_handler(nc, MG_EV_POLL, NULL);
  ASSERT_EQ(nc->send_mbuf.len, 0); /* Not yet */
  ASSERT_EQ(pd->last_control_time, before);
  sleep(1);
  msleep(2);
  nc->proto_handler(nc, MG_EV_POLL, NULL);
  ASSERT_GT(nc->send_mbuf.len, 0); /* PINGREQ sent */
  got = nc->send_mbuf.buf;
  ASSERT_EQ(nc->send_mbuf.len, 2);
  ASSERT_EQ((got[0] & 0xf0), (MG_MQTT_CMD_PINGREQ << 4));
  ASSERT_GT(pd->last_control_time, before); /* Resets itself */

  destroy_test_connection(nc);
  return NULL;
}

static const char *test_mqtt_broker(void) {
  struct mg_mgr mgr;
  struct mg_mqtt_broker brk;
  struct mg_connection *brk_nc;
  struct mg_connection *cln_nc;
  const char *brk_local_addr = "127.0.0.1:7777";
  int brk_data = 0, cln_data = 0;

  mg_mgr_init(&mgr, NULL);
  mg_mqtt_broker_init(&brk, &brk_data);

  ASSERT((brk_nc = mg_bind(&mgr, brk_local_addr, mg_mqtt_broker)) != NULL);
  brk_nc->priv_2 = &brk;

  ASSERT((cln_nc = mg_connect(&mgr, brk_local_addr, brk_cln_cb1)) != NULL);
  cln_nc->user_data = &cln_data;

  /* Run event loop. Use more cycles to let client and broker communicate. */
  poll_until(&mgr, 1, c_int_eq, &cln_data, (void *) 1);

  ASSERT_EQ(cln_data, 1);

  mg_mgr_free(&mgr);

  return NULL;
}
#endif /* MG_ENABLE_MQTT_BROKER */

static void cb5(struct mg_connection *nc, int ev, void *ev_data) {
  switch (ev) {
    case MG_EV_CONNECT:
      sprintf((char *) nc->user_data, "%d", *(int *) ev_data);
      break;
    default:
      break;
  }
}

static const char *test_connect_fail(void) {
  struct mg_mgr mgr;
  struct mg_connection *nc;
  char buf[100] = "0";

  mg_mgr_init(&mgr, NULL);
  ASSERT((nc = mg_connect(&mgr, "127.0.0.1:33211", cb5)) != NULL);
  nc->user_data = buf;
  poll_until(&mgr, 1, c_str_ne, buf, (void *) "0");
  mg_mgr_free(&mgr);

/* printf("failed connect status: [%s]\n", buf); */
/* TODO(lsm): fix this for Win32 */
#ifndef _WIN32
  ASSERT(strcmp(buf, "0") != 0);
#endif

  return NULL;
}

static void cb6(struct mg_connection *nc, int ev, void *ev_data) {
  (void) ev;
  (void) ev_data;
  nc->flags |= MG_F_USER_4;
  nc->flags |= MG_F_WANT_READ; /* Should not be allowed. */
}

static const char *test_connect_opts(void) {
  struct mg_mgr mgr;
  struct mg_connection *nc;
  struct mg_connect_opts opts;

  memset(&opts, 0, sizeof(opts));
  opts.user_data = (void *) 0xdeadbeef;
  opts.flags = MG_F_USER_6;
  opts.flags |= MG_F_WANT_READ; /* Should not be allowed. */

  mg_mgr_init(&mgr, NULL);
  ASSERT((nc = mg_connect_opt(&mgr, "127.0.0.1:33211", cb6, opts)) != NULL);
  ASSERT(nc->user_data == (void *) 0xdeadbeef);
  ASSERT(nc->flags & MG_F_USER_6);
  ASSERT(!(nc->flags & MG_F_WANT_READ));
  /* TODO(rojer): find a way to test this w/o touching nc (already freed).
    poll_mgr(&mgr, 25);
    ASSERT(nc->flags & MG_F_USER_4);
    ASSERT(nc->flags & MG_F_USER_6);
    ASSERT(!(nc->flags & MG_F_WANT_READ));
  */
  mg_mgr_free(&mgr);
  return NULL;
}

static const char *test_connect_opts_error_string(void) {
  struct mg_mgr mgr;
  struct mg_connection *nc;
  struct mg_connect_opts opts;
  const char *error_string = NULL;

  memset(&opts, 0, sizeof(opts));
  opts.error_string = &error_string;

  mg_mgr_init(&mgr, NULL);
  ASSERT((nc = mg_connect_opt(&mgr, "127.0.0.1:65537", cb6, opts)) == NULL);
  ASSERT(error_string != NULL);
  ASSERT_STREQ(error_string, "cannot parse address");
  mg_mgr_free(&mgr);
  return NULL;
}

#ifndef NO_DNS_TEST
static const char *test_resolve(void) {
  char buf[20];

  ASSERT(mg_resolve("localhost", buf, sizeof(buf)) > 0);
  ASSERT_STREQ(buf, "127.0.0.1");

  ASSERT_EQ(mg_resolve("please_dont_name_a_host_like_ths", buf, sizeof(buf)),
            0);
  return NULL;
}
#endif

static const char *test_base64(void) {
  const char *cases[] = {"test", "longer string"};
  unsigned long i;
  char enc[8192];
  char dec[8192];

  for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
    mg_base64_encode((unsigned char *) cases[i], strlen(cases[i]), enc);
    mg_base64_decode((unsigned char *) enc, strlen(enc), dec);

    ASSERT_EQ(strcmp(cases[i], dec), 0);
  }

  ASSERT_EQ(mg_base64_decode((unsigned char *) "кю", 4, dec), 0);
  ASSERT_EQ(mg_base64_decode((unsigned char *) "AAAA----", 8, dec), 4);
  ASSERT_EQ(mg_base64_decode((unsigned char *) "Q2VzYW50YQ==", 12, dec), 12);
  ASSERT_STREQ(dec, "Cesanta");

  return NULL;
}

static const char *test_sock_addr_to_str(void) {
  char buf[60];
  buf[0] = '\0';
  {
    union socket_address a4;
    memset(&a4, 0, sizeof(a4));
    a4.sa.sa_family = AF_INET;
    a4.sin.sin_addr.s_addr = inet_addr("127.0.0.1");
    a4.sin.sin_port = htons(12345);
    mg_sock_addr_to_str(&a4, buf, sizeof(buf), 0);
    ASSERT_STREQ(buf, "");
    mg_sock_addr_to_str(&a4, buf, sizeof(buf), MG_SOCK_STRINGIFY_IP);
    ASSERT_STREQ(buf, "127.0.0.1");
    mg_sock_addr_to_str(&a4, buf, sizeof(buf), MG_SOCK_STRINGIFY_PORT);
    ASSERT_STREQ(buf, "12345");
    mg_sock_addr_to_str(&a4, buf, sizeof(buf),
                        MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT);
    ASSERT_STREQ(buf, "127.0.0.1:12345");
  }
#if MG_ENABLE_IPV6 && !defined(_WIN32)
  {
    union socket_address a6;
    memset(&a6, 0, sizeof(a6));
    a6.sa.sa_family = AF_INET6;
    ASSERT_EQ(inet_pton(AF_INET6, "2001::123", &a6.sin6.sin6_addr), 1);
    a6.sin6.sin6_port = htons(12345);
    mg_sock_addr_to_str(&a6, buf, sizeof(buf), 0);
    ASSERT_STREQ(buf, "");
    mg_sock_addr_to_str(&a6, buf, sizeof(buf), MG_SOCK_STRINGIFY_IP);
    ASSERT_STREQ(buf, "2001::123");
    mg_sock_addr_to_str(&a6, buf, sizeof(buf), MG_SOCK_STRINGIFY_PORT);
    ASSERT_STREQ(buf, "12345");
    mg_sock_addr_to_str(&a6, buf, sizeof(buf),
                        MG_SOCK_STRINGIFY_IP | MG_SOCK_STRINGIFY_PORT);
    ASSERT_STREQ(buf, "[2001::123]:12345");
  }
#endif
  return NULL;
}

static const char *test_hexdump(void) {
  const char *src = "\1\2\3\4abcd";
  char got[256];

  const char *want =
      "0000  01 02 03 04 61 62 63 64"
      "                          ....abcd\n";
  ASSERT_EQ(mg_hexdump(src, strlen(src), got, sizeof(got)), (int) strlen(want));
  ASSERT_STREQ(got, want);
  return NULL;
}

static const char *test_hexdump_file(void) {
  const char *path = "test_hexdump";
  char *data, *got;
  size_t size;
  FILE *fp;
  struct mg_connection *nc = create_test_connection();

  /* "In the GNU system, non-null pointers are printed as unsigned integers,
   * as if a `%#x' conversion were used. Null pointers print as `(nil)'.
   * (Pointers might print differently in other systems.)"
   * indeed it prints 0x0 on apple.
   */
  nc->user_data = (void *) 0xbeef;

  /* truncate file. open+O_TRUNC works on wine but not on real windows */
  fp = fopen(path, "w");
  ASSERT(fp != NULL);
  fclose(fp);

  mbuf_append(&nc->send_mbuf, "foo", 3);
  mbuf_append(&nc->recv_mbuf, "bar", 3);
  mg_hexdump_connection(nc, path, nc->send_mbuf.buf, 3, MG_EV_SEND);

  mbuf_free(&nc->send_mbuf);
  mbuf_free(&nc->recv_mbuf);
  free(nc);

  ASSERT((data = read_file(path, &size)) != NULL);
  unlink(path);

  got = data;
  while (got - data < (int) size && *got++ != ' ')
    ;
  size -= got - data;

  ASSERT(strstr(got,
                "0000  66 6f 6f                "
                "                         foo") != NULL);

  free(data);
  return NULL;
}

static const char *test_basic_auth_helpers() {
  struct mbuf buf;
  mbuf_init(&buf, 0);
  mg_basic_auth_header(mg_mk_str("foo"), mg_mk_str("bar"), &buf);
  ASSERT_EQ(buf.len, 35);
  ASSERT_EQ(memcmp(buf.buf, "Authorization: Basic Zm9vOmJhcg==\r\n", 35), 0);

  buf.len = 0;
  mg_basic_auth_header(mg_mk_str("foo:bar"), mg_mk_str_n(NULL, 0), &buf);
  ASSERT_EQ(buf.len, 35);
  ASSERT_EQ(memcmp(buf.buf, "Authorization: Basic Zm9vOmJhcg==\r\n", 35), 0);

  mbuf_free(&buf);

  {
    char user[256];
    char pass[256];
    size_t i;

    struct {
      const char *hdr;
      const char *user;
      const char *pass;
      int res;
    } cases[] = {
        {"Basic Zm9vOmJhcg==", "foo", "bar", 0},         /* foo:bar */
        {"Basic Zm9v", "foo", "", 0},                    /* foo */
        {"Basic Zm9vOmJhcjpiYXo=", "foo", "bar:baz", 0}, /* foo:bar:baz */
        {"Basic Zm9vOg==", "foo", "", 0},                /* foo: */
        {"Basic OmJhcg==", "", "", -1},                  /* :bar */
    };

    for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
      struct mg_str hdr = mg_mk_str(cases[i].hdr);
      int res;

      memset(user, 0, sizeof(user));
      memset(pass, 0, sizeof(pass));
      res = mg_parse_http_basic_auth(&hdr, user, sizeof(user), pass,
                                     sizeof(pass));
      ASSERT_EQ(res, cases[i].res);
      ASSERT_EQ(strcmp(user, cases[i].user), 0);
      ASSERT_EQ(strcmp(pass, cases[i].pass), 0);
    }
  }
  return NULL;
}

static void test_auth_server_handler(struct mg_connection *nc, int ev,
                                     void *ev_data) {
  struct http_message *hm = (struct http_message *) ev_data;
  struct mg_str *hdr;
  char user[256] = "";
  char pass[256] = "";
  int res;

  switch (ev) {
    case MG_EV_WEBSOCKET_HANDSHAKE_REQUEST:
    case MG_EV_HTTP_REQUEST: {
      hdr = mg_get_http_header(hm, "Sec-WebSocket-Protocol");
      if (hdr != NULL) {
        if (mg_vcmp(hdr, "myproto") == 0) {
          *(int *) nc->user_data = 2;
          break;
        }
      }

      res = mg_get_http_basic_auth(hm, user, sizeof(user), pass, sizeof(pass));
      if (res == -1) {
        *(int *) nc->user_data = -1;
        break;
      }

      if (strcmp(user, "foo") == 0 && strcmp(pass, "bar") == 0) {
        *(int *) nc->user_data = 1;
      } else {
        *(int *) nc->user_data = -2;
      }

      break;
    }
  }
}

static void test_auth_client_handler(struct mg_connection *nc, int ev,
                                     void *ev_data) {
  (void) nc;
  (void) ev;
  (void) ev_data;
}

static const char *test_http_auth() {
  int connected;
  struct mg_connection *nc;
  struct mg_mgr mgr;
  mg_mgr_init(&mgr, NULL);

  nc = mg_bind(&mgr, "127.0.0.1:1234", test_auth_server_handler);
  mg_set_protocol_http_websocket(nc);
  nc->user_data = &connected;

  connected = 0;
  mg_connect_http(&mgr, test_auth_client_handler,
                  "http://foo:bar@127.0.0.1:1234", NULL, NULL);
  poll_until(&mgr, 4, c_int_eq, &connected, (void *) 1);
  ASSERT_EQ(connected, 1);

  connected = 0;
  mg_connect_ws(&mgr, test_auth_client_handler, "ws://foo:bar@127.0.0.1:1234",
                NULL, NULL);
  poll_until(&mgr, 4, c_int_eq, &connected, (void *) 1);
  ASSERT_EQ(connected, 1);

  /* check that we didn't break proto or other headers */
  connected = 0;
  mg_connect_ws(&mgr, test_auth_client_handler, "ws://127.0.0.1:1234",
                "myproto", NULL);
  poll_until(&mgr, 4, c_int_eq, &connected, (void *) 2);
  ASSERT_EQ(connected, 2);

  mg_mgr_free(&mgr);
  return NULL;
}

void tunnel_server_test_handler(struct mg_connection *nc, int ev,
                                void *ev_data) {
  (void) ev_data;
  switch (ev) {
    case MG_EV_HTTP_REQUEST:
      if (nc->user_data) break;

      nc->user_data = (void *) malloc(sizeof(int));
      (*(int *) nc->user_data) = 0;
      mg_set_timer(nc, mg_time() + 0.01);

      mg_printf(nc, "%s",
                "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n");
      break;
    case MG_EV_TIMER:
      mg_printf_http_chunk(nc, "%d", (*(int *) nc->user_data)++);
      if (*(int *) nc->user_data == 10) {
        mg_send_http_chunk(nc, "", 0);
        nc->flags |= MG_F_SEND_AND_CLOSE;
      } else {
        mg_set_timer(nc, mg_time() + 0.01);
      }
      break;
    case MG_EV_CLOSE:
      free(nc->user_data);
    default:
      break;
  }
}

void tunnel_client_test_handler(struct mg_connection *nc, int ev,
                                void *ev_data) {
  struct http_message *hm = (struct http_message *) ev_data;
  (void) nc;
  (void) ev_data;
  switch (ev) {
    case MG_EV_HTTP_REPLY:
      if (hm->body.len == 10 && strcmp(hm->body.p, "0123456789") == 0) {
        *(int *) nc->user_data = 1;
      } else {
        fprintf(stderr, "want: 10, got:%d \"%.*s\"\n", (int) hm->body.len,
                (int) hm->body.len, hm->body.p);
      }
      break;
    default:
      break;
  }
}

/*
 * NOTE(mkm): this test requires compiling the unit_test.c file with
 * //tuna/dispatcher.c .
 * Windows test runner doesn't use a makefile that's checked in
 * and I can't shave this yak now.
 * This doesn't mean the tunnel is not supposed to work on windows; there
 * is no fundamental reason it shouldn't, but obviously we should shave
 * the yak and make it testable there as well.
 */
#ifndef _WIN32
static const char *test_tunnel(void) {
  struct mg_connection *client, *server;
  struct mg_mgr mgr;
  int sentinel = 0;
  mg_mgr_init(&mgr, NULL);

  mg_tund_bind(&mgr, "localhost:4321");

  server = mg_bind(&mgr, "ws://localhost:4321", tunnel_server_test_handler);
  mg_set_protocol_http_websocket(server);
  /*
   * Connection happens only at the next poll (because we need to give the app
   * a chance to set MG_F_TUN_DO_NOT_RECONNECT), so let's poll once
   */
  mg_mgr_poll(&mgr, 1);
  mg_mgr_poll(&mgr, 1);
  client = mg_connect_http(&mgr, tunnel_client_test_handler,
                           "http://localhost:4321/foo", NULL, NULL);
  client->user_data = (void *) &sentinel;

  poll_until(&mgr, 1, c_int_eq, &sentinel, (void *) 1);
  ASSERT_EQ(sentinel, 1);

  mg_mgr_free(&mgr);
  return NULL;
}
#endif

static const char *test_http_chunk(void) {
  struct mg_connection nc;
  init_test_connection(&nc);

  mg_printf_http_chunk(&nc, "%d %s", 123, ":-)");
  ASSERT_EQ(nc.send_mbuf.len, 12);
  ASSERT_EQ(memcmp(nc.send_mbuf.buf, "7\r\n123 :-)\r\n", 12), 0);
  mbuf_free(&nc.send_mbuf);

  mg_send_http_chunk(&nc, "", 0);
  ASSERT_EQ(nc.send_mbuf.len, 5);
  ASSERT_EQ(memcmp(nc.send_mbuf.buf, "0\r\n\r\n", 3), 0);
  mbuf_free(&nc.send_mbuf);

  return NULL;
}

static int s_handle_chunk_event = 0;
static char s_events[100];

static void eh_chunk2(struct mg_connection *nc, int ev, void *ev_data) {
  if (ev == MG_EV_HTTP_CHUNK) {
    if (s_handle_chunk_event) {
      nc->flags |= MG_F_DELETE_CHUNK;
    }
  }
  snprintf(s_events + strlen(s_events), sizeof(s_events) - strlen(s_events),
           "_%d", ev);
  (void) ev_data;
}

static const char *test_http_extra_headers(void) {
  char buf[FETCH_BUF_SIZE];
  const char *expect = "MyHdr: my_val\r\n";
  s_http_server_opts.extra_headers = "MyHdr: my_val";
  fetch_http(buf, "%s", "GET / HTTP/1.0\r\n\r\n");
  ASSERT(strstr(buf, expect) != NULL);
  s_http_server_opts.extra_headers = NULL;
  return NULL;
}

static const char *test_http_not_modified(void) {
  struct http_message hm;
  cs_stat_t st;
  const char *req1 =
      "GET / HTTP/1.0\nIf-None-Match: \"0.123\"\n"
      "If-Modified-Since: Tue 17 Nov 2015 21:01:56 GMT\n\n";
  const char *req4 =
      "GET / HTTP/1.0\nIf-None-Match: \"0.7\"\n"
      "If-Modified-Since: Tue 17 Nov 2015 21:01:56 GMT\n\n";
  const char *req2 =
      "GET / HTTP/1.0\n"
      "If-Modified-Since: Tue 17 Nov 2015 21:01:56 GMT\n\n";
  const char *req3 = "GET / HTTP/1.0\n\n";

  st.st_size = 7;
  st.st_mtime = (time_t) 0;

  /* Not modified according to If-Modified-Since, but modified by Etag. */
  ASSERT(mg_parse_http(req1, strlen(req1), &hm, 1) > 0);
  ASSERT(mg_is_not_modified(&hm, &st) == 0);

  /* Not modified according to If-Modified-Since, and no Etag. */
  ASSERT(mg_parse_http(req2, strlen(req2), &hm, 1) > 0);
  ASSERT(mg_is_not_modified(&hm, &st) != 0);

  ASSERT(mg_parse_http(req3, strlen(req3), &hm, 1) > 0);
  ASSERT(mg_is_not_modified(&hm, &st) == 0);

  ASSERT(mg_parse_http(req4, strlen(req4), &hm, 1) > 0);
  ASSERT(mg_is_not_modified(&hm, &st) != 0);

  return NULL;
}

static const char *test_http_chunk2(void) {
  struct mg_connection nc;
  struct http_message hm;
  char buf[100] = "5\r\nhe";
  struct mg_mgr mgr;
  mg_mgr_init(&mgr, NULL);

  memset(&nc, 0, sizeof(nc));
  memset(&hm, 0, sizeof(hm));
  nc.mgr = &mgr;
  nc.sock = INVALID_SOCKET;
  nc.handler = eh_chunk2;
  hm.message.len = hm.body.len = ~0;

  s_handle_chunk_event = 0;
  ASSERT_EQ(mg_handle_chunked(&nc, &hm, buf, strlen(buf)), 0);

  /* Simulate arrival of chunks. MG_EV_HTTP_CHUNK events are not handled. */
  strcat(buf, "llo\r");
  ASSERT_EQ(mg_handle_chunked(&nc, &hm, buf, strlen(buf)), 0);
  ASSERT_STREQ(buf, "5\r\nhello\r");

  strcat(buf, "\n");
  ASSERT_EQ(mg_handle_chunked(&nc, &hm, buf, strlen(buf)), 5);
  ASSERT_STREQ(buf, "hello");

  s_handle_chunk_event = 1;
  strcat(buf, "3\r\n:-)\r\n");
  ASSERT_EQ(mg_handle_chunked(&nc, &hm, buf, strlen(buf)), 8);
  ASSERT_STREQ(buf, "");

  s_handle_chunk_event = 0;
  strcat(buf, "3\r\n...\r\na\r\n0123456789\r\n0\r");
  ASSERT_EQ(mg_handle_chunked(&nc, &hm, buf, strlen(buf)), 13);
  ASSERT_STREQ(buf, "...01234567890\r");
  ASSERT_EQ(hm.message.len, (size_t) ~0);

  strcat(buf, "\n\r\n");
  ASSERT_EQ(mg_handle_chunked(&nc, &hm, buf, strlen(buf)), 13);
  ASSERT_STREQ(buf, "...0123456789");
  ASSERT_EQ(hm.message.len, 13);

  ASSERT_STREQ(s_events, "_102_102_102_102");

  ASSERT(nc.proto_data != NULL);
  nc.proto_data_destructor(nc.proto_data);

  mg_mgr_free(&mgr);

  return NULL;
}

static const char *test_parse_date_string(void) {
  ASSERT_EQ(mg_parse_date_string("Sat, 31 Oct 2019 10:51:12 GMT"), 1572519072);
  ASSERT_EQ(mg_parse_date_string("31/Oct/2019 10:51:12 GMT"), 1572519072);
  ASSERT_EQ(mg_parse_date_string("31 Oct 2019 10:51:12 GMT"), 1572519072);
  ASSERT_EQ(mg_parse_date_string("31-Oct-2019 10:51:12 GMT"), 1572519072);
  return NULL;
}

#if MG_ENABLE_HTTP_STREAMING_MULTIPART
struct cb_mp_srv_data {
  struct mbuf res;
  int request_end;
};

static void cb_mp_srv(struct mg_connection *nc, int ev, void *p) {
  struct cb_mp_srv_data *mpd = (struct cb_mp_srv_data *) nc->user_data;
  struct mbuf *data = &mpd->res;
  if (nc->listener == NULL) return;

  switch (ev) {
    case MG_EV_HTTP_MULTIPART_REQUEST: {
      struct http_message *hm = (struct http_message *) p;
      mbuf_append(data, "<MPRQ", 5);
      mbuf_append(data, hm->uri.p, hm->uri.len);
      break;
    }
    case MG_EV_HTTP_PART_BEGIN: {
      struct mg_http_multipart_part *mp = (struct mg_http_multipart_part *) p;
      if (mp->status < 0) {
        mbuf_append(data, "ERROR", 5);
      } else {
        DBG(("BEGIN %s %s", mp->var_name, mp->file_name));
        mbuf_append(data, mp->var_name, strlen(mp->var_name));
        mbuf_append(data, mp->file_name, strlen(mp->file_name));
      }
      break;
    }
    case MG_EV_HTTP_PART_DATA: {
      struct mg_http_multipart_part *mp = (struct mg_http_multipart_part *) p;
      if (mp->status < 0) {
        mbuf_append(data, "ERROR", 5);
      } else {
        DBG(("DATA %d", (int) mp->data.len));
        mbuf_append(data, mp->data.p, mp->data.len);
      }
      break;
    }
    case MG_EV_HTTP_PART_END: {
      struct mg_http_multipart_part *mp = (struct mg_http_multipart_part *) p;
      struct mbuf *data = (struct mbuf *) nc->listener->user_data;
      mbuf_append(data, mp->var_name, strlen(mp->var_name));
      mbuf_append(data, mp->file_name, strlen(mp->file_name));
      if (mp->status < 0) {
        DBG(("PARTERR %s %s", mp->var_name, mp->file_name));
        mbuf_append(data, "ERROR", 5);
      } else {
        DBG(("END %s %s", mp->var_name, mp->file_name));
        mbuf_append(data, "FIN", 3);
      }
      break;
    }
    case MG_EV_HTTP_MULTIPART_REQUEST_END: {
      struct mg_http_multipart_part *mp = (struct mg_http_multipart_part *) p;
      DBG(("RQEND %p %p", (void *) mp->var_name, (void *) mp->file_name));
      mbuf_append(data, (mp->status < 0 ? "-" : "+"), 1);
      mbuf_append(data, (mp->var_name == NULL ? "1" : "0"), 1);
      mbuf_append(data, (mp->file_name == NULL ? "1" : "0"), 1);
      mbuf_append(data, "MPRQ>", 5);
      mpd->request_end = 1;
      break;
    }
  }
}

static void cb_mp_send_one_byte(struct mg_connection *nc, int ev, void *p) {
  static int i;
  (void) p;
  if (ev == MG_EV_POLL) {
    char ch = ((char *) nc->user_data)[i++];
    int l = strlen((char *) nc->user_data);
    if (ch != '\0') {
      mg_send(nc, &ch, 1);
      DBG(("%p sent %d of %d", (void *) nc, i, l));
    }
  }
}

static void cb_mp_empty(struct mg_connection *nc, int ev, void *p) {
  (void) nc;
  (void) ev;
  (void) p;
}

static const char b1[] =
    "111111111111111111111111111111111111111111111111111111111111111\r\n"
    "111111111111111111111111111111111111111111111111111111111111111\r\n"
    "111111111111111111111111111111111111111111111111111111111111111\r\n"
    "111111111111111111111111111111111111111111111111111111111111111\r\n"
    "111111111111111111111111111111111111111111111111111111111111111\r\n"
    "111111111111111111111111111111111111111111111111111111111111111\r\n"
    "111111111111111111111111111111111111111111111111111111111111111\r\n"
    "111111111111111111111111111111111111111111111111111111111111111\r\n"
    "111111111111111111111111111111111111111111111111111111111111111\r\n"
    "111111111111111111111111111111111111111111111111111111111111111";
static const char b2[] =
    "222222222222222222222222222222222222222222222222222222222222222\r\n"
    "222222222222222222222222222222222222222222222222222222222222222\r\n"
    "222222222222222222222222222222222222222222222222222222222222222\r\n"
    "222222222222222222222222222222222222222222222222222222222222222\r\n"
    "222222222222222222222222222222222222222222222222222222222222222\r\n"
    "222222222222222222222222222222222222222222222222222222222222222\r\n"
    "222222222222222222222222222222222222222222222222222222222222222\r\n"
    "222222222222222222222222222222222222222222222222222222222222222\r\n"
    "222222222222222222222222222222222222222222222222222222222222222\r\n"
    "222222222222222222222222222222222222222222222222222222222222222\r\n"
    "222222222222222222222222222222222222222222222222222222222222222\r\n";
static const char b4[] =
    "444444444444444444444444444444444444444444444444444444444444444\r\n"
    "444444444444444444444444444444444444444444444444444444444444444\r\n"
    "444444444444444444444444444444444444444444444444444444444444444\r\n"
    "444444444444444444444444444444444444444444444444444444444444444\r\n"
    "444444444444444444444444444444444444444444444444444444444444444\r\n"
    "444444444444444444444444444444444444444444444444444444444444444\r\n"
    "444444444444444444444444444444444444444444444444444444444444444\r\n"
    "444444444444444444444444444444444444444444444444444444444444444\r\n"
    "444444444444444444444444444444444444444444444444444444444444444\r\n"
    "444444444444444444444444444444444444444444444444444444444444444\r\n"
    "444444444444444444444444444444444444444444444444444444444444444";

static const char *test_http_multipart_check_res(struct mbuf *res) {
  const char *ptr = res->buf;
  ASSERT_STREQ_NZ(ptr, "<MPRQ/test");
  ptr += 10;
  ASSERT_STREQ_NZ(ptr, "afoo");
  ptr += 4;
  ASSERT_STREQ_NZ(ptr, b1);
  ptr += sizeof(b1) - 1;
  ASSERT_STREQ_NZ(ptr, "afooFIN");
  ptr += 7;
  /* No file_name for second part */
  ASSERT_STREQ_NZ(ptr, "b");
  ptr++;
  ASSERT_STREQ_NZ(ptr, b2);
  ptr += sizeof(b2) - 1;
  ASSERT_STREQ_NZ(ptr, "bFIN");
  ptr += 4;
  ASSERT_STREQ_NZ(ptr, "cbar");
  ptr += 4;
  ASSERT_STREQ_NZ(ptr, b4);
  ptr += sizeof(b4) - 1;
  ASSERT_STREQ_NZ(ptr, "cbarFIN");
  ptr += 7;
  ASSERT_STREQ_NZ(ptr, "+11MPRQ>");
  ptr += 8;
  ASSERT_EQ((size_t)(ptr - res->buf), res->len);
  return NULL;
}

static const char *test_http_multipart2(void) {
  struct mg_mgr mgr;
  struct mg_connection *nc_listen;

  const char multi_part_req_fmt[] =
      "%s"
      "Content-Disposition: form-data; name=\"a\"; filename=\"foo\"\r\n"
      "\r\n"
      "%s"
      "\r\n--Asrf456BGe4h\r\n"
      "Content-Disposition: form-data; name=\"b\"\r\n"
      "\r\n"
      "%s"
      "\r\n--Asrf456BGe4h\r\n"
      "Content-Disposition: form-data; name=\"c\"; filename=\"bar\"\r\n"
      "\r\n"
      "%s"
      "\r\n--Asrf456BGe4h--\r\n"
      "\r\n";

  char multi_part_req[1024 * 5];
  int i;
  struct mg_connection *c;
  const char *r;

  struct cb_mp_srv_data mpd;
  memset(&mpd, 0, sizeof(mpd));
  mbuf_init(&mpd.res, 0);

  mg_mgr_init(&mgr, NULL);
  nc_listen = mg_bind(&mgr, "8765", cb_mp_srv);
  nc_listen->user_data = &mpd;

  mg_set_protocol_http_websocket(nc_listen);

  snprintf(multi_part_req, sizeof(multi_part_req), multi_part_req_fmt, "", b1,
           b2, b4);

  ASSERT((c = mg_connect_http(&mgr, cb_mp_send_one_byte,
                              "http://127.0.0.1:8765/test",
                              "Content-Type: "
                              "multipart/form-data;boundary=Asrf456BGe4h",
                              "\r\n--Asrf456BGe4h\r\n")) != NULL);

  c->user_data = multi_part_req;

  poll_until(&mgr, 10, c_int_eq, &mpd.request_end, (void *) 1);

  if ((r = test_http_multipart_check_res(&mpd.res)) != NULL) return r;

  c->flags |= MG_F_CLOSE_IMMEDIATELY;
  mbuf_free(&mpd.res);
  memset(&mpd, 0, sizeof(mpd));
  mbuf_init(&mpd.res, 0);

  snprintf(multi_part_req, sizeof(multi_part_req), multi_part_req_fmt,
           "\r\n--Asrf456BGe4h\r\n", b1, b2, b4);

  /* Testing delivering to endpoint handler*/
  nc_listen = mg_bind(&mgr, "8766", cb_mp_empty);
  nc_listen->user_data = &mpd;

  mg_set_protocol_http_websocket(nc_listen);
  mg_register_http_endpoint(nc_listen, "/test", cb_mp_srv MG_UD_ARG(NULL));

  ASSERT((c = mg_connect_http(&mgr, cb_mp_empty, "http://127.0.0.1:8766/test",
                              "Connection: keep-alive\r\nContent-Type: "
                              "multipart/form-data; boundary=Asrf456BGe4h",
                              multi_part_req)) != NULL);

  poll_until(&mgr, 3, c_int_eq, &mpd.request_end, (void *) 1);
  mg_mgr_poll(&mgr, 1); /* Consume epilogue. */

  if ((r = test_http_multipart_check_res(&mpd.res)) != NULL) return r;

  /* Keep the connection alive */

  mbuf_free(&mpd.res);
  memset(&mpd, 0, sizeof(mpd));
  mbuf_init(&mpd.res, 0);

  mg_printf(c,
            "POST /test HTTP/1.1\r\nHost: 127.0.0.1:8766\r\nContent-Length: "
            "%" SIZE_T_FMT
            "\r\nConnection: keep-alive\r\nContent-Type: multipart/form-data; "
            "boundary=Asrf456BGe4h\r\n%s",
            strlen(multi_part_req), multi_part_req);

  poll_until(&mgr, 3, c_int_eq, &mpd.request_end, (void *) 1);

  if ((r = test_http_multipart_check_res(&mpd.res)) != NULL) return r;

  c->flags |= MG_F_CLOSE_IMMEDIATELY;

  mbuf_free(&mpd.res);
  memset(&mpd, 0, sizeof(mpd));
  mbuf_init(&mpd.res, 0);
  mbuf_remove(&c->recv_mbuf, c->recv_mbuf.len);

  /* Test interrupted request */
  multi_part_req[1800] = '\0';
  c = mg_connect_http(
      &mgr, cb_mp_empty, "http://127.0.0.1:8765",
      "Content-Type: multipart/form-data; boundary=Asrf456BGe4h",
      multi_part_req);

  ASSERT(c != NULL);
  for (i = 0; i < 20; i++) {
    mg_mgr_poll(&mgr, 1);
  }

  c->flags |= MG_F_CLOSE_IMMEDIATELY;

  poll_until(&mgr, 3, c_int_eq, &mpd.request_end, (void *) 1);

  ASSERT_STREQ_NZ(mpd.res.buf + mpd.res.len - 17, "cbarERROR-11MPRQ>");

  mbuf_free(&mpd.res);
  memset(&mpd, 0, sizeof(mpd));
  mbuf_init(&mpd.res, 0);

  ASSERT(
      mg_connect_http(
          &mgr, cb_mp_empty, "http://127.0.0.1:8766/test",
          "Content-Type: multipart/form-data; boundary=Asrf456BGe4h",
          "\r\n--Asrf456BGe4h\r\n"
          "Content-Disposition: form-data; name=\"d\"; filename=\"small\"\r\n"
          "\r\n"
          "boooo"
          "\r\n--Asrf456BGe4h--\r\n"
          "\r\n") != NULL);

  poll_until(&mgr, 3, c_int_eq, &mpd.request_end, (void *) 1);

  ASSERT(mpd.res.len != 0);
  ASSERT_STREQ_NZ(mpd.res.buf, "<MPRQ/testdsmallboooodsmallFIN+11MPRQ>");
  mbuf_free(&mpd.res);

  /*
   * Test malformed packet
   * See https://github.com/cesanta/dev/issues/6974
   * This request should not lead to crash
   */
  c = mg_connect(&mgr, "127.0.0.1:8766", cb_mp_empty);
  mg_printf(c,
            "POST /test HTTP/1.1\r\n"
            "Connection: keep-alive\r\n"
            "Content-Type: multipart/form-data; Content-Length: 1\r\n"
            "1\r\n\r\n");
  for (i = 0; i < 20; i++) {
    mg_mgr_poll(&mgr, 1);
  }

  mg_mgr_free(&mgr);

  return NULL;
}
#endif /* MG_ENABLE_HTTP_STREAMING_MULTIPART */

static const char *test_http_multipart(void) {
  struct http_message hm;
  char buf[FETCH_BUF_SIZE] = "", var_name[100], file_name[100];
  const char *chunk;
  size_t chunk_len, ofs;

  fetch_http(buf, "%s", "GET /data/multipart.txt HTTP/1.0\r\n\r\n");
  mg_parse_http(buf, strlen(buf), &hm, 1);

  ofs = mg_parse_multipart(hm.body.p, hm.body.len, var_name, sizeof(var_name),
                           file_name, sizeof(file_name), &chunk, &chunk_len);
  ASSERT(ofs < hm.body.len);
  ASSERT(ofs > 0);
  ASSERT_EQ(chunk_len, 10);
  ASSERT_EQ(memcmp(chunk, "file1 data", chunk_len), 0);

  ofs = mg_parse_multipart(hm.body.p + ofs, hm.body.len - ofs, var_name,
                           sizeof(var_name), file_name, sizeof(file_name),
                           &chunk, &chunk_len);
  ASSERT(ofs < hm.body.len);
  ASSERT(ofs > 0);
  ASSERT_EQ(chunk_len, 10);
  ASSERT_EQ(memcmp(chunk, "file2 data", chunk_len), 0);

  ofs = mg_parse_multipart(hm.body.p + ofs, hm.body.len - ofs, var_name,
                           sizeof(var_name), file_name, sizeof(file_name),
                           &chunk, &chunk_len);
  ASSERT(ofs == 0);

  return NULL;
}

static const char *test_dns_encode(void) {
  struct mg_connection nc;
  const char *got;
  int query_types[] = {MG_DNS_A_RECORD, MG_DNS_AAAA_RECORD};
  size_t i;
  init_test_connection(&nc);

  /*
   * Testing TCP encoding since when the connection
   * is in UDP mode the data is not stored in the send buffer.
   */

  for (i = 0; i < ARRAY_SIZE(query_types); i++) {
    mg_send_dns_query(&nc, "www.cesanta.com", query_types[i]);
    got = nc.send_mbuf.buf;
    ASSERT_EQ(nc.send_mbuf.len, 12 + 4 + 13 + 4 + 2);
    ASSERT_EQ(got[14], 3);
    ASSERT_STREQ_NZ(&got[15], "www");
    ASSERT_EQ(got[18], 7);
    ASSERT_STREQ_NZ(&got[19], "cesanta");
    ASSERT_EQ(got[26], 3);
    ASSERT_STREQ_NZ(&got[27], "com");
    ASSERT_EQ(got[30], 0);
    ASSERT_EQ(got[31], 0);
    ASSERT_EQ(got[32], query_types[i]);
    ASSERT_EQ(got[33], 0);
    ASSERT_EQ(got[34], 1);

    mbuf_free(&nc.send_mbuf);
  }
  return NULL;
}

static const char *test_dns_uncompress(void) {
  /*
   * Order or string constants is important. Names being uncompressed
   * must be before the end of the DNS packet end.
   */
  const char *s =
      "\03www\07cesanta\03com\0\03www\xc0\06\05dummy\07cesanta\3com\0";
  struct mg_dns_message msg;
  struct mg_str name = mg_mk_str(s);
  struct mg_str comp_name = mg_mk_str_n(s + 17, 6);
  struct mg_str n;
  char dst[256];
  int len;
  size_t i;

  const char *cases[] = {"www.cesanta.com", "www", "ww", "www.", "www.c"};

  memset(&msg, 0, sizeof(msg));
  msg.pkt.p = s + 23;
  msg.pkt.len = strlen(msg.pkt.p) + 1;

  memset(dst, 'X', sizeof(dst));
  n = mg_mk_str_n(msg.pkt.p, msg.pkt.len);
  len = mg_dns_uncompress_name(&msg, &n, dst, sizeof(dst));
  ASSERT_EQ(len, 17);
  ASSERT_EQ(len, (int) strlen(dst));
  ASSERT_STREQ_NZ(dst, "dummy.cesanta.com");

  for (i = 0; i < ARRAY_SIZE(cases); i++) {
    size_t l = strlen(cases[i]);
    memset(dst, 'X', sizeof(dst));
    len = mg_dns_uncompress_name(&msg, &name, dst, l);
    ASSERT_EQ(len, (int) l);
    ASSERT_EQ(strncmp(dst, cases[i], l), 0);
    ASSERT_EQ(dst[l], 'X');
  }

  /* if dst has enough space, check the trailing '\0' */
  memset(dst, 'X', sizeof(dst));
  len = mg_dns_uncompress_name(&msg, &name, dst, sizeof(dst));
  ASSERT_EQ(len, 15);
  ASSERT_EQ(len, (int) strlen(dst));
  ASSERT_STREQ_NZ(dst, "www.cesanta.com");
  ASSERT_EQ(dst[15], 0);

  /* check compressed name */
  memset(dst, 'X', sizeof(dst));
  len = mg_dns_uncompress_name(&msg, &comp_name, dst, sizeof(dst));
  printf("%d [%s]\n", len, dst);
  ASSERT_EQ(len, 15);
  ASSERT_EQ(len, (int) strlen(dst));
  ASSERT_STREQ_NZ(dst, "www.cesanta.com");
  ASSERT_EQ(dst[15], 0);

  /* check parsing loop avoidance */
  msg.pkt.p = "\05dummy\xc0\x06\0";
  msg.pkt.len = strlen(msg.pkt.p) + 1;
  memset(dst, 'X', sizeof(dst));
  len = mg_dns_uncompress_name(&msg, &comp_name, dst, sizeof(dst));
  ASSERT_EQ(len, 0);

  return NULL;
}

static const char *test_dns_decode(void) {
  struct mg_dns_message msg;
  char name[256];
  const char *hostname = "go.cesanta.com";
  const char *cname = "ghs.googlehosted.com";
  struct mg_dns_resource_record *r;
  uint16_t tiny;
  struct in_addr ina;
  int n;

  /*
   * Response for a record A query host for `go.cesanta.com`.
   * The response contains two answers:
   *
   * CNAME go.cesanta.com -> ghs.googlehosted.com
   * A ghs.googlehosted.com -> 74.125.136.121
   *
   * Captured from a reply generated by Google DNS server (8.8.8.8)
   */
  const unsigned char pkt[] = {
      0xa1, 0x00, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
      0x02, 0x67, 0x6f, 0x07, 0x63, 0x65, 0x73, 0x61, 0x6e, 0x74, 0x61, 0x03,
      0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x05,
      0x00, 0x01, 0x00, 0x00, 0x09, 0x52, 0x00, 0x13, 0x03, 0x67, 0x68, 0x73,
      0x0c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x68, 0x6f, 0x73, 0x74, 0x65,
      0x64, 0xc0, 0x17, 0xc0, 0x2c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01,
      0x2b, 0x00, 0x04, 0x4a, 0x7d, 0x88, 0x79};

  ASSERT_EQ(mg_parse_dns((const char *) pkt, sizeof(pkt), &msg), 0);
  ASSERT_EQ(msg.num_questions, 1);
  ASSERT_EQ(msg.num_answers, 2);

  r = &msg.questions[0];
  ASSERT_EQ(mg_dns_uncompress_name(&msg, &r->name, name, sizeof(name)),
            strlen(hostname));
  ASSERT_EQ(strncmp(name, hostname, strlen(hostname)), 0);

  r = &msg.answers[0];
  ASSERT_EQ(mg_dns_uncompress_name(&msg, &r->name, name, sizeof(name)),
            strlen(hostname));
  ASSERT_EQ(strncmp(name, hostname, strlen(hostname)), 0);

  ASSERT_EQ(mg_dns_uncompress_name(&msg, &r->rdata, name, sizeof(name)),
            strlen(cname));
  ASSERT_EQ(strncmp(name, cname, strlen(cname)), 0);

  r = &msg.answers[1];
  ASSERT_EQ(mg_dns_uncompress_name(&msg, &r->name, name, sizeof(name)),
            strlen(cname));
  ASSERT_EQ(strncmp(name, cname, strlen(cname)), 0);
  ASSERT_EQ(mg_dns_parse_record_data(&msg, r, &tiny, sizeof(tiny)), -1);
  ASSERT_EQ(mg_dns_parse_record_data(&msg, r, &ina, sizeof(ina)), 0);
  ASSERT_EQ(ina.s_addr, inet_addr("74.125.136.121"));

  /* Test iteration */
  n = 0;
  r = NULL;
  while ((r = mg_dns_next_record(&msg, MG_DNS_A_RECORD, r))) {
    n++;
  }
  ASSERT_EQ(n, 1);

  n = 0;
  r = NULL;
  while ((r = mg_dns_next_record(&msg, MG_DNS_CNAME_RECORD, r))) {
    n++;
  }
  ASSERT_EQ(n, 1);

  /* Test unknown record type */
  r = mg_dns_next_record(&msg, MG_DNS_A_RECORD, r);
  r->rtype = 0xff;
  ASSERT_EQ(mg_dns_parse_record_data(&msg, r, &ina, sizeof(ina)), -1);

  return NULL;
}

static const char *test_dns_decode_truncated(void) {
  struct mg_dns_message msg;
  char name[256];
  const char *hostname = "go.cesanta.com";
  const char *cname = "ghs.googlehosted.com";
  struct mg_dns_resource_record *r;
  uint16_t tiny;
  struct in_addr ina;
  int n;
  int i;

  const unsigned char src[] = {
      0xa1, 0x00, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00,
      0x02, 0x67, 0x6f, 0x07, 0x63, 0x65, 0x73, 0x61, 0x6e, 0x74, 0x61, 0x03,
      0x63, 0x6f, 0x6d, 0x00, 0x00, 0x01, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x05,
      0x00, 0x01, 0x00, 0x00, 0x09, 0x52, 0x00, 0x13, 0x03, 0x67, 0x68, 0x73,
      0x0c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x68, 0x6f, 0x73, 0x74, 0x65,
      0x64, 0xc0, 0x17, 0xc0, 0x2c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01,
      0x2b, 0x00, 0x04, 0x4a, 0x7d, 0x88, 0x79};
  char *pkt = NULL;

#define WONDER(expr) \
  if (!(expr)) continue

  for (i = sizeof(src) - 1; i > 0; i--) {
    free(pkt);
    pkt = (char *) malloc(i);
    memcpy(pkt, src, i);

    WONDER(mg_parse_dns((const char *) pkt, i, &msg) == 0);
    WONDER(msg.num_questions == 1);
    WONDER(msg.num_answers == 2);

    r = &msg.questions[0];
    WONDER(mg_dns_uncompress_name(&msg, &r->name, name, sizeof(name)) ==
           strlen(hostname));
    WONDER(strncmp(name, hostname, strlen(hostname)) == 0);

    r = &msg.answers[0];
    WONDER(mg_dns_uncompress_name(&msg, &r->name, name, sizeof(name)) ==
           strlen(hostname));
    WONDER(strncmp(name, hostname, strlen(hostname)) == 0);

    WONDER(mg_dns_uncompress_name(&msg, &r->rdata, name, sizeof(name)) ==
           strlen(cname));
    WONDER(strncmp(name, cname, strlen(cname)) == 0);

    r = &msg.answers[1];
    WONDER(mg_dns_uncompress_name(&msg, &r->name, name, sizeof(name)) ==
           strlen(cname));
    WONDER(strncmp(name, cname, strlen(cname)) == 0);
    WONDER(mg_dns_parse_record_data(&msg, r, &tiny, sizeof(tiny)) == -1);
    WONDER(mg_dns_parse_record_data(&msg, r, &ina, sizeof(ina)) == 0);
    WONDER(ina.s_addr == inet_addr("74.125.136.121"));

    /* Test iteration */
    n = 0;
    r = NULL;
    while ((r = mg_dns_next_record(&msg, MG_DNS_A_RECORD, r))) {
      n++;
    }
    WONDER(n == 1);

    n = 0;
    r = NULL;
    while ((r = mg_dns_next_record(&msg, MG_DNS_CNAME_RECORD, r))) {
      n++;
    }
    WONDER(n == 1);

    /* Test unknown record type */
    r = mg_dns_next_record(&msg, MG_DNS_A_RECORD, r);
    WONDER(r != NULL);
    printf("GOT %p\n", (void *) r);
    r->rtype = 0xff;
    WONDER(mg_dns_parse_record_data(&msg, r, &ina, sizeof(ina)) == -1);

    ASSERT("Should have failed" == NULL);
  }
  free(pkt);
  return NULL;
}

static const char *check_www_cesanta_com_reply(const char *pkt, size_t len) {
  char name[256];

  in_addr_t addr = inet_addr("54.194.65.250");
  struct in_addr ina;
  struct mg_dns_message msg;

  memset(&msg, 0, sizeof(msg));
  ASSERT(mg_parse_dns(pkt, len, &msg) != -1);

  memset(name, 0, sizeof(name));
  ASSERT(mg_dns_uncompress_name(&msg, &msg.questions[0].name, name,
                                sizeof(name)) > 0);
  ASSERT_STREQ_NZ(name, "www.cesanta.com");
  memset(name, 0, sizeof(name));
  ASSERT(mg_dns_uncompress_name(&msg, &msg.answers[0].name, name,
                                sizeof(name)) > 0);
  ASSERT_STREQ_NZ(name, "www.cesanta.com");
  ASSERT_EQ(msg.answers[0].rtype, MG_DNS_CNAME_RECORD);
  memset(name, 0, sizeof(name));
  ASSERT(mg_dns_parse_record_data(&msg, &msg.answers[0], name, sizeof(name)) !=
         -1);
  ASSERT_STREQ_NZ(name, "cesanta.com");
  memset(name, 0, sizeof(name));
  ASSERT(mg_dns_uncompress_name(&msg, &msg.answers[1].name, name,
                                sizeof(name)) > 0);
  ASSERT_STREQ_NZ(name, "cesanta.com");

  ASSERT_EQ(msg.answers[1].rtype, MG_DNS_A_RECORD);
  ASSERT(mg_dns_parse_record_data(&msg, &msg.answers[1], &ina, sizeof(ina)) !=
         -1);
  ASSERT_EQ(ina.s_addr, addr);

  return NULL;
}

static const char *test_dns_reply_encode(void) {
  const char *err;
  struct mg_dns_message msg;
  struct mg_dns_resource_record *rr;
  char name[256];
  in_addr_t addr = inet_addr("54.194.65.250");
  struct mbuf pkt;
  struct mg_connection nc;

  mbuf_init(&pkt, 0);
  init_test_connection(&nc);

  /* create a fake query */

  mg_send_dns_query(&nc, "www.cesanta.com", MG_DNS_A_RECORD);
  /* remove message length from tcp buffer */
  mbuf_remove(&nc.send_mbuf, 2);

  ASSERT_EQ(mg_parse_dns(nc.send_mbuf.buf, nc.send_mbuf.len, &msg), 0);

  /* build an answer */

  msg.num_answers = 2;
  mg_dns_insert_header(&pkt, 0, &msg);
  mg_dns_copy_questions(&pkt, &msg);

  mg_dns_uncompress_name(&msg, &msg.questions[0].name, name, sizeof(name));

  rr = &msg.answers[0];
  *rr = msg.questions[0];
  rr->rtype = MG_DNS_CNAME_RECORD;
  rr->ttl = 3600;
  rr->kind = MG_DNS_ANSWER;
  ASSERT(mg_dns_encode_record(&pkt, rr, "www.cesanta.com", 15,
                              (void *) "cesanta.com", 11) != -1);

  rr = &msg.answers[1];
  *rr = msg.questions[0];
  rr->ttl = 3600;
  rr->kind = MG_DNS_ANSWER;
  ASSERT(mg_dns_encode_record(&pkt, rr, "cesanta.com", 11, &addr, 4) != -1);

  if ((err = check_www_cesanta_com_reply(pkt.buf, pkt.len)) != NULL) {
    return err;
  }

  mbuf_free(&pkt);
  mbuf_free(&nc.send_mbuf);
  return NULL;
}

#if MG_ENABLE_DNS_SERVER
static void dns_server_eh(struct mg_connection *nc, int ev, void *ev_data) {
  struct mg_dns_message *msg;
  struct mg_dns_resource_record *rr;
  struct mg_dns_reply reply;
  char name[512];
  int i;

  name[511] = 0;
  switch (ev) {
    case MG_DNS_MESSAGE:
      msg = (struct mg_dns_message *) ev_data;
      reply = mg_dns_create_reply(&nc->send_mbuf, msg);

      for (i = 0; i < msg->num_questions; i++) {
        rr = &msg->questions[i];
        if (rr->rtype == MG_DNS_A_RECORD) {
          mg_dns_uncompress_name(msg, &rr->name, name, sizeof(name) - 1);

          if (strcmp(name, "cesanta.com") == 0) {
            mg_dns_reply_record(&reply, rr, NULL, rr->rtype, 3600,
                                nc->user_data, 4);
          } else if (strcmp(name, "www.cesanta.com") == 0) {
            mg_dns_reply_record(&reply, rr, NULL, MG_DNS_CNAME_RECORD, 3600,
                                "cesanta.com", strlen("cesanta.com"));

            mg_dns_reply_record(&reply, rr, "cesanta.com", rr->rtype, 3600,
                                nc->user_data, 4);
          }
        }
      }

      /*
       * We don't set the error flag even if there were no answers
       * maching the MG_DNS_A_RECORD query type.
       * This indicates that we have (syntetic) answers for MG_DNS_A_RECORD.
       * See http://goo.gl/QWvufr for a distinction between NXDOMAIN and NODATA.
       */

      mg_dns_send_reply(nc, &reply);
      nc->flags |= MG_F_SEND_AND_CLOSE;
      break;
  }
}

static int check_record_name(struct mg_dns_message *msg, struct mg_str *n,
                             const char *want) {
  char name[512];
  if (mg_dns_uncompress_name(msg, n, name, sizeof(name)) == 0) {
    return 0;
  }
  return strncmp(name, want, sizeof(name)) == 0;
}

static const char *test_dns_server(void) {
  const char *err;
  struct mg_connection nc;
  struct mg_dns_message msg;
  in_addr_t addr = inet_addr("54.194.65.250");
  int ilen;

  init_test_connection(&nc);

  nc.handler = dns_server_eh;
  nc.user_data = &addr;
  mg_set_protocol_dns(&nc);

  mg_send_dns_query(&nc, "www.cesanta.com", MG_DNS_A_RECORD);

  nc.recv_mbuf = nc.send_mbuf;
  mbuf_init(&nc.send_mbuf, 0);

  ilen = nc.recv_mbuf.len;
  nc.proto_handler(&nc, MG_EV_RECV, &ilen);
  mbuf_free(&nc.recv_mbuf);
  /* remove message length from tcp buffer before manually checking */
  mbuf_remove(&nc.send_mbuf, 2);

  if ((err = check_www_cesanta_com_reply(nc.send_mbuf.buf, nc.send_mbuf.len)) !=
      NULL) {
    mbuf_free(&nc.send_mbuf);
    return err;
  }

  mbuf_free(&nc.send_mbuf);

  /* test mg_dns_reply_record */
  mg_send_dns_query(&nc, "cesanta.com", MG_DNS_A_RECORD);

  nc.recv_mbuf = nc.send_mbuf;
  mbuf_init(&nc.send_mbuf, 0);

  ilen = nc.recv_mbuf.len;
  nc.proto_handler(&nc, MG_EV_RECV, &ilen);
  /* remove message length from tcp buffer before manually checking */
  mbuf_remove(&nc.send_mbuf, 2);

  ASSERT(mg_parse_dns(nc.send_mbuf.buf, nc.send_mbuf.len, &msg) != -1);
  ASSERT_EQ(msg.num_answers, 1);
  ASSERT_EQ(msg.answers[0].rtype, MG_DNS_A_RECORD);
  ASSERT(check_record_name(&msg, &msg.answers[0].name, "cesanta.com"));

  mbuf_free(&nc.send_mbuf);
  mbuf_free(&nc.recv_mbuf);

  /* check malformed request error */
  memset(&msg, 0, sizeof(msg));
  ilen = 0;
  nc.proto_handler(&nc, MG_EV_RECV, &ilen);
  /* remove message length from tcp buffer before manually checking */
  mbuf_remove(&nc.send_mbuf, 2);

  ASSERT(mg_parse_dns(nc.send_mbuf.buf, nc.send_mbuf.len, &msg) != -1);
  ASSERT(msg.flags & 1);
  ASSERT_EQ(msg.num_questions, 0);
  ASSERT_EQ(msg.num_answers, 0);

  mbuf_free(&nc.recv_mbuf);
  mbuf_free(&nc.send_mbuf);
  return NULL;
}
#endif /* MG_ENABLE_DNS_SERVER */

static void dns_resolve_cb(struct mg_dns_message *msg, void *data,
                           enum mg_resolve_err e) {
  struct mg_dns_resource_record *rr;
  char cname[256];
  struct in_addr got_addr;
  in_addr_t want_addr = inet_addr("131.107.255.255");

  (void) e;
  rr = mg_dns_next_record(msg, MG_DNS_A_RECORD, NULL);
  if (rr != NULL) {
    mg_dns_parse_record_data(msg, rr, &got_addr, sizeof(got_addr));
  }

  rr = mg_dns_next_record(msg, MG_DNS_CNAME_RECORD, NULL);
  if (rr != NULL) {
    mg_dns_parse_record_data(msg, rr, cname, sizeof(cname));
  }

  /*
   * We saw cases when A query returns only A record, or A and CNAME records.
   * Expect A answer, and optionally CNAME answer.
   */
  if (want_addr == got_addr.s_addr || strcmp(cname, "dns.msftncsi.com") == 0) {
    *(int *) data = 1; /* Success */
  } else {
    *(int *) data = 2; /* Error */
  }
}

static const char *test_dns_resolve(void) {
  struct mg_mgr mgr;
  struct mg_mgr_init_opts opts;

  int data = 0;

  mg_mgr_init(&mgr, NULL);

  /* Microsoft promises dns.msftncsi.com is always 131.107.255.255 */
  mg_resolve_async(&mgr, "dns.msftncsi.com", MG_DNS_A_RECORD, dns_resolve_cb,
                   &data);

  poll_until(&mgr, 1, c_int_ne, &data, (void *) 0);
  ASSERT_EQ(data, 1);

  mg_mgr_free(&mgr);

  data = 0;
  memset(&opts, 0, sizeof(opts));
  opts.nameserver = "8.8.4.4";
  mg_mgr_init_opt(&mgr, NULL, opts);

  /* Microsoft promises dns.msftncsi.com is always 131.107.255.255 */
  mg_resolve_async(&mgr, "dns.msftncsi.com", MG_DNS_A_RECORD, dns_resolve_cb,
                   &data);

  poll_until(&mgr, 1, c_int_ne, &data, (void *) 0);
  ASSERT_EQ(data, 1);

  mg_mgr_free(&mgr);

  data = 0;
  mg_mgr_init(&mgr, NULL);
  mg_set_nameserver(&mgr, "8.8.4.4");

  /* Microsoft promises dns.msftncsi.com is always 131.107.255.255 */
  mg_resolve_async(&mgr, "dns.msftncsi.com", MG_DNS_A_RECORD, dns_resolve_cb,
                   &data);

  poll_until(&mgr, 1, c_int_ne, &data, (void *) 0);
  ASSERT_EQ(data, 1);

  mg_mgr_free(&mgr);

  return NULL;
}

static void dns_resolve_timeout_cb(struct mg_dns_message *msg, void *data,
                                   enum mg_resolve_err e) {
  (void) e;
  if (msg == NULL) {
    *(int *) data = 1;
  }
}

extern char mg_dns_server[256];

static const char *test_dns_resolve_timeout(void) {
  struct mg_mgr mgr;
  struct mg_resolve_async_opts opts;
  int data = 0;
  mg_mgr_init(&mgr, NULL);
  memset(&opts, 0, sizeof(opts));

  opts.nameserver = "7.7.7.7";
  opts.timeout = -1; /* 0 would be the default timeout */
  opts.max_retries = 1;
  mg_resolve_async_opt(&mgr, "www.cesanta.com", MG_DNS_A_RECORD,
                       dns_resolve_timeout_cb, &data, opts);

  poll_until(&mgr, 5, c_int_eq, &data, (void *) 1);
  ASSERT_EQ(data, 1);

  mg_mgr_free(&mgr);
  return NULL;
}

#ifndef NO_RESOLVE_HOSTS_TEST
static const char *test_dns_resolve_hosts(void) {
  union socket_address sa;
  in_addr_t want_addr = inet_addr("127.0.0.1");

  memset(&sa, 0, sizeof(sa));
  ASSERT_EQ(mg_resolve_from_hosts_file("localhost", &sa), 0);
  ASSERT_EQ(sa.sin.sin_addr.s_addr, want_addr);
  ASSERT_EQ(mg_resolve_from_hosts_file("does_not,exist!in_host*file", &sa), -1);

  return NULL;
}
#endif

static void ehb_srv(struct mg_connection *nc, int ev, void *p) {
  struct mbuf *io = &nc->recv_mbuf;
  (void) io;
  (void) p;

  if (ev == MG_EV_RECV) {
    if (*(int *) p == 1) (*(int *) nc->mgr->user_data)++;
    mbuf_remove(io, *(int *) p);
  }
}

static void ehb_srv2(struct mg_connection *c, int ev, void *p) {
  if (ev == MG_EV_CLOSE) {
    if (c->recv_mbuf.size == 1) (*(int *) c->mgr->user_data) = 1;
    (void) p;
  }
}

static const char *test_buffer_limit(void) {
  struct mg_mgr mgr;
  struct mg_connection *clnt, *srv;
  const char *address = "tcp://127.0.0.1:7878";
  int res = 0;

  mg_mgr_init(&mgr, &res);
  ASSERT((srv = mg_bind(&mgr, address, ehb_srv)) != NULL);
  srv->recv_mbuf_limit = 1;
  ASSERT((clnt = mg_connect(&mgr, address, NULL)) != NULL);
  mg_printf(clnt, "abcd");

  poll_until(&mgr, 1, c_int_eq, &res, (void *) 4);

  /* expect four single byte read events */
  ASSERT_EQ(res, 4);

  /*
   * Make sure we're not closing full non-drained connections without any
   * protocol handler
   */
  res = 0;
  srv->handler = ehb_srv2;
  ASSERT((clnt = mg_connect(&mgr, address, NULL)) != NULL);
  mg_printf(clnt, "abcd");
  poll_until(&mgr, 1, c_int_eq, &res, (void *) 1);
  ASSERT_EQ(res, 0);

  /*
   * Make sure we're closing full non-drained connections with mqtt protocol
   * handler
   */
  res = 0;
  srv->handler = ehb_srv2;
  mg_set_protocol_mqtt(srv);
  ASSERT((clnt = mg_connect(&mgr, address, NULL)) != NULL);
  mg_printf(clnt, "abcd");
  poll_until(&mgr, 1, c_int_eq, &res, (void *) 1);
  ASSERT_EQ(res, 1);

  mg_mgr_free(&mgr);
  return NULL;
}

static const char *test_http_parse_header(void) {
  static struct mg_str h = MG_MK_STR(
      "xx=1 kl yy, ert=234 kl=123, "
      "uri=\"/?naii=x,y\";ii=\"12\\\"34\" zz='aa bb',tt=2,gf=\"xx d=1234");
  char buf[20];

  ASSERT_EQ(mg_http_parse_header(&h, "ert", buf, sizeof(buf)), 3);
  ASSERT_STREQ(buf, "234");
  ASSERT_EQ(mg_http_parse_header(&h, "ert", buf, 2), 0);
  ASSERT_EQ(mg_http_parse_header(&h, "ert", buf, 3), 0);
  ASSERT_EQ(mg_http_parse_header(&h, "ert", buf, 4), 3);
  ASSERT_EQ(mg_http_parse_header(&h, "gf", buf, sizeof(buf)), 0);
  ASSERT_EQ(mg_http_parse_header(&h, "zz", buf, sizeof(buf)), 5);
  ASSERT_STREQ(buf, "aa bb");
  ASSERT_EQ(mg_http_parse_header(&h, "d", buf, sizeof(buf)), 4);
  ASSERT_STREQ(buf, "1234");
  buf[0] = 'x';
  ASSERT_EQ(mg_http_parse_header(&h, "MMM", buf, sizeof(buf)), 0);
  ASSERT_EQ(buf[0], '\0');
  ASSERT_EQ(mg_http_parse_header(&h, "kl", buf, sizeof(buf)), 3);
  ASSERT_STREQ(buf, "123");
  ASSERT_EQ(mg_http_parse_header(&h, "xx", buf, sizeof(buf)), 1);
  ASSERT_STREQ(buf, "1");
  ASSERT_EQ(mg_http_parse_header(&h, "ii", buf, sizeof(buf)), 5);
  ASSERT_STREQ(buf, "12\"34");
  ASSERT_EQ(mg_http_parse_header(&h, "tt", buf, sizeof(buf)), 1);
  ASSERT_STREQ(buf, "2");
  ASSERT(mg_http_parse_header(&h, "uri", buf, sizeof(buf)) > 0);

  return NULL;
}

#if MG_ENABLE_COAP
struct results {
  int server;
  int client;
};

static void coap_handler_1(struct mg_connection *nc, int ev, void *p) {
  switch (ev) {
    case MG_EV_CONNECT: {
      struct mg_coap_message cm;
      memset(&cm, 0, sizeof(cm));
      cm.msg_id = 1;
      cm.msg_type = MG_COAP_MSG_CON;
      mg_coap_send_message(nc, &cm);
      break;
    }
    case MG_EV_COAP_ACK: {
      struct mg_coap_message *cm = (struct mg_coap_message *) p;
      ((struct results *) (nc->user_data))->client = cm->msg_id + cm->msg_type;
      break;
    }
    case MG_EV_COAP_CON: {
      struct mg_coap_message *cm = (struct mg_coap_message *) p;
      ((struct results *) (nc->user_data))->server = cm->msg_id + cm->msg_type;
      mg_coap_send_ack(nc, cm->msg_id);
      break;
    }
  }
}

static const char *test_coap(void) {
  struct mbuf packet_in, packet_out;
  struct mg_coap_message cm;
  uint32_t res;

  unsigned char coap_packet_1[] = {0x42, 0x01, 0xe9, 0x1b, 0x07, 0x90, 0xb8,
                                   0x73, 0x65, 0x70, 0x61, 0x72, 0x61, 0x74,
                                   0x65, 0x10, 0xd1, 0x23, 0x11};
  unsigned char coap_packet_2[] = {0x60, 0x00, 0xe9, 0x1b};
  unsigned char coap_packet_3[] = {
      0x42, 0x45, 0x57, 0x0f, 0x07, 0x90, 0xff, 0x54, 0x68, 0x69, 0x73, 0x20,
      0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x20, 0x77, 0x61, 0x73, 0x20,
      0x73, 0x65, 0x6e, 0x74, 0x20, 0x62, 0x79, 0x20, 0x61, 0x20, 0x73, 0x65,
      0x70, 0x61, 0x72, 0x61, 0x74, 0x65, 0x20, 0x72, 0x65, 0x73, 0x70, 0x6f,
      0x6e, 0x73, 0x65, 0x2e, 0x0a, 0x59, 0x6f, 0x75, 0x72, 0x20, 0x63, 0x6c,
      0x69, 0x65, 0x6e, 0x74, 0x20, 0x77, 0x69, 0x6c, 0x6c, 0x20, 0x6e, 0x65,
      0x65, 0x64, 0x20, 0x74, 0x6f, 0x20, 0x61, 0x63, 0x6b, 0x6e, 0x6f, 0x77,
      0x6c, 0x65, 0x64, 0x67, 0x65, 0x20, 0x69, 0x74, 0x2c, 0x20, 0x6f, 0x74,
      0x68, 0x65, 0x72, 0x77, 0x69, 0x73, 0x65, 0x20, 0x69, 0x74, 0x20, 0x77,
      0x69, 0x6c, 0x6c, 0x20, 0x62, 0x65, 0x20, 0x72, 0x65, 0x74, 0x72, 0x61,
      0x6e, 0x73, 0x6d, 0x69, 0x74, 0x74, 0x65, 0x64, 0x2e};
  unsigned char coap_packet_4[] = {0x60, 0x00, 0x57, 0x0f};
  unsigned char coap_packet_5[] = {
      0x40, 0x03, 0x95, 0x22, 0xb7, 0x73, 0x74, 0x6f, 0x72, 0x61,
      0x67, 0x65, 0x0a, 0x6d, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x75,
      0x72, 0x63, 0x65, 0xff, 0x6d, 0x79, 0x64, 0x61, 0x74, 0x61};
  unsigned char coap_packet_6[] = {0xFF, 0x00, 0xFF, 0x00};
  unsigned char coap_packet_7[] = {
      0x40, 0x03, 0x95, 0x22, 0xb7, 0x73, 0x74, 0x6f, 0x72, 0x61,
      0x67, 0x65, 0x0a, 0x6d, 0x79, 0x72, 0x65, 0x73, 0x6f, 0x75,
      0x72, 0x63, 0x65, 0xf1, 0x6d, 0x79, 0x64, 0x61, 0x74, 0x61};

  mbuf_init(&packet_in, 0);
  /* empty buf */
  res = mg_coap_parse(&packet_in, &cm);
  ASSERT((res & MG_COAP_NOT_ENOUGH_DATA) != 0);
  mg_coap_free_options(&cm);
  mbuf_free(&packet_in);

  mbuf_init(&packet_out, 0);
  /* ACK, MID: 59675, Empty Message */
  packet_in.buf = (char *) coap_packet_2;
  packet_in.len = sizeof(coap_packet_2);
  res = mg_coap_parse(&packet_in, &cm);
  ASSERT_EQ((res & MG_COAP_ERROR), 0);
  ASSERT_EQ(cm.code_class, 0);
  ASSERT_EQ(cm.code_detail, 0);
  ASSERT_EQ(cm.msg_id, 59675);
  ASSERT_EQ(cm.msg_type, MG_COAP_MSG_ACK);
  ASSERT(cm.options == NULL);
  ASSERT_EQ(cm.payload.len, 0);
  ASSERT(cm.payload.p == NULL);
  ASSERT_EQ(cm.token.len, 0);
  ASSERT(cm.token.p == NULL);
  res = mg_coap_compose(&cm, &packet_out);
  ASSERT_EQ(res, 0);
  ASSERT_EQ(packet_out.len, sizeof(coap_packet_2));
  ASSERT_EQ(memcmp(packet_out.buf, coap_packet_2, packet_out.len), 0);
  mg_coap_free_options(&cm);
  mbuf_free(&packet_out);

  /* ACK, MID: 22287, Empty Message */
  packet_in.buf = (char *) coap_packet_4;
  packet_in.len = sizeof(coap_packet_4);
  res = mg_coap_parse(&packet_in, &cm);
  ASSERT_EQ((res & MG_COAP_ERROR), 0);
  ASSERT_EQ(cm.code_class, 0);
  ASSERT_EQ(cm.code_detail, 0);
  ASSERT_EQ(cm.msg_id, 22287);
  ASSERT_EQ(cm.msg_type, MG_COAP_MSG_ACK);
  ASSERT(cm.options == NULL);
  ASSERT_EQ(cm.payload.len, 0);
  ASSERT(cm.payload.p == NULL);
  ASSERT_EQ(cm.token.len, 0);
  ASSERT(cm.token.p == NULL);
  res = mg_coap_compose(&cm, &packet_out);
  ASSERT_EQ(res, 0);
  ASSERT_EQ(packet_out.len, sizeof(coap_packet_4));
  ASSERT_EQ(memcmp(packet_out.buf, coap_packet_4, packet_out.len), 0);
  mg_coap_free_options(&cm);
  mbuf_free(&packet_out);

  /* CON, MID: 59675 ... */
  packet_in.buf = (char *) coap_packet_1;
  packet_in.len = sizeof(coap_packet_1);
  res = mg_coap_parse(&packet_in, &cm);
  ASSERT_EQ((res & MG_COAP_ERROR), 0);
  ASSERT_EQ(cm.code_class, 0);
  ASSERT_EQ(cm.code_detail, 1);
  ASSERT_EQ(cm.msg_id, 59675);
  ASSERT_EQ(cm.msg_type, MG_COAP_MSG_CON);
  ASSERT(cm.options != 0);
  ASSERT_EQ(cm.options->number, 11);
  ASSERT_EQ(cm.options->value.len, 8);
  ASSERT_STREQ_NZ(cm.options->value.p, "separate");
  ASSERT(cm.options->next != 0);
  ASSERT_EQ(cm.options->next->number, 12);
  ASSERT_EQ(cm.options->next->value.len, 0);
  ASSERT(cm.options->next->next != 0);
  ASSERT_EQ(cm.options->next->next->number, 60);
  ASSERT_EQ(cm.options->next->next->value.len, 1);
  ASSERT_EQ(*cm.options->next->next->value.p, 0x11);
  ASSERT(cm.options->next->next->next == NULL);
  ASSERT_EQ(cm.payload.len, 0);
  ASSERT(cm.payload.p == NULL);
  ASSERT_EQ(cm.token.len, 2);
  ASSERT_EQ(*cm.token.p, 0x07);
  ASSERT_EQ((unsigned char) *(cm.token.p + 1), 0x90);
  res = mg_coap_compose(&cm, &packet_out);
  ASSERT_EQ(res, 0);
  ASSERT_EQ(packet_out.len, sizeof(coap_packet_1));
  ASSERT_EQ(memcmp(packet_out.buf, coap_packet_1, packet_out.len), 0);
  mg_coap_free_options(&cm);
  mbuf_free(&packet_out);

  /* CON, MID: 22287 ... */
  packet_in.buf = (char *) coap_packet_3;
  packet_in.len = sizeof(coap_packet_3);
  res = mg_coap_parse(&packet_in, &cm);
  ASSERT_EQ((res & MG_COAP_ERROR), 0);
  ASSERT_EQ(cm.code_class, 2);
  ASSERT_EQ(cm.code_detail, 5);
  ASSERT_EQ(cm.msg_id, 22287);
  ASSERT_EQ(cm.msg_type, MG_COAP_MSG_CON);
  ASSERT(cm.options == NULL);
  ASSERT_EQ(cm.token.len, 2);
  ASSERT_EQ(*cm.token.p, 0x07);
  ASSERT_EQ((unsigned char) *(cm.token.p + 1), 0x90);
  ASSERT_EQ(cm.payload.len, 122);
  ASSERT(strncmp(cm.payload.p,
                 "This message was sent by a separate response.\n"
                 "Your client will need to acknowledge it,"
                 " otherwise it will be retransmitted.",
                 122) == 0);
  res = mg_coap_compose(&cm, &packet_out);
  ASSERT_EQ(res, 0);
  ASSERT_EQ(packet_out.len, sizeof(coap_packet_3));
  ASSERT_EQ(memcmp(packet_out.buf, coap_packet_3, packet_out.len), 0);
  mg_coap_free_options(&cm);
  mbuf_free(&packet_out);

  packet_in.buf = (char *) coap_packet_5;
  packet_in.len = sizeof(coap_packet_5);
  res = mg_coap_parse(&packet_in, &cm);
  ASSERT_EQ((res & MG_COAP_ERROR), 0);
  ASSERT_EQ(cm.code_class, 0);
  ASSERT_EQ(cm.code_detail, 3);
  ASSERT_EQ(cm.msg_id, 38178);
  ASSERT_EQ(cm.msg_type, MG_COAP_MSG_CON);
  ASSERT(cm.options != 0);
  ASSERT_EQ(cm.options->number, 11);
  ASSERT_EQ(cm.options->value.len, 7);
  ASSERT_STREQ_NZ(cm.options->value.p, "storage");
  ASSERT(cm.options->next != 0);
  ASSERT_EQ(cm.options->next->number, 11);
  ASSERT_EQ(cm.options->next->value.len, 10);
  ASSERT_STREQ_NZ(cm.options->next->value.p, "myresource");
  ASSERT(cm.options->next->next == NULL);
  ASSERT_EQ(cm.token.len, 0);
  ASSERT_EQ(cm.payload.len, 6);
  ASSERT_STREQ_NZ(cm.payload.p, "mydata");
  res = mg_coap_compose(&cm, &packet_out);
  ASSERT_EQ(res, 0);
  ASSERT_EQ(packet_out.len, sizeof(coap_packet_5));
  ASSERT_EQ(memcmp(packet_out.buf, coap_packet_5, packet_out.len), 0);
  mg_coap_free_options(&cm);
  mbuf_free(&packet_out);

  packet_in.buf = (char *) coap_packet_6;
  packet_in.len = sizeof(coap_packet_6);
  res = mg_coap_parse(&packet_in, &cm);
  ASSERT((res & MG_COAP_ERROR) != 0);
  mg_coap_free_options(&cm);

  packet_in.buf = (char *) coap_packet_7;
  packet_in.len = sizeof(coap_packet_7);
  res = mg_coap_parse(&packet_in, &cm);
  ASSERT((res & MG_COAP_ERROR) != 0);
  mg_coap_free_options(&cm);

  {
    unsigned char coap_packet_2_broken[] = {0x6F, 0x00, 0xe9, 0x1b};
    packet_in.buf = (char *) coap_packet_2_broken;
    packet_in.len = sizeof(coap_packet_2_broken);
    res = mg_coap_parse(&packet_in, &cm);
    ASSERT((res & MG_COAP_FORMAT_ERROR) != 0);
  }

  {
    unsigned char coap_packet_2_broken[] = {0x65, 0x00, 0xe9, 0x1b};
    packet_in.buf = (char *) coap_packet_2_broken;
    packet_in.len = sizeof(coap_packet_2_broken);
    res = mg_coap_parse(&packet_in, &cm);
    ASSERT((res & MG_COAP_NOT_ENOUGH_DATA) != 0);
  }

  memset(&cm, 0, sizeof(cm));
  mg_coap_add_option(&cm, 10, 0, 0);
  ASSERT_EQ(cm.options->number, 10);
  ASSERT(cm.options->next == NULL);
  mg_coap_add_option(&cm, 5, 0, 0);
  ASSERT_EQ(cm.options->number, 5);
  ASSERT_EQ(cm.options->next->number, 10);
  ASSERT(cm.options->next->next == NULL);
  mg_coap_add_option(&cm, 7, 0, 0);
  ASSERT_EQ(cm.options->number, 5);
  ASSERT_EQ(cm.options->next->number, 7);
  ASSERT_EQ(cm.options->next->next->number, 10);
  ASSERT(cm.options->next->next->next == NULL);
  mg_coap_add_option(&cm, 1, 0, 0);
  ASSERT_EQ(cm.options->number, 1);
  ASSERT_EQ(cm.options->next->number, 5);
  ASSERT_EQ(cm.options->next->next->number, 7);
  ASSERT_EQ(cm.options->next->next->next->number, 10);
  ASSERT(cm.options->next->next->next->next == NULL);
  mg_coap_free_options(&cm);

  {
    unsigned char value16[] = {0xCC, 0xDD};
    packet_in.buf = (char *) coap_packet_4;
    packet_in.len = sizeof(coap_packet_4);
    res = mg_coap_parse(&packet_in, &cm);
    ASSERT_EQ((res & MG_COAP_ERROR), 0);
    mg_coap_add_option(&cm, 0xAABB, (char *) value16, sizeof(value16));
    res = mg_coap_compose(&cm, &packet_out);
    mg_coap_free_options(&cm);
    ASSERT_EQ(res, 0);
    res = mg_coap_parse(&packet_out, &cm);
    ASSERT_EQ((res & MG_COAP_ERROR), 0);
    ASSERT_EQ(cm.options->number, 0xAABB);
    ASSERT_EQ(cm.options->value.len, 2);
    ASSERT_EQ(memcmp(cm.options->value.p, value16, cm.options->value.len), 0);
    mg_coap_free_options(&cm);
    mbuf_free(&packet_out);
  }

  memset(&cm, 0, sizeof(cm));
  cm.msg_id = 1;
  cm.msg_type = MG_COAP_MSG_MAX + 1;
  res = mg_coap_compose(&cm, &packet_out);
  ASSERT((res & MG_COAP_ERROR) != 0 && (res & MG_COAP_MSG_TYPE_FIELD) != 0);

  cm.msg_type = MG_COAP_MSG_ACK;
  cm.token.len = 10000;
  res = mg_coap_compose(&cm, &packet_out);
  ASSERT((res & MG_COAP_ERROR) != 0 && (res & MG_COAP_TOKEN_FIELD) != 0);

  cm.token.len = 0;
  cm.code_class = 0xFF;
  res = mg_coap_compose(&cm, &packet_out);
  ASSERT((res & MG_COAP_ERROR) != 0 && (res & MG_COAP_CODE_CLASS_FIELD) != 0);

  cm.code_class = 0;
  cm.code_detail = 0xFF;
  res = mg_coap_compose(&cm, &packet_out);
  ASSERT((res & MG_COAP_ERROR) != 0 && (res & MG_COAP_CODE_DETAIL_FIELD) != 0);

  cm.code_detail = 0;
  mg_coap_add_option(&cm, 0xFFFFFFF, 0, 0);
  res = mg_coap_compose(&cm, &packet_out);
  ASSERT((res & MG_COAP_ERROR) != 0 && (res & MG_COAP_OPTIOMG_FIELD) != 0);
  mg_coap_free_options(&cm);

  {
    struct mg_mgr mgr;
    struct mg_connection *nc;
    const char *address = "tcp://127.0.0.1:8686";

    mg_mgr_init(&mgr, 0);

    nc = mg_bind(&mgr, address, coap_handler_1);
    ASSERT(nc != NULL);
    ASSERT_EQ(mg_set_protocol_coap(nc), -1);

    mg_mgr_free(&mgr);
  }

  {
    struct results res;
    struct mg_mgr mgr;
    struct mg_connection *nc1, *nc2;
    const char *address = "udp://127.0.0.1:5683";

    mg_mgr_init(&mgr, 0);

    nc1 = mg_bind(&mgr, address, coap_handler_1);
    mg_set_protocol_coap(nc1);
    nc1->user_data = &res;

    nc2 = mg_connect(&mgr, address, coap_handler_1);
    mg_set_protocol_coap(nc2);
    nc2->user_data = &res;

    poll_until(&mgr, 5, c_int_eq, &res.client, (void *) 3);

    mg_mgr_free(&mgr);

    ASSERT_EQ(res.server, 1);
    ASSERT_EQ(res.client, 3);
  }

  return NULL;
}
#endif

static const char *test_strcmp(void) {
  struct mg_str s1;

  s1.p = "aa";
  s1.len = strlen(s1.p);
  ASSERT_EQ(mg_vcasecmp(&s1, "aa"), 0);
  ASSERT_EQ(mg_vcmp(&s1, "aa"), 0);
  ASSERT(mg_vcasecmp(&s1, "ab") < 0);
  ASSERT(mg_vcmp(&s1, "ab") < 0);
  ASSERT(mg_vcasecmp(&s1, "abb") < 0);
  ASSERT(mg_vcmp(&s1, "abb") < 0);
  ASSERT(mg_vcasecmp(&s1, "b") < 0);
  ASSERT(mg_vcmp(&s1, "b") < 0);
  return NULL;
}

#if MG_ENABLE_SNTP
static const char *test_sntp(void) {
  const char sntp_good[] =
      "\x24\x02\x00\xeb\x00\x00\x00\x1e\x00\x00\x07\xb6\x3e"
      "\xc9\xd6\xa2\xdb\xde\xea\x30\x91\x86\xb7\x10\xdb\xde"
      "\xed\x98\x00\x00\x00\xde\xdb\xde\xed\x99\x0a\xe2\xc7"
      "\x96\xdb\xde\xed\x99\x0a\xe4\x6b\xda";
  const char bad_good[] =
      "\x55\x02\x00\xeb\x00\x00\x00\x1e\x00\x00\x07\xb6\x3e"
      "\xc9\xd6\xa2\xdb\xde\xea\x30\x91\x86\xb7\x10\xdb\xde"
      "\xed\x98\x00\x00\x00\xde\xdb\xde\xed\x99\x0a\xe2\xc7"
      "\x96\xdb\xde\xed\x99\x0a\xe4\x6b\xda";

  int res;
  struct mg_sntp_message msg;
  struct tm *tm;
  time_t t;
  memset(&msg, 0, sizeof(0));

  res = mg_sntp_parse_reply(sntp_good, sizeof(sntp_good) - 1, &msg);
  ASSERT_EQ(res, 0);
  t = (time_t) msg.time;
  tm = gmtime(&t);
  ASSERT_EQ(tm->tm_year, 116);
  ASSERT_EQ(tm->tm_mon, 10);
  ASSERT_EQ(tm->tm_mday, 22);
  ASSERT_EQ(tm->tm_hour, 16);
  ASSERT_EQ(tm->tm_min, 15);
  ASSERT_EQ(tm->tm_sec, 21);
  res = mg_sntp_parse_reply(bad_good, sizeof(bad_good) - 1, &msg);
  ASSERT_EQ(res, -1);

  return NULL;
}
#endif

#if MG_ENABLE_SOCKS
static const char *test_socks(void) {
  struct mg_mgr mgr;
  struct mg_connection *c;
  struct mg_connect_opts opts;
  const char *this_binary;
  const char *web_addr = "127.0.0.1:7257";
  const char *proxy_addr = "127.0.0.1:7258";
  char status[100] = "";

  mg_mgr_init(&mgr, NULL);
#if 0
  mgr.hexdump_file = "-";
#endif

#if 1
  cs_log_set_level(LL_INFO);
#endif

  /* Start web server */
  ASSERT((c = mg_bind(&mgr, web_addr, cb1)) != NULL);
  mg_set_protocol_http_websocket(c);

  /* Start socks proxy */
  ASSERT((c = mg_bind(&mgr, proxy_addr, ev_handler_empty)) != NULL);
  mg_set_protocol_socks(c);

  /* Create HTTP client that uses socks proxy */
  memset(&opts, 0, sizeof(opts));
  opts.iface = mg_socks_mk_iface(&mgr, proxy_addr);

  ASSERT((c = mg_connect_opt(&mgr, web_addr, cb7, opts)) != NULL);
  mg_set_protocol_http_websocket(c);
  c->user_data = status;
  /* Wine and GDB set argv0 to full path: strip the dir component */
  if ((this_binary = strrchr(s_argv_0, '\\')) != NULL) {
    this_binary++;
  } else if ((this_binary = strrchr(s_argv_0, '/')) != NULL) {
    this_binary++;
  } else {
    this_binary = s_argv_0;
  }
  mg_printf(c, "GET /%s HTTP/1.0\n\n", this_binary);

  /* Run event loop. Use more cycles to let file download complete. */
  poll_until(&mgr, 5, c_str_ne, status, (void *) "");
  mg_mgr_free(&mgr);
  ASSERT_STREQ(status, "success");

  return NULL;
}
#endif

static const char *run_tests(const char *filter, double *total_elapsed) {
  RUN_TEST(test_mbuf);
  RUN_TEST(test_parse_uri);
  RUN_TEST(test_assemble_uri);
  RUN_TEST(test_parse_address);
  RUN_TEST(test_mg_normalize_uri_path);
  RUN_TEST(test_mg_uri_to_local_path);
  RUN_TEST(test_mg_url_encode);
  RUN_TEST(test_check_ip_acl);
  RUN_TEST(test_connect_opts);
  RUN_TEST(test_connect_opts_error_string);
  RUN_TEST(test_to64);
  RUN_TEST(test_alloc_vprintf);
  RUN_TEST(test_socketpair);
  RUN_TEST(test_timer);
#ifdef __linux__
  RUN_TEST(test_simple);
#endif
#if MG_ENABLE_THREADS
  RUN_TEST(test_thread);
#endif
  RUN_TEST(test_mgr);
  RUN_TEST(test_parse_http_message);
  RUN_TEST(test_get_http_var);
  RUN_TEST(test_http_serve_file);
  RUN_TEST(test_http_serve_file_streaming);
  RUN_TEST(test_http);
  RUN_TEST(test_http_send_redirect);
  RUN_TEST(test_http_digest_auth);
  RUN_TEST(test_http_errors);
  RUN_TEST(test_http_index);
  RUN_TEST(test_http_parse_header);
  RUN_TEST(test_ssi);
#ifndef NO_CGI_TEST
  RUN_TEST(test_cgi);
#endif
  RUN_TEST(test_http_rewrites);
  RUN_TEST(test_http_dav);
  RUN_TEST(test_http_range);
  RUN_TEST(test_http_multipart);
#if MG_ENABLE_HTTP_STREAMING_MULTIPART
  RUN_TEST(test_http_multipart2);
#endif
  RUN_TEST(test_parse_date_string);
  RUN_TEST(test_websocket);
  RUN_TEST(test_websocket_endpoint);
  RUN_TEST(test_connect_ws);
  RUN_TEST(test_websocket_big);
  RUN_TEST(test_http_chunk);
  RUN_TEST(test_http_chunk2);
  RUN_TEST(test_http_not_modified);
  RUN_TEST(test_http_extra_headers);
  RUN_TEST(test_http_endpoints);
  RUN_TEST(test_mqtt_handshake);
  RUN_TEST(test_mqtt_publish);
  RUN_TEST(test_mqtt_subscribe);
  RUN_TEST(test_mqtt_unsubscribe);
  RUN_TEST(test_mqtt_connack);
  RUN_TEST(test_mqtt_suback);
  RUN_TEST(test_mqtt_simple_acks);
  RUN_TEST(test_mqtt_nullary);
  RUN_TEST(test_mqtt_parse_mqtt);
  RUN_TEST(test_mqtt_parse_mqtt_qos1);
  RUN_TEST(test_mqtt_match_topic_expression);
  RUN_TEST(test_mqtt_client_keep_alive);
#if MG_ENABLE_MQTT_BROKER
  RUN_TEST(test_mqtt_broker);
#endif
  RUN_TEST(test_dns_encode);
  RUN_TEST(test_dns_uncompress);
  RUN_TEST(test_dns_decode);
  RUN_TEST(test_dns_decode_truncated);
  RUN_TEST(test_dns_reply_encode);
#if MG_ENABLE_DNS_SERVER
  RUN_TEST(test_dns_server);
#endif
  RUN_TEST(test_dns_resolve);
  RUN_TEST(test_dns_resolve_timeout);
#ifndef NO_RESOLVE_HOSTS_TEST
  RUN_TEST(test_dns_resolve_hosts);
#endif
  RUN_TEST(test_buffer_limit);
  RUN_TEST(test_connection_errors);
  RUN_TEST(test_connect_fail);
#ifndef NO_DNS_TEST
  RUN_TEST(test_resolve);
#endif
  RUN_TEST(test_base64);
  RUN_TEST(test_sock_addr_to_str);
  RUN_TEST(test_hexdump);
  RUN_TEST(test_hexdump_file);
  RUN_TEST(test_basic_auth_helpers);
  RUN_TEST(test_http_auth);
#ifndef _WIN32
  RUN_TEST(test_tunnel);
#endif
#if MG_ENABLE_SSL
  RUN_TEST(test_ssl);
#ifdef OPENSSL_VERSION_NUMBER
  RUN_TEST(test_modern_crypto);
#endif
#endif
  RUN_TEST(test_udp);
#if MG_ENABLE_COAP
  RUN_TEST(test_coap);
#endif
#if MG_ENABLE_SNTP
  RUN_TEST(test_sntp);
#endif
  RUN_TEST(test_strcmp);
#if MG_ENABLE_SOCKS
  RUN_TEST(test_socks);
#endif
  return NULL;
}

#if defined(_MSC_VER) && _MSC_VER >= 1900
int __cdecl CrtDbgHook(int nReportType, char *szMsg, int *pnRet) {
  (void) nReportType;
  (void) szMsg;
  (void) pnRet;

  fprintf(stderr, "CRT debug hook: type: %d, msg: %s\n", nReportType, szMsg);
  /* Return true - Abort,Retry,Ignore dialog will *not* be displayed */
  return 1;
}
#endif

void setup() {
  test_iface = mg_if_create_iface(mg_ifaces[MG_MAIN_IFACE], NULL);
}

void teardown() {
  free(test_iface);
}

int __cdecl main(int argc, char *argv[]) {
  const char *fail_msg;
  const char *filter = argc > 1 ? argv[1] : "";
  double total_elapsed = 0.0;

  setvbuf(stdout, NULL, _IONBF, 0);
  setvbuf(stderr, NULL, _IONBF, 0);

#if defined(_MSC_VER) && _MSC_VER >= 1900
  /* NOTE: not available on wine/vc6 */
  _CrtSetReportHook2(_CRT_RPTHOOK_INSTALL, CrtDbgHook);
#endif

  setup();

  s_argv_0 = argv[0];
  fail_msg = run_tests(filter, &total_elapsed);
  printf("%s, run %d in %.3lfs\n", fail_msg ? "FAIL" : "PASS", num_tests,
         total_elapsed);

  teardown();
  if (fail_msg != NULL) {
    /* Prevent leak analyzer from running: there will be "leaks" because of
     * premature return from the test, and in this case we don't care. */
    _exit(EXIT_FAILURE);
  }
  return EXIT_SUCCESS;
}
