// Copyright 2015-2024 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "test.h"

#include <stdio.h>
#include <string.h>
#include <errno.h>
#ifdef _WIN32
#else
#include <dirent.h>
#include <sys/stat.h>
#include <execinfo.h>
#endif

#include "buf.h"
#include "timer.h"
#include "url.h"
#include "opts.h"
#include "../src/util.h"
#include "hash.h"
#include "conn.h"
#include "sub.h"
#include "msg.h"
#include "stats.h"
#include "crypto.h"
#include "nkeys.h"
#include "parser.h"
#include "js.h"
#include "kv.h"
#include "microp.h"
#include "glib/glibp.h"

#if defined(NATS_HAS_STREAMING)

#include "stan/conn.h"
#include "stan/pub.h"
#include "stan/sub.h"
#include "stan/copts.h"
#include "stan/sopts.h"

static const char *clientName = "client";

#endif // NATS_HAS_STREAMING

typedef void (*testFunc)(void);

typedef struct __testInfo
{
    const char *name;
    testFunc func;
} testInfo;

#define _TEST_PROTO
#include "list.h"
#undef _TEST_PROTO

#define _TEST_LIST
testInfo allTests[] =
{
#include "list.h"
};
#undef _TEST_LIST

static int  tests  = 0;
bool        failed = false;

bool keepServerOutput    = false;
static bool valgrind            = false;
static bool runOnTravis         = false;

static const char *serverVersion = NULL;

static const char *natsStreamingServerExe = "nats-streaming-server";

natsMutex *slMu  = NULL;
natsHash  *slMap = NULL;

#define test(s)         { printf("#%02d ", ++tests); printf("%s", (s)); fflush(stdout); }
#define testf(s, ...)   { printf("#%02d ", ++tests); printf((s), __VA_ARGS__); fflush(stdout); }

#ifdef _WIN32
#define testCond(c)         if(c) { printf("PASSED\n"); fflush(stdout); } else { printf("FAILED\n"); nats_PrintLastErrorStack(stdout); fflush(stdout); failed=true; return; }
#define testCondNoReturn(c) if(c) { printf("PASSED\n"); fflush(stdout); } else { printf("FAILED\n"); nats_PrintLastErrorStack(stdout); fflush(stdout); failed=true; }
#else
#define testCond(c)         if(c) { printf("\033[0;32mPASSED\033[0;0m\n"); fflush(stdout); } else { printf("\033[0;31mFAILED\033[0;0m\n"); nats_PrintLastErrorStack(stdout); fflush(stdout); failed=true; return; }
#define testCondNoReturn(c) if(c) { printf("\033[0;32mPASSED\033[0;0m\n"); fflush(stdout); } else { printf("\033[0;31mFAILED\033[0;0m\n"); nats_PrintLastErrorStack(stdout); fflush(stdout); failed=true; }
#endif

static const char *testServers[] = {"nats://127.0.0.1:1222",
                                    "nats://127.0.0.1:1223",
                                    "nats://127.0.0.1:1224",
                                    "nats://127.0.0.1:1225",
                                    "nats://127.0.0.1:1226",
                                    "nats://127.0.0.1:1227",
                                    "nats://127.0.0.1:1228"};

// Forward declaration
static void _startMockupServerThread(void *closure);
static void _createConfFile(char *buf, int bufLen, const char *content);

typedef natsStatus (*testCheckInfoCB)(char *buffer);

struct threadArg
{
    natsMutex       *m;
    natsThread      *t;
    natsCondition   *c;
    natsCondition   *b;
    int             control;
    bool            current;
    int             sum;
    int             timerFired;
    int             timerStopped;
    natsStrHash     *inboxes;
    natsStatus      status;
    char            lastErrorBuf[256];
    const char*     string;
    int             N;
    bool            connected;
    bool            disconnected;
    int64_t         disconnectedAt[4];
    int64_t         disconnects;
    bool            closed;
    bool            reconnected;
    int64_t         reconnectedAt[4];
    int             reconnects;
    bool            msgReceived;
    int             microRunningServiceCount;
    bool            microAllDone;
    bool            done;
    int             results[10];
    const char      *tokens[3];
    int             tokenCallCount;
    testCheckInfoCB checkInfoCB;
    natsSock        sock;

    natsSubscription *sub;
    natsOptions      *opts;
    natsConnection   *nc;
    jsCtx            *js;
    natsBuffer       *buf;

#if defined(NATS_HAS_STREAMING)
    stanConnection   *sc;
    int              redelivered;
    const char*      channel;
    stanMsg          *sMsg;
#endif

    int              attached;
    int              detached;

    bool             doRead;
    bool             doWrite;

};

static bool
serverVersionAtLeast(int major, int minor, int update)
{
    int     ma       = 0;
    int     mi       = 0;
    int     up       = 0;
    char    *version = NULL;

    if (serverVersion == NULL)
        return false;

    version = strstr(serverVersion, "version ");
    if (version != NULL)
    {
        version += 8;
    }
    else
    {
        version = strstr(serverVersion, " v");
        if (version == NULL)
            return false;

        version += 2;
    }

    sscanf(version, "%d.%d.%d", &ma, &mi, &up);
    if ((ma > major) || ((ma == major) && (mi > minor)) || ((ma == major) && (mi == minor) && (up >= update)))
        return true;

    return false;
}

static void
_waitSubPending(natsSubscription *sub, int expected)
{
    int mc = 0;
    do
    {
        natsSubscription_GetPending(sub, &mc, NULL);
        if (mc != expected)
            nats_Sleep(15);
        }
    while (mc != expected);
}

static natsStatus
_createDefaultThreadArgsForCbTests(
    struct threadArg    *arg)
{
    natsStatus s;

    memset(arg, 0, sizeof(struct threadArg));

    s = natsMutex_Create(&(arg->m));
    if (s == NATS_OK)
        s = natsCondition_Create(&(arg->c));

    return s;
}

void
_destroyDefaultThreadArgs(struct threadArg *args)
{
    if (valgrind)
        nats_Sleep(100);

    natsMutex_Destroy(args->m);
    natsCondition_Destroy(args->c);
}

void test_natsNowAndSleep(void)
{
    int64_t start;
    int64_t end;

    test("Check now and sleep: ")
    start = nats_Now();
    nats_Sleep(1000);
    end = nats_Now();
    testCond(((end - start) >= 990) && ((end - start) <= 1010));
}

void test_natsAllocSprintf(void)
{
    char smallStr[20];
    char mediumStr[256]; // This is the size of the temp buffer in nats_asprintf
    char largeStr[1024];
    char *ptr = NULL;
    int ret;

    memset(smallStr, 'A', sizeof(smallStr) - 1);
    smallStr[sizeof(smallStr) - 1] = '\0';

    memset(mediumStr, 'B', sizeof(mediumStr) - 1);
    mediumStr[sizeof(mediumStr) - 1] = '\0';

    memset(largeStr, 'C', sizeof(largeStr) - 1);
    largeStr[sizeof(largeStr) - 1] = '\0';

    test("Check alloc sprintf with small string: ");
    ret = nats_asprintf(&ptr, "%s", smallStr);
    testCond((ret >= 0)
             && (strcmp(ptr, smallStr) == 0));

    free(ptr);
    ptr = NULL;

    test("Check alloc sprintf with medium string: ");
    ret = nats_asprintf(&ptr, "%s", mediumStr);
    testCond((ret >= 0)
             && (strcmp(ptr, mediumStr) == 0));

    free(ptr);
    ptr = NULL;

    test("Check alloc sprintf with large string: ");
    ret = nats_asprintf(&ptr, "%s", largeStr);
    testCond((ret >= 0)
             && (strcmp(ptr, largeStr) == 0));

    free(ptr);
    ptr = NULL;
}

void test_natsStrCaseStr(void)
{
    const char *s1 = "Hello World!";
    const char *s2 = "wo";
    const char *res = NULL;

    test("StrStr case insensitive (equal): ");
    res = nats_strcasestr(s1, s1);
    testCond((res != NULL)
             && (strcmp(res, s1) == 0)
             && (res == s1));

    test("StrStr case insensitive (match): ");
    res = nats_strcasestr(s1, s2);
    testCond((res != NULL)
             && (strcmp(res, "World!") == 0)
             && (res == (s1 + 6)));

    test("StrStr case insensitive (no match): ");
    res = nats_strcasestr(s1, "xx");
    testCond(res == NULL);
}

void test_natsSnprintf(void)
{
#if _WIN32
    // This test is specific to older version of Windows
    // that did not provide snprintf...
    char buf[5];

    test("snprintf over limit: ");
    snprintf(buf, sizeof(buf), "%s", "abcdefghijklmnopqrstuvwxyz");
    testCond(strcmp(buf, "abcd") == 0);
#else
    test("Skip when not running on Windows: ");
    testCond(true);
#endif
}

void test_natsBuffer(void)
{
    natsStatus  s;
    char        backend[10];
    natsBuffer  *buf = NULL;
    natsBuffer  stackBuf;
    int         oldCapacity = 0;

    printf("== Buffer without data ==\n");

    test("Create buffer owning its data: ");
    s = natsBuf_Create(&buf, 1);
    testCond((s == NATS_OK)
             && (natsBuf_Len(buf) == 0)
             && (natsBuf_Capacity(buf) == 1));

    test("Append less than capacity does not expand buffer: ");
    s = natsBuf_Append(buf, "a", 1);
    testCond((s == NATS_OK)
            && (natsBuf_Len(buf) == 1)
            && (natsBuf_Capacity(buf) == 1)
            && (natsBuf_Available(buf) == 0));

    test("Appending one more (AppendByte) increases capacity: ");
    oldCapacity = natsBuf_Capacity(buf);
    s = natsBuf_AppendByte(buf, 'b');
    testCond((s == NATS_OK)
             && (natsBuf_Len(buf) == 2)
             && (natsBuf_Capacity(buf) > oldCapacity)
             && (natsBuf_Available(buf) > 0));

    test("Checking content: ");
    testCond((s == NATS_OK)
             && (natsBuf_Data(buf) != NULL)
             && (strncmp(natsBuf_Data(buf), "ab", 2) == 0));

    natsBuf_Destroy(buf);
    buf = NULL;

    oldCapacity = 0;
    test("Appending one more byte increases capacity: ");
    s = natsBuf_Create(&buf, 1);
    IFOK(s, natsBuf_Append(buf, "a", 1));
    if (s == NATS_OK)
    {
        oldCapacity = natsBuf_Capacity(buf);

        // Add one more!
        s = natsBuf_Append(buf, "b", 1);
    }
    testCond((s == NATS_OK)
             && (natsBuf_Len(buf) == 2)
             && (natsBuf_Capacity(buf) > oldCapacity)
             && (natsBuf_Available(buf) > 0));

    natsBuf_Destroy(buf);
    buf = NULL;

    printf("\n== Buffer with data ==\n");

    memset(backend, 0, sizeof(backend));

    test("Create buffer with backend: ");
    s = natsBuf_CreateWithBackend(&buf, backend, 0, 5);
    testCond((s == NATS_OK)
             && (natsBuf_Len(buf) == 0)
             && (natsBuf_Capacity(buf) == 5));

    test("Check that changes are reflected in backend")
    s = natsBuf_Append(buf, "abcd", 4);
    testCond((s == NATS_OK)
            && (natsBuf_Len(buf) == 4)
            && (natsBuf_Capacity(buf) == 5)
            && (natsBuf_Available(buf) > 0)
            && (strcmp(backend, "abcd") == 0));

    test("Changing backend is reflected in buffer: ");
    backend[1] = 'x';
    testCond((s == NATS_OK)
             && (natsBuf_Data(buf)[1] == 'x'));

    test("Append less than capacity does not expand buffer: ");
    s = natsBuf_AppendByte(buf, 'e');
    testCond((s == NATS_OK)
            && (natsBuf_Len(buf) == 5)
            && (natsBuf_Capacity(buf) == 5)
            && (natsBuf_Available(buf) == 0));

    test("Check natsBuf_Expand returns error for invalid arguments: ");
    {
        natsStatus ls;

        ls = natsBuf_Expand(buf, -10);
        if (ls != NATS_OK)
            ls = natsBuf_Expand(buf, 0);
        if (ls != NATS_OK)
            ls = natsBuf_Expand(buf, natsBuf_Capacity(buf));
        testCond(ls != NATS_OK);
    }

    test("Adding more causes expand: ");
    oldCapacity = natsBuf_Capacity(buf);
    s = natsBuf_Append(buf, "fghij", 5);
    testCond((s == NATS_OK)
            && (natsBuf_Len(buf) == 10)
            && (natsBuf_Capacity(buf) > oldCapacity));

    test("Check that the backend did not change");
    testCond((s == NATS_OK)
             && (strcmp(backend, "axcde") == 0));

    test("Checking content: ");
    testCond((s == NATS_OK)
             && (natsBuf_Data(buf) != NULL)
             && (strncmp(natsBuf_Data(buf), "axcdefghij", 10) == 0));

    test("Destroying buffer does not affect backend: ");
    natsBuf_Destroy(buf);
    buf = NULL;
    testCond(strcmp(backend, "axcde") == 0);

    printf("\n== Buffer Init without data ==\n");

    test("Create buffer owning its data: ");
    s = natsBuf_Init(&stackBuf, 10);
    testCond((s == NATS_OK)
             && (buf = &stackBuf)
             && (natsBuf_Len(buf) == 0)
             && (natsBuf_Capacity(buf) == 10));

    test("Append less than capacity does not expand buffer: ");
    s = natsBuf_Append(buf, "abcdefghij", 10);
    testCond((s == NATS_OK)
            && (natsBuf_Len(buf) == 10)
            && (natsBuf_Capacity(buf) == 10)
            && (natsBuf_Available(buf) == 0));

    test("Appending one more increases capacity: ");
    oldCapacity = natsBuf_Capacity(buf);
    s = natsBuf_AppendByte(buf, 'k');
    testCond((s == NATS_OK)
              && (natsBuf_Len(buf) == 11)
              && (natsBuf_Capacity(buf) > oldCapacity)
              && (natsBuf_Available(buf) > 0));

    test("Checking content: ");
    testCond((s == NATS_OK)
             && (natsBuf_Data(buf) != NULL)
             && (strncmp(natsBuf_Data(buf), "abcdefghijk", 11) == 0));

    test("Destroying buffer: ");
    natsBuf_Destroy(buf);
    testCond((natsBuf_Data(buf) == NULL)
                && (natsBuf_Len(buf) == 0)
                && (natsBuf_Capacity(buf) == 0)
                && (natsBuf_Available(buf) == 0));
    buf = NULL;

    printf("\n== Buffer Init with data ==\n");

    memset(backend, 0, sizeof(backend));

    test("Create buffer with backend: ");
    s = natsBuf_InitWithBackend(&stackBuf, backend, 0, 5);
    testCond((s == NATS_OK)
             && (buf = &stackBuf)
             && (natsBuf_Len(buf) == 0)
             && (natsBuf_Capacity(buf) == 5));

    test("Check that changes are reflected in backend: ")
    s = natsBuf_Append(buf, "abcd", 4);
    testCond((s == NATS_OK)
            && (natsBuf_Len(buf) == 4)
            && (natsBuf_Capacity(buf) == 5)
            && (natsBuf_Available(buf) > 0)
            && (strcmp(backend, "abcd") == 0));

    test("Changing backend is reflected in buffer: ");
    testCond((s == NATS_OK)
             && (backend[1] = 'x')
             && (natsBuf_Data(buf)[1] == 'x'));

    test("Append less than capacity does not expand buffer: ");
    s = natsBuf_AppendByte(buf, 'e');
    testCond((s == NATS_OK)
            && (natsBuf_Len(buf) == 5)
            && (natsBuf_Capacity(buf) == 5)
            && (natsBuf_Available(buf) == 0));

    test("Adding more causes expand: ");
    s = natsBuf_Append(buf, "fghij", 5);
    testCond((s == NATS_OK)
            && (natsBuf_Len(buf) == 10)
            && (natsBuf_Capacity(buf) >= 10));

    test("Check that the backend did not change");
    testCond((s == NATS_OK)
             && (strcmp(backend, "axcde") == 0));

    test("Checking content: ");
    testCond((s == NATS_OK)
             && (natsBuf_Data(buf) != NULL)
             && (strncmp(natsBuf_Data(buf), "axcdefghij", 10) == 0));

    test("Destroying buffer does not affect backend: ");
    natsBuf_Destroy(buf);
    testCond(strcmp(backend, "axcde") == 0);

    test("Destroyed buffer state is clean: ");
    testCond((s == NATS_OK)
             && (natsBuf_Data(buf) == NULL)
             && (natsBuf_Len(buf) == 0)
             && (natsBuf_Capacity(buf) == 0)
             && (natsBuf_Available(buf) == 0));

    buf = NULL;

    test("Check maximum size: ");
    s = natsBuf_Create(&buf, 5);
    IFOK(s, natsBuf_Append(buf, "abcd", 4));
    IFOK(s, natsBuf_Append(buf, "fake size that goes over int max size", 0x7FFFFFFC));
    testCond(s == NATS_NO_MEMORY);

    test("Check maximum size (append byte): ");
    buf->len = 0x7FFFFFFE;
    s = natsBuf_Append(buf, "e", 1);
    testCond(s == NATS_NO_MEMORY);

    natsBuf_Destroy(buf);
    buf = NULL;

    test("Consume half: ");
    s = natsBuf_Create(&buf, 10);
    IFOK(s, natsBuf_Append(buf, "abcdefghij", 10));
    if (s == NATS_OK)
        natsBuf_Consume(buf, 5);
    testCond((s == NATS_OK)
             && (natsBuf_Len(buf) == 5)
             && (strncmp(natsBuf_Data(buf), "fghij", 5) == 0)
             && (natsBuf_Available(buf) == 5)
             && (*(buf->pos) == 'f'));

    test("Consume rest: ");
    natsBuf_Consume(buf, 5);
    testCond((s == NATS_OK)
             && (natsBuf_Len(buf) == 0)
             && (natsBuf_Available(buf) == 10)
             && (*(buf->pos) == 'f'));

    natsBuf_Destroy(buf);
    buf = NULL;

    test("MoveTo (forward): ");
    s = natsBuf_Create(&buf, 100);
    if (s == NATS_OK)
    {
        memcpy(natsBuf_Data(buf), "this is a test", 14);
        natsBuf_MoveTo(buf, 14);
        memcpy(natsBuf_Data(buf)+14, " of move by", 11);
        natsBuf_MoveTo(buf, 14+11);
    }
    IFOK(s, natsBuf_AppendByte(buf, '\0'));
    testCond((s == NATS_OK)
                && (natsBuf_Len(buf) == 26)
                && (strcmp(natsBuf_Data(buf), "this is a test of move by") == 0));

    test("MoveTo (backward): ");
    natsBuf_MoveTo(buf, 14);
    s = natsBuf_AppendByte(buf, '\0');
    testCond((s == NATS_OK)
            && (natsBuf_Len(buf) == 15)
            && (strcmp(natsBuf_Data(buf), "this is a test") == 0));

    natsBuf_Destroy(buf);
    buf = NULL;
}

void test_natsParseInt64(void)
{
    int64_t n;

    test("Parse with non numeric: ");
    n = nats_ParseInt64("a", 1);
    testCond(n == -1);

    test("Parse with NULL buffer: ");
    n = nats_ParseInt64(NULL, 0);
    testCond(n == -1);

    test("Parse with 0 buffer size: ");
    n = nats_ParseInt64("whatever", 0);
    testCond(n == -1);

    test("Parse with '0': ");
    n = nats_ParseInt64("0", 1);
    testCond(n == 0);

    test("Parse with '1': ");
    n = nats_ParseInt64("1", 1);
    testCond(n == 1);

    test("Parse with '12': ");
    n = nats_ParseInt64("12", 2);
    testCond(n == 12);

    test("Parse with '-12': ");
    n = nats_ParseInt64("-12", 3);
    testCond(n == -1);

    test("Parse with trailing spaces: ");
    n = nats_ParseInt64("12 ", 3);
    testCond(n == -1);

    test("Parse with leading spaces: ");
    n = nats_ParseInt64(" 12", 3);
    testCond(n == -1);

    test("Parse with 'INT64_MAX': ");
    n = nats_ParseInt64("9223372036854775807", 19);
    testCond(n == INT64_MAX);

    test("Parse with overflow(1): ");
    n = nats_ParseInt64("9223372036854775809", 19);
    testCond(n == -1);

    test("Parse with overflow(2): ");
    n = nats_ParseInt64("92233720368547758099223372036854775809", 38);
    testCond(n == -1);

    test("Parse with '12345': ");
    n = nats_ParseInt64("12345", 5);
    testCond(n == 12345);

    test("Parse with '123.45': ");
    n = nats_ParseInt64("123.45", 6);
    testCond(n == -1);
}

void test_natsParseControl(void)
{
    natsStatus  s;
    natsControl c;

    c.op   = NULL;
    c.args = NULL;

    test("Test with NULL line: ");
    s = nats_ParseControl(&c, NULL);
    testCond(s == NATS_PROTOCOL_ERROR);

    test("Test line with single op: ");
    s = nats_ParseControl(&c, "op");
    testCond((s == NATS_OK)
             && (c.op != NULL)
             && (strcmp(c.op, "op") == 0)
             && (c.args == NULL));

    free(c.op);
    free(c.args);
    c.op = NULL;
    c.args = NULL;

    test("Test line with trailing spaces: ");
    s = nats_ParseControl(&c, "op   ");
    testCond((s == NATS_OK)
             && (c.op != NULL)
             && (strcmp(c.op, "op") == 0)
             && (c.args == NULL));

    free(c.op);
    free(c.args);
    c.op = NULL;
    c.args = NULL;

    test("Test line with op and args: ");
    s = nats_ParseControl(&c, "op    args");
    testCond((s == NATS_OK)
             && (c.op != NULL)
             && (strcmp(c.op, "op") == 0)
             && (c.args != NULL)
             && (strcmp(c.args, "args") == 0));

    free(c.op);
    free(c.args);
    c.op = NULL;
    c.args = NULL;

    test("Test line with op and args and trailing spaces: ");
    s = nats_ParseControl(&c, "op   args  ");
    testCond((s == NATS_OK)
             && (c.op != NULL)
             && (strcmp(c.op, "op") == 0)
             && (c.args != NULL)
             && (strcmp(c.args, "args") == 0));

    free(c.op);
    free(c.args);
    c.op = NULL;
    c.args = NULL;

    test("Test line with op and args args: ");
    s = nats_ParseControl(&c, "op   args  args   ");
    testCond((s == NATS_OK)
             && (c.op != NULL)
             && (strcmp(c.op, "op") == 0)
             && (c.args != NULL)
             && (strcmp(c.args, "args  args") == 0));

    free(c.op);
    free(c.args);
    c.op = NULL;
    c.args = NULL;
}

void test_natsNormalizeErr(void)
{
    char error[1024];
    char expected[256];

    test("Check typical -ERR: ");

    snprintf(expected, sizeof(expected), "%s", "Simple Error");
    snprintf(error, sizeof(error), "-ERR '%s'", expected);
    nats_NormalizeErr(error);
    testCond(strcmp(error, expected) == 0);

    test("Check -ERR without quotes: ");
    snprintf(expected, sizeof(expected), "%s", "Error Without Quotes");
    snprintf(error, sizeof(error), "-ERR %s", expected);
    nats_NormalizeErr(error);
    testCond(strcmp(error, expected) == 0);

    test("Check -ERR with spaces: ");
    snprintf(expected, sizeof(expected), "%s", "Error With Surrounding Spaces");
    snprintf(error, sizeof(error), "-ERR    '%s'    ", expected);
    nats_NormalizeErr(error);
    testCond(strcmp(error, expected) == 0);

    test("Check -ERR with spaces and without quotes: ");
    snprintf(expected, sizeof(expected), "%s", "Error With Surrounding Spaces And Without Quotes");
    snprintf(error, sizeof(error), "-ERR     %s     ", expected);
    nats_NormalizeErr(error);
    testCond(strcmp(error, expected) == 0);

    test("Check -ERR with quote on the left: ");
    snprintf(expected, sizeof(expected), "%s", "Error With Quote On Left");
    snprintf(error, sizeof(error), "-ERR '%s", expected);
    nats_NormalizeErr(error);
    testCond(strcmp(error, expected) == 0);

    test("Check -ERR with quote on right: ");
    snprintf(expected, sizeof(expected), "%s", "Error With Quote On Right");
    snprintf(error, sizeof(error), "-ERR %s'", expected);
    nats_NormalizeErr(error);
    testCond(strcmp(error, expected) == 0);

    test("Check -ERR with spaces and single quote: ");
    snprintf(error, sizeof(error), "%s", "-ERR      '      ");
    nats_NormalizeErr(error);
    testCond(error[0] == '\0');
}

void test_natsMutex(void)
{
    natsStatus  s;
    natsMutex   *m = NULL;
    bool        locked = false;

    test("Create mutex: ");
    s = natsMutex_Create(&m);
    testCond(s == NATS_OK);

    test("Lock: ");
    natsMutex_Lock(m);
    testCond(1);

    test("Recursive locking: ");
    locked = natsMutex_TryLock(m);
    testCond(locked);

    test("Release recursive lock: ");
    natsMutex_Unlock(m);
    testCond(1);

    test("Unlock: ");
    natsMutex_Unlock(m);
    testCond(1);

    test("Destroy: ");
    natsMutex_Destroy(m);
    testCond(1);
}

static void
testThread(void *arg)
{
    struct threadArg *tArg = (struct threadArg*) arg;

    natsMutex_Lock(tArg->m);

    tArg->control = 1;
    tArg->current = natsThread_IsCurrent(tArg->t);

    natsMutex_Unlock(tArg->m);
}

static void
sumThread(void *arg)
{
    struct threadArg *tArg = (struct threadArg*) arg;

    natsMutex_Lock(tArg->m);

    tArg->sum++;

    natsMutex_Unlock(tArg->m);
}

static int NUM_THREADS = 1000;

void test_natsThread(void)
{
    natsStatus          s  = NATS_OK;
    natsMutex           *m = NULL;
    natsThread          *t = NULL;
    bool                current = false;
    struct threadArg    tArgs;
    natsThread          **threads = NULL;
    int                 i,j;

    if (valgrind)
        NUM_THREADS = 100;

    test("Create threads array: ");
    threads = (natsThread**) calloc(NUM_THREADS, sizeof(natsThread*));
    if (threads == NULL)
        s = NATS_NO_MEMORY;
    IFOK(s, natsMutex_Create(&m));
    testCond(s == NATS_OK);

    natsMutex_Lock(m);

    tArgs.m         = m;
    tArgs.control   = 0;
    tArgs.current   = false;

    test("Create thread: ");
    s = natsThread_Create(&t, testThread, &tArgs);
    testCond(s == NATS_OK);

    tArgs.t = t;

    test("Check if thread current from other thread: ");
    current = natsThread_IsCurrent(t);
    testCond(!current);

    natsMutex_Unlock(m);

    test("Joining thread: ")
    natsThread_Join(t);
    testCond(1);

    natsMutex_Lock(m);

    test("Control updated: ");
    testCond(tArgs.control == 1);

    test("Check thread current works from current thread: ");
    testCond(tArgs.current);

    test("Destroy thread: ");
    natsThread_Destroy(t);
    testCond(1);

    tArgs.sum = 0;

    test("Creating multiple threads: ");
    for (i=0; (s == NATS_OK) && (i<NUM_THREADS); i++)
        s = natsThread_Create(&(threads[i]), sumThread, &tArgs);
    testCond(s == NATS_OK);

    if (s != NATS_OK)
        i--;

    natsMutex_Unlock(m);

    test("Waiting all done: ");
    for (j=0; j<i; j++)
    {
        natsThread_Join(threads[j]);
        natsThread_Destroy(threads[j]);
    }
    testCond(s == NATS_OK);

    test("Checking sum: ");
    testCond((s == NATS_OK) && (tArgs.sum == NUM_THREADS));

    natsMutex_Destroy(m);

    free(threads);
}

static void
testSignal(void *arg)
{
    struct threadArg *tArg = (struct threadArg*) arg;

    natsMutex_Lock(tArg->m);

    tArg->control = 1;
    natsCondition_Signal(tArg->c);

    natsMutex_Unlock(tArg->m);
}

static void
testBroadcast(void *arg)
{
    struct threadArg *tArg = (struct threadArg*) arg;

    natsMutex_Lock(tArg->m);

    tArg->sum++;
    natsCondition_Signal(tArg->c);

    while (tArg->control == 0)
        natsCondition_Wait(tArg->b, tArg->m);

    tArg->sum--;

    natsMutex_Unlock(tArg->m);
}

static void
_unblockLongWait(void *closure)
{
    struct threadArg *args = (struct threadArg*) closure;

    nats_Sleep(500);
    natsMutex_Lock(args->m);
    natsCondition_Signal(args->c);
    natsMutex_Unlock(args->m);
}

void test_natsCondition(void)
{
    natsStatus          s;
    natsMutex           *m  = NULL;
    natsThread          *t1 = NULL;
    natsThread          *t2 = NULL;
    natsCondition       *c1 = NULL;
    natsCondition       *c2 = NULL;
    struct threadArg    tArgs;
    int64_t             before = 0;
    int64_t             diff   = 0;
    int64_t             target = 0;

    test("Create mutex: ");
    s = natsMutex_Create(&m);
    testCond(s == NATS_OK);

    test("Create condition variables: ");
    s = natsCondition_Create(&c1);
    IFOK(s, natsCondition_Create(&c2));
    testCond(s == NATS_OK);

    natsMutex_Lock(m);

    tArgs.m       = m;
    tArgs.c       = c1;
    tArgs.control = 0;

    test("Create thread: ");
    s = natsThread_Create(&t1, testSignal, &tArgs);
    testCond(s == NATS_OK);

    test("Wait for signal: ");
    while (tArgs.control != 1)
        natsCondition_Wait(c1, m);

    natsThread_Join(t1);
    natsThread_Destroy(t1);
    t1 = NULL;
    testCond(tArgs.control == 1);

    test("Wait timeout: ");
    before = nats_Now();
    s = natsCondition_TimedWait(c1, m, 1000);
    diff = (nats_Now() - before);
    testCond((s == NATS_TIMEOUT)
             && (diff >= 985) && (diff <= 1015));

    test("Wait timeout with 0: ");
    before = nats_Now();
    s = natsCondition_TimedWait(c1, m, 0);
    diff = (nats_Now() - before);
    testCond((s == NATS_TIMEOUT)
             && (diff >= 0) && (diff <= 10));

    test("Wait timeout with negative: ");
    before = nats_Now();
    s = natsCondition_TimedWait(c1, m, -10);
    diff = (nats_Now() - before);
    testCond((s == NATS_TIMEOUT)
             && (diff >= 0) && (diff <= 10));

    test("Wait absolute time: ");
    before = nats_Now();
    target = nats_setTargetTime(1000);
    s = natsCondition_AbsoluteTimedWait(c1, m, target);
    diff = (nats_Now() - before);
    testCond((s == NATS_TIMEOUT)
             && (diff >= 985) && (diff <= 1015));

    test("Wait absolute time in the past: ");
    before = nats_Now();
    target = nats_setTargetTime(-1000);
    s = natsCondition_AbsoluteTimedWait(c1, m, target);
    diff = (nats_Now() - before);
    testCond((s == NATS_TIMEOUT)
             && (diff >= 0) && (diff <= 10));

    test("Wait absolute time with very large value: ");
    tArgs.control = 0;
    s = natsThread_Create(&t1, _unblockLongWait, &tArgs);
    if (s == NATS_OK)
    {
        before = nats_Now();
        target = nats_setTargetTime(0x7FFFFFFFFFFFFFFF);
        s = natsCondition_AbsoluteTimedWait(c1, m, target);
        diff = (nats_Now() - before);
    }
    testCond((s == NATS_OK)
             && (diff >= 400) && (diff <= 600));

    natsThread_Join(t1);
    natsThread_Destroy(t1);
    t1 = NULL;

    test("Signal before wait: ");
    tArgs.control = 0;

    test("Create thread: ");
    s = natsThread_Create(&t1, testSignal, &tArgs);
    testCond(s == NATS_OK);

    while (tArgs.control == 0)
    {
        natsMutex_Unlock(m);
        nats_Sleep(1000);
        natsMutex_Lock(m);
    }

    s = natsCondition_TimedWait(c1, m, 1000);
    testCond(s == NATS_TIMEOUT);

    natsThread_Join(t1);
    natsThread_Destroy(t1);
    t1 = NULL;

    test("Broadcast: ");
    tArgs.control = 0;
    tArgs.sum     = 0;
    tArgs.b       = c2;

    s = natsThread_Create(&t1, testBroadcast, &tArgs);
    IFOK(s, natsThread_Create(&t2, testBroadcast, &tArgs));
    if (s != NATS_OK)
    {
        natsMutex_Unlock(m);
        FAIL("Unable to run test_natsCondition because got an error while creating thread!");
    }

    while (tArgs.sum != 2)
        natsCondition_Wait(c1, m);

    natsMutex_Unlock(m);

    nats_Sleep(1000);

    natsMutex_Lock(m);

    tArgs.control = 1;
    natsCondition_Broadcast(c2);

    natsMutex_Unlock(m);

    natsThread_Join(t1);
    natsThread_Destroy(t1);
    t1 = NULL;

    natsThread_Join(t2);
    natsThread_Destroy(t2);
    t2 = NULL;

    testCond(tArgs.sum == 0);

    test("Destroy condition: ");
    natsCondition_Destroy(c1);
    natsCondition_Destroy(c2);
    testCond(1);

    natsMutex_Destroy(m);
}

static void
testTimerCb(natsTimer *timer, void *arg)
{
    struct threadArg *tArg = (struct threadArg*) arg;

    natsMutex_Lock(tArg->m);

    tArg->timerFired++;
    natsCondition_Signal(tArg->c);

    natsMutex_Unlock(tArg->m);

    if (tArg->control == 1)
        natsTimer_Reset(timer, 500);
    else if (tArg->control == 2)
        natsTimer_Stop(timer);
    else if (tArg->control == 3)
        nats_Sleep(500);

    natsMutex_Lock(tArg->m);

    natsCondition_Signal(tArg->c);

    natsMutex_Unlock(tArg->m);
}

static void
stopTimerCb(natsTimer *timer, void *arg)
{
    struct threadArg *tArg = (struct threadArg*) arg;

    natsMutex_Lock(tArg->m);

    tArg->timerStopped++;
    natsCondition_Signal(tArg->c);

    natsMutex_Unlock(tArg->m);
}

static void
_dummyTimerCB(natsTimer *timer, void *arg) {}

static void
_timerStopCB(natsTimer *timer, void *arg)
{
    natsTimer_Release(timer);
}

#define STOP_TIMER_AND_WAIT_STOPPED \
        natsTimer_Stop(t); \
        natsMutex_Lock(tArg.m); \
        while (tArg.timerStopped == 0) \
            natsCondition_Wait(tArg.c, tArg.m); \
        natsMutex_Unlock(tArg.m)

void test_natsTimer(void)
{
    natsStatus          s;
    natsTimer           *t = NULL;
    struct threadArg    tArg;
    int                 refs;

    test("Setup test: ");
    s = _createDefaultThreadArgsForCbTests(&tArg);
    testCond(s == NATS_OK);

    tArg.control      = 0;
    tArg.timerFired   = 0;
    tArg.timerStopped = 0;

    test("Create timer: ");
    s = natsTimer_Create(&t, testTimerCb, stopTimerCb, 400, &tArg);
    testCond(s == NATS_OK);

    test("Stop timer: ");
    tArg.control = 0;
    natsTimer_Stop(t);
    nats_Sleep(600);
    natsMutex_Lock(t->mu);
    refs = t->refs;
    natsMutex_Unlock(t->mu);
    natsMutex_Lock(tArg.m);
    testCond((tArg.timerFired == 0)
             && (tArg.timerStopped == 1)
             && (refs == 1)
             && (nats_getTimersCount() == 0));
    natsMutex_Unlock(tArg.m);

    test("Firing of timer: ")
    tArg.control      = 0;
    tArg.timerStopped = 0;
    natsTimer_Reset(t, 200);
    nats_Sleep(1100);
    natsTimer_Stop(t);
    nats_Sleep(600);
    natsMutex_Lock(t->mu);
    refs = t->refs;
    natsMutex_Unlock(t->mu);
    natsMutex_Lock(tArg.m);
    testCond((tArg.timerFired > 0)
             && (tArg.timerFired <= 5)
             && (tArg.timerStopped == 1)
             && (refs == 1)
             && (nats_getTimersCount() == 0));
    natsMutex_Unlock(tArg.m);

    test("Stop stopped timer: ");
    tArg.control      = 0;
    tArg.timerFired   = 0;
    tArg.timerStopped = 0;
    natsTimer_Reset(t, 100);
    nats_Sleep(300);
    natsTimer_Stop(t);
    nats_Sleep(100);
    natsTimer_Stop(t);
    nats_Sleep(100);
    natsMutex_Lock(t->mu);
    refs = t->refs;
    natsMutex_Unlock(t->mu);
    natsMutex_Lock(tArg.m);
    testCond((tArg.timerFired > 0)
             && (tArg.timerStopped == 1)
             && (refs == 1)
             && (nats_getTimersCount() == 0));
    natsMutex_Unlock(tArg.m);

    tArg.control      = 1;
    tArg.timerFired   = 0;
    tArg.timerStopped = 0;
    test("Reset from callback: ");
    natsTimer_Reset(t, 250);
    nats_Sleep(900);
    natsTimer_Stop(t);
    nats_Sleep(600);
    natsMutex_Lock(t->mu);
    refs = t->refs;
    natsMutex_Unlock(t->mu);
    natsMutex_Lock(tArg.m);
    testCond((tArg.timerFired == 2)
             && (tArg.timerStopped == 1)
             && (refs == 1)
             && nats_getTimersCount() == 0);
    natsMutex_Unlock(tArg.m);

    tArg.control      = 0;
    tArg.timerFired   = 0;
    tArg.timerStopped = 0;
    test("Multiple Reset: ");
    natsTimer_Reset(t, 1000);
    natsTimer_Reset(t, 800);
    natsTimer_Reset(t, 200);
    natsTimer_Reset(t, 500);
    nats_Sleep(600);
    natsMutex_Lock(t->mu);
    refs = t->refs;
    natsMutex_Unlock(t->mu);
    natsMutex_Lock(tArg.m);
    testCond((tArg.timerFired == 1)
             && (tArg.timerStopped == 0)
             && (refs == 1)
             && nats_getTimersCount() == 1);
    natsMutex_Unlock(tArg.m);

    STOP_TIMER_AND_WAIT_STOPPED;

    tArg.control      = 3;
    tArg.timerFired   = 0;
    tArg.timerStopped = 0;
    test("Check refs while in callback: ");
    natsTimer_Reset(t, 1);

    // Wait that it is in callback
    natsMutex_Lock(tArg.m);
    while (tArg.timerFired != 1)
        natsCondition_Wait(tArg.c, tArg.m);
    natsMutex_Unlock(tArg.m);

    natsMutex_Lock(t->mu);
    refs = t->refs;
    natsMutex_Unlock(t->mu);
    testCond((refs == 2)
             && nats_getTimersCountInList() == 0
             && nats_getTimersCount() == 1);

    STOP_TIMER_AND_WAIT_STOPPED;

    tArg.control        = 2;
    tArg.timerFired     = 0;
    tArg.timerStopped   = 0;
    test("Stop from callback: ");
    natsTimer_Reset(t, 250);
    nats_Sleep(500);
    natsMutex_Lock(t->mu);
    refs = t->refs;
    natsMutex_Unlock(t->mu);
    natsMutex_Lock(tArg.m);
    testCond((tArg.timerFired == 1)
             && (tArg.timerStopped == 1)
             && (refs == 1)
             && (nats_getTimersCount() == 0));
    natsMutex_Unlock(tArg.m);

    tArg.control        = 3;
    tArg.timerFired     = 0;
    tArg.timerStopped   = 0;
    test("Slow callback: ");
    natsTimer_Reset(t, 100);
    nats_Sleep(800);
    natsTimer_Stop(t);
    nats_Sleep(500);
    natsMutex_Lock(t->mu);
    refs = t->refs;
    natsMutex_Unlock(t->mu);
    natsMutex_Lock(tArg.m);
    testCond((tArg.timerFired <= 3)
             && (tArg.timerStopped == 1)
             && (refs == 1)
             && (nats_getTimersCount() == 0));
    natsMutex_Unlock(tArg.m);

    tArg.control        = 3;
    tArg.timerFired     = 0;
    tArg.timerStopped   = 0;
    test("Stopped while in callback: ");
    natsTimer_Reset(t, 100);
    nats_Sleep(200);
    natsTimer_Stop(t);
    nats_Sleep(700);
    natsMutex_Lock(t->mu);
    refs = t->refs;
    natsMutex_Unlock(t->mu);
    natsMutex_Lock(tArg.m);
    testCond((tArg.timerFired == 1)
             && (tArg.timerStopped == 1)
             && (refs == 1)
             && (nats_getTimersCount() == 0));
    natsMutex_Unlock(tArg.m);

    tArg.control        = 4;
    tArg.timerFired     = 0;
    tArg.timerStopped   = 0;
    test("Use very large timeout: ");
    natsTimer_Reset(t, 0x7FFFFFFFFFFFFFFF);
    nats_Sleep(200);
    natsTimer_Stop(t);
    natsMutex_Lock(t->mu);
    refs = t->refs;
    natsMutex_Unlock(t->mu);
    natsMutex_Lock(tArg.m);
    testCond((tArg.timerFired == 0)
             && (tArg.timerStopped == 1)
             && (refs == 1)
             && (nats_getTimersCount() == 0));
    natsMutex_Unlock(tArg.m);

    test("Destroy timer: ");
    natsMutex_Lock(t->mu);
    t->refs++;
    natsMutex_Unlock(t->mu);
    natsTimer_Destroy(t);
    natsMutex_Lock(t->mu);
    refs = t->refs;
    natsMutex_Unlock(t->mu);
    testCond(refs == 1);
    natsTimer_Release(t);

    _destroyDefaultThreadArgs(&tArg);

    // Create a timer that will not be stopped here to exercise
    // code that cleans up timers when library is unloaded.
    test("Create timer: ");
    s = natsTimer_Create(&t, _dummyTimerCB, _timerStopCB, 1000, NULL);
    testCond(s == NATS_OK);
}

void test_natsUrl(void)
{
    natsStatus  s;
    natsUrl     *u = NULL;

    test("NULL: ");
    s = natsUrl_Create(&u, NULL);
    testCond((s != NATS_OK) && (u == NULL));

    test("EMPTY: ");
    s = natsUrl_Create(&u, "");
    testCond((s != NATS_OK) && (u == NULL));

    nats_clearLastError();

    test("'localhost:':");
    s = natsUrl_Create(&u, "localhost:");
    testCond((s == NATS_OK)
            && (u != NULL)
            && (u->host != NULL)
            && (strcmp(u->host, "localhost") == 0)
            && (u->username == NULL)
            && (u->password == NULL)
            && (u->port == 4222)
            && (strcmp(u->fullUrl, "nats://localhost:4222") == 0));
    natsUrl_Destroy(u);
    u = NULL;

    test("'localhost:4223':");
    s = natsUrl_Create(&u, "localhost:4223");
    testCond((s == NATS_OK)
            && (u != NULL)
            && (u->host != NULL)
            && (strcmp(u->host, "localhost") == 0)
            && (u->username == NULL)
            && (u->password == NULL)
            && (u->port == 4223)
            && (strcmp(u->fullUrl, "nats://localhost:4223") == 0));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://localhost:4222':");
    s = natsUrl_Create(&u, "tcp://localhost:4222");
    testCond((s == NATS_OK)
            && (u != NULL)
            && (u->host != NULL)
            && (strcmp(u->host, "localhost") == 0)
            && (u->username == NULL)
            && (u->password == NULL)
            && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://localhost':");
    s = natsUrl_Create(&u, "tcp://localhost");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "localhost") == 0)
              && (u->username == NULL)
              && (u->password == NULL)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'localhost':");
    s = natsUrl_Create(&u, "localhost");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "localhost") == 0)
              && (u->username == NULL)
              && (u->password == NULL)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://[::1]:4222':");
    s = natsUrl_Create(&u, "tcp://[::1]:4222");
    testCond((s == NATS_OK)
            && (u != NULL)
            && (u->host != NULL)
            && (strcmp(u->host, "[::1]") == 0)
            && (u->username == NULL)
            && (u->password == NULL)
            && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://[::1]:':");
    s = natsUrl_Create(&u, "tcp://[::1]:");
    testCond((s == NATS_OK)
            && (u != NULL)
            && (u->host != NULL)
            && (strcmp(u->host, "[::1]") == 0)
            && (u->username == NULL)
            && (u->password == NULL)
            && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://[::1]':");
    s = natsUrl_Create(&u, "tcp://[::1]");
    testCond((s == NATS_OK)
            && (u != NULL)
            && (u->host != NULL)
            && (strcmp(u->host, "[::1]") == 0)
            && (u->username == NULL)
            && (u->password == NULL)
            && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://':");
    s = natsUrl_Create(&u, "tcp://");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "localhost") == 0)
              && (u->username == NULL)
              && (u->password == NULL)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://:':");
    s = natsUrl_Create(&u, "tcp://:");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "localhost") == 0)
              && (u->username == NULL)
              && (u->password == NULL)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://ivan:localhost:4222':");
    s = natsUrl_Create(&u, "tcp://ivan:localhost:4222");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "ivan:localhost") == 0)
              && (u->username == NULL)
              && (u->password == NULL)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://ivan:pwd:localhost:4222':");
    s = natsUrl_Create(&u, "tcp://ivan:pwd:localhost:4222");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "ivan:pwd:localhost") == 0)
              && (u->username == NULL)
              && (u->password == NULL)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://ivan@localhost:4222':");
    s = natsUrl_Create(&u, "tcp://ivan@localhost:4222");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "localhost") == 0)
              && (u->username != NULL)
              && (strcmp(u->username, "ivan") == 0)
              && (u->password == NULL)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://ivan:pwd@localhost:4222':");
    s = natsUrl_Create(&u, "tcp://ivan:pwd@localhost:4222");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "localhost") == 0)
              && (u->username != NULL)
              && (strcmp(u->username, "ivan") == 0)
              && (u->password != NULL)
              && (strcmp(u->password, "pwd") == 0)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://ivan:pwd@localhost':");
    s = natsUrl_Create(&u, "tcp://ivan:pwd@localhost");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "localhost") == 0)
              && (u->username != NULL)
              && (strcmp(u->username, "ivan") == 0)
              && (u->password != NULL)
              && (strcmp(u->password, "pwd") == 0)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://@localhost:4222':");
    s = natsUrl_Create(&u, "tcp://@localhost:4222");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "localhost") == 0)
              && (u->username == NULL)
              && (u->password == NULL)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://@@localhost:4222':");
    s = natsUrl_Create(&u, "tcp://@@localhost:4222");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "localhost") == 0)
              && (u->username != NULL)
              && (strcmp(u->username, "@") == 0)
              && (u->password == NULL)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://a:b:c@localhost:4222':");
    s = natsUrl_Create(&u, "tcp://a:b:c@localhost:4222");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "localhost") == 0)
              && (u->username != NULL)
              && (strcmp(u->username, "a") == 0)
              && (u->password != NULL)
              && (strcmp(u->password, "b:c") == 0)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://::a:b:c@localhost:4222':");
    s = natsUrl_Create(&u, "tcp://::a:b:c@localhost:4222");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "localhost") == 0)
              && (u->username == NULL)
              && (u->password != NULL)
              && (strcmp(u->password, ":a:b:c") == 0)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://a:b@[::1]:4222':");
    s = natsUrl_Create(&u, "tcp://a:b@[::1]:4222");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "[::1]") == 0)
              && (u->username != NULL)
              && (strcmp(u->username, "a") == 0)
              && (u->password != NULL)
              && (strcmp(u->password, "b") == 0)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://a@[::1]:4222':");
    s = natsUrl_Create(&u, "tcp://a@[::1]:4222");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "[::1]") == 0)
              && (u->username != NULL)
              && (strcmp(u->username, "a") == 0)
              && (u->password == NULL)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://a:b@[::1]:4222':");
    s = natsUrl_Create(&u, "tcp://a:b@[::1]:4222");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "[::1]") == 0)
              && (u->username != NULL)
              && (strcmp(u->username, "a") == 0)
              && (u->password != NULL)
              && (strcmp(u->password, "b") == 0)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://a:b@[::1]':");
    s = natsUrl_Create(&u, "tcp://a:b@[::1]");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "[::1]") == 0)
              && (u->username != NULL)
              && (strcmp(u->username, "a") == 0)
              && (u->password != NULL)
              && (strcmp(u->password, "b") == 0)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("' tcp://localhost:4222':");
    s = natsUrl_Create(&u, " tcp://localhost:4222");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "localhost") == 0)
              && (u->username == NULL)
              && (u->password == NULL)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://localhost:4222 ':");
    s = natsUrl_Create(&u, "tcp://localhost:4222 ");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "localhost") == 0)
              && (u->username == NULL)
              && (u->password == NULL)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("' tcp://localhost:4222 ':");
    s = natsUrl_Create(&u, " tcp://localhost:4222 ");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "localhost") == 0)
              && (u->username == NULL)
              && (u->password == NULL)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'nats://localhost:4222/subject':");
    s = natsUrl_Create(&u, " nats://localhost:4222/subject");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "localhost") == 0)
              && (u->username == NULL)
              && (u->password == NULL)
              && (u->port == 4222)
              && (strcmp(u->fullUrl, "nats://localhost:4222/subject") == 0));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://%4C%65v%00ignored:p%77d%00ignoredalso@localhost':");
    s = natsUrl_Create(&u, "tcp://%4C%65v%00ignored:p%77d%00ignoredalso@localhost");
    testCond((s == NATS_OK)
              && (u != NULL)
              && (u->host != NULL)
              && (strcmp(u->host, "localhost") == 0)
              && (u->username != NULL)
              && (strcmp(u->username, "Lev") == 0)
              && (u->password != NULL)
              && (strcmp(u->password, "pwd") == 0)
              && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://%4C%65v:p%77d@localhost':");
    s = natsUrl_Create(&u, "tcp://%4C%65v:p%77d@localhost");
    testCond((s == NATS_OK) && (u != NULL) && (u->host != NULL) && (strcmp(u->host, "localhost") == 0) && (u->username != NULL) && (strcmp(u->username, "Lev") == 0) && (u->password != NULL) && (strcmp(u->password, "pwd") == 0) && (u->port == 4222));
    natsUrl_Destroy(u);
    u = NULL;

    test("'tcp://%4c%65v:p%@localhost':");
    s = natsUrl_Create(&u, "tcp://%4c%65v:p%@localhost");
    testCond((s == NATS_ERR) && (u == NULL) && (strstr(nats_GetLastError(NULL), "invalid percent encoding in URL: p%") != NULL));
    nats_clearLastError();

    test("'tcp://%4H%65v:p%@localhost':");
    s = natsUrl_Create(&u, "tcp://%4H%65v:p%@localhost");
    testCond((s == NATS_ERR) && (u == NULL) && (strstr(nats_GetLastError(NULL), "invalid percent encoding in URL: %4H%65v") != NULL));
    nats_clearLastError();

    test("'tcp://localhost: 4222':");
    s = natsUrl_Create(&u, "tcp://localhost: 4222");
    testCond((s == NATS_INVALID_ARG)
              && (u == NULL)
              && (strstr(nats_GetLastError(NULL), "invalid port ' 4222'") != NULL));
    nats_clearLastError();

    test("'tcp://localhost:a4222':");
    s = natsUrl_Create(&u, "tcp://localhost:a4222");
    testCond((s == NATS_INVALID_ARG)
              && (u == NULL)
              && (strstr(nats_GetLastError(NULL), "invalid port 'a4222'") != NULL));
    nats_clearLastError();

    test("'tcp://localhost:2147483648':");
    s = natsUrl_Create(&u, "tcp://localhost:2147483648");
    testCond((s == NATS_INVALID_ARG)
              && (u == NULL)
              && (strstr(nats_GetLastError(NULL), "invalid port '2147483648'") != NULL));
}

void test_natsCreateStringFromBuffer(void)
{
    natsStatus  s = NATS_OK;
    natsBuffer  buf;
    char        *str = NULL;

    test("NULL buffer: ");
    s = nats_CreateStringFromBuffer(&str, NULL);
    testCond((s == NATS_OK)
             && (str == NULL))

    test("Init buffer: ");
    s = natsBuf_Init(&buf, 10);
    testCond(s == NATS_OK);

    test("Empty buffer: ");
    s = nats_CreateStringFromBuffer(&str, &buf);
    testCond((s == NATS_OK)
             && (str == NULL))

    test("Append to buf: ");
    s = natsBuf_Append(&buf, "123", 3);
    testCond(s == NATS_OK);

    test("Buffer containing '123': ");
    s = nats_CreateStringFromBuffer(&str, &buf);
    testCond((s == NATS_OK)
             && (str != NULL)
             && (strlen(str) == 3)
             && (strcmp(str, "123") == 0));

    test("Destroying the buffer does not affect the created string: ");
    natsBuf_Cleanup(&buf);
    testCond((str != NULL)
             && (strlen(str) == 3)
             && (strcmp(str, "123") == 0));

    free(str);
}

#define INBOX_THREADS_COUNT     (10)
#define INBOX_COUNT_PER_THREAD  (10000)
#define INBOX_TOTAL             (INBOX_THREADS_COUNT * INBOX_COUNT_PER_THREAD)

static void
_testInbox(void *closure)
{
    struct threadArg    *args = (struct threadArg*) closure;
    natsStatus          s     = NATS_OK;
    natsInbox           *inbox;
    void                *oldValue;

    for (int i=0; (s == NATS_OK) && (i<INBOX_COUNT_PER_THREAD); i++)
    {
        inbox    = NULL;
        oldValue = NULL;

        s = natsInbox_Create(&inbox);
        if (s == NATS_OK)
            s = natsStrHash_Set(args->inboxes, inbox, true, (void*) 1, (void**) &oldValue);
        if ((s == NATS_OK) && (oldValue != NULL))
        {
            printf("Duplicate inbox: %s\n", inbox);
            s = NATS_ERR;
        }

        natsInbox_Destroy(inbox);
    }

    args->status = s;
}

void test_natsInbox(void)
{
    natsStatus          s      = NATS_OK;
    natsThread          *threads[INBOX_THREADS_COUNT];
    struct threadArg    args[INBOX_THREADS_COUNT];
    int                 i, j;
    natsInbox           *key;
    void                *oldInbox;
    natsStrHash         *inboxes = NULL;
    natsStrHashIter     iter;

    test("Test inboxes are unique: ");
    for (i=0; i<INBOX_THREADS_COUNT; i++)
    {
        args[i].status = NATS_OK;
        args[i].inboxes    = NULL;
        threads[i]         = NULL;
    }

    s = natsStrHash_Create(&inboxes, 16);

    for (i=0; (s == NATS_OK) && (i<INBOX_THREADS_COUNT); i++)
    {
        s = natsStrHash_Create(&(args[i].inboxes), 16);
        if (s == NATS_OK)
            s = natsThread_Create(&(threads[i]), _testInbox, &(args[i]));
    }

    for (i=0; (i<INBOX_THREADS_COUNT); i++)
    {
        natsThread_Join(threads[i]);

        if (s == NATS_OK)
            s = args[i].status;
        if (s == NATS_OK)
        {
            j = 0;

            natsStrHashIter_Init(&iter, args[i].inboxes);
            while ((s == NATS_OK)
                    && natsStrHashIter_Next(&iter, &key, NULL))
            {
                j++;

                s = natsStrHash_Set(inboxes, key, true, (void*) 1, &oldInbox);

                natsStrHashIter_RemoveCurrent(&iter);
            }

            if (j != INBOX_COUNT_PER_THREAD)
                s = NATS_ERR;

            natsStrHashIter_Done(&iter);
        }

        natsThread_Destroy(threads[i]);
    }
    testCond(s == NATS_OK);

    for (i=0; i<INBOX_THREADS_COUNT; i++)
        natsStrHash_Destroy(args[i].inboxes);

    natsStrHash_Destroy(inboxes);
}

static int HASH_ITER = 10000000;

void test_natsHashing(void)
{
    const char *keys[] = {"foo",
                          "bar",
                          "apcera.continuum.router.foo.bar",
                          "apcera.continuum.router.foo.bar.baz"};
    const char *longKey = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@$#%^&*()";

    uint32_t results[] = {1058908168, 1061739001, 4242539713, 3332038527};
    uint32_t r = 0;
    uint32_t lr = 0;
    natsStatus s = NATS_OK;
    int64_t start, end;
    int sizeLongKey = (int) strlen(longKey);

    if (valgrind)
        HASH_ITER = 10000;

    test("Test hashing algo: ");
    for (int i=0; i<(int)(sizeof(keys)/sizeof(char*)); i++)
    {
        r = natsStrHash_Hash(keys[i], (int) strlen(keys[i]));
        if (r != results[i])
        {
            printf("Expected: %u got: %u\n", results[i], r);
            s = NATS_ERR;
            break;
        }
    }
    testCond(s == NATS_OK);

    test("Hashing performance: ");
    s = NATS_OK;
    start = nats_Now();
    for (int i=0; i<HASH_ITER; i++)
    {
        r = natsStrHash_Hash(longKey, sizeLongKey);
        if ((i > 0) && (r != lr))
        {
            s = NATS_ERR;
            break;
        }
        lr = r;
    }
    end = nats_Now();
    testCond((s == NATS_OK) && ((end - start) < 1000));
}

void test_natsHash(void)
{
    natsStatus  s;
    natsHash    *hash = NULL;
    const char  *t1 = "this is a test";
    const char  *t2 = "this is another test";
    void        *oldval = NULL;
    int         lastNumBkts = 0;
    int         i;
    int64_t     key;
    int         values[40];
    natsHashIter iter;

    for (int i=0; i<40; i++)
        values[i] = (i+1);

    test("Create hash with invalid 0 size: ");
    s = natsHash_Create(&hash, 0);
    testCond((s != NATS_OK) && (hash == NULL));

    test("Create hash with invalid negative size: ");
    s = natsHash_Create(&hash, -2);
    testCond((s != NATS_OK) && (hash == NULL));

    nats_clearLastError();

    test("Create hash ok: ");
    s = natsHash_Create(&hash, 7);
    testCond((s == NATS_OK) && (hash != NULL) && (hash->used == 0)
             && (hash->numBkts == 8));

    test("Set: ");
    s = natsHash_Set(hash, 1234, (void*) t1, &oldval);
    testCond((s == NATS_OK) && (oldval == NULL) && (hash->used == 1));

    test("Set, get old value: ");
    s = natsHash_Set(hash, 1234, (void*) t2, &oldval);
    testCond((s == NATS_OK) && (oldval == t1) && (hash->used == 1))

    test("Get, not found: ");
    oldval = NULL;
    oldval = natsHash_Get(hash, 3456);
    testCond(oldval == NULL);

    test("Get, found: ");
    oldval = NULL;
    oldval = natsHash_Get(hash, 1234);
    testCond(oldval == t2);

    test("Remove, not found: ");
    oldval = NULL;
    oldval = natsHash_Remove(hash, 3456);
    testCond(oldval == NULL);

    test("Remove, found: ");
    oldval = NULL;
    oldval = natsHash_Remove(hash, 1234);
    testCond((oldval == t2) && (hash->used == 0));

    test("Test collision: ");
    oldval = NULL;
    s = natsHash_Set(hash, 2, (void*) t1, &oldval);
    if ((s == NATS_OK) && (oldval == NULL))
        s = natsHash_Set(hash, 10, (void*) t2, &oldval);
    testCond((s == NATS_OK)
             && (oldval == NULL)
             && (hash->used == 2)
             && (hash->bkts[2] != NULL)
             && (hash->bkts[2]->key == 10)
             && (hash->bkts[2]->next != NULL)
             && (hash->bkts[2]->next->key == 2));

    test("Remove from collisions (front to back): ");
    oldval = NULL;
    oldval = natsHash_Remove(hash, 10);
    if (oldval != t2)
        s = NATS_ERR;
    if (s == NATS_OK)
    {
        oldval = natsHash_Remove(hash, 2);
        if (oldval != t1)
            s = NATS_ERR;
    }
    testCond((s == NATS_OK) && (hash->used == 0));

    test("Remove from collisions (back to front): ");
    oldval = NULL;
    s = natsHash_Set(hash, 2, (void*) t1, &oldval);
    if ((s == NATS_OK) && (oldval == NULL))
        s = natsHash_Set(hash, 10, (void*) t2, &oldval);
    if (s == NATS_OK)
    {
        oldval = natsHash_Remove(hash, 2);
        if (oldval != t1)
            s = NATS_ERR;
    }
    if (s == NATS_OK)
    {
        oldval = natsHash_Remove(hash, 10);
        if (oldval != t2)
            s = NATS_ERR;
    }
    testCond((s == NATS_OK) && (hash->used == 0));

    test("Grow: ");
    for (int i=0; i<40; i++)
    {
        s = natsHash_Set(hash, (i+1), &(values[i]), &oldval);
        if (oldval != NULL)
            s = NATS_ERR;
        if (s != NATS_OK)
            break;
    }
    if (s == NATS_OK)
    {
        for (int i=0; i<40; i++)
        {
            oldval = natsHash_Get(hash, (i+1));
            if ((oldval == NULL)
                || ((*(int*)oldval) != values[i]))
            {
                s = NATS_ERR;
                break;
            }
        }
    }
    testCond((s == NATS_OK)
             && (hash->used == 40)
             && (hash->numBkts > 8));
    lastNumBkts = hash->numBkts;

    test("Shrink: ");
    for (int i=0; i<31; i++)
    {
        oldval = natsHash_Remove(hash, (i+1));
        if ((oldval == NULL)
            || ((*(int*)oldval) != values[i]))
        {
            s = NATS_ERR;
            break;
        }
    }
    testCond((s == NATS_OK)
             && (hash->used == 9)
             && (hash->numBkts < lastNumBkts));

    test("Iterator: ");
    natsHashIter_Init(&iter, hash);
    i = 0;
    while (natsHashIter_Next(&iter, &key, &oldval))
    {
        i++;
        if (((key < 31) || (key > 40))
            || (oldval == NULL)
            || ((*(int*)oldval) != values[key-1]))
        {
            s = NATS_ERR;
            break;
        }
    }
    natsHashIter_Done(&iter);
    testCond((s == NATS_OK) && (i == natsHash_Count(hash)));

    test("Iterator, remove current: ");
    natsHashIter_Init(&iter, hash);
    while (natsHashIter_Next(&iter, &key, NULL))
    {
        s = natsHashIter_RemoveCurrent(&iter);
        if (s != NATS_OK)
            break;
    }
    testCond((s == NATS_OK)
             && (natsHash_Count(hash) == 0)
             && (hash->canResize == false)
             && (hash->numBkts > 8));

    natsHashIter_Done(&iter);

    test("Grow again: ");
    oldval = NULL;
    for (int i=0; i<40; i++)
    {
        s = natsHash_Set(hash, (i+1), &(values[i]), &oldval);
        if (oldval != NULL)
            s = NATS_ERR;
        if (s != NATS_OK)
            break;
    }
    testCond((s == NATS_OK)
             && (hash->used == 40)
             && (hash->numBkts > 8));
    lastNumBkts = hash->numBkts;

    test("Iterator, remove current, hash does not shrink: ");
    natsHashIter_Init(&iter, hash);
    i = 0;
    while (natsHashIter_Next(&iter, &key, NULL))
    {
        s = natsHashIter_RemoveCurrent(&iter);
        if ((s != NATS_OK) || (++i == 31))
            break;
    }
    testCond((s == NATS_OK)
             && (natsHash_Count(hash) == 9)
             && (hash->canResize == false)
             && (hash->numBkts == lastNumBkts));

    natsHashIter_Done(&iter);

    test("After iterator done, shrink works: ");
    oldval = NULL;
    s = natsHash_Set(hash, 100, (void*) "last", &oldval);
    if ((s == NATS_OK) && (oldval == NULL))
    {
        oldval = natsHash_Remove(hash, 100);
        if ((oldval == NULL)
            || (strcmp((const char*) oldval, "last") != 0))
        {
            s = NATS_ERR;
        }
    }
    testCond((s == NATS_OK)
             && hash->canResize
             && (hash->numBkts != lastNumBkts));

    test("Destroy: ");
    natsHash_Destroy(hash);
    hash = NULL;
    testCond(1);

    test("Create new: ");
    s = natsHash_Create(&hash, 4);
    testCond(s == NATS_OK);

    test("Populate: ");
    s = natsHash_Set(hash, 1, (void*) 1, NULL);
    IFOK(s, natsHash_Set(hash, 2, (void*) 2, NULL));
    IFOK(s, natsHash_Set(hash, 3, (void*) 3, NULL));
    testCond(s == NATS_OK);

    test("Remove one: ");
    s = (natsHash_Remove(hash, 2) == (void*) 2) ? NATS_OK : NATS_ERR;
    testCond(s == NATS_OK);

    test("RemoveSingle fails if more than one: ");
    s = natsHash_RemoveSingle(hash, &key, NULL);
    testCond(s == NATS_ERR);
    nats_clearLastError();

    test("Remove second: ");
    s = (natsHash_Remove(hash, 1) == (void*) 1) ? NATS_OK : NATS_ERR;
    testCond(s == NATS_OK);

    test("Remove single: ");
    key = 0;
    oldval = NULL;
    s = natsHash_RemoveSingle(hash, &key, &oldval);
    testCond((s == NATS_OK)
                && (hash->used == 0)
                && (key == 3)
                && (oldval == (void*) 3));

    natsHash_Destroy(hash);
}

void test_natsStrHash(void)
{
    natsStatus  s;
    natsStrHash *hash = NULL;
    const char  *t1 = "this is a test";
    const char  *t2 = "this is another test";
    void        *oldval = NULL;
    char        *myKey = NULL;
    int         lastNumBkts = 0;
    int         i;
    char        *key;
    int         values[40];
    char        k[64];
    uint32_t    hk;
    natsStrHashIter iter;

    for (int i=0; i<40; i++)
        values[i] = (i+1);

    test("Create hash with invalid 0 size: ");
    s = natsStrHash_Create(&hash, 0);
    testCond((s != NATS_OK) && (hash == NULL));

    test("Create hash with invalid negative size: ");
    s = natsStrHash_Create(&hash, -2);
    testCond((s != NATS_OK) && (hash == NULL));

    nats_clearLastError();

    test("Create hash ok: ");
    s = natsStrHash_Create(&hash, 7);
    testCond((s == NATS_OK) && (hash != NULL) && (hash->used == 0)
             && (hash->numBkts == 8));

    test("Set: ");
    s = natsStrHash_Set(hash, (char*) "1234", false, (void*) t1, &oldval);
    testCond((s == NATS_OK) && (oldval == NULL) && (hash->used == 1));

    test("Set, get old value: ");
    s = natsStrHash_Set(hash, (char*) "1234", false, (void*) t2, &oldval);
    testCond((s == NATS_OK) && (oldval == t1) && (hash->used == 1))

    test("Get, not found: ");
    oldval = NULL;
    oldval = natsStrHash_Get(hash, (char*) "3456");
    testCond(oldval == NULL);

    test("Get, found: ");
    oldval = NULL;
    oldval = natsStrHash_Get(hash, (char*) "1234");
    testCond(oldval == t2);

    test("Remove, not found: ");
    oldval = NULL;
    oldval = natsStrHash_Remove(hash, (char*) "3456");
    testCond(oldval == NULL);

    test("Remove, found: ");
    oldval = NULL;
    oldval = natsStrHash_Remove(hash, (char*) "1234");
    testCond((oldval == t2) && (hash->used == 0));

    test("Grow: ");
    for (int i=0; i<40; i++)
    {
        snprintf(k, sizeof(k), "%d", (i+1));
        s = natsStrHash_Set(hash, k, true, &(values[i]), &oldval);
        if (oldval != NULL)
            s = NATS_ERR;
        if (s != NATS_OK)
            break;
    }
    if (s == NATS_OK)
    {
        for (int i=0; i<40; i++)
        {
            snprintf(k, sizeof(k), "%d", (i+1));
            oldval = natsStrHash_Get(hash, k);
            if ((oldval == NULL)
                || ((*(int*)oldval) != values[i]))
            {
                s = NATS_ERR;
                break;
            }
        }
    }
    testCond((s == NATS_OK)
             && (hash->used == 40)
             && (hash->numBkts > 8));
    lastNumBkts = hash->numBkts;

    test("Shrink: ");
    for (int i=0; i<31; i++)
    {
        snprintf(k, sizeof(k), "%d", (i+1));
        oldval = natsStrHash_Remove(hash, k);
        if ((oldval == NULL)
            || ((*(int*)oldval) != values[i]))
        {
            s = NATS_ERR;
            break;
        }
    }
    testCond((s == NATS_OK)
             && (hash->used == 9)
             && (hash->numBkts < lastNumBkts));

    test("Iterator: ");
    natsStrHashIter_Init(&iter, hash);
    i = 0;
    while (natsStrHashIter_Next(&iter, &key, &oldval))
    {
        i++;
        if (((atoi(key) < 31) || (atoi(key) > 40))
            || (oldval == NULL)
            || ((*(int*)oldval) != values[atoi(key)-1]))
        {
            s = NATS_ERR;
            break;
        }
    }
    natsStrHashIter_Done(&iter);
    testCond((s == NATS_OK) && (i == natsStrHash_Count(hash)));

    test("Iterator, remove current: ");
    natsStrHashIter_Init(&iter, hash);
    while (natsStrHashIter_Next(&iter, &key, NULL))
    {
        s = natsStrHashIter_RemoveCurrent(&iter);
        if (s != NATS_OK)
            break;
    }
    testCond((s == NATS_OK)
             && (natsStrHash_Count(hash) == 0)
             && (hash->canResize == false)
             && (hash->numBkts > 8));

    natsStrHashIter_Done(&iter);

    test("Grow again: ");
    oldval = NULL;
    for (int i=0; i<40; i++)
    {
        snprintf(k, sizeof(k), "%d", (i+1));
        s = natsStrHash_Set(hash, k, true, &(values[i]), &oldval);
        if (oldval != NULL)
            s = NATS_ERR;
        if (s != NATS_OK)
            break;
    }
    testCond((s == NATS_OK)
             && (hash->used == 40)
             && (hash->numBkts > 8));
    lastNumBkts = hash->numBkts;

    test("Iterator, remove current, hash does not shrink: ");
    natsStrHashIter_Init(&iter, hash);
    i = 0;
    while (natsStrHashIter_Next(&iter, &key, NULL))
    {
        s = natsStrHashIter_RemoveCurrent(&iter);
        if ((s != NATS_OK) || (++i == 31))
            break;
    }
    testCond((s == NATS_OK)
             && (natsStrHash_Count(hash) == 9)
             && (hash->canResize == false)
             && (hash->numBkts == lastNumBkts));

    natsStrHashIter_Done(&iter);

    test("After iterator done, shrink works: ");
    oldval = NULL;
    s = natsStrHash_Set(hash, (char*) "100", true, (void*) "last", &oldval);
    if ((s == NATS_OK) && (oldval == NULL))
    {
        oldval = natsStrHash_Remove(hash, (char*) "100");
        if ((oldval == NULL)
            || (strcmp((const char*) oldval, "last") != 0))
        {
            s = NATS_ERR;
        }
    }
    testCond((s == NATS_OK)
             && hash->canResize
             && (hash->numBkts != lastNumBkts));

    test("Copy key: ");
    snprintf(k, sizeof(k), "%s", "keycopied");
    hk = natsStrHash_Hash(k, (int) strlen(k));
    s = natsStrHash_Set(hash, k, true, (void*) t1, &oldval);
    if (s == NATS_OK)
    {
        // Changing the key does not affect the hash
        snprintf(k, sizeof(k), "%s", "keychanged");
        if (natsStrHash_Get(hash, (char*) "keycopied") != t1)
            s = NATS_ERR;
    }
    testCond((s == NATS_OK)
              && (oldval == NULL)
              && (hash->bkts[hk & hash->mask]->hk == hk)
              && (hash->bkts[hk & hash->mask]->freeKey == true));

    test("Key referenced: ");
    snprintf(k, sizeof(k), "%s", "keyreferenced");
    hk = natsStrHash_Hash(k, (int) strlen(k));
    s = natsStrHash_Set(hash, k, false, (void*) t2, &oldval);
    if (s == NATS_OK)
    {
        // Changing the key affects the hash
        snprintf(k, sizeof(k), "%s", "keychanged");
        if (natsStrHash_Get(hash, (char*) "keyreferenced") == t2)
            s = NATS_ERR;
    }
    testCond((s == NATS_OK)
              && (oldval == NULL)
              && (hash->bkts[hk & hash->mask]->hk == hk)
              && (hash->bkts[hk & hash->mask]->freeKey == false)
              && (strcmp(hash->bkts[hk & hash->mask]->key, "keychanged") == 0));

    test("Key not copied, but asking to free when destroyed: ");
    myKey = strdup("mykey");
    hk = natsStrHash_Hash(myKey, (int) strlen(myKey));
    s = natsStrHash_SetEx(hash, myKey, false, true, (void*) t1, &oldval);
    testCond((s == NATS_OK)
              && (oldval == NULL)
              && (hash->bkts[hk & hash->mask]->hk == hk)
              && (hash->bkts[hk & hash->mask]->freeKey == true));

    test("Destroy: ");
    natsStrHash_Destroy(hash);
    hash = NULL;
    testCond(1);

    test("Create new: ");
    s = natsStrHash_Create(&hash, 4);
    testCond(s == NATS_OK);

    test("Populate: ");
    s = natsStrHash_Set(hash, (char*) "1", true, (void*) 1, NULL);
    IFOK(s, natsStrHash_Set(hash, (char*) "2", true, (void*) 2, NULL));
    IFOK(s, natsStrHash_Set(hash, (char*) "3", true, (void*) 3, NULL));
    testCond(s == NATS_OK);

    test("Remove one: ");
    s = (natsStrHash_Remove(hash, (char*) "2") == (void*) 2) ? NATS_OK : NATS_ERR;
    testCond(s == NATS_OK);

    test("RemoveSingle fails if more than one: ");
    s = natsStrHash_RemoveSingle(hash, &key, NULL);
    testCond(s == NATS_ERR);
    nats_clearLastError();

    test("Remove second: ");
    s = (natsStrHash_Remove(hash, (char*) "1") == (void*) 1) ? NATS_OK : NATS_ERR;
    testCond(s == NATS_OK);

    test("Remove single (copy of key): ");
    key = NULL;
    oldval = NULL;
    s = natsStrHash_RemoveSingle(hash, &key, &oldval);
    testCond((s == NATS_OK)
                && (hash->used == 0)
                && (strcmp(key, "3") == 0)
                && (oldval == (void*) 3));
    free(key);
    key = NULL;
    oldval = NULL;

    test("Add key without copy: ");
    s = natsStrHash_Set(hash, (char*) "4", false, (void*) 4, NULL);
    testCond(s == NATS_OK);

    test("Remove single (no copy of key): ");
    s = natsStrHash_RemoveSingle(hash, &key, &oldval);
    testCond((s == NATS_OK)
                && (hash->used == 0)
                && (strcmp(key, "4") == 0)
                && (oldval == (void*) 4));

    natsStrHash_Destroy(hash);
}

static const char*
_dummyTokenHandler(void *closure)
{
    return "token";
}

static void
_dummyErrHandler(natsConnection *nc, natsSubscription *sub, natsStatus err,
                 void *closure)
{
    // do nothing
}

static void
_dummyConnHandler(natsConnection *nc, void *closure)
{
    // do nothing
}

static natsStatus
_dummyUserJWTCb(char **userJWT, char **customErrTxt, void *closure)
{
    // do nothing
    return NATS_OK;
}

static natsStatus
_dummySigCb(char **customErrTxt, unsigned char **psig, int *sigLen, const char* nonce, void *closure)
{
    // do nothing
    return NATS_OK;
}

void test_natsOptions(void)
{
    natsStatus  s;
    natsOptions *opts = NULL;
    natsOptions *cloned = NULL;
    const char  *servers[] = {"1", "2", "3"};
    const char  *servers2[] = {"1", "2", "3", "4"};
    const char  *servers3[] = {" nats://localhost:4222", "nats://localhost:4223 ", " nats://localhost:4224 "};
    const char  *servers3t[] = {"nats://localhost:4222", "nats://localhost:4223", "nats://localhost:4224"};

    test("Create options: ");
    s = natsOptions_Create(&opts);
    testCond(s == NATS_OK);

    test("Test defaults: ");
    testCond((opts->allowReconnect == true)
             && (opts->maxReconnect == 60)
             && (opts->reconnectWait == 2 * 1000)
             && (opts->timeout == 2 * 1000)
             && (opts->pingInterval == 2 * 60 *1000)
             && (opts->maxPingsOut == 2)
             && (opts->ioBufSize == 32 * 1024)
             && (opts->maxPendingMsgs == 65536)
             && (opts->maxPendingBytes == -1)
             && (opts->user == NULL)
             && (opts->password == NULL)
             && (opts->token == NULL)
             && (opts->tokenCb == NULL)
             && (opts->orderIP == 0)
             && (opts->writeDeadline == nats_lib()->config.DefaultWriteDeadline)
             && !opts->noEcho
             && !opts->retryOnFailedConnect
             && !opts->ignoreDiscoveredServers
             && !opts->tlsHandshakeFirst);

    test("Add URL: ");
    s = natsOptions_SetURL(opts, "test");
    testCond((s == NATS_OK)
              && (opts->url != NULL)
              && (strcmp(opts->url, "test") == 0));

    test("Replace URL: ");
    s = natsOptions_SetURL(opts, "test2");
    testCond((s == NATS_OK)
              && (opts->url != NULL)
              && (strcmp(opts->url, "test2") == 0));

    test("URL trimmed: ");
    s = natsOptions_SetURL(opts, "   nats://localhost:4222   ");
    testCond((s == NATS_OK)
              && (opts->url != NULL)
              && (strcmp(opts->url, "nats://localhost:4222") == 0));

    test("Remove URL: ");
    s = natsOptions_SetURL(opts, NULL);
    testCond((s == NATS_OK)
              && (opts->url == NULL));

    test("Set Servers (invalid args): ");
    s = natsOptions_SetServers(opts, servers, -2);
    if (s != NATS_OK)
        s = natsOptions_SetServers(opts, servers, 0);
    testCond(s != NATS_OK);

    test("Set Servers: ");
    s = natsOptions_SetServers(opts, servers, 3);
    testCond((s == NATS_OK)
             && (opts->servers != NULL)
             && (opts->serversCount == 3));

    test("Replace Servers: ");
    s = natsOptions_SetServers(opts, servers2, 4);
    if ((s == NATS_OK) && (opts->servers != NULL) && (opts->serversCount == 4))
    {
        for (int i=0; i<4; i++)
        {
            if (strcmp(opts->servers[i], servers2[i]) != 0)
            {
                s = NATS_ERR;
                break;
            }
        }
    }
    testCond(s == NATS_OK);

    test("Trimmed servers: ")
    s = natsOptions_SetServers(opts, servers3, 3);
    if ((s == NATS_OK) && (opts->servers != NULL) && (opts->serversCount == 3))
    {
        for (int i=0; i<3; i++)
        {
            if (strcmp(opts->servers[i], servers3t[i]) != 0)
            {
                s = NATS_ERR;
                break;
            }
        }
    }
    testCond(s == NATS_OK);

    test("Remove servers: ");
    s = natsOptions_SetServers(opts, NULL, 0);
    testCond((s == NATS_OK)
             && (opts->servers == NULL)
             && (opts->serversCount == 0));

    test("Set NoRandomize: ");
    s = natsOptions_SetNoRandomize(opts, true);
    testCond((s == NATS_OK) && (opts->noRandomize == true));

    test("Remove NoRandomize: ");
    s = natsOptions_SetNoRandomize(opts, false);
    testCond((s == NATS_OK) && (opts->noRandomize == false));

    test("Set Timeout (invalid args): ");
    s = natsOptions_SetTimeout(opts, -10);
    testCond(s != NATS_OK);

    test("Set Timeout to zero: ");
    s = natsOptions_SetTimeout(opts, 0);
    testCond((s == NATS_OK) && (opts->timeout == 0));

    test("Set Timeout: ");
    s = natsOptions_SetTimeout(opts, 2000);
    testCond((s == NATS_OK) && (opts->timeout == 2000));

    test("Set Name: ");
    s = natsOptions_SetName(opts, "test");
    testCond((s == NATS_OK) && (opts->name != NULL)
             && (strcmp(opts->name, "test") == 0));

    test("Remove Name: ");
    s = natsOptions_SetName(opts, NULL);
    testCond((s == NATS_OK) && (opts->name == NULL));

    test("Set Verbose: ");
    s = natsOptions_SetVerbose(opts, true);
    testCond((s == NATS_OK) && (opts->verbose == true));

    test("Remove Verbose: ");
    s = natsOptions_SetVerbose(opts, false);
    testCond((s == NATS_OK) && (opts->verbose == false));

    test("Set NoEcho: ");
    s = natsOptions_SetNoEcho(opts, true);
    testCond((s == NATS_OK) && (opts->noEcho == true));

    test("Remove NoEcho: ");
    s = natsOptions_SetNoEcho(opts, false);
    testCond((s == NATS_OK) && (opts->noEcho == false));

    test("Set RetryOnFailedConnect: ");
    s = natsOptions_SetRetryOnFailedConnect(opts, true, _dummyConnHandler, (void*)1);
    testCond((s == NATS_OK)
            && (opts->retryOnFailedConnect == true)
            && (opts->connectedCb == _dummyConnHandler)
            && (opts->connectedCbClosure == (void*) 1));

    test("Remove RetryOnFailedConnect: ");
    // If `retry` is false, connect CB and closure are ignored and should
    // be internally set to NULL.
    s = natsOptions_SetRetryOnFailedConnect(opts, false, _dummyConnHandler, (void*)1);
    testCond((s == NATS_OK)
            && (opts->retryOnFailedConnect == false)
            && (opts->connectedCb == NULL)
            && (opts->connectedCbClosure == NULL));

    test("Set Secure: ");
    s = natsOptions_SetSecure(opts, true);
#if defined(NATS_HAS_TLS)
    testCond((s == NATS_OK) && (opts->secure == true));
#else
    testCond((s == NATS_ILLEGAL_STATE) && (opts->secure == false));
#endif

    test("Remove Secure: ");
    s = natsOptions_SetSecure(opts, false);
#if defined(NATS_HAS_TLS)
    testCond((s == NATS_OK) && (opts->secure == false));
#else
    testCond((s == NATS_ILLEGAL_STATE) && (opts->secure == false));
#endif

    test("Set TLSHandshakeFirst: ");
    s = natsOptions_TLSHandshakeFirst(opts);
#if defined(NATS_HAS_TLS)
    testCond((s == NATS_OK) && (opts->tlsHandshakeFirst == true) && (opts->secure == true));
#else
    testCond((s == NATS_ILLEGAL_STATE) && (opts->secure == false) && (opts->tlsHandshakeFirst == false));
#endif

    test("Set Pedantic: ");
    s = natsOptions_SetPedantic(opts, true);
    testCond((s == NATS_OK) && (opts->pedantic == true));

    test("Remove Pedantic: ");
    s = natsOptions_SetPedantic(opts, false);
    testCond((s == NATS_OK) && (opts->pedantic == false));

    test("Set Ping Interval (negative or 0 ok): ");
    s = natsOptions_SetPingInterval(opts, -1000);
    if ((s == NATS_OK) && (opts->pingInterval != -1000))
        s = NATS_ERR;
    IFOK(s, natsOptions_SetPingInterval(opts, 0));
    if ((s == NATS_OK) && (opts->pingInterval != 0))
        s = NATS_ERR;
    IFOK(s, natsOptions_SetPingInterval(opts, 1000));
    testCond((s == NATS_OK) && (opts->pingInterval == 1000));

    test("Set MaxPingsOut: ");
    s = natsOptions_SetMaxPingsOut(opts, -2);
    IFOK(s, natsOptions_SetMaxPingsOut(opts, 0));
    IFOK(s, natsOptions_SetMaxPingsOut(opts, 1));
    IFOK(s, natsOptions_SetMaxPingsOut(opts, 10));
    testCond((s == NATS_OK) && (opts->maxPingsOut == 10));

    test("Set IOBufSize: ");
    s = natsOptions_SetIOBufSize(opts, -1);
    if ((s != NATS_OK) && (opts->ioBufSize == NATS_OPTS_DEFAULT_IO_BUF_SIZE))
        s = natsOptions_SetIOBufSize(opts, 0);
    if ((s == NATS_OK) && (opts->ioBufSize == 0))
        s = natsOptions_SetIOBufSize(opts, 1024 * 1024);
    testCond((s == NATS_OK) && (opts->ioBufSize == 1024 * 1024));

    test("Set AllowReconnect: ");
    s = natsOptions_SetAllowReconnect(opts, true);
    testCond((s == NATS_OK) && (opts->allowReconnect == true));

    test("Remove AllowReconnect: ");
    s = natsOptions_SetAllowReconnect(opts, false);
    testCond((s == NATS_OK) && (opts->allowReconnect == false));

    test("Set MaxReconnect (negative ok): ");
    s = natsOptions_SetMaxReconnect(opts, -10);
    if ((s == NATS_OK) && (opts->maxReconnect != -10))
        s = NATS_ERR;
    IFOK(s, natsOptions_SetMaxReconnect(opts, 0));
    if ((s == NATS_OK) && (opts->maxReconnect != 0))
        s = NATS_ERR;
    IFOK(s, natsOptions_SetMaxReconnect(opts, 10));
    testCond((s == NATS_OK) && (opts->maxReconnect == 10));

    test("Set Reconnect Wait (invalid args: ");
    s = natsOptions_SetReconnectWait(opts, -1000);
    testCond(s != NATS_OK);

    test("Set Reconnect Wait: ");
    s = natsOptions_SetReconnectWait(opts, 1000);
    testCond((s == NATS_OK) && (opts->reconnectWait == 1000));

    test("Remove Reconnect Wait: ");
    s = natsOptions_SetReconnectWait(opts, 0);
    testCond((s == NATS_OK) && (opts->reconnectWait == 0));

    test("Set Max Pending Msgs (invalid args: ");
    s = natsOptions_SetMaxPendingMsgs(opts, -1000);
    if (s != NATS_OK)
        s = natsOptions_SetMaxPendingMsgs(opts, 0);
    testCond(s != NATS_OK);

    test("Set Max Pending Msgs : ");
    s = natsOptions_SetMaxPendingMsgs(opts, 10000);
    testCond((s == NATS_OK) && (opts->maxPendingMsgs == 10000));

    test("Set Max Pending Bytes : ");
    s = natsOptions_SetMaxPendingBytes(opts, 1000000);
    testCond((s == NATS_OK) && (opts->maxPendingBytes == 1000000))

    test("Set Error Handler: ");
    s = natsOptions_SetErrorHandler(opts, _dummyErrHandler, NULL);
    testCond((s == NATS_OK) && (opts->asyncErrCb == _dummyErrHandler));

    test("Remove Error Handler: ");
    s = natsOptions_SetErrorHandler(opts, NULL, NULL);
    testCond((s == NATS_OK) && (opts->asyncErrCb == natsConn_defaultErrHandler));

    test("Set ClosedCB: ");
    s = natsOptions_SetClosedCB(opts, _dummyConnHandler, NULL);
    testCond((s == NATS_OK) && (opts->closedCb == _dummyConnHandler));

    test("Remove ClosedCB: ");
    s = natsOptions_SetClosedCB(opts, NULL, NULL);
    testCond((s == NATS_OK) && (opts->closedCb == NULL));

    test("Set DisconnectedCB: ");
    s = natsOptions_SetDisconnectedCB(opts, _dummyConnHandler, NULL);
    testCond((s == NATS_OK) && (opts->disconnectedCb == _dummyConnHandler));

    test("Remove DisconnectedCB: ");
    s = natsOptions_SetDisconnectedCB(opts, NULL, NULL);
    testCond((s == NATS_OK) && (opts->disconnectedCb == NULL));

    test("Set ReconnectedCB: ");
    s = natsOptions_SetReconnectedCB(opts, _dummyConnHandler, NULL);
    testCond((s == NATS_OK) && (opts->reconnectedCb == _dummyConnHandler));

    test("Remove ReconnectedCB: ");
    s = natsOptions_SetReconnectedCB(opts, NULL, NULL);
    testCond((s == NATS_OK) && (opts->reconnectedCb == NULL));

    test("Set UserInfo: ");
    s = natsOptions_SetUserInfo(opts, "ivan", "pwd");
    testCond((s == NATS_OK)
                && (strcmp(opts->user, "ivan") == 0)
                && (strcmp(opts->password, "pwd") == 0));

    test("Remove UserInfo: ");
    s = natsOptions_SetUserInfo(opts, NULL, NULL);
    testCond((s == NATS_OK) && (opts->user == NULL) && (opts->password == NULL));

    test("Set Token: ");
    s = natsOptions_SetToken(opts, "token");
    testCond((s == NATS_OK) && (strcmp(opts->token, "token") == 0));

    test("Remove Token: ");
    s = natsOptions_SetToken(opts, NULL);
    testCond((s == NATS_OK) && (opts->token == NULL));

    test("Set TokenHandler: ");
    s = natsOptions_SetTokenHandler(opts, _dummyTokenHandler, NULL);
    testCond((s == NATS_OK) && (opts->tokenCb == _dummyTokenHandler)
             && (strcmp(opts->tokenCb(NULL), "token") == 0));

    test("Remove TokenHandler: ");
    s = natsOptions_SetTokenHandler(opts, NULL, NULL);
    testCond((s == NATS_OK) && (opts->tokenCb == NULL));

    test("Set write deadline: ");
    s = natsOptions_SetWriteDeadline(opts, 1000);
    testCond((s == NATS_OK) && (opts->writeDeadline == 1000));

    test("Remove write deadline: ");
    s = natsOptions_SetWriteDeadline(opts, 0);
    testCond((s == NATS_OK) && (opts->writeDeadline == 0));

    test("IP order invalid values: ");
    s = natsOptions_IPResolutionOrder(opts, -1);
    if (s != NATS_OK)
        s = natsOptions_IPResolutionOrder(opts, 1);
    if (s != NATS_OK)
        s = natsOptions_IPResolutionOrder(opts, 466);
    if (s != NATS_OK)
        s = natsOptions_IPResolutionOrder(opts, 644);
    testCond(s != NATS_OK);

    test("IP order valid values: ");
    s = natsOptions_IPResolutionOrder(opts, 0);
    if ((s == NATS_OK) && (opts->orderIP == 0))
        s = natsOptions_IPResolutionOrder(opts, 4);
    if ((s == NATS_OK) && (opts->orderIP == 4))
        s = natsOptions_IPResolutionOrder(opts, 6);
    if ((s == NATS_OK) && (opts->orderIP == 6))
        s = natsOptions_IPResolutionOrder(opts, 46);
    if ((s == NATS_OK) && (opts->orderIP == 46))
        s = natsOptions_IPResolutionOrder(opts, 64);
    testCond((s == NATS_OK) && (opts->orderIP == 64));

    test("Set UseOldRequestStyle: ");
    s = natsOptions_UseOldRequestStyle(opts, true);
    testCond((s == NATS_OK) && (opts->useOldRequestStyle == true));

    test("Remove UseOldRequestStyle: ");
    s = natsOptions_UseOldRequestStyle(opts, false);
    testCond((s == NATS_OK) && (opts->useOldRequestStyle == false));

    test("Set SendAsap: ");
    s = natsOptions_SetSendAsap(opts, true);
    testCond((s == NATS_OK) && (opts->sendAsap == true));

    test("Remove SendAsap: ");
    s = natsOptions_SetSendAsap(opts, false);
    testCond((s == NATS_OK) && (opts->sendAsap == false));

    test("Set UserCreds: ");
    s = natsOptions_SetUserCredentialsCallbacks(opts, _dummyUserJWTCb, (void*) 1, _dummySigCb, (void*) 2);
    testCond((s == NATS_OK)
                && (opts->userJWTHandler == _dummyUserJWTCb)
                && (opts->userJWTClosure == (void*) 1)
                && (opts->sigHandler == _dummySigCb)
                && (opts->sigClosure == (void*) 2));

    test("Remove UserCreds: ");
    s = natsOptions_SetUserCredentialsCallbacks(opts, NULL, NULL, NULL, NULL);
    testCond((s == NATS_OK)
                && (opts->userJWTHandler == NULL)
                && (opts->userJWTClosure == NULL)
                && (opts->sigHandler == NULL)
                && (opts->sigClosure == NULL));

    test("Set UserCredsFromFile: ");
    s = natsOptions_SetUserCredentialsFromFiles(opts, "foo", "bar");
    testCond((s == NATS_OK)
                && (opts->userCreds != NULL)
                && (strcmp(opts->userCreds->userOrChainedFile, "foo") == 0)
                && (strcmp(opts->userCreds->seedFile, "bar") == 0)
                && (opts->userCreds->jwtAndSeedContent == NULL)
                && (opts->userJWTHandler == natsConn_userCreds)
                && (opts->userJWTClosure == (void*) opts->userCreds)
                && (opts->sigHandler == natsConn_signatureHandler)
                && (opts->sigClosure == (void*) opts->userCreds));

    test("Remove UserCredsFromFile: ");
    s = natsOptions_SetUserCredentialsFromFiles(opts, NULL, NULL);
    testCond((s == NATS_OK)
            && (opts->userCreds == NULL)
            && (opts->userJWTHandler == NULL)
            && (opts->userJWTClosure == NULL)
            && (opts->sigHandler == NULL)
            && (opts->sigClosure == NULL));

    test("Set UserCredsFromMemory: ");
    const char *jwtAndSeed = "-----BEGIN NATS USER JWT----\nsome user jwt\n-----END NATS USER JWT-----\n\n-----BEGIN USER NKEY SEED-----\nSUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY\n-----END USER NKEY SEED-----\n";
    s = natsOptions_SetUserCredentialsFromMemory(opts, jwtAndSeed);
    testCond((s == NATS_OK)
            && (opts->userCreds != NULL)
            && (opts->userCreds->userOrChainedFile == NULL)
            && (opts->userCreds->seedFile == NULL)
            && (strcmp(opts->userCreds->jwtAndSeedContent, jwtAndSeed) == 0)
            && (opts->userJWTHandler == natsConn_userCreds)
            && (opts->userJWTClosure == (void*) opts->userCreds)
            && (opts->sigHandler == natsConn_signatureHandler)
            && (opts->sigClosure == (void*) opts->userCreds));

    test("Remove UserCredsFromMemory: ");
    s = natsOptions_SetUserCredentialsFromMemory(opts, NULL);
    testCond((s == NATS_OK)
            && (opts->userCreds == NULL)
            && (opts->userJWTHandler == NULL)
            && (opts->userJWTClosure == NULL)
            && (opts->sigHandler == NULL)
            && (opts->sigClosure == NULL));

    test("Set NKey: ");
    s = natsOptions_SetNKey(opts, "pubkey", _dummySigCb, (void*) 1);
    testCond((s == NATS_OK)
                && (opts->nkey != NULL)
                && (strcmp(opts->nkey, "pubkey") == 0)
                && (opts->sigHandler == _dummySigCb)
                && (opts->sigClosure == (void*) 1));

    test("Remove NKey: ");
    s = natsOptions_SetNKey(opts, NULL, NULL, NULL);
    testCond((s == NATS_OK)
                && (opts->nkey == NULL)
                && (opts->sigHandler == NULL)
                && (opts->sigClosure == NULL));

    test("Set NKeyFromSeed: ");
    s = natsOptions_SetNKeyFromSeed(opts, "pubkey", "seed.file");
    testCond((s == NATS_OK)
                && (opts->nkey != NULL)
                && (strcmp(opts->nkey, "pubkey") == 0)
                && (opts->sigHandler == natsConn_signatureHandler)
                && (opts->sigClosure == (void*) opts->userCreds)
                && (opts->userCreds != NULL)
                && (opts->userCreds->seedFile != NULL)
                && (strcmp(opts->userCreds->seedFile, "seed.file") == 0));

    test("Remove NKeyFromSeed: ");
    s = natsOptions_SetNKeyFromSeed(opts, NULL, NULL);
    testCond((s == NATS_OK)
                && (opts->nkey == NULL)
                && (opts->sigHandler == NULL)
                && (opts->sigClosure == NULL)
                && (opts->userCreds == NULL));

    test("Disable no responders: ");
    s = natsOptions_DisableNoResponders(opts, true);
    testCond((s == NATS_OK) && opts->disableNoResponders);

    test("Enable no responders: ");
    s = natsOptions_DisableNoResponders(opts, false);
    testCond((s == NATS_OK) && !opts->disableNoResponders);

    test("Set custom inbox prefix: ");
    s = natsOptions_SetCustomInboxPrefix(opts, "my.prefix");
    testCond((s == NATS_OK)
                && (opts->inboxPfx != NULL)
                && (strcmp(opts->inboxPfx, "my.prefix.") == 0));

    test("Clear custom inbox prefix: ");
    s = natsOptions_SetCustomInboxPrefix(opts, NULL);
    if ((s == NATS_OK) && (opts->inboxPfx == NULL))
        s = natsOptions_SetCustomInboxPrefix(opts, "");
    testCond((s == NATS_OK) && (opts->inboxPfx == NULL));

    test("Set ignore discovered servers: ");
    s = natsOptions_SetIgnoreDiscoveredServers(opts, true);
    testCond((s == NATS_OK) && opts->ignoreDiscoveredServers);

    test("Reset ignore discovered servers: ");
    s = natsOptions_SetIgnoreDiscoveredServers(opts, false);
    testCond((s == NATS_OK) && !opts->ignoreDiscoveredServers);

    // Prepare some values for the clone check
    s = natsOptions_SetURL(opts, "url");
    IFOK(s, natsOptions_SetServers(opts, servers, 3));
    IFOK(s, natsOptions_SetName(opts, "name"));
    IFOK(s, natsOptions_SetPingInterval(opts, 3000));
    IFOK(s, natsOptions_SetErrorHandler(opts, _dummyErrHandler, NULL));
    IFOK(s, natsOptions_SetUserInfo(opts, "ivan", "pwd"));
    IFOK(s, natsOptions_SetToken(opts, "token"));
    IFOK(s, natsOptions_IPResolutionOrder(opts, 46));
    IFOK(s, natsOptions_SetNoEcho(opts, true));
    IFOK(s, natsOptions_SetRetryOnFailedConnect(opts, true, _dummyConnHandler, NULL));
    if (s != NATS_OK)
        FAIL("Unable to test natsOptions_clone() because of failure while setting");

    test("Cloning: ");
    s = NATS_OK;
    cloned = natsOptions_clone(opts);
    if (cloned == NULL)
        s = NATS_NO_MEMORY;
    else if ((cloned->pingInterval != 3000)
             || (cloned->asyncErrCb != _dummyErrHandler)
             || (cloned->name == NULL)
             || (strcmp(cloned->name, "name") != 0)
             || (cloned->url == NULL)
             || (strcmp(cloned->url, "url") != 0)
             || (cloned->servers == NULL)
             || (cloned->serversCount != 3)
             || (strcmp(cloned->user, "ivan") != 0)
             || (strcmp(cloned->password, "pwd") != 0)
             || (strcmp(cloned->token, "token") != 0)
             || (cloned->orderIP != 46)
             || (!cloned->noEcho)
             || (!cloned->retryOnFailedConnect)
             || (cloned->connectedCb != _dummyConnHandler))
    {
        s = NATS_ERR;
    }
    if (s == NATS_OK)
    {
        for (int i=0; i<3; i++)
        {
            if (strcmp(cloned->servers[i], servers[i]) != 0)
            {
                s = NATS_ERR;
                break;
            }
        }
    }
    testCond(s == NATS_OK);

    test("Destroy original does not affect clone: ");
    natsOptions_Destroy(opts);
    testCond((cloned != NULL)
             && (cloned->url != NULL)
             && (strcmp(cloned->url, "url") == 0));

    natsOptions_Destroy(cloned);
    opts = NULL;
    cloned = NULL;

    test("If original has default err handler, cloned has it too: ");
    s = natsOptions_Create(&opts);
    if (s == NATS_OK)
        cloned = natsOptions_clone(opts);
    testCond((s == NATS_OK) && (cloned != NULL)
                && (cloned->asyncErrCb == natsConn_defaultErrHandler)
                && (cloned->asyncErrCbClosure == NULL));
    natsOptions_Destroy(cloned);
    natsOptions_Destroy(opts);
}

void test_natsSock_ReadLine(void)
{
    char        buffer[20];
    natsStatus  s;
    natsSockCtx ctx;

    memset(&ctx, 0, sizeof(natsSockCtx));

    snprintf(buffer, sizeof(buffer), "%s", "+OK\r\nPONG\r\nFOO\r\nxxx");
    buffer[3] = '\0';

    test("Read second line from buffer: ");
    s = natsSock_ReadLine(&ctx, buffer, sizeof(buffer));
    testCond((s == NATS_OK) && (strcmp(buffer, "PONG") == 0));

    test("Read third line from buffer: ");
    s = natsSock_ReadLine(&ctx, buffer, sizeof(buffer));
    testCond((s == NATS_OK) && (strcmp(buffer, "FOO") == 0));

    test("Next call should trigger recv, which is expected to fail: ");
    s = natsSock_ReadLine(&ctx, buffer, sizeof(buffer));
    testCond(s != NATS_OK);
}

static natsStatus
_dummyJSONCb(void *userInfo, const char *fieldName, nats_JSONField *f)
{
    if (userInfo == NULL)
        return NATS_OK;

    if (strcmp(fieldName, "fail") == 0)
        return nats_setError(NATS_INVALID_ARG, "%s", "on purpose");

    *(uint64_t*)userInfo = f->value.vuint;
    return NATS_OK;
}

void test_natsJSON(void)
{
    natsStatus  s;
    nats_JSON   *json = NULL;
    char        buf[256];
    int         i;
    int         intVal = 0;
    int64_t     longVal = 0;
    char        *strVal = NULL;
    bool        boolVal = false;
    long double doubleVal = 0;
    char        **arrVal = NULL;
    bool        *arrBoolVal = NULL;
    long double *arrDoubleVal = NULL;
    int         *arrIntVal = NULL;
    int64_t     *arrLongVal = NULL;
    uint64_t    *arrULongVal = NULL;
    nats_JSON   **arrObjVal = NULL;
    nats_JSONArray **arrArrVal = NULL;
    int         arrCount = 0;
    uint64_t    ulongVal = 0;
    nats_JSON   *obj1 = NULL;
    nats_JSON   *obj2 = NULL;
    nats_JSON   *obj3 = NULL;
    int32_t     int32Val = 0;
    uint16_t    uint16Val = 0;
    const char  *wrong[] = {
            "{",
            "}",
            "{start quote missing\":0}",
            "{\"end quote missing: 0}",
            "{\"test\":start quote missing\"}",
            "{\"test\":\"end quote missing}",
            "{\"test\":1.2x}",
            "{\"test\":tRUE}",
            "{\"test\":true,}",
            "{\"test\":true}, xxx}",
            "{\"test\": \"abc\\error here\"}",
            "{\"test\": \"abc\\u123\"}",
            "{\"test\": \"abc\\u123g\"}",
            "{\"test\": \"abc\\u 23f\"}",
            ("{\"test\": \"abc\\"""),
            "{\"test\": \"abc\\u1234",
            "{\"test\": \"abc\\uabc",
            "{\"test\" \"separator missing\"}",
            "{\"test\":[1, \"abc\", true]}",
    };
    const char  *good[] = {
            "{}",
            " {}",
            " { }",
            " { } ",
            "{ \"test\":{}}",
            "{ \"test\":1.2}",
            "{ \"test\" :1.2}",
            "{ \"test\" : 1.2}",
            "{ \"test\" : 1.2 }",
            "{ \"test\" : 1.2,\"test2\":1}",
            "{ \"test\" : 1.2, \"test2\":1}",
            "{ \"test\":0}",
            "{ \"test\" :0}",
            "{ \"test\" : 0}",
            "{ \"test\" : 0 }",
            "{ \"test\" : 0,\"test2\":1}",
            "{ \"test\" : 0, \"test2\":1}",
            "{ \"test\":true}",
            "{ \"test\": true}",
            "{ \"test\": true }",
            "{ \"test\":true,\"test2\":1}",
            "{ \"test\": true,\"test2\":1}",
            "{ \"test\": true ,\"test2\":1}",
            "{ \"test\":false}",
            "{ \"test\": false}",
            "{ \"test\": false }",
            "{ \"test\":false,\"test2\":1}",
            "{ \"test\": false,\"test2\":1}",
            "{ \"test\": false ,\"test2\":1}",
            "{ \"test\":\"abc\"}",
            "{ \"test\": \"abc\"}",
            "{ \"test\": \"abc\" }",
            "{ \"test\":\"abc\",\"test2\":1}",
            "{ \"test\": \"abc\",\"test2\":1}",
            "{ \"test\": \"abc\" ,\"test2\":1}",
            "{ \"test\": \"a\\\"b\\\"c\" }",
            "{ \"test\": [\"a\", \"b\", \"c\"]}",
            "{ \"test\": [\"a\\\"b\\\"c\"]}",
            "{ \"test\": [\"abc,def\"]}",
            "{ \"test\": [{\"a\": 1}, {\"b\": \"c\"}]}",
            "{ \"test\": [[{\"a\": 1}], [{\"b\": \"c\"}]]}",
            "{ \"test\": []}",
            "{ \"test\": {\"inner\":\"a\",\"inner2\":2,\"inner3\":false,\"inner4\":{\"inner_inner1\" : 1.234}}}",
            "{ \"test\": \"a\\\"b\\\"c\"}",
            "{ \"test\": \"\\\"\\\\/\b\f\n\r\t\\uabcd\"}",
            "{ \"test\": \"\\ua12f\"}",
            "{ \"test\": \"\\uA01F\"}",
            "{ \"test\": null}",
    };
    nats_JSONField  *f      = NULL;
    unsigned char   *bytes  = NULL;
    int             bl      = 0;

    for (i=0; i<(int)(sizeof(wrong)/sizeof(char*)); i++)
    {
        snprintf(buf, sizeof(buf), "Negative test %d: ", (i+1));
        test(buf);
        s = nats_JSONParse(&json, wrong[i], -1);
        testCond((s != NATS_OK) && (json == NULL));
        json = NULL;
    }
    nats_clearLastError();

    for (i=0; i<(int)(sizeof(good)/sizeof(char*)); i++)
    {
        snprintf(buf, sizeof(buf), "Positive test %d: ", (i+1));
        test(buf);
        s = nats_JSONParse(&json, good[i], -1);
        testCond((s == NATS_OK) && (json != NULL));
        nats_JSONDestroy(json);
        json = NULL;
    }
    nats_clearLastError();

    // Check values
    test("Empty string: ");
    s = nats_JSONParse(&json, "{}", -1);
    IFOK(s, nats_JSONGetInt(json, "test", &intVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 0)
                && (intVal == 0));
    nats_JSONDestroy(json);
    json = NULL;

    test("Single field, string: ");
    s = nats_JSONParse(&json, "{\"test\":\"abc\"}", -1);
    IFOK(s, nats_JSONGetStr(json, "test", &strVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (strcmp(strVal, "abc") == 0));
    nats_JSONDestroy(json);
    json = NULL;
    free(strVal);
    strVal = NULL;

    test("Single field, string with escape chars: ");
    s = nats_JSONParse(&json, "{\"test\":\"\\\"\\\\\\/\\b\\f\\n\\r\\t\"}", -1);
    IFOK(s, nats_JSONGetStr(json, "test", &strVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (strcmp(strVal, "\"\\/\b\f\n\r\t") == 0));
    nats_JSONDestroy(json);
    json = NULL;
    free(strVal);
    strVal = NULL;

    test("Single field, string with unicode: ");
    s = nats_JSONParse(&json, "{\"test\":\"\\u0026\\u003c\\u003e\"}", -1);
    IFOK(s, nats_JSONGetStr(json, "test", &strVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (strcmp(strVal, "&<>") == 0));
    nats_JSONDestroy(json);
    json = NULL;
    free(strVal);
    strVal = NULL;

    test("Single field, int: ");
    s = nats_JSONParse(&json, "{\"test\":1234}", -1);
    IFOK(s, nats_JSONGetInt(json, "test", &intVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (intVal == 1234));
    nats_JSONDestroy(json);
    json = NULL;
    intVal = 0;

    test("Single field, int32: ");
    s = nats_JSONParse(&json, "{\"test\":1234}", -1);
    IFOK(s, nats_JSONGetInt32(json, "test", &int32Val));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (int32Val == 1234));
    nats_JSONDestroy(json);
    json = NULL;
    int32Val = 0;

    test("Single field, uint16: ");
    s = nats_JSONParse(&json, "{\"test\":1234}", -1);
    IFOK(s, nats_JSONGetUInt16(json, "test", &uint16Val));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (uint16Val == 1234));
    nats_JSONDestroy(json);
    json = NULL;
    uint16Val = 0;

    test("Single field, long: ");
    s = nats_JSONParse(&json, "{\"test\":9223372036854775807}", -1);
    IFOK(s, nats_JSONGetLong(json, "test", &longVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (longVal == 9223372036854775807L));
    nats_JSONDestroy(json);
    json = NULL;
    longVal = 0;

    test("Single field, neg long: ");
    s = nats_JSONParse(&json, "{\"test\":-9223372036854775808}", -1);
    IFOK(s, nats_JSONGetLong(json, "test", &longVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (longVal == (int64_t) 0x8000000000000000));
    nats_JSONDestroy(json);
    json = NULL;
    longVal = 0;

    test("Single field, neg long as ulong: ");
    s = nats_JSONParse(&json, "{\"test\":-123456789}", -1);
    IFOK(s, nats_JSONGetULong(json, "test", &ulongVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (ulongVal == 0xFFFFFFFFF8A432EB));
    nats_JSONDestroy(json);
    json = NULL;
    ulongVal = 0;

    test("Single field, ulong: ");
    s = nats_JSONParse(&json, "{\"test\":18446744073709551615}", -1);
    IFOK(s, nats_JSONGetULong(json, "test", &ulongVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (ulongVal == 0xFFFFFFFFFFFFFFFF));
    nats_JSONDestroy(json);
    json = NULL;
    ulongVal = 0;

    test("Single field, ulong: ");
    s = nats_JSONParse(&json, "{\"test\":9007199254740993}", -1);
    IFOK(s, nats_JSONGetULong(json, "test", &ulongVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (ulongVal == 9007199254740993));
    nats_JSONDestroy(json);
    json = NULL;
    ulongVal = 0;

    test("Single field, double: ");
    s = nats_JSONParse(&json, "{\"test\":1234.5e3}", -1);
    IFOK(s, nats_JSONGetDouble(json, "test", &doubleVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (doubleVal == (long double) 1234.5e+3));
    nats_JSONDestroy(json);
    json = NULL;
    doubleVal = 0;

    test("Single field, double negative: ");
    s = nats_JSONParse(&json, "{\"test\":-1234}", -1);
    IFOK(s, nats_JSONGetDouble(json, "test", &doubleVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (doubleVal == (long double) -1234));
    nats_JSONDestroy(json);
    json = NULL;
    doubleVal = 0;

    test("Single field, double exp negative 1: ");
    s = nats_JSONParse(&json, "{\"test\":1234e-3}", -1);
    IFOK(s, nats_JSONGetDouble(json, "test",&doubleVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (doubleVal == (long double) 1234.0/1000.0));
    nats_JSONDestroy(json);
    json = NULL;
    doubleVal = 0;

    test("Single field, double exp negative 2: ");
    s = nats_JSONParse(&json, "{\"test\":1234.5e-3}", -1);
    IFOK(s, nats_JSONGetDouble(json, "test", &doubleVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (doubleVal == (long double) 12345.0/10000.0));
    nats_JSONDestroy(json);
    json = NULL;
    doubleVal = 0;

    test("Single field, double exp negative 3: ");
    s = nats_JSONParse(&json, "{\"test\":1234.5e-1}", -1);
    IFOK(s, nats_JSONGetDouble(json, "test", &doubleVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (doubleVal == (long double) 12345.0/100.0));
    nats_JSONDestroy(json);
    json = NULL;
    doubleVal = 0;

    test("Single field, double exp negative 4: ");
    s = nats_JSONParse(&json, "{\"test\":1234.5e-0}", -1);
    IFOK(s, nats_JSONGetDouble(json, "test", &doubleVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (doubleVal == (long double) 12345.0/10.0));
    nats_JSONDestroy(json);
    json = NULL;
    doubleVal = 0;

    test("Single field, double exp positive 1: ");
    s = nats_JSONParse(&json, "{\"test\":1234e+3}", -1);
    IFOK(s, nats_JSONGetDouble(json, "test", &doubleVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (doubleVal == (long double) 1234.0*1000));
    nats_JSONDestroy(json);
    json = NULL;
    doubleVal = 0;

    test("Single field, double exp positive 2: ");
    s = nats_JSONParse(&json, "{\"test\":1234.5e+3}", -1);
    IFOK(s, nats_JSONGetDouble(json, "test", &doubleVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (doubleVal == (long double) 12345.0*100.0));
    nats_JSONDestroy(json);
    json = NULL;
    doubleVal = 0;

    test("Single field, double exp positive 3: ");
    s = nats_JSONParse(&json, "{\"test\":1234.5678e+2}", -1);
    IFOK(s, nats_JSONGetDouble(json, "test", &doubleVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (doubleVal == (long double) 12345678.0/100.0));
    nats_JSONDestroy(json);
    json = NULL;
    doubleVal = 0;

    test("Single field, double exp positive 4: ");
    s = nats_JSONParse(&json, "{\"test\":1234.5678e+4}", -1);
    IFOK(s, nats_JSONGetDouble(json, "test", &doubleVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (doubleVal == (long double) 12345678.0/10000.0));
    nats_JSONDestroy(json);
    json = NULL;
    doubleVal = 0;

    test("Single field, double exp positive 5: ");
    s = nats_JSONParse(&json, "{\"test\":1234.5678e+5}", -1);
    IFOK(s, nats_JSONGetDouble(json, "test", &doubleVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (doubleVal == (long double) 12345678.0*10.0));
    nats_JSONDestroy(json);
    json = NULL;
    doubleVal = 0;

    test("Single field, double exp positive 6: ");
    s = nats_JSONParse(&json, "{\"test\":1234.5678e+0}", -1);
    IFOK(s, nats_JSONGetDouble(json, "test", &doubleVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (doubleVal == (long double) 12345678.0/10000.0));
    nats_JSONDestroy(json);
    json = NULL;
    doubleVal = 0;

    test("Single field, double exp positive 6: ");
    s = nats_JSONParse(&json, "{\"test\":1234.5678e1}", -1);
    IFOK(s, nats_JSONGetDouble(json, "test", &doubleVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (doubleVal == (long double) 12345678.0/1000.0));
    nats_JSONDestroy(json);
    json = NULL;
    doubleVal = 0;

    test("Single field, bool: ");
    s = nats_JSONParse(&json, "{\"test\":true}", -1);
    IFOK(s, nats_JSONGetBool(json, "test", &boolVal));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && boolVal);
    nats_JSONDestroy(json);
    json = NULL;
    boolVal = false;

    test("Single field, string array: ");
    s = nats_JSONParse(&json, "{\"test\":[\"a\",\"b\",\"c\",\"d\",\"e\"]}", -1);
    IFOK(s, nats_JSONGetArrayStr(json, "test", &arrVal, &arrCount));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (arrCount == 5)
                && (strcmp(arrVal[0], "a") == 0)
                && (strcmp(arrVal[1], "b") == 0)
                && (strcmp(arrVal[2], "c") == 0)
                && (strcmp(arrVal[3], "d") == 0)
                && (strcmp(arrVal[4], "e") == 0));
    nats_JSONDestroy(json);
    json = NULL;
    for (i=0; i<arrCount; i++)
        free(arrVal[i]);
    free(arrVal);
    arrVal = NULL;
    arrCount = 0;

    test("Single field, null string array: ");
    s = nats_JSONParse(&json, "{\"test\": null}", -1);
    IFOK(s, nats_JSONGetArrayStr(json, "test", &arrVal, &arrCount));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (arrVal == NULL)
                && (arrCount == 0));
    nats_JSONDestroy(json);
    json = NULL;

    test("Single field, bool array: ");
    s = nats_JSONParse(&json, "{\"test\":[true, false, true]}", -1);
    IFOK(s, nats_JSONGetArrayBool(json, "test", &arrBoolVal, &arrCount));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (arrCount == 3)
                && arrBoolVal[0]
                && !arrBoolVal[1]
                && arrBoolVal[2]);
    nats_JSONDestroy(json);
    json = NULL;
    free(arrBoolVal);
    arrBoolVal = NULL;
    arrCount = 0;

    test("Single field, double array: ");
    s = nats_JSONParse(&json, "{\"test\":[1.0, 2.0, 3.0]}", -1);
    IFOK(s, nats_JSONGetArrayDouble(json, "test", &arrDoubleVal, &arrCount));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (arrCount == 3)
                && (arrDoubleVal[0] == 1.0)
                && (arrDoubleVal[1] == 2.0)
                && (arrDoubleVal[2] == 3.0));
    nats_JSONDestroy(json);
    json = NULL;
    free(arrDoubleVal);
    arrDoubleVal = NULL;
    arrCount = 0;

    test("Single field, int array: ");
    s = nats_JSONParse(&json, "{\"test\":[1, 2, 3]}", -1);
    IFOK(s, nats_JSONGetArrayInt(json, "test", &arrIntVal, &arrCount));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (arrCount == 3)
                && (arrIntVal[0] == 1)
                && (arrIntVal[1] == 2)
                && (arrIntVal[2] == 3));
    nats_JSONDestroy(json);
    json = NULL;
    free(arrIntVal);
    arrIntVal = NULL;
    arrCount = 0;

    test("Single field, long array: ");
    s = nats_JSONParse(&json, "{\"test\":[1, 2, 3]}", -1);
    IFOK(s, nats_JSONGetArrayLong(json, "test", &arrLongVal, &arrCount));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (arrCount == 3)
                && (arrLongVal[0] == 1)
                && (arrLongVal[1] == 2)
                && (arrLongVal[2] == 3));
    nats_JSONDestroy(json);
    json = NULL;
    free(arrLongVal);
    arrLongVal = NULL;
    arrCount = 0;

    test("Single field, ulong array: ");
    s = nats_JSONParse(&json, "{\"test\":[1, 2, 3]}", -1);
    IFOK(s, nats_JSONGetArrayULong(json, "test", &arrULongVal, &arrCount));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (arrCount == 3)
                && (arrULongVal[0] == 1)
                && (arrULongVal[1] == 2)
                && (arrULongVal[2] == 3));
    nats_JSONDestroy(json);
    json = NULL;
    free(arrULongVal);
    arrULongVal = NULL;
    arrCount = 0;

    test("Single field, object array: ");
    s = nats_JSONParse(&json, "{\"test\":[{\"a\": 1},{\"b\": true}]}", -1);
    IFOK(s, nats_JSONGetArrayObject(json, "test", &arrObjVal, &arrCount));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (arrCount == 2)
                && (nats_JSONGetInt(arrObjVal[0], "a", &intVal) == NATS_OK)
                && (intVal == 1)
                && (nats_JSONGetBool(arrObjVal[1], "b", &boolVal) == NATS_OK)
                && boolVal);
    nats_JSONDestroy(json);
    json = NULL;
    free(arrObjVal);
    arrObjVal = NULL;
    arrCount = 0;
    intVal = 0;
    boolVal = false;

    test("Single field, array null: ");
    s = nats_JSONParse(&json, "{\"test\":null}", -1);
    IFOK(s, nats_JSONGetArrayObject(json, "test", &arrObjVal, &arrCount));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (arrObjVal == NULL)
                && (arrCount == 0));
    nats_JSONDestroy(json);
    json = NULL;

    test("Single field, array array: ");
    s = nats_JSONParse(&json, "{\"test\":[[\"a\", \"b\"],[1, 2, 3],[{\"c\": true}]]}", -1);
    IFOK(s, nats_JSONGetArrayArray(json, "test", &arrArrVal, &arrCount));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (arrCount == 3)
                && (nats_JSONArrayAsStrings(arrArrVal[0], &arrVal, &arrCount) == NATS_OK)
                && (arrCount == 2)
                && (strcmp(arrVal[0], "a") == 0)
                && (strcmp(arrVal[1], "b") == 0)
                && (nats_JSONArrayAsInts(arrArrVal[1], &arrIntVal, &arrCount) == NATS_OK)
                && (arrCount == 3)
                && (arrIntVal[0] == 1)
                && (arrIntVal[1] == 2)
                && (arrIntVal[2] == 3)
                && (nats_JSONArrayAsObjects(arrArrVal[2], &arrObjVal, &arrCount) == NATS_OK)
                && (arrCount == 1)
                && (nats_JSONGetBool(arrObjVal[0], "c", &boolVal) == NATS_OK)
                && boolVal);
    nats_JSONDestroy(json);
    json = NULL;
    for (i=0; i<2; i++)
        free(arrVal[i]);
    free(arrVal);
    arrVal = NULL;
    free(arrIntVal);
    arrIntVal = NULL;
    free(arrArrVal);
    arrArrVal = NULL;
    free(arrObjVal);
    arrObjVal = NULL;
    boolVal = false;
    arrCount = 0;

    test("Object: ");
    s = nats_JSONParse(&json, "{\"obj1\":{\"obj2\":{\"obj3\":{\"a\": 1},\"b\":true},\"c\":1.2},\"d\":3}", -1);
    IFOK(s, nats_JSONGetObject(json, "obj1", &obj1));
    IFOK(s, nats_JSONGetObject(obj1, "obj2", &obj2));
    IFOK(s, nats_JSONGetObject(obj2, "obj3", &obj3));
    IFOK(s, nats_JSONGetInt(obj3, "a", &intVal));
    IFOK(s, nats_JSONGetBool(obj2, "b", &boolVal));
    IFOK(s, nats_JSONGetDouble(obj1, "c", &doubleVal));
    IFOK(s, nats_JSONGetLong(json, "d", &longVal));
    testCond((s == NATS_OK)
                && (intVal == 1)
                && boolVal
                && (doubleVal == (long double) 12.0/10.0)
                && (longVal == 3));
    nats_JSONDestroy(json);
    json = NULL;
    intVal = 0;
    boolVal = false;
    doubleVal = 0.0;
    longVal = 0;

    test("Object, null: ");
    s = nats_JSONParse(&json, "{\"obj\":null}", -1);
    IFOK(s, nats_JSONGetObject(json, "obj", &obj1));
    testCond((s == NATS_OK) && (obj1 == NULL));
    nats_JSONDestroy(json);
    json = NULL;

    test("All field types: ");
    s = nats_JSONParse(&json, "{\"bool\":true,\"str\":\"abc\",\"int\":123,\"long\":456,\"double\":123.5,\"array\":[\"a\"]}", -1);
    IFOK(s, nats_JSONGetBool(json, "bool", &boolVal));
    IFOK(s, nats_JSONGetStr(json, "str", &strVal));
    IFOK(s, nats_JSONGetInt(json, "int", &intVal));
    IFOK(s, nats_JSONGetLong(json, "long", &longVal));
    IFOK(s, nats_JSONGetDouble(json, "double", &doubleVal));
    IFOK(s, nats_JSONGetArrayStr(json, "array", &arrVal, &arrCount));
    testCond((s == NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 6)
                && boolVal
                && (strcmp(strVal, "abc") == 0)
                && (intVal == 123)
                && (longVal == 456)
                && (doubleVal == (long double) 1235.0/10.0)
                && (arrCount == 1)
                && (strcmp(arrVal[0], "a") == 0));
    test("Unknown field type: ");
    if (s == NATS_OK)
        s = nats_JSONGetField(json, "int", 255, &f);
    testCond(s != NATS_OK);
    nats_JSONDestroy(json);
    json = NULL;
    free(strVal);
    strVal = NULL;
    boolVal = false;
    intVal = 0;
    longVal = 0;
    doubleVal = 0;
    for (i=0; i<arrCount; i++)
        free(arrVal[i]);
    free(arrVal);
    arrVal = NULL;
    arrCount = 0;

    test("Ask for wrong type: ");
    s = nats_JSONParse(&json, "{\"test\":true}", -1);
    IFOK(s, nats_JSONGetInt(json, "test", &intVal));
    testCond((s != NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (intVal == 0));
    nats_JSONDestroy(json);
    json = NULL;

    test("Ask for wrong type (array): ");
    s = nats_JSONParse(&json, "{\"test\":[\"a\", \"b\"]}", -1);
    IFOK(s, nats_JSONGetArrayField(json, "test", TYPE_INT, &f));
    testCond((s != NATS_OK)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (arrCount == 0)
                && (arrVal == NULL));
    nats_JSONDestroy(json);
    json = NULL;

    test("Ask for unknown type: ");
    s = nats_JSONParse(&json, "{\"test\":true}", -1);
    IFOK(s, nats_JSONGetField(json, "test", 9999, &f));
    testCond((s == NATS_INVALID_ARG)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1));
    nats_JSONDestroy(json);
    json = NULL;

    test("Ask for unknown type (array): ");
    s = nats_JSONParse(&json, "{\"test\":true}", -1);
    IFOK(s, nats_JSONGetArrayField(json, "test", 9999, &f));
    testCond((s == NATS_INVALID_ARG)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1));
    nats_JSONDestroy(json);
    json = NULL;

    test("Check no error and set to default for vars for unknown fields: ");
    {
        const char *initStr = "test";
        const char *initStrArr[] = {"a", "b"};

        strVal = (char*) initStr;
        boolVal = true;
        intVal = 123;
        longVal = 456;
        doubleVal = 789;
        arrVal = (char**)initStrArr;
        arrCount = 2;
        s = nats_JSONParse(&json, "{\"test\":true}", -1);
        IFOK(s, nats_JSONGetStr(json, "str", &strVal));
        IFOK(s, nats_JSONGetInt(json, "int", &intVal));
        IFOK(s, nats_JSONGetLong(json, "long", &longVal));
        IFOK(s, nats_JSONGetBool(json, "bool", &boolVal));
        IFOK(s, nats_JSONGetDouble(json, "bool", &doubleVal));
        IFOK(s, nats_JSONGetArrayStr(json, "array", &arrVal, &arrCount));
        testCond((s == NATS_OK)
                    && (strVal == NULL)
                    && (boolVal == false)
                    && (intVal == 0)
                    && (longVal == 0)
                    && (doubleVal == 0)
                    && (arrCount == 0)
                    && (arrVal == NULL));
        nats_JSONDestroy(json);
        json = NULL;
    }

    test("Wrong string type: ");
    strVal = NULL;
    s = nats_JSONParse(&json, "{\"test\":12345678901112}", -1);
    IFOK(s, nats_JSONGetStr(json, "test", &strVal));
    testCond((s == NATS_INVALID_ARG)
                && (json != NULL)
                && (json->fields != NULL)
                && (json->fields->used == 1)
                && (strVal == NULL));
    nats_JSONDestroy(json);
    json = NULL;

    test("NULL string with -1 len: ");
    s = nats_JSONParse(&json, NULL, -1);
    testCond((s == NATS_INVALID_ARG) && (json == NULL));
    nats_clearLastError();

    test("Field reused: ");
    s = nats_JSONParse(&json, "{\"field\":1,\"field\":2}", -1);
    IFOK(s, nats_JSONGetInt(json, "field", &intVal));
    testCond((s == NATS_OK) && (intVal == 2));
    nats_JSONDestroy(json);
    json = NULL;

    test("Nested arrays ok: ");
    jsonMaxNested = 10;
    s = nats_JSONParse(&json, "{\"test\":[[[1, 2]]]}", -1);
    testCond(s == NATS_OK);
    nats_JSONDestroy(json);
    json = NULL;

    test("Nested arrays not ok: ");
    jsonMaxNested = 10;
    s = nats_JSONParse(&json, "{\"test\":[[[[[[[[[[[[[1, 2]]]]]]]]]]]]]}", -1);
    testCond((s == NATS_ERR) && (json == NULL)
                && (strstr(nats_GetLastError(NULL), " nested arrays of 10") != NULL));
    nats_clearLastError();

    test("Nested objects ok: ");
    s = nats_JSONParse(&json, "{\"test\":{\"a\":{\"b\":{\"c\":1}}}}", -1);
    testCond(s == NATS_OK);
    nats_JSONDestroy(json);
    json = NULL;

    test("Nested arrays not ok: ");
    jsonMaxNested = 10;
    s = nats_JSONParse(&json, "{\"test\":{\"a\":{\"b\":{\"c\":{\"d\":{\"e\":{\"f\":{\"g\":{\"h\":{\"i\":{\"j\":{\"k\":{\"l\":{\"m\":1}}}}}}}}}}}}}}", -1);
    testCond((s == NATS_ERR) && (json == NULL)
                && (strstr(nats_GetLastError(NULL), " nested objects of 10") != NULL));
    nats_clearLastError();
    jsonMaxNested = JSON_MAX_NEXTED;

    // Negative tests
    {
        const char *badTimes[] = {
            "{\"time\":\"too small\"}",
            "{\"time\":\"2021-06-23T18:22:00.123456789-08:00X\"}",
            "{\"time\":\"2021-06-23T18:22:00X\"}",
            "{\"time\":\"2021-06-23T18:22:00-0800\"}",
            "{\"time\":\"2021-06-23T18:22:00-08.00\"}",
            "{\"time\":\"2021-06-23T18:22:00.Z\"}",
            "{\"time\":\"2021-06-23T18:22:00.abcZ\"}",
            "{\"time\":\"2021-06-23T18:22:00.abc-08:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.1234567890-08:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.1234567890Z\"}",
            "{\"time\":\"2021-06-23T18:22:00.123-0800\"}",
        };
        const char *errorsTxt[] = {
            "too small",
            "too long",
            "invalid UTC offset",
            "invalid UTC offset",
            "invalid UTC offset",
            "is invalid",
            "is invalid",
            "is invalid",
            "too long",
            "second fraction",
            "invalid UTC offset",
        };
        for (i=0; i<(int)(sizeof(errorsTxt)/sizeof(char*)); i++)
        {
            longVal = 0;
            snprintf(buf, sizeof(buf), "Bad time '%s': ", badTimes[i]);
            test(buf);
            s = nats_JSONParse(&json, badTimes[i], -1);
            IFOK(s, nats_JSONGetTime(json, "time", &longVal));
            testCond((s != NATS_OK)
                        && (json != NULL)
                        && (longVal == 0)
                        && (strstr(nats_GetLastError(NULL), errorsTxt[i]) != NULL));
            nats_clearLastError();
            nats_JSONDestroy(json);
            json = NULL;
        }
    }

    // Positive tests
    {
        const char *goodTimes[] = {
            "{\"time\":\"0001-01-01T00:00:00Z\"}",
            "{\"time\":\"1970-01-01T01:00:00+01:00\"}",
            "{\"time\":\"2021-06-23T18:22:00Z\"}",
            "{\"time\":\"2021-06-23T18:22:00.1Z\"}",
            "{\"time\":\"2021-06-23T18:22:00.12Z\"}",
            "{\"time\":\"2021-06-23T18:22:00.123Z\"}",
            "{\"time\":\"2021-06-23T18:22:00.1234Z\"}",
            "{\"time\":\"2021-06-23T18:22:00.12345Z\"}",
            "{\"time\":\"2021-06-23T18:22:00.123456Z\"}",
            "{\"time\":\"2021-06-23T18:22:00.1234567Z\"}",
            "{\"time\":\"2021-06-23T18:22:00.12345678Z\"}",
            "{\"time\":\"2021-06-23T18:22:00.123456789Z\"}",
            "{\"time\":\"2021-06-23T18:22:00-07:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.1-07:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.12-07:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.123-07:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.1234-07:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.12345-07:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.123456-07:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.1234567-07:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.12345678-07:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.123456789-07:00\"}",
            "{\"time\":\"2021-06-23T18:22:00+01:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.1+01:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.12+01:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.123+01:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.1234+01:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.12345+01:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.123456+01:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.1234567+01:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.12345678+01:00\"}",
            "{\"time\":\"2021-06-23T18:22:00.123456789+01:00\"}",
        };
        int64_t results[] = {
            0,
            0,
            1624472520000000000,
            1624472520100000000,
            1624472520120000000,
            1624472520123000000,
            1624472520123400000,
            1624472520123450000,
            1624472520123456000,
            1624472520123456700,
            1624472520123456780,
            1624472520123456789,
            1624497720000000000,
            1624497720100000000,
            1624497720120000000,
            1624497720123000000,
            1624497720123400000,
            1624497720123450000,
            1624497720123456000,
            1624497720123456700,
            1624497720123456780,
            1624497720123456789,
            1624468920000000000,
            1624468920100000000,
            1624468920120000000,
            1624468920123000000,
            1624468920123400000,
            1624468920123450000,
            1624468920123456000,
            1624468920123456700,
            1624468920123456780,
            1624468920123456789,
        };
        for (i=0; i<(int)(sizeof(results)/sizeof(int64_t)); i++)
        {
            longVal = 0;
            snprintf(buf, sizeof(buf), "Time '%s' -> %" PRId64 ": ", goodTimes[i], results[i]);
            test(buf);
            s = nats_JSONParse(&json, goodTimes[i], -1);
            IFOK(s, nats_JSONGetTime(json, "time", &longVal));
            testCond((s == NATS_OK)
                        && (json != NULL)
                        && (longVal == results[i]));
            nats_JSONDestroy(json);
            json = NULL;
        }
    }

    test("GetStr bad type: ");
    s = nats_JSONParse(&json, "{\"test\":true}", -1);
    IFOK(s, nats_JSONGetStrPtr(json, "test", (const char**) &strVal));
    testCond((s != NATS_OK) && (strVal == NULL));
    nats_clearLastError();
    nats_JSONDestroy(json);
    json = NULL;

    test("GetStr: ");
    s = nats_JSONParse(&json, "{\"test\":\"direct\"}", -1);
    IFOK(s, nats_JSONGetStrPtr(json, "test", (const char**) &strVal));
    testCond((s == NATS_OK) && (strVal != NULL) && (strcmp(strVal, "direct") == 0));
    nats_JSONDestroy(json);
    json = NULL;

    test("GetBytes bad type: ");
    s = nats_JSONParse(&json, "{\"test\":true}", -1);
    IFOK(s, nats_JSONGetBytes(json, "test", &bytes, &bl));
    testCond((s != NATS_OK) && (bytes == NULL) && (bl == 0));
    nats_clearLastError();
    nats_JSONDestroy(json);
    json = NULL;

    test("GetBytes: ");
    s = nats_JSONParse(&json, "{\"test\":\"dGhpcyBpcyB0ZXN0aW5nIGJhc2U2NCBlbmNvZGluZw==\"}", -1);
    IFOK(s, nats_JSONGetBytes(json, "test", &bytes, &bl));
    testCond((s == NATS_OK) && (bytes != NULL) && (bl == 31)
                && (strncmp((const char*) bytes, "this is testing base64 encoding", bl) == 0));
    nats_clearLastError();
    nats_JSONDestroy(json);
    json = NULL;
    free(bytes);

    test("Range with wrong type: ");
    s = nats_JSONParse(&json, "{\"test\":123}", -1);
    IFOK(s, nats_JSONRange(json, TYPE_STR, 0, _dummyJSONCb, NULL));
    testCond((s == NATS_ERR)
                && (strstr(nats_GetLastError(NULL), "expected value type of")));
    nats_clearLastError();

    test("Range with wrong num type: ");
    s = nats_JSONRange(json, TYPE_NUM, TYPE_INT, _dummyJSONCb, NULL);
    testCond((s == NATS_ERR)
                && (strstr(nats_GetLastError(NULL), "expected numeric type of")));
    nats_clearLastError();

    test("Range ok: ");
    ulongVal = 0;
    s = nats_JSONRange(json, TYPE_NUM, TYPE_UINT, _dummyJSONCb, &ulongVal);
    testCond((s == NATS_OK) && (ulongVal == 123));
    nats_JSONDestroy(json);
    json = NULL;

    test("Range cb returns error: ");
    ulongVal = 0;
    s = nats_JSONParse(&json, "{\"fail\":123}", -1);
    IFOK(s, nats_JSONRange(json, TYPE_NUM, TYPE_UINT, _dummyJSONCb, &ulongVal));
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), "on purpose")));
    nats_clearLastError();
    nats_JSONDestroy(json);
    json = NULL;

    test("Parse empty array: ");
    s = nats_JSONParse(&json, "{\"empty\":[]}", -1);
    testCond(s == NATS_OK);

    test("Get empty array array: ");
    s = nats_JSONGetArrayArray(json, "empty", &arrArrVal, &arrCount);
    testCond((s == NATS_OK) && (arrArrVal == NULL) && (arrCount == 0));

    test("Get empty obj array: ");
    s = nats_JSONGetArrayObject(json, "empty", &arrObjVal, &arrCount);
    testCond((s == NATS_OK) && (arrObjVal == NULL) && (arrCount == 0));

    test("Get empty ulong array: ");
    s = nats_JSONGetArrayULong(json, "empty", &arrULongVal, &arrCount);
    testCond((s == NATS_OK) && (arrULongVal == NULL) && (arrCount == 0));

    test("Get empty long array: ");
    s = nats_JSONGetArrayLong(json, "empty", &arrLongVal, &arrCount);
    testCond((s == NATS_OK) && (arrLongVal == NULL) && (arrCount == 0));

    test("Get empty int array: ");
    s = nats_JSONGetArrayInt(json, "empty", &arrIntVal, &arrCount);
    testCond((s == NATS_OK) && (arrIntVal == NULL) && (arrCount == 0));

    test("Get empty double array: ");
    s = nats_JSONGetArrayDouble(json, "empty", &arrDoubleVal, &arrCount);
    testCond((s == NATS_OK) && (arrDoubleVal == NULL) && (arrCount == 0));

    test("Get empty bool array: ");
    s = nats_JSONGetArrayBool(json, "empty", &arrBoolVal, &arrCount);
    testCond((s == NATS_OK) && (arrBoolVal == NULL) && (arrCount == 0));

    test("Get empty string array: ");
    s = nats_JSONGetArrayStr(json, "empty", &arrVal, &arrCount);
    testCond((s == NATS_OK) && (arrVal == NULL) && (arrCount == 0));

    nats_JSONDestroy(json);
    json = NULL;

}

void test_natsEncodeTimeUTC(void)
{
    natsStatus  s;
    char        buf[36] = {'\0'};
    int         i;
    int64_t     times[] = {
        0,
        1624472520000000000,
        1624472520100000000,
        1624472520120000000,
        1624472520123000000,
        1624472520123400000,
        1624472520123450000,
        1624472520123456000,
        1624472520123456700,
        1624472520123456780,
        1624472520123456789,
    };
    const char  *results[] = {
        "0001-01-01T00:00:00Z",
        "2021-06-23T18:22:00Z",
        "2021-06-23T18:22:00.1Z",
        "2021-06-23T18:22:00.12Z",
        "2021-06-23T18:22:00.123Z",
        "2021-06-23T18:22:00.1234Z",
        "2021-06-23T18:22:00.12345Z",
        "2021-06-23T18:22:00.123456Z",
        "2021-06-23T18:22:00.1234567Z",
        "2021-06-23T18:22:00.12345678Z",
        "2021-06-23T18:22:00.123456789Z",
    };

    test("Buffer too small: ");
    s = nats_EncodeTimeUTC(buf, 10, 0);
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), "too small") != NULL));
    nats_clearLastError();

    for (i=0; i<(int)(sizeof(times)/sizeof(int64_t)); i++)
    {
        char txt[100];

        snprintf(txt, sizeof(txt), "Time %" PRId64 " -> '%s': ", times[i], results[i]);
        test(txt);
        s = nats_EncodeTimeUTC(buf, sizeof(buf), times[i]);
        testCond((s == NATS_OK) && (strcmp(buf, results[i]) == 0));
    }
}

void test_natsErrWithLongText(void)
{
    natsStatus  s;
    char        errTxt[300];
    const char  *output = NULL;
    int         i;

    nats_clearLastError();
    for (i=0; i<(int) sizeof(errTxt)-1; i++)
        errTxt[i] = 'A';
    errTxt[i-1] = '\0';

    test("nats_setError with long text: ");
    s = nats_setError(NATS_ERR, "This is the error: %s", errTxt);
    if (s == NATS_ERR)
        output = nats_GetLastError(&s);
    if (output != NULL)
    {
        int pos = ((int) strlen(output))-1;

        // End of text should contain `...` to indicate that it was truncated.
        for (i=0; i<3; i++)
        {
            if (output[pos--] != '.')
            {
                s = NATS_ILLEGAL_STATE;
                break;
            }
        }
    }
    else
    {
        s = NATS_ILLEGAL_STATE;
    }
    testCond(s == NATS_ERR);
    nats_clearLastError();
}

void test_natsErrStackMoreThanMaxFrames(void)
{
    int             i;
    const int       total = MAX_FRAMES+10;
    char            funcName[MAX_FRAMES+10][64];
    char            result[(MAX_FRAMES+10)*100];
    natsStatus      s = NATS_OK;

    test("Check natsUpdateErrStack called more than MAX_FRAMES: ");
    // When a stack trace is formed, it goes from the most recent
    // function called to the oldest. We are going to call more than
    // MAX_FRAMES with function names being numbers from total down
    // to 0. We expect not to crash and have at least from total to
    // total-MAX_FRAMES.
    for (i=total-1;i>=0;i--)
    {
        snprintf(funcName[i], sizeof(funcName[i]), "%d", (i+1));
        nats_updateErrStack(NATS_ERR, funcName[i]);
    }
    s = nats_GetLastErrorStack(result, sizeof(result));
    if (s == NATS_OK)
    {
        char *ptr = result;
        int   funcID;
        char  expected[64];

        snprintf(expected, sizeof(expected), "%d more...", total-MAX_FRAMES);

        for (i=total;i>total-MAX_FRAMES;i--)
        {
            if (sscanf(ptr, "%d", &funcID) != 1)
            {
                s = NATS_ERR;
                break;
            }
            if (funcID != i)
            {
                s = NATS_ERR;
                break;
            }
            if (funcID > 10)
                ptr += 3;
            else
                ptr +=2;
        }
        // The last should be something like: xx more...
        // where xx is total-MAX_FRAMES
        if ((s == NATS_OK) && (strcmp(ptr, expected) != 0))
            s = NATS_ERR;
    }
    testCond(s == NATS_OK);
}

void test_natsMsg(void)
{
    natsMsg     *msg = NULL;
    natsStatus  s    = NATS_OK;

    test("Check invalid subj (NULL): ");
    s = natsMsg_Create(&msg, NULL, "reply", "data", 4);
    testCond((msg == NULL) && (s == NATS_INVALID_ARG));

    test("Check invalid subj (empty): ");
    s = natsMsg_Create(&msg, "", "reply", "data", 4);
    testCond((msg == NULL) && (s == NATS_INVALID_ARG));

    test("Check invalid reply (empty): ");
    s = natsMsg_Create(&msg, "foo", "", "data", 4);
    testCond((msg == NULL) && (s == NATS_INVALID_ARG));

    test("GetSubject with NULL msg: ");
    testCond(natsMsg_GetSubject(NULL) == NULL);

    test("GetReply with NULL msg: ");
    testCond(natsMsg_GetReply(NULL) == NULL);

    test("GetData with NULL msg: ");
    testCond(natsMsg_GetData(NULL) == NULL);

    test("GetDataLength with NULL msg: ");
    testCond(natsMsg_GetDataLength(NULL) == 0);

    test("Create ok: ");
    s = natsMsg_Create(&msg, "foo", "reply", "data", 4);
    testCond((s == NATS_OK) && (msg != NULL));

    natsMsg_Destroy(msg);
}

void test_natsBase32Decode(void)
{
    natsStatus      s;
    const char      *src       = "KRUGS4ZANFZSA5DIMUQHEZLTOVWHIIDPMYQGCIDCMFZWKMZSEBSGKY3PMRUW4ZY";
    const char      *expected  = "This is the result of a base32 decoding";
    char            dst[256];
    int             dstLen = 0;

    test("Decode: ");
    s = nats_Base32_DecodeString((char*) src, dst, (int) sizeof(dst), &dstLen);
    testCond((s == NATS_OK)
                && (dstLen == (int) strlen(expected))
                && (memcmp((void*) expected, (void*) dst, dstLen) == 0));

    test("Dest too small: ");
    s = nats_Base32_DecodeString((char*) src, dst, 10, &dstLen);
    testCond((s == NATS_INSUFFICIENT_BUFFER) && (dstLen == 0));
    nats_clearLastError();

    test("Invalid string: ");
    s = nats_Base32_DecodeString((char*)"This is invalid content", dst, (int) sizeof(dst), &dstLen);
    testCond((s == NATS_ERR)
                && (nats_GetLastError(NULL) != NULL)
                && (strstr(nats_GetLastError(NULL), "invalid") != NULL));
}

void test_natsBase64Encode(void)
{
    natsStatus  s;
    char        *enc = NULL;
    int         i;
    const char  *testStrings[] = {
            "this is testing base64 encoding",
            "dfslfdlkjsfdllkjfds dfsjlklkfsda dfsalkjklfdsalkj adfskjllkjfdaslkjfdslk",
            "This is another with numbers like 12345678.90 and special characters !@#$%^&*()-=+/",
    };
    const char  *expectedResults[] = {
            "dGhpcyBpcyB0ZXN0aW5nIGJhc2U2NCBlbmNvZGluZw",
            "ZGZzbGZkbGtqc2ZkbGxramZkcyBkZnNqbGtsa2ZzZGEgZGZzYWxramtsZmRzYWxraiBhZGZza2psbGtqZmRhc2xramZkc2xr",
            "VGhpcyBpcyBhbm90aGVyIHdpdGggbnVtYmVycyBsaWtlIDEyMzQ1Njc4LjkwIGFuZCBzcGVjaWFsIGNoYXJhY3RlcnMgIUAjJCVeJiooKS09Ky8",
    };
    const char  *expectedResultsStd[] = {
            "dGhpcyBpcyB0ZXN0aW5nIGJhc2U2NCBlbmNvZGluZw==",
            "ZGZzbGZkbGtqc2ZkbGxramZkcyBkZnNqbGtsa2ZzZGEgZGZzYWxramtsZmRzYWxraiBhZGZza2psbGtqZmRhc2xramZkc2xr",
            "VGhpcyBpcyBhbm90aGVyIHdpdGggbnVtYmVycyBsaWtlIDEyMzQ1Njc4LjkwIGFuZCBzcGVjaWFsIGNoYXJhY3RlcnMgIUAjJCVeJiooKS09Ky8=",
    };
    const uint8_t   someBytes[] = {0, 2, 0, 3, 4, 5, 0, 6, 7, 8, 0, 9, 0};
    const char      *sbe = "AAIAAwQFAAYHCAAJAA==";
    int             sbl  = 13;
    int             sl   = 0;
    int             dl   = 0;
    unsigned char   *dec = NULL;

    test("EncodeURL nil: ");
    s = nats_Base64RawURL_EncodeString(NULL, 0, &enc);
    testCond((s == NATS_OK) && (enc == NULL));

    test("EncodeURL empty: ");
    s = nats_Base64RawURL_EncodeString((const unsigned char*) "", 0, &enc);
    testCond((s == NATS_OK) && (enc == NULL));

    test("EncodeURL strings: ");
    for (i=0; i<(int)(sizeof(testStrings)/sizeof(char*)); i++)
    {
        s = nats_Base64RawURL_EncodeString((const unsigned char*) testStrings[i], (int)strlen(testStrings[i]), &enc);
        if ((s == NATS_OK) && ((enc == NULL) || (strcmp(enc, expectedResults[i]) != 0)))
            s = NATS_ERR;

        free(enc);
        enc = NULL;
        if (s != NATS_OK)
            break;
    }
    testCond(s == NATS_OK);

    test("EncodeURL bytes: ");
    {
        s = nats_Base64RawURL_EncodeString((const unsigned char*) &someBytes, sbl, &enc);
        if ((s == NATS_OK) && ((enc == NULL) || (strcmp(enc, "AAIAAwQFAAYHCAAJAA") != 0)))
            s = NATS_ERR;

        free(enc);
        enc = NULL;
    }
    testCond(s == NATS_OK);

    test("Encode nil: ");
    s = nats_Base64_Encode(NULL, 0, &enc);
    testCond((s == NATS_OK) && (enc == NULL));

    test("Encode empty: ");
    s = nats_Base64_Encode((const unsigned char*) "", 0, &enc);
    testCond((s == NATS_OK) && (enc == NULL));

    test("Encode strings: ");
    for (i=0; i<(int)(sizeof(testStrings)/sizeof(char*)); i++)
    {
        s = nats_Base64_Encode((const unsigned char*) testStrings[i], (int)strlen(testStrings[i]), &enc);
        if ((s == NATS_OK) && ((enc == NULL) || (strcmp(enc, expectedResultsStd[i]) != 0)))
            s = NATS_ERR;

        free(enc);
        enc = NULL;
        if (s != NATS_OK)
            break;
    }
    testCond(s == NATS_OK);

    test("Encode bytes: ");
    {
        s = nats_Base64_Encode((const unsigned char*) &someBytes, sbl, &enc);
        if ((s == NATS_OK) && ((enc == NULL) || (strcmp(enc, sbe) != 0)))
            s = NATS_ERR;

        free(enc);
        enc = NULL;
    }
    testCond(s == NATS_OK);

    test("DecodeLen src needed: ");
    s = nats_Base64_DecodeLen(NULL, &sl, &dl);
    if (s == NATS_INVALID_ARG)
        s = nats_Base64_DecodeLen("", &sl, &dl);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("DecodeLen bad src len: ");
    s = nats_Base64_DecodeLen("foo", &sl, &dl);
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), "invalid base64 length") != NULL));
    nats_clearLastError();

    test("DecodeLen bad content: ");
    s = nats_Base64_DecodeLen("f=oo", &sl, &dl);
    if (s == NATS_INVALID_ARG)
        s = nats_Base64_DecodeLen("@!^*.#_$(foo", &sl, &dl);
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), "invalid base64 character") != NULL));
    nats_clearLastError();

    test("DecodeLen: ");
    s = nats_Base64_DecodeLen(sbe, &sl, &dl);
    testCond((s == NATS_OK) && (sl == (int) strlen(sbe)) && (dl == sbl));

    test("Decode strings: ");
    for (i=0; i<(int)(sizeof(expectedResultsStd)/sizeof(char*)); i++)
    {
        s = nats_Base64_Decode(expectedResultsStd[i], &dec, &dl);
        if ((s == NATS_OK)
                && ((dec == NULL) || (dl != (int) strlen(testStrings[i]))
                    || (strncmp((const char*) dec, testStrings[i], dl) != 0)))
        {
            s = NATS_ERR;
        }
        free(dec);
        dec = NULL;
        dl = 0;
    }
    testCond(s == NATS_OK);

    test("Decode bytes: ");
    s = nats_Base64_Decode(sbe, &dec, &dl);
    if ((s == NATS_OK)
        && ((dec == NULL) || (dl != sbl)
            || (memcmp((const void*) someBytes, (const void*) dec, sbl) != 0)))
    {
        s = NATS_ERR;
    }
    testCond(s == NATS_OK);
    free(dec);
}

void test_natsCRC16(void)
{
    unsigned char   a[] = {153, 209, 36, 74, 103, 32, 65, 34, 111, 68, 104, 156, 50, 14, 164, 140, 144, 230};
    uint16_t        crc = 0;
    uint16_t        expected = 10272;

    test("Compute: ");
    crc = nats_CRC16_Compute(a, (int)sizeof(a));
    testCond(crc == expected);

    test("Verify: ");
    testCond(nats_CRC16_Validate(a, (int)sizeof(a), expected));

    test("Expect failure: ");
    a[3] = 63;
    testCond(!nats_CRC16_Validate(a, (int)sizeof(a), expected));
}

void test_natsKeys(void)
{
    natsStatus          s;
    unsigned char       sig[NATS_CRYPTO_SIGN_BYTES];
    const char          *nonceVal  = "nonce";
    const unsigned char *nonce     = (const unsigned char*) nonceVal;
    const unsigned char expected[] = {
            155, 157,   8, 183,  93, 154,  78,   7,
            219,  39,  11,  16, 134, 231,  46, 142,
            168,  87, 110, 202, 187, 180, 179,  62,
             49, 255, 225,  74,  48,  80, 176, 111,
            248, 162, 121, 188, 203, 101, 100, 195,
            162,  70, 213, 182, 220,  14,  71, 113,
             93, 239, 141, 131,  66, 190, 237, 127,
            104, 191, 138, 217, 227,   1,  92,  14,
    };

    test("Invalid key: ");
    s = natsKeys_Sign("ABC", nonce, 0, sig);
    testCond((s == NATS_ERR)
            && (nats_GetLastError(NULL) != NULL)
            && (strstr(nats_GetLastError(NULL), NKEYS_INVALID_ENCODED_KEY) != NULL));
    nats_clearLastError();

    // This is generated from XYTHISISNOTAVALIDSEED with correct checksum.
    // Expect to get invalid seed
    test("Invalid seed: ");
    s = natsKeys_Sign("LBMVISCJKNEVGTSPKRAVMQKMJFCFGRKFIQ52C", nonce, 0, sig);
    testCond((s == NATS_ERR)
                && (nats_GetLastError(NULL) != NULL)
                && (strstr(nats_GetLastError(NULL), NKEYS_INVALID_SEED) != NULL));
    nats_clearLastError();

    test("Invalid prefix: ");
    s = natsKeys_Sign("SBAUEQ2EIVDEOSCJJJFUYTKOJ5IFCUSTKRKVMV2YLFNECQSDIRCUMR2IJFFEWTCNJZHVAUKSKNKFKVSXLBMVUQKCINCEKRSHJBEUUS2MJVHE6UCRKJJVIVKWK5MFSWV2QA", nonce, 0, sig);
    testCond((s == NATS_ERR)
                && (nats_GetLastError(NULL) != NULL)
                && (strstr(nats_GetLastError(NULL), NKEYS_INVALID_PREFIX) != NULL));
    nats_clearLastError();

    // This is the valid seed: SUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY
    // Make the checksum incorrect by changing last 2 bytes.
    test("Invalid checksum: ");
    s = natsKeys_Sign("SUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4AA", nonce, 0, sig);
    testCond((s == NATS_ERR)
                && (nats_GetLastError(NULL) != NULL)
                && (strstr(nats_GetLastError(NULL), NKEYS_INVALID_CHECKSUM) != NULL));
    nats_clearLastError();

    // Now use valid SEED
    test("Sign ok: ");
    s = natsKeys_Sign("SUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY", nonce, 0, sig);
    testCond((s == NATS_OK)
                && (memcmp(sig, expected, sizeof(expected)) == 0));
}

void test_natsReadFile(void)
{
    natsStatus      s    = NATS_OK;
    natsBuffer      *buf = NULL;
    FILE            *f   = NULL;
    const char      *fn  = "test_readfile.txt";
    const char      *content = "This is some content.\nThere are 2 lines in this file.\n";

    test("Invalid arg 1: ");
    s = nats_ReadFile(&buf, 0, "file.txt");
    testCond((s == NATS_INVALID_ARG) && (buf == NULL));

    test("Invalid arg 2: ");
    s = nats_ReadFile(&buf, -1, "file.txt");
    testCond((s == NATS_INVALID_ARG) && (buf == NULL));

    test("Invalid arg 3: ");
    s = nats_ReadFile(&buf, 100, NULL);
    testCond((s == NATS_INVALID_ARG) && (buf == NULL));

    test("Invalid arg 4: ");
    s = nats_ReadFile(&buf, 100, "");
    testCond((s == NATS_INVALID_ARG) && (buf == NULL));

    test("File not found: ");
    s = nats_ReadFile(&buf, 100, "fileNotFound.txt");
    testCond((s == NATS_ERR)
                && (strstr(nats_GetLastError(NULL), "fileNotFound.txt") != NULL)
                && (buf == NULL));
    nats_clearLastError();

    // Create temp file with some content...
    f = fopen(fn, "w");
    if (f == NULL)
    {
        FAIL("Unable to create test file");
    }
    else
    {
        int res = fputs(content, f);
        if (res < 0)
            FAIL("Unable to write content of test file");

        fclose(f);
        f = NULL;
    }

    test("Read with large buffer: ");
    s = nats_ReadFile(&buf, 1024, fn);
    testCond((s == NATS_OK)
                && (buf != NULL)
                && (natsBuf_Capacity(buf) == 1024)
                && (natsBuf_Len(buf) == (int) strlen(content)+1)
                && (strcmp(natsBuf_Data(buf), content) == 0));
    natsBuf_Destroy(buf);
    buf = NULL;

    test("Read with small buffer: ");
    // The content is 55 bytes. We start with buffer of 10 and x2
    // when expanding, so capacity should be 10, 20, 40, 80.
    s = nats_ReadFile(&buf, 10, fn);
    testCond((s == NATS_OK)
                && (buf != NULL)
                && (natsBuf_Capacity(buf) == 80)
                && (natsBuf_Len(buf) == (int) strlen(content)+1)
                && (strcmp(natsBuf_Data(buf), content) == 0));
    natsBuf_Destroy(buf);
    buf = NULL;

    test("Read with buffer of exact file content: ");
    // Use a buf capacity that matches exactly the content on file.
    // But since we need to add the terminating `\0`, then we will
    // need to expand.
    s = nats_ReadFile(&buf, (int)strlen(content), fn);
    testCond((s == NATS_OK)
                && (buf != NULL)
                && (natsBuf_Capacity(buf) == (int)strlen(content)*2)
                && (natsBuf_Len(buf) == (int) strlen(content)+1)
                && (strcmp(natsBuf_Data(buf), content) == 0));
    natsBuf_Destroy(buf);
    buf = NULL;

    s = nats_ReadFile(&buf, (int)strlen(content)+1, fn);
    testCond((s == NATS_OK)
                && (buf != NULL)
                && (natsBuf_Capacity(buf) == (int)strlen(content)+1)
                && (natsBuf_Len(buf) == (int) strlen(content)+1)
                && (strcmp(natsBuf_Data(buf), content) == 0));
    natsBuf_Destroy(buf);
    buf = NULL;

    remove(fn);
}

void test_natsGetJWTOrSeed(void)
{
    natsStatus  s;
    char        *val = NULL;
    char        buf[256];
    const char  *valids[] = {
        "--- START JWT ---\nsome value\n--- END JWT ---\n",
        "\r\n\r\n--- START JWT ---\r\nsome value\r\n--- END JWT ---\r\n",
        "--- ---\nsome value\n--- ---\n",
        "------\nsome value\n------\n",
        "---\nabc\n--\n---START---\nsome value\n---END---\n----\ndef\n--- ---\n",
        "nothing first\nthen it starts\n\r\n  --- START ---\r\n\n\n\r\nsome value\n--- END ---\n\n---START---\nof something else\n---END---\n",
        "--- START ---\nsome value\n\n\n--- END ---\n",
    };
    const char *invalids[] = {
        "-- JWT -- START ----\nsome value\n--- END ---\n",
        "--- START --- \nsome value\n--- END ---\n",
        "--- START ---\nsome value\n-- END ---\n",
        "--- START ---\nsome value\n---- END --- \n",
    };
    int i;
    int iter;

    for (iter=0; iter<2; iter++)
    {
        for (i=0; i<(int)(sizeof(valids)/sizeof(char*));i++)
        {
            snprintf(buf, sizeof(buf), "%s %d: ", (iter==0 ? "JWT" : "Seed"), (i+1));
            test(buf);
            snprintf(buf, sizeof(buf), "%s%s", (iter == 0 ? "" : "------\njwt\n------\n"), valids[i]);
            s = nats_GetJWTOrSeed(&val, buf, iter);
            testCond((s == NATS_OK) && (val != NULL) && (strcmp(val, "some value") == 0));
            free(val);
            val = NULL;
        }

        for (i=0; i<(int)(sizeof(invalids)/sizeof(char*));i++)
        {
            snprintf(buf, sizeof(buf), "%s invalid %d: ", (iter == 0 ? "JWT" : "Seed"), (i+1));
            test(buf);
            snprintf(buf, sizeof(buf), "%s%s", (iter == 0 ? "" : "------\njwt\n------\n"), invalids[i]);
            s = nats_GetJWTOrSeed(&val, buf, iter);
            testCond((s == NATS_NOT_FOUND) && (val == NULL));
        }
    }
}

void test_natsHostIsIP(void)
{
    struct _testHost {
        const char *host;
        bool       isIP;
    };
    const struct _testHost hosts[] = {
        { "1.2.3.4", true },
        { "::1", true },
        { "localhost", false },
        { "www.host.name.com", false },
    };
    int i;

    for (i=0; i<(int)(sizeof(hosts)/sizeof(struct _testHost)); i++)
    {
        char buf[256];

        snprintf(buf, sizeof(buf), "Check '%s': ", hosts[i].host);
        test(buf);
        testCond(nats_HostIsIP(hosts[i].host) == hosts[i].isIP)
    }
}

static void
_testWaitReadyServer(void *closure)
{
    struct addrinfo     *servinfo = NULL;
    natsStatus          s         = NATS_OK;
    natsSock            sock      = NATS_SOCK_INVALID;
    natsSock            cliSock   = NATS_SOCK_INVALID;
    struct threadArg    *arg      = (struct threadArg*) closure;
    struct addrinfo     hints;
    int                 res;

    memset(&hints,0,sizeof(hints));

    hints.ai_family   = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags    = AI_PASSIVE;

    if ((res = getaddrinfo("127.0.0.1", "1234", &hints, &servinfo)) != 0)
         s = NATS_SYS_ERROR;
    if (s == NATS_OK)
    {
        sock = socket(servinfo->ai_family, servinfo->ai_socktype,
                      servinfo->ai_protocol);
        if (sock == NATS_SOCK_INVALID)
            s = NATS_SYS_ERROR;

        IFOK(s, natsSock_SetCommonTcpOptions(sock));
        IFOK(s, natsSock_SetBlocking(sock, true));
    }
    if ((s == NATS_OK)
        && (bind(sock, servinfo->ai_addr, (natsSockLen) servinfo->ai_addrlen) == NATS_SOCK_ERROR))
    {
        s = NATS_SYS_ERROR;
    }

    if ((s == NATS_OK) && (listen(sock, 100) == 0))
    {
        cliSock = accept(sock, NULL, NULL);
        if ((cliSock != NATS_SOCK_INVALID)
                && (natsSock_SetCommonTcpOptions(cliSock) == NATS_OK))
        {
            nats_Sleep(500);

            send(cliSock, "*", 1, 0);

            natsMutex_Lock(arg->m);
            while ((s != NATS_TIMEOUT) && !arg->done)
                s = natsCondition_TimedWait(arg->c, arg->m, 10000);
            natsMutex_Unlock(arg->m);

            natsSock_Close(cliSock);
        }
    }
    natsSock_Close(sock);
    nats_FreeAddrInfo(servinfo);
}

static void
_testSockShutdownThread(void *closure)
{
    natsSockCtx *ctx = (natsSockCtx*) closure;

    nats_Sleep(500);
    natsSock_Shutdown(ctx->fd);
}

void test_natsWaitReady(void)
{
    natsStatus          s  = NATS_OK;
    natsThread          *t = NULL;
    natsThread          *t2 = NULL;
    natsSockCtx         ctx;
    int64_t             start, dur;
    char                buffer[1];
    int                 i;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    if (natsThread_Create(&t, _testWaitReadyServer, &arg) != NATS_OK)
    {
        _destroyDefaultThreadArgs(&arg);
        FAIL("Unable to setup test");
    }

    test("Connect: ");
    natsSock_Init(&ctx);
    ctx.orderIP = 4;
    natsSock_ClearDeadline(&ctx);
    for (i=0; i<20; i++)
    {
        s = natsSock_ConnectTcp(&ctx, "127.0.0.1", 1234);
        if (s == NATS_OK)
            break;
        nats_Sleep(100);
    }
    testCond(s == NATS_OK);

    test("Set non blocking: ");
    s = natsSock_SetCommonTcpOptions(ctx.fd);
    IFOK(s, natsSock_SetBlocking(ctx.fd, false));
    testCond(s == NATS_OK);

    // Ensure that we get a would_block on read..
    while (recv(ctx.fd, buffer, 1, 0) != -1) {}

    test("WaitReady no deadline: ");
    natsSock_ClearDeadline(&ctx);
    start = nats_Now();
    s = natsSock_WaitReady(WAIT_FOR_READ, &ctx);
    dur = nats_Now()-start;
    testCond((s == NATS_OK) && (dur >= 450) && (dur <= 600));

    // Ensure that we get a would_block on read..
    while (recv(ctx.fd, buffer, 1, 0) != -1) {}

    test("WaitReady deadline timeout: ");
    natsSock_InitDeadline(&ctx, 50);
    start = nats_Now();
    s = natsSock_WaitReady(WAIT_FOR_READ, &ctx);
    dur = nats_Now()-start;
    testCond((s == NATS_TIMEOUT) && (dur >= 40) && (dur <= 100));

    // Ensure that we get a would_block on read..
    while (recv(ctx.fd, buffer, 1, 0) != -1) {}

    test("WaitReady kicked out by shutdown: ");
    natsSock_ClearDeadline(&ctx);
    start = nats_Now();
    s = natsThread_Create(&t2, _testSockShutdownThread, &ctx);
    IFOK(s, natsSock_WaitReady(WAIT_FOR_READ, &ctx));
    dur = nats_Now()-start;
    testCond((s == NATS_OK) && (dur <= 3000));

    natsSock_Close(ctx.fd);

    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    natsThread_Join(t);
    natsThread_Destroy(t);

    if (t2 != NULL)
    {
        natsThread_Join(t2);
        natsThread_Destroy(t2);
    }

    _destroyDefaultThreadArgs(&arg);
}

void test_natsSign(void)
{
    unsigned char   *sig   = NULL;
    int             sigLen = 0;
    char            *sig64 = NULL;
    natsStatus      s;

    test("nats_Sign invalid param 1: ");
    s = nats_Sign(NULL, "nonce", &sig, &sigLen);
    testCond(s == NATS_INVALID_ARG);

    test("nats_Sign invalid param 2: ");
    s = nats_Sign("seed", NULL, &sig, &sigLen);
    testCond(s == NATS_INVALID_ARG);

    test("nats_Sign invalid param 3: ");
    s = nats_Sign("seed", "nonce", NULL, &sigLen);
    testCond(s == NATS_INVALID_ARG);

    test("nats_Sign invalid param 4: ");
    s = nats_Sign("seed", "nonce", &sig, NULL);
    testCond(s == NATS_INVALID_ARG);

    nats_clearLastError();

    test("Sign ok: ");
    s = nats_Sign(
        "SUACSSL3UAHUDXKFSNVUZRF5UHPMWZ6BFDTJ7M6USDXIEDNPPQYYYCU3VY",
        "nonce", &sig, &sigLen);
    IFOK(s, nats_Base64RawURL_EncodeString((const unsigned char*) sig, sigLen, &sig64));
    testCond((s == NATS_OK)
        && (sig != NULL)
        && (sig64 != NULL)
        && (sigLen == NATS_CRYPTO_SIGN_BYTES)
        && (memcmp((void*) sig64,
                   (void*) "AVfpO7Pw3rc56hoO1OJcFxXUCfBmO2qouchBchSlL45Fuur9zS15UzytEI1QC5wwVG7uiHIdqyfmOS6uPrwqCg",
                   NATS_CRYPTO_SIGN_BYTES) == 0));

    free(sig);
    free(sig64);
}

static void
_testHeader(const char *testName, char *buf, natsStatus expected, const char *errTxt,
            const char *key, const char *value)
{
    natsStatus  s    = NATS_OK;
    natsMsg     *msg = NULL;
    const char  *val = NULL;
    const char  *k   = (key == NULL ? "k" : key);

    test(testName);
    s = natsMsg_create(&msg, "foo", 3, NULL, 0, buf, (int)strlen(buf), (int) strlen(buf));
    IFOK(s, natsMsgHeader_Get(msg, k, &val));
    if (expected == NATS_OK)
    {
        testCond((s == NATS_OK) && (val != NULL) && (strcmp(val, value) == 0));
    }
    else
    {
        const char *le = nats_GetLastError(&s);
        testCond((s == expected) && (strstr(le, errTxt) != NULL));
        nats_clearLastError();
    }

    natsMsg_Destroy(msg);
}

static void
_testStatus(const char *testName, char *buf, const char *expectedStatus, const char *expectedDescription)
{
    natsStatus  s     = NATS_OK;
    natsMsg     *msg  = NULL;
    const char  *sts  = NULL;
    const char  *desc = NULL;

    test(testName);
    s = natsMsg_create(&msg, "foo", 3, NULL, 0, buf, (int)strlen(buf), (int) strlen(buf));
    IFOK(s, natsMsgHeader_Get(msg, STATUS_HDR, &sts));
    IFOK(s, natsMsgHeader_Get(msg, DESCRIPTION_HDR, &desc));
    testCond((s == (expectedDescription == NULL ? NATS_NOT_FOUND : NATS_OK)
            && ((sts != NULL) && (strcmp(sts, expectedStatus) == 0))
            && (expectedDescription == NULL
                    ? desc == NULL
                    : ((desc != NULL) && (strcmp(desc, expectedDescription) == 0)))));

    natsMsg_Destroy(msg);
}

void test_HeadersLift(void)
{
    char buf[512];

    snprintf(buf, sizeof(buf), "%sk:v\r\n\r\n", HDR_LINE);
    _testHeader("Valid simple header: ", buf, NATS_OK, "", "k", "v");

    snprintf(buf, sizeof(buf), "%sk e y:v\r\n\r\n", HDR_LINE);
    _testHeader("Key with spaces ok: ", buf, NATS_OK, "", "k e y", "v");

    snprintf(buf, sizeof(buf), "%sk e y  :v\r\n\r\n", HDR_LINE);
    _testHeader("Key with spaces (including traling) ok: ", buf, NATS_OK, "", "k e y  ", "v");

    snprintf(buf, sizeof(buf), "%sk:  v   \r\n\r\n", HDR_LINE);
    _testHeader("Trim spaces for value: ", buf, NATS_OK, "", "k", "v");

    snprintf(buf, sizeof(buf), "%sk: a\r\n   bc\r\n def\r\n\r\n", HDR_LINE);
    _testHeader("Multiline values: ", buf, NATS_OK, "", "k", "a bc def");

    snprintf(buf, sizeof(buf), "%s", "NATS\r\nk:v\r\n\r\n");
    _testHeader("NATS header missing: ", buf, NATS_PROTOCOL_ERROR, "header prefix missing", NULL, NULL);

    snprintf(buf, sizeof(buf), "%s", HDR_LINE);
    _testHeader("NATS header missing CRLF: ", buf, NATS_PROTOCOL_ERROR, "early termination of headers", NULL, NULL);

    snprintf(buf, sizeof(buf), "%sk:v\r\n\rbad\r\n", HDR_LINE);
    _testHeader("Invalid key start: ", buf, NATS_PROTOCOL_ERROR, "invalid start of a key", NULL, NULL);

    snprintf(buf, sizeof(buf), "%s k:v\r\n\r\n", HDR_LINE);
    _testHeader("Space in key name: ", buf, NATS_PROTOCOL_ERROR, "key cannot start with a space", NULL, NULL);

    snprintf(buf, sizeof(buf), "%sk\r\n\r\n", HDR_LINE);
    _testHeader("Column missing: ", buf, NATS_PROTOCOL_ERROR, "column delimiter not found", NULL, NULL);

    snprintf(buf, sizeof(buf), "%sk:\r\n\r\n", HDR_LINE);
    _testHeader("No value: ", buf, NATS_PROTOCOL_ERROR, "no value found for key", NULL, NULL);

    snprintf(buf, sizeof(buf), "%sk:       \r\n\r\n", HDR_LINE);
    _testHeader("No value (extra spaces): ", buf, NATS_PROTOCOL_ERROR, "no value found for key", NULL, NULL);

    // Check status description in header line prefix...

    snprintf(buf, sizeof(buf), "%s 503\r\n\r\n", HDR_LINE_PRE);
    _testStatus("Status no description: ", buf, "503", NULL);

    snprintf(buf, sizeof(buf), "%s  503 \r\n\r\n", HDR_LINE_PRE);
    _testStatus("Status no description (extra space): ", buf, "503", NULL);

    snprintf(buf, sizeof(buf), "%s  503  \r\n\r\n", HDR_LINE_PRE);
    _testStatus("Status no description (extra spaces): ", buf, "503", NULL);

    snprintf(buf, sizeof(buf), "%s 503 No Responders\r\n\r\n", HDR_LINE_PRE);
    _testStatus("Status with description: ", buf, "503", "No Responders");

    snprintf(buf, sizeof(buf), "%s 404 No Messages \r\n\r\n", HDR_LINE_PRE);
    _testStatus("Status with description (extra space): ", buf, "404", "No Messages");

    snprintf(buf, sizeof(buf), "%s  404   No Messages   \r\n\r\n", HDR_LINE_PRE);
    _testStatus("Status with description (extra spaces): ", buf, "404", "No Messages");
}

void test_natsMsgHeaderAPIs(void)
{
    natsStatus  s        = NATS_OK;
    natsMsg     *msg     = NULL;
    const char  *val     = NULL;
    const char* *values  = NULL;
    const char* *keys    = NULL;
    int         count    = 0;
    const char  *longKey = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";

    test("Create message: ");
    s = natsMsg_Create(&msg, "foo", NULL, "body", 4);
    testCond(s == NATS_OK);

    test("Key cannot be NULL: ");
    s = natsMsgHeader_Set(msg, NULL, "value");
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Key cannot be empty: ");
    s = natsMsgHeader_Set(msg, "", "value");
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Set msg cannot be NULL: ");
    s = natsMsgHeader_Set(NULL, "key", "value");
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Set value: ");
    s = natsMsgHeader_Set(msg, "my-key", "value1");
    testCond(s == NATS_OK);

    test("Get msg cannot be NULL: ");
    s = natsMsgHeader_Get(NULL, "my-key", &val);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Get must provide mem location: ");
    s = natsMsgHeader_Get(msg, "my-key", NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Get: ");
    s = natsMsgHeader_Get(msg, "my-key", &val);
    testCond((s == NATS_OK) &&
                (val != NULL) &&
                (strcmp(val, "value1") == 0));
    val = NULL;

    test("Get value with different case: ");
    s = natsMsgHeader_Get(msg, "my-Key", &val);
    testCond((s == NATS_NOT_FOUND) && (val == NULL));
    val = NULL;

    test("Key not found: ");
    s = natsMsgHeader_Get(msg, "unknown-key", &val);
    testCond((s == NATS_NOT_FOUND) && (val == NULL));
    val = NULL;

    test("Set value replace old: ");
    s = natsMsgHeader_Set(msg, "my-key", "value2");
    testCond(s == NATS_OK);

    test("Get value: ");
    s = natsMsgHeader_Get(msg, "my-key", &val);
    testCond((s == NATS_OK) &&
                (val != NULL) &&
                (strcmp(val, "value2") == 0));
    val = NULL;

    test("Set NULL value: ");
    s = natsMsgHeader_Set(msg, "my-key", NULL);
    testCond(s == NATS_OK);

    test("Get value: ");
    s = natsMsgHeader_Get(msg, "my-key", &val);
    testCond((s == NATS_OK) && (val == NULL));
    val = NULL;

    test("Set empty value: ");
    s = natsMsgHeader_Set(msg, "my-key", "");
    testCond(s == NATS_OK);

    test("Get value: ");
    s = natsMsgHeader_Get(msg, "my-key", &val);
    testCond((s == NATS_OK) && (val != NULL) && (val[0] == '\0'));
    val = NULL;

    test("Add msg cannot be NULL: ");
    s = natsMsgHeader_Add(NULL, "key", "value");
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Add first: ");
    s = natsMsgHeader_Add(msg, "two-fields", "val1");
    testCond(s == NATS_OK);

    test("Add second: ");
    s = natsMsgHeader_Add(msg, "two-fields", "val2");
    testCond(s == NATS_OK);

    test("Get should return first: ");
    s = natsMsgHeader_Get(msg, "two-fields", &val);
    testCond((s == NATS_OK) &&
                (val != NULL) &&
                (strcmp(val, "val1") == 0));
    val = NULL;

    test("Values: ");
    s = natsMsgHeader_Values(msg, "two-fields", &values, &count);
    testCond((s == NATS_OK) && (values != NULL) && (count == 2) &&
                (strcmp(values[0], "val1") == 0) &&
                (strcmp(values[1], "val2") == 0));

    if (values != NULL)
        free((void*) values);
    values = NULL;
    count  = 0;

    test("Add after a Set: ");
    s = natsMsgHeader_Set(msg, "my-other-key", "val3");
    IFOK(s, natsMsgHeader_Add(msg, "my-other-key", "val4"));
    IFOK(s, natsMsgHeader_Values(msg, "my-other-key", &values, &count));
    testCond((s == NATS_OK) && (values != NULL) && (count == 2) &&
                (strcmp(values[0], "val3") == 0) &&
                (strcmp(values[1], "val4") == 0));

    if (values != NULL)
        free((void*) values);
    values = NULL;
    count  = 0;

    test("Keys msg cannot be NULL: ");
    s = natsMsgHeader_Keys(NULL, &keys, &count);
    testCond((s == NATS_INVALID_ARG) && (keys == NULL) && (count == 0));
    if (s == NATS_INVALID_ARG)
    {
        s = NATS_OK;
        nats_clearLastError();
    }

    test("Keys keys cannot be NULL: ");
    s = natsMsgHeader_Keys(msg, NULL, &count);
    testCond((s == NATS_INVALID_ARG) && (keys == NULL) && (count == 0));
    nats_clearLastError();

    test("Keys count cannot be NULL: ");
    s = natsMsgHeader_Keys(msg, &keys, NULL);
    testCond((s == NATS_INVALID_ARG) && (keys == NULL) && (count == 0));
    nats_clearLastError();

    test("Keys: ");
    s = natsMsgHeader_Keys(msg, &keys, &count);
    if ((s == NATS_OK) && ((keys == NULL) || (count != 3)))
    {
        s = NATS_ERR;
    }
    else
    {
        int i;
        bool ok1 = false;
        bool ok2 = false;
        bool ok3 = false;

        for (i=0; i<count; i++)
        {
            if (strcmp(keys[i], "my-key") == 0)
                ok1 = true;
            else if (strcmp(keys[i], "two-fields") == 0)
                ok2 = true;
            else if (strcmp(keys[i], "my-other-key") == 0)
                ok3 = true;
        }
        if (!ok1 || !ok2 || !ok3)
            s = NATS_ERR;
    }
    testCond(s == NATS_OK);
    if (keys != NULL)
        free((void*) keys);
    count = 0;

    test("Set with long key: ");
    s = natsMsgHeader_Set(msg, longKey, "val1");
    testCond(s == NATS_OK);

    test("Add with long key: ");
    s = natsMsgHeader_Add(msg, longKey, "val2");
    testCond(s == NATS_OK);

    test("Get with long key: ");
    s = natsMsgHeader_Get(msg, longKey, &val);
    testCond((s == NATS_OK) && (val != NULL) && (strcmp(val, "val1") == 0));

    test("Values with long key: ");
    s = natsMsgHeader_Values(msg, longKey, &values, &count);
    testCond((s == NATS_OK) && (values != NULL) && (count == 2) &&
                (strcmp(values[0], "val1") == 0) &&
                (strcmp(values[1], "val2") == 0));
    free((void*) values);
    count = 0;

    test("Delete msg cannot be NULL: ");
    s = natsMsgHeader_Delete(NULL, "key");
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Delete key cannot be NULL: ");
    s = natsMsgHeader_Delete(msg, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Delete key cannot be empty: ");
    s = natsMsgHeader_Delete(msg, "");
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Delete: ");
    s = natsMsgHeader_Delete(msg, "my-other-key");
    testCond(s == NATS_OK);

    test("Should be gone: ");
    s = natsMsgHeader_Get(msg, "my-other-key", &val);
    testCond((s == NATS_NOT_FOUND) && (val == NULL));

    natsMsg_Destroy(msg);
}

void test_natsMsgIsJSCtrl(void)
{
    struct testCase {
        const char  *buf;
        int         dataLen;
        bool        isCtrl;
        int         ct;
    };
    const struct testCase cases[] = {
        {"key:value\r\n\r\n", 0, false, 0},
        {"data", 4, false, 0},
        {"NATS/1.0 \r\n\r\n", 0, false, 0},
        {"NATS/1.0 100 Idle Heartbeat\r\n\r\ndata", 4, false, 0},
        {"NATS/1.0 200 Some status...\r\n\r\n", 0, false, 0},
        {"NATS/1.0100 Idle Heartbeat\r\n\r\n", 0,  false, 0},
        {"NATS/1.0 1000 Some status\r\n\r\n", 0, false, 0},
        {"NATS/1.0 100-Some status\r\n\r\n", 0, false, 0},
        {"NATS/1.0 100\r\n\r\n", 0, true, 0},
        {"NATS/1.0  100\r\n\r\n", 0, true, 0},
        {"NATS/1.0   100\r\n\r\n", 0, true, 0},
        {"NATS/1.0   100 \r\n\r\n", 0, true, 0},
        {"NATS/1.0   100  \r\n\r\n", 0, true, 0},
        {"NATS/1.0  100  \r\n\r\n", 0, true, 0},
        {"NATS/1.0 100  \r\n\r\n", 0, true, 0},
        {"NATS/1.0 100 \r\n\r\n", 0, true, 0},
        {"NATS/1.0 100\n\r\n", 0, true, 0},
        {"NATS/1.0 100  Idle Heartbeat\r\n\r\n", 0, true, jsCtrlHeartbeat},
        {"NATS/1.0 100 Idle Heartbeat\r\n\r\n", 0, true, jsCtrlHeartbeat},
        {"NATS/1.0 100  FlowControl Request\r\n\r\n", 0, true, jsCtrlFlowControl},
        {"NATS/1.0 100 FlowControl Request\r\n\r\n", 0, true, jsCtrlFlowControl},
    };
    natsStatus  s;
    natsMsg     *msg = NULL;
    char        temp[64];
    int         i;

    for (i=0; i<(int)(sizeof(cases)/sizeof(struct testCase)); i++)
    {
        int bufLen;

        snprintf(temp, sizeof(temp), "Case %d - isCtrl=%d: ", (i+1), cases[i].isCtrl);
        test(temp);
        bufLen = (int)strlen(cases[i].buf);
        s = natsMsg_create(&msg, "foo", 3, NULL, 0, cases[i].buf, bufLen, bufLen-cases[i].dataLen);
        if (s == NATS_OK)
        {
            int ctrlType = 0;
            bool isCtrl  = false;

            isCtrl = natsMsg_isJSCtrl(msg, &ctrlType);
            s = ((isCtrl == cases[i].isCtrl) && (ctrlType == cases[i].ct) ? NATS_OK : NATS_ERR);
        }
        testCond(s == NATS_OK);
        natsMsg_Destroy(msg);
        msg = NULL;
    }
}

void test_natsSrvVersionAtLeast(void)
{
    natsOptions     *opts   = NULL;
    natsConnection  *nc     = NULL;
    const char      *infos[]= {
        "INFO {\"version\":\"2.7.2\"}\r\n",
        "INFO {\"version\":\"bad.version\"}\r\n",
        "INFO {\"no_version\":\"2.7.2\"}\r\n",
        "INFO {\"version\":\"2.7.3-beta01\"}\r\n",
    };
    const int res[][3] = {
        {2, 7, 2},
        {0, 0, 0},
        {0, 0, 0},
        {2, 7, 3},
    };
    const int checksOK[][3] = {
        {1, 0, 0},
        {1, 1, 4},
        {1, 8, 0},
        {1, 8, 4},
        {2, 7, 3},
        {2, 6, 4},
    };
    const int checksBad[][3] = {
        {2, 7, 4},
        {2, 8, 0},
        {3, 0, 0},
    };
    natsStatus s = NATS_OK;
    int i;

    s = natsOptions_Create(&opts);
    IFOK(s, natsConn_create(&nc, opts));
    IFOK(s, natsParser_Create(&(nc->ps)));
    if (s != NATS_OK)
        FAIL("Unable to setup the test");

    test("Check version parsing: ");
    for (i=0; (s == NATS_OK) && (i<(int)(sizeof(infos)/sizeof(char*))); i++)
    {
        IFOK(s, natsParser_Parse(nc, (char*) infos[i], (int) strlen(infos[i])));
        if (s == NATS_OK)
        {
            natsConn_Lock(nc);
            if ((nc->srvVersion.ma != res[i][0]) || (nc->srvVersion.mi != res[i][1])
                || (nc->srvVersion.up != res[i][2]))
            {
                s = NATS_ERR;
            }
            natsConn_Unlock(nc);
        }
    }
    testCond(s == NATS_OK);

    // Server version is 2.7.3 from last parsing

    test("Check OK: ");
    for (i=0; (s == NATS_OK) && (i<(int)(sizeof(checksOK)/sizeof(int[3]))); i++)
        s = natsConn_srvVersionAtLeast(nc, checksOK[i][0], checksOK[i][1], checksOK[i][2]) ? NATS_OK : NATS_ERR;
    testCond(s == NATS_OK);

    test("Check Bad: ");
    for (i=0; (s == NATS_OK) && (i<(int)(sizeof(checksBad)/sizeof(int[3]))); i++)
        s = natsConn_srvVersionAtLeast(nc, checksBad[i][0], checksBad[i][1], checksBad[i][2]) ? NATS_ERR : NATS_OK;
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
}

void test_natsFormatStringArray(void)
{
    natsStatus s;
    size_t i, N;

    typedef struct
    {
        const char *name;
        const char **in;
        int inLen;
        const char *out;
    } TC;

    TC tcs[] = {
        {"Empty", NULL, 0, "[]"},
        {"One", (const char *[]){"one"}, 1, "[\"one\"]"},
        {"Three", (const char *[]){"one", "two", "three"}, 3, "[\"one\",\"two\",\"three\"]"},
        {"NULL", (const char *[]){NULL}, 1, "[\"(null)\"]"},
    };

    char *out[sizeof(tcs) / sizeof(TC)];
    memset(out, 0, sizeof(out));

    N = sizeof(tcs) / sizeof(TC);
    for (i = 0; i < N; i++)
    {
        test(tcs[i].name);
        s = nats_formatStringArray(&out[i], tcs[i].in, tcs[i].inLen);
        testCond((s == NATS_OK) && (out[i] != NULL) && (strcmp(out[i], tcs[i].out) == 0));
    }

    NATS_FREE_STRINGS(out, N);
}

static natsPid
_startStreamingServer(const char* url, const char *cmdLineOpts, bool checkStart)
{
    return _startServerImpl(natsStreamingServerExe, url, cmdLineOpts, checkStart);
}

void test_natsSock_IPOrder(void)
{
    natsStatus  s;
    natsPid     serverPid;

    test("Server listen to IPv4: ");
    serverPid = _startServer("", "-a 127.0.0.1 -p 4222", false);
    testCond(true);
    test("IPv4 only: ");
    s = _checkStart("nats://localhost:4222", 4, 5);
    testCond(s == NATS_OK);
    test("IPv4+v6: ");
    s = _checkStart("nats://localhost:4222", 46, 5);
    testCond(s == NATS_OK);
    test("IPv6+v4: ");
    s = _checkStart("nats://localhost:4222", 64, 5);
    testCond(s == NATS_OK);
    test("IP any: ");
    s = _checkStart("nats://localhost:4222", 0, 5);
    testCond(s == NATS_OK);
    // This one should fail.
    test("IPv6 only: ");
    s = _checkStart("nats://localhost:4222", 6, 5);
    testCond(s != NATS_OK);
    _stopServer(serverPid);
    serverPid = NATS_INVALID_PID;

    if (!runOnTravis)
    {
        test("Server listen to IPv6: ");
        serverPid = _startServer("", "-a :: -p 4222", false);
        testCond(true);
        test("IPv6 only: ");
        s = _checkStart("nats://localhost:4222", 6, 5);
        testCond(s == NATS_OK);
        test("IPv4+v6: ");
        s = _checkStart("nats://localhost:4222", 46, 5);
        testCond(s == NATS_OK);
        test("IPv6+v4: ");
        s = _checkStart("nats://localhost:4222", 64, 5);
        testCond(s == NATS_OK);
        test("IP any: ");
        s = _checkStart("nats://localhost:4222", 0, 5);
        testCond(s == NATS_OK);
        // This one should fail, but the server when listening
        // to [::] is actually accepting IPv4 connections,
        // so be tolerant of that.
        test("IPv4 only: ");
        s = _checkStart("nats://localhost:4222", 4, 5);
        if (s == NATS_OK)
            fprintf(stderr, ">>>> IPv4 to [::] should have failed, but server accepted it\n");
        else
            s = NATS_OK;
        testCond(s == NATS_OK);
        _stopServer(serverPid);
    }
}

void test_natsSock_ConnectTcp(void)
{
    natsPid     serverPid = NATS_INVALID_PID;

    test("Check connect tcp: ");
    serverPid = _startServer("nats://127.0.0.1:4222", "-p 4222", true);
    testCond(serverPid != NATS_INVALID_PID);
    _stopServer(serverPid);
    serverPid = NATS_INVALID_PID;

    test("Check connect tcp hostname: ");
    serverPid = _startServer("nats://localhost:4222", "-p 4222", true);
    testCond(serverPid != NATS_INVALID_PID);
    _stopServer(serverPid);
    serverPid = NATS_INVALID_PID;

    test("Check connect tcp (force server to listen to IPv4): ");
    serverPid = _startServer("nats://127.0.0.1:4222", "-a 127.0.0.1 -p 4222", true);
    testCond(serverPid != NATS_INVALID_PID);
    _stopServer(serverPid);
    serverPid = NATS_INVALID_PID;
}

static bool
listOrder(struct addrinfo *head, bool ordered)
{
    struct addrinfo *p;
    int i;

    p = head;
    for (i=0; i<10; i++)
    {
        if (ordered && (p->ai_flags != (i+1)))
            return false;
        p = p->ai_next;
    }
    return true;
}

void test_natsSock_ShuffleIPs(void)
{
    struct addrinfo *tmp[10];
    struct addrinfo *head = NULL;
    struct addrinfo *tail = NULL;
    struct addrinfo *list = NULL;
    struct addrinfo *p;
    natsSockCtx     ctx;
    int i=0;

    // Create a fake list that has `ai_flags` set to 1 to 10.
    // We will use that to check that the list is shuffled or not.
    for (i=0; i<10; i++)
    {
        p = calloc(1, sizeof(struct addrinfo));
        p->ai_flags = (i+1);
        if (head == NULL)
            head=p;
        if (tail != NULL)
            tail->ai_next = p;
        tail = p;
    }

    test("No randomize, so no shuffling: ");
    natsSock_Init(&ctx);
    ctx.noRandomize = true;
    list = head;
    natsSock_ShuffleIPs(&ctx, tmp, sizeof(tmp), &list, 10);
    testCond((list == head) && listOrder(list, true));

    test("Shuffling bad args 2: ");
    natsSock_Init(&ctx);
    list = head;
    natsSock_ShuffleIPs(&ctx, tmp, sizeof(tmp), NULL, 10);
    testCond((list == head) && listOrder(list, true));

    test("Shuffling bad args 1: ");
    natsSock_Init(&ctx);
    list = head;
    natsSock_ShuffleIPs(&ctx, tmp, sizeof(tmp), &list, 0);
    testCond((list == head) && listOrder(list, true));

    test("No shuffling count==1: ");
    natsSock_Init(&ctx);
    list = head;
    natsSock_ShuffleIPs(&ctx, tmp, sizeof(tmp), &list, 1);
    testCond((list == head) && listOrder(list, true));

    test("Shuffling: ");
    natsSock_Init(&ctx);
    list = head;
    natsSock_ShuffleIPs(&ctx, tmp, sizeof(tmp), &list, 10);
    testCond(listOrder(list, false));

    // Reorder the list, and we will try with a tmp buffer too small,
    // so API is going to allocate memory.
    p = list;
    for (i=0; i<10; i++)
        p->ai_flags = (i+1);
    head = list;
    test("Shuffling mem alloc: ");
    natsSock_Init(&ctx);
    natsSock_ShuffleIPs(&ctx, tmp, 5, &list, 10);
    testCond(listOrder(list, false));

    for (p = list; p != NULL; p = list)
    {
        list = list->ai_next;
        free(p);
    }
}

static natsOptions*
_createReconnectOptions(void)
{
    natsStatus  s;
    natsOptions *opts = NULL;

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:22222"));
    IFOK(s, natsOptions_SetAllowReconnect(opts, true));
    IFOK(s, natsOptions_SetMaxReconnect(opts, 10));
    IFOK(s, natsOptions_SetReconnectWait(opts, 100));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));
    if (s == NATS_OK)
#ifdef WIN32
        s = natsOptions_SetTimeout(opts, 500);
#else
        s = natsOptions_SetTimeout(opts, NATS_OPTS_DEFAULT_TIMEOUT);
#endif

    if (s != NATS_OK)
    {
        natsOptions_Destroy(opts);
        opts = NULL;
    }

    return opts;
}

static void
_reconnectedCb(natsConnection *nc, void *closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;
    int64_t             now  = nats_Now();

    natsMutex_Lock(arg->m);
    arg->reconnected = true;
    arg->reconnects++;
    if (arg->control == 9)
    {
        if (arg->reconnects <= 4)
            arg->reconnectedAt[arg->reconnects-1] = now;
    }
    natsCondition_Broadcast(arg->c);
    natsMutex_Unlock(arg->m);
}

void test_ReconnectServerStats(void)
{
    natsStatus      s;
    natsConnection  *nc       = NULL;
    natsOptions     *opts     = NULL;
    natsSrv         *srv      = NULL;
    natsPid         serverPid = NATS_INVALID_PID;
    natsStatistics  *stats    = NULL;
    uint64_t        reconnects= 0;
    struct threadArg args;

    test("Reconnect Server Stats: ");

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s == NATS_OK)
        opts = _createReconnectOptions();
    if (opts == NULL)
        FAIL("Unable to create reconnect options!");

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsOptions_SetDisconnectedCB(opts, _reconnectedCb, &args);
    IFOK(s, natsConnection_Connect(&nc, opts));
    IFOK(s, natsConnection_Flush(nc));

    _stopServer(serverPid);
    serverPid = NATS_INVALID_PID;

    if (s == NATS_OK)
    {
        serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
        CHECK_SERVER_STARTED(serverPid);

        natsMutex_Lock(args.m);
        while ((s != NATS_TIMEOUT) && !args.reconnected)
            s = natsCondition_TimedWait(args.c, args.m, 5000);
        natsMutex_Unlock(args.m);

        IFOK(s, natsConnection_FlushTimeout(nc, 5000));
    }

    if (s == NATS_OK)
    {
        srv = natsSrvPool_GetCurrentServer(nc->srvPool, nc->cur, NULL);
        if (srv == NULL)
            s = NATS_ILLEGAL_STATE;
    }

    testCond((s == NATS_OK) && (srv->reconnects == 0));

    test("Tracking Reconnects stats: ");
    s = natsStatistics_Create(&stats);
    IFOK(s, natsConnection_GetStats(nc, stats));
    IFOK(s, natsStatistics_GetCounts(stats, NULL, NULL, NULL, NULL, &reconnects));
    testCond((s == NATS_OK) && (reconnects == 1));

    natsStatistics_Destroy(stats);
    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _stopServer(serverPid);

    _destroyDefaultThreadArgs(&args);
}

static void
_disconnectedCb(natsConnection *nc, void *closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;
    int64_t             now  = nats_Now();

    natsMutex_Lock(arg->m);
    arg->disconnected = true;
    arg->disconnects++;
    if ((arg->control == 9) && (arg->disconnects > 1))
    {
        if (arg->disconnects <= 5)
            arg->disconnectedAt[arg->disconnects-2] = now;
    }
    natsCondition_Broadcast(arg->c);
    natsMutex_Unlock(arg->m);
}

static void
_recvTestString(natsConnection *nc, natsSubscription *sub, natsMsg *msg,
                void *closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;
    bool                doSignal = true;

    natsMutex_Lock(arg->m);

    // up to 12 now
    switch (arg->control)
    {
        case 0:
        {
            if (strncmp(arg->string,
                        natsMsg_GetData(msg),
                        natsMsg_GetDataLength(msg)) != 0)
            {
                arg->status = NATS_ERR;
            }
            break;
        }
        case 1:
        {
            if (sub == NULL)
                arg->status = NATS_ERR;
            else if (strncmp(arg->string,
                    natsMsg_GetData(msg),
                    natsMsg_GetDataLength(msg)) != 0)
            {
                arg->status = NATS_ERR;
            }
            break;
        }
        case 2:
        {
            if (strcmp(arg->string, natsMsg_GetReply(msg)) != 0)
            {
                arg->status = NATS_ERR;
            }
            break;
        }
        case 3:
        case 9:
        case 12:
        {
            doSignal = false;
            arg->sum++;

            if ((arg->control == 3) && (arg->sum == 10))
            {
                arg->status = natsSubscription_Unsubscribe(sub);
                doSignal = true;
            }
            else if ((arg->control == 12) && (arg->sum == arg->N))
            {
                arg->status = NATS_OK;
                doSignal = true;
            }
            else if ((arg->control == 12) && (arg->sum > arg->N))
            {
                doSignal = true;
                arg->status = NATS_ERR;
            }
            break;
        }
        case 11:
        case 4:
        {
            arg->status = natsConnection_PublishString(nc,
                                                       natsMsg_GetReply(msg),
                                                       arg->string);
            if (arg->status == NATS_OK)
                arg->status = natsConnection_Flush(nc);
            if (arg->control == 11)
                arg->sum++;
            break;
        }
        case 5:
        {
            arg->status = natsConnection_Flush(nc);
            break;
        }
        case 6:
        {
            char seqnoStr[10];
            int seqno = 0;

            doSignal = false;

            snprintf(seqnoStr, sizeof(seqnoStr), "%.*s", natsMsg_GetDataLength(msg),
                     natsMsg_GetData(msg));

            seqno = atoi(seqnoStr);
            if (seqno >= 10)
                arg->status = NATS_ERR;
            else
                arg->results[seqno] = (arg->results[seqno] + 1);

            break;
        }
        case 7:
        {
            arg->msgReceived = true;
            natsCondition_Signal(arg->c);

            while (!arg->closed)
                natsCondition_Wait(arg->c, arg->m);

            break;
        }
        case 8:
        {
            arg->sum++;

            while (!arg->closed)
                natsCondition_Wait(arg->c, arg->m);

            break;
        }
        case 10:
        {
            arg->status = (natsMsg_IsNoResponders(msg) ? NATS_OK : NATS_ERR);
            break;
        }
    }

    natsMsg_Destroy(msg);

    if (doSignal)
    {
        arg->msgReceived = true;
        natsCondition_Broadcast(arg->c);
    }
    natsMutex_Unlock(arg->m);
}

static void
_closedCb(natsConnection *nc, void *closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);
    arg->closed = true;
    natsCondition_Broadcast(arg->c);
    natsMutex_Unlock(arg->m);
}

static natsStatus
_waitForConnClosed(struct threadArg *arg)
{
    natsStatus s = NATS_OK;

    natsMutex_Lock(arg->m);
    while ((s != NATS_TIMEOUT) && !arg->closed)
        s = natsCondition_TimedWait(arg->c, arg->m, 2000);
    arg->closed = false;
    natsMutex_Unlock(arg->m);

    return s;
}

void test_ParseStateReconnectFunctionality(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    test("Parse State Reconnect Functionality: ");

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
    {
        arg.string = "bar";
        arg.status = NATS_OK;
    }
    if (s == NATS_OK)
        opts = _createReconnectOptions();

    if ((opts == NULL)
        || (natsOptions_SetDisconnectedCB(opts, _disconnectedCb, &arg) != NATS_OK)
        || (natsOptions_SetClosedCB(opts, _closedCb, &arg) != NATS_OK))
    {
        FAIL("Unable to create reconnect options!");
    }

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, &arg));
    IFOK(s, natsConnection_Flush(nc));

    if (s == NATS_OK)
    {
        // Simulate partialState, this needs to be cleared
        natsConn_Lock(nc);
        nc->ps->state = OP_PON;
        natsConn_Unlock(nc);
    }

    _stopServer(serverPid);
    serverPid = NATS_INVALID_PID;

    if (s == NATS_OK)
    {
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.disconnected)
            s = natsCondition_TimedWait(arg.c, arg.m, 500);
        natsMutex_Unlock(arg.m);
    }

    IFOK(s, natsConnection_PublishString(nc, "foo", arg.string));

    if (s == NATS_OK)
    {
        serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
        CHECK_SERVER_STARTED(serverPid);
    }

    IFOK(s, natsConnection_FlushTimeout(nc, 5000));

    if (s == NATS_OK)
    {
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.msgReceived)
            s = natsCondition_TimedWait(arg.c, arg.m, 1500);
        natsMutex_Unlock(arg.m);

        if (s == NATS_OK)
            s = arg.status;
    }

    testCond((s == NATS_OK) && (nc->stats.reconnects == 1));

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _waitForConnClosed(&arg);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_ServersRandomize(void)
{
    natsStatus      s;
    natsOptions     *opts   = NULL;
    natsConnection  *nc     = NULL;
    natsPid         pid     = NATS_INVALID_PID;
    int serversCount;

    serversCount = sizeof(testServers) / sizeof(char *);

    test("Server Pool with Randomize: ");

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetServers(opts, testServers, serversCount));
    if (s == NATS_OK)
    {
        int same = 0;
        int allSame = 0;

        for (int iter=0; (s == NATS_OK) && (iter<1000); iter++)
        {
            s = natsConn_create(&nc, natsOptions_clone(opts));
            if (s == NATS_OK)
            {
                // In theory, this could happen...
                for (int i=0; i<serversCount; i++)
                {
                    if (strcmp(testServers[i],
                               nc->srvPool->srvrs[i]->url->fullUrl) == 0)
                    {
                        same++;
                    }
                }
                if (same == serversCount)
                    allSame++;
            }
            natsConn_release(nc);
            nc = NULL;
        }

        if (allSame > 10)
            s = NATS_ERR;
    }
    testCond(s == NATS_OK);

    // Now test that we do not randomize if proper flag is set.
    test("Server Pool With NoRandomize: ")
    s = natsOptions_SetNoRandomize(opts, true);
    IFOK(s, natsConn_create(&nc, natsOptions_clone(opts)));
    if (s == NATS_OK)
    {
        for (int i=0; i<serversCount; i++)
            if (strcmp(testServers[i],
                       nc->srvPool->srvrs[i]->url->fullUrl) != 0)
            {
                s = NATS_ERR;
                break;
            }
    }
    testCond(s == NATS_OK);
    natsConn_release(nc);
    nc = NULL;

    // Although the original intent was that if Opts.Url is
    // set, Opts.Servers is not (and vice versa), the behavior
    // is that Opts.Url is always first, even when randomization
    // is enabled. So make sure that this is still the case.
    test("If Options.URL is set, it should be first: ")
    s = natsOptions_SetNoRandomize(opts, false);
    IFOK(s, natsOptions_SetURL(opts, NATS_DEFAULT_URL));
    IFOK(s, natsConn_create(&nc, natsOptions_clone(opts)));
    if (s == NATS_OK)
    {
        int same = 0;
        // In theory, this could happen...
        for (int i=0; i<serversCount; i++)
        {
            if (strcmp(testServers[i],
                       nc->srvPool->srvrs[i+1]->url->fullUrl) == 0)
            {
                same++;
            }
        }
        if (same == serversCount)
            s = NATS_ERR;
    }
    if ((s == NATS_OK)
            && strcmp(nc->srvPool->srvrs[0]->url->fullUrl,
                      NATS_DEFAULT_URL) != 0)
    {
        s = NATS_ERR;
    }
    testCond(s == NATS_OK);

    natsConn_release(nc);
    nc = NULL;

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("NoRandomize==true passed to context: ");
    s = natsOptions_SetNoRandomize(opts, true);
    IFOK(s, natsOptions_SetURL(opts, NATS_DEFAULT_URL));
    IFOK(s, natsConnection_Connect(&nc, opts));
    if (s == NATS_OK)
    {
        natsConn_Lock(nc);
        if (!nc->sockCtx.noRandomize)
            s = NATS_ERR;
        natsConn_Unlock(nc);
    }
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    nc = NULL;

    test("NoRandomize==false passed to context: ");
    s = natsOptions_SetNoRandomize(opts, false);
    IFOK(s, natsOptions_SetURL(opts, NATS_DEFAULT_URL));
    IFOK(s, natsConnection_Connect(&nc, opts));
    if (s == NATS_OK)
    {
        natsConn_Lock(nc);
        if (nc->sockCtx.noRandomize)
            s = NATS_ERR;
        natsConn_Unlock(nc);
    }
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);
    _stopServer(pid);
}

void test_SelectNextServer(void)
{
    natsStatus      s;
    natsOptions     *opts   = NULL;
    natsConnection  *nc     = NULL;
    natsSrv         *srv    = NULL;
    int             serversCount;

    serversCount = sizeof(testServers) / sizeof(char *);

    test("Test default server pool selection: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetServers(opts, testServers, serversCount));
    IFOK(s, natsOptions_SetNoRandomize(opts, true));
    IFOK(s, natsConn_create(&nc, natsOptions_clone(opts)));
    testCond((s == NATS_OK)
             && (nc->cur->url == nc->srvPool->srvrs[0]->url));

    test("Get next server: ");
    if (s == NATS_OK)
    {
        srv = natsSrvPool_GetNextServer(nc->srvPool, nc->opts, nc->cur);
        if (srv != NULL)
            nc->cur = srv;
    }
    testCond((s == NATS_OK)
             && (nc != NULL)
             && (nc->cur != NULL));

    test("Check list size: ");
    testCond((s == NATS_OK)
             && (nc != NULL)
             && (nc->srvPool != NULL)
             && (nc->srvPool->size == serversCount));

    test("Check selection: ");
    testCond((s == NATS_OK)
             && (nc != NULL)
             && (nc->cur->url != NULL)
             && (nc->cur->url->fullUrl != NULL)
             && (strcmp(nc->cur->url->fullUrl, testServers[1]) == 0));

    test("Check old was pushed to last position: ");
    testCond((s == NATS_OK)
             && (nc != NULL)
             && (nc->srvPool != NULL)
             && (nc->srvPool->srvrs != NULL)
             && (nc->srvPool->size > 0)
             && (nc->srvPool->srvrs[nc->srvPool->size - 1] != NULL)
             && (nc->srvPool->srvrs[nc->srvPool->size - 1]->url != NULL)
             && (nc->srvPool->srvrs[nc->srvPool->size - 1]->url->fullUrl != NULL)
             && (strcmp(nc->srvPool->srvrs[nc->srvPool->size - 1]->url->fullUrl,
                        testServers[0]) == 0));

    test("Got correct server: ");
    testCond((s == NATS_OK)
             && (srv != NULL)
             && (nc != NULL)
             && (nc->srvPool != NULL)
             && (nc->srvPool->srvrs != NULL)
             && (nc->srvPool->size > 0)
             && (srv == (nc->srvPool->srvrs[0])));

    // Test that we do not keep servers where we have tried to reconnect past our limit.
    if (s == NATS_OK)
    {
        test("Get next server: ");
        if ((nc == NULL)
            || (nc->srvPool == NULL)
            || (nc->srvPool->srvrs == NULL)
            || (nc->srvPool->srvrs[0] == NULL))
        {
            s = NATS_ERR;
        }
        else
        {
            nc->srvPool->srvrs[0]->reconnects = nc->opts->maxReconnect;
        }
        if (s == NATS_OK)
        {
            srv = natsSrvPool_GetNextServer(nc->srvPool, nc->opts, nc->cur);
            if (srv != NULL)
                nc->cur = srv;
        }
        testCond((s == NATS_OK) && (nc->cur != NULL));
    }

    // Check that we are now looking at #3, and current is not in the list.
    test("Check list size: ");
    testCond((s == NATS_OK)
             && (nc->srvPool->size == (serversCount - 1)));

    test("Check selection: ");
    testCond((s == NATS_OK)
             && (nc != NULL)
             && (nc->cur != NULL)
             && (nc->cur->url != NULL)
             && (nc->cur->url->fullUrl != NULL)
             && (strcmp(nc->cur->url->fullUrl, testServers[2]) == 0));

    test("Check last server was discarded: ");
    testCond((s == NATS_OK)
             && (nc != NULL)
             && (nc->srvPool != NULL)
             && (nc->srvPool->srvrs != NULL)
             && (nc->srvPool->size > 0)
             && (nc->srvPool->srvrs[nc->srvPool->size - 1] != NULL)
             && (nc->srvPool->srvrs[nc->srvPool->size - 1]->url != NULL)
             && (nc->srvPool->srvrs[nc->srvPool->size - 1]->url->fullUrl != NULL)
             && (strcmp(nc->srvPool->srvrs[nc->srvPool->size - 1]->url->fullUrl,
                        testServers[1]) != 0));

    natsConn_release(nc);
    natsOptions_Destroy(opts);
}

static void
parserNegTest(int lineNum)
{
    char txt[64];

    snprintf(txt, sizeof(txt), "Test line %d: ", lineNum);
    test(txt);
}

#define PARSER_START_TEST parserNegTest(__LINE__)

void test_ParserPing(void)
{
    natsConnection  *nc = NULL;
    natsOptions     *opts = NULL;
    natsStatus      s;
    char            ping[64];

    s = natsOptions_Create(&opts);
    IFOK(s, natsConn_create(&nc, opts));
    IFOK(s, natsParser_Create(&(nc->ps)));
    IFOK(s, natsBuf_Create(&(nc->pending), 1000));
    if (s == NATS_OK)
        nc->usePending = true;
    if (s != NATS_OK)
        FAIL("Unable to setup test");


    PARSER_START_TEST;
    testCond(nc->ps->state == OP_START);

    snprintf(ping, sizeof(ping), "PING\r\n");

    PARSER_START_TEST;
    s = natsParser_Parse(nc, ping, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_P));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, ping + 1, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_PI));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, ping + 2, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_PIN));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, ping + 3, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_PING));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, ping + 4, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_PING));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, ping + 5, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_START));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, ping, (int)strlen(ping));
    testCond((s == NATS_OK) && (nc->ps->state == OP_START));

    // Should tolerate spaces
    snprintf(ping, sizeof(ping), "%s", "PING  \r");
    PARSER_START_TEST;
    s = natsParser_Parse(nc, ping, (int)strlen(ping));
    testCond((s == NATS_OK) && (nc->ps->state == OP_PING));

    nc->ps->state = OP_START;
    snprintf(ping, sizeof(ping), "%s", "PING  \r  \n");
    PARSER_START_TEST;
    s = natsParser_Parse(nc, ping, (int)strlen(ping));
    testCond((s == NATS_OK) && (nc->ps->state == OP_START));

    natsConnection_Destroy(nc);
}

void test_ParserErr(void)
{
    natsConnection  *nc = NULL;
    natsOptions     *opts = NULL;
    natsStatus      s;
    char            errProto[1024];
    char            expected[256];
    int             len;

    s = natsOptions_Create(&opts);
    IFOK(s, natsConn_create(&nc, opts));
    IFOK(s, natsParser_Create(&(nc->ps)));
    IFOK(s, natsBuf_Create(&(nc->pending), 1000));
    if (s == NATS_OK)
    {
        nc->usePending = true;
        nc->status = NATS_CONN_STATUS_CLOSED;
    }
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    // This test focuses on the parser only, not how the error is
    // actually processed by the upper layer.

    PARSER_START_TEST;
    testCond(nc->ps->state == OP_START);

    snprintf(expected, sizeof(expected), "%s", "'Any kind of error'");
    snprintf(errProto, sizeof(errProto), "-ERR  %s\r\n", expected);
    len = (int) strlen(errProto);

    PARSER_START_TEST;
    s = natsParser_Parse(nc, errProto, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_MINUS));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, errProto + 1, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_MINUS_E));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, errProto + 2, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_MINUS_ER));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, errProto + 3, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_MINUS_ERR));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, errProto + 4, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_MINUS_ERR_SPC));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, errProto + 5, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_MINUS_ERR_SPC));

    // Check with split arg buffer
    PARSER_START_TEST;
    s = natsParser_Parse(nc, errProto + 6, 1);
    testCond((s == NATS_OK) && (nc->ps->state == MINUS_ERR_ARG));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, errProto + 7, 3);
    testCond((s == NATS_OK) && (nc->ps->state == MINUS_ERR_ARG));

    // Verify content
    PARSER_START_TEST;
    s = natsParser_Parse(nc, errProto + 10, len - 10 - 2);
    testCond((s == NATS_OK)
             && (nc->ps->state == MINUS_ERR_ARG)
             && (nc->ps->argBuf != NULL)
             && (strncmp(nc->ps->argBuf->data, expected, nc->ps->argBuf->len) == 0));

    // Finish parsing
    PARSER_START_TEST;
    s = natsParser_Parse(nc, errProto + len - 1, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_START));

    // Check without split arg buffer
    snprintf(errProto, sizeof(errProto), "-ERR '%s'\r\n", "Any Error");
    PARSER_START_TEST;
    s = natsParser_Parse(nc, errProto, (int)strlen(errProto));
    testCond((s == NATS_OK) && (nc->ps->state == OP_START));

    natsConnection_Destroy(nc);
}

void test_ParserOK(void)
{
    natsConnection  *nc = NULL;
    natsOptions     *opts = NULL;
    natsStatus      s;
    char            okProto[256];

    s = natsOptions_Create(&opts);
    IFOK(s, natsConn_create(&nc, opts));
    IFOK(s, natsParser_Create(&(nc->ps)));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    PARSER_START_TEST;
    testCond(nc->ps->state == OP_START);

    snprintf(okProto, sizeof(okProto), "+OKay\r\n");

    PARSER_START_TEST;
    s = natsParser_Parse(nc, okProto, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_PLUS));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, okProto + 1, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_PLUS_O));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, okProto + 2, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_PLUS_OK));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, okProto + 3, (int)strlen(okProto) - 3);
    testCond((s == NATS_OK) && (nc->ps->state == OP_START));

    natsConnection_Destroy(nc);
}

void test_ParseINFO(void)
{
    natsConnection  *nc = NULL;
    natsOptions     *opts = NULL;
    natsStatus      s;
    char            infoProto[256];

    s = natsOptions_Create(&opts);
    IFOK(s, natsConn_create(&nc, opts));
    IFOK(s, natsParser_Create(&(nc->ps)));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    PARSER_START_TEST;
    testCond(nc->ps->state == OP_START);

    snprintf(infoProto, sizeof(infoProto), "INFO \t{\"server_id\": \"abc\"}\r\n");

    PARSER_START_TEST;
    s = natsParser_Parse(nc, infoProto, 7);
    testCond((s == NATS_OK) && (nc->ps->state == INFO_ARG) && (infoProto[nc->ps->afterSpace] == '{'));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, infoProto +7, (int)strlen(infoProto) - 7);
    testCond((s == NATS_OK)
                && (nc->ps->state == OP_START)
                && (nc->info.id != NULL) && (strcmp(nc->info.id, "abc") == 0));

    natsConnection_Destroy(nc);
}

void test_ParserShouldFail(void)
{
    natsConnection  *nc = NULL;
    natsOptions     *opts = NULL;
    natsStatus      s;
    char            buf[64];

    s = natsOptions_Create(&opts);
    IFOK(s, natsConn_create(&nc, opts));
    IFOK(s, natsParser_Create(&(nc->ps)));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    // Negative tests:

    PARSER_START_TEST;
    snprintf(buf, sizeof(buf), "%s", " PING");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "POO");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "Px");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "PIx");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "PINx");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);
    // Stop here because 'PING' protos are tolerant for anything between PING and \n

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "POx");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "PONx");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);
    // Stop here because 'PONG' protos are tolerant for anything between PONG and \n

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "ZOO");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "Mx\r\n");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "MSx\r\n");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "MSGx\r\n");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "MSG  foo\r\n");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "MSG \r\n");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "MSG foo 1\r\n");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "MSG foo bar 1\r\n");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "MSG foo bar 1 baz\r\n");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "MSG foo 1 bar baz\r\n");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "+x\r\n");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "+Ox\r\n");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "-x\r\n");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "-Ex\r\n");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "-ERx\r\n");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    PARSER_START_TEST;
    nc->ps->state = OP_START;
    snprintf(buf, sizeof(buf), "%s", "-ERRx\r\n");
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond(s != NATS_OK);

    natsConnection_Destroy(nc);
}

void test_ParserSplitMsg(void)
{
    natsConnection  *nc = NULL;
    natsOptions     *opts = NULL;
    natsStatus      s;
    char            buf[2*MAX_CONTROL_LINE_SIZE];
    uint64_t        expectedCount;
    uint64_t        expectedSize;
    int             msgSize, start, i;

    s = natsOptions_Create(&opts);
    IFOK(s, natsConn_create(&nc, opts));
    IFOK(s, natsParser_Create(&(nc->ps)));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    expectedCount = 1;
    expectedSize = 3;

    snprintf(buf, sizeof(buf), "%s", "MSG a 1 3\r\nfoo\r\n");

    // parsing: 'MSG a'
    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf, 5);
    testCond((s == NATS_OK) && (nc->ps->argBuf != NULL));

    // parsing: ' 1 3\r\nf'
    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf + 5, 7);
    testCond((s == NATS_OK)
             && (nc->ps->ma.size == 3)
             && (nc->ps->ma.sid == 1)
             && (nc->ps->ma.subject->len == 1)
             && (strncmp(nc->ps->ma.subject->data, "a", 1) == 0)
             && (nc->ps->msgBuf != NULL));

    // parsing: 'oo\r\n'
    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf + 12, (int)strlen(buf) - 12);
    testCond((s == NATS_OK)
             && (nc->stats.inMsgs == expectedCount)
             && (nc->stats.inBytes == expectedSize)
             && (nc->ps->argBuf == NULL)
             && (nc->ps->msgBuf == NULL)
             && (nc->ps->state == OP_START));

    // parsing: 'MSG a 1 3\r\nfo'
    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf, 13);
    testCond((s == NATS_OK)
             && (nc->ps->ma.size == 3)
             && (nc->ps->ma.sid == 1)
             && (nc->ps->ma.subject->len == 1)
             && (strncmp(nc->ps->ma.subject->data, "a", 1) == 0)
             && (nc->ps->argBuf != NULL)
             && (nc->ps->msgBuf != NULL));

    expectedCount++;
    expectedSize += 3;

    // parsing: 'o\r\n'
    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf + 13, (int)strlen(buf) - 13);
    testCond((s == NATS_OK)
             && (nc->stats.inMsgs == expectedCount)
             && (nc->stats.inBytes == expectedSize)
             && (nc->ps->argBuf == NULL)
             && (nc->ps->msgBuf == NULL)
             && (nc->ps->state == OP_START));

    snprintf(buf, sizeof(buf), "%s", "MSG a 1 6\r\nfoobar\r\n");

    // parsing: 'MSG a 1 6\r\nfo'
    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf, 13);
    testCond((s == NATS_OK)
             && (nc->ps->ma.size == 6)
             && (nc->ps->ma.sid == 1)
             && (nc->ps->ma.subject->len == 1)
             && (strncmp(nc->ps->ma.subject->data, "a", 1) == 0)
             && (nc->ps->argBuf != NULL)
             && (nc->ps->msgBuf != NULL));

    // parsing: 'ob'
    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf + 13, 2);
    testCond(s == NATS_OK)

    expectedCount++;
    expectedSize += 6;

    // parsing: 'ar\r\n'
    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf + 15, (int)strlen(buf) - 15);
    testCond((s == NATS_OK)
             && (nc->stats.inMsgs == expectedCount)
             && (nc->stats.inBytes == expectedSize)
             && (nc->ps->argBuf == NULL)
             && (nc->ps->msgBuf == NULL)
             && (nc->ps->state == OP_START));

    // Let's have a msg that is bigger than the parser's scratch size.
    // Since we prepopulate the msg with 'foo', adding 3 to the size.
    msgSize = sizeof(nc->ps->scratch) + 100 + 3;

    snprintf(buf, sizeof(buf), "MSG a 1 b %d\r\nfoo", msgSize);
    start = (int)strlen(buf);
    for (i=0; i<msgSize - 3; i++)
        buf[start + i] = 'a' + (i % 26);
    buf[start + i++] = '\r';
    buf[start + i++] = '\n';
    buf[start + i++] = '\0';

    // parsing: 'MSG a 1 b <size>\r\nfoo'
    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf, start);
    testCond((s == NATS_OK)
             && (nc->ps->ma.size == msgSize)
             && (nc->ps->ma.sid == 1)
             && (nc->ps->ma.subject->len == 1)
             && (strncmp(nc->ps->ma.subject->data, "a", 1) == 0)
             && (nc->ps->ma.reply->len == 1)
             && (strncmp(nc->ps->ma.reply->data, "b", 1) == 0)
             && (nc->ps->argBuf != NULL)
             && (nc->ps->msgBuf != NULL));

    expectedCount++;
    expectedSize += (uint64_t)msgSize;

    // parsing: 'abcde...'
    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf + start, (int)strlen(buf) - start - 2);
    testCond((s == NATS_OK)
             && (nc->ps->argBuf != NULL)
             && (nc->ps->msgBuf != NULL)
             && (nc->ps->state == MSG_PAYLOAD));

    // Verify content
    PARSER_START_TEST;
    s = ((strncmp(nc->ps->msgBuf->data, "foo", 3) == 0) ? NATS_OK : NATS_ERR);
    if (s == NATS_OK)
    {
        int k;

        for (k=3; (s == NATS_OK) && (k<nc->ps->ma.size); k++)
            s = (nc->ps->msgBuf->data[k] == (char)('a' + ((k-3) % 26)) ? NATS_OK : NATS_ERR);
    }
    testCond(s == NATS_OK)

    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf + (int)strlen(buf) - 2, 2);
    testCond((s == NATS_OK)
             && (nc->stats.inMsgs == expectedCount)
             && (nc->stats.inBytes == expectedSize)
             && (nc->ps->argBuf == NULL)
             && (nc->ps->msgBuf == NULL)
             && (nc->ps->state == OP_START));

    natsConnection_Destroy(nc);
}

#define RECREATE_PARSER \
    natsParser_Destroy(nc->ps); \
    s = natsParser_Create(&(nc->ps)); \
    if (s != NATS_OK) \
        FAIL("Unable to setup test"); \

void test_ProcessMsgArgs(void)
{
    natsConnection  *nc = NULL;
    natsOptions     *opts = NULL;
    natsStatus      s;
    char            buf[2048];
    const char*     le = NULL;

    s = natsOptions_Create(&opts);
    IFOK(s, natsConn_create(&nc, opts));
    IFOK(s, natsParser_Create(&(nc->ps)));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    snprintf(buf, sizeof(buf), "%s", "MSG a b c d e\r\n");

    test("Parsing MSG with too many arguments: ")
    // parsing: 'MSG a'
    natsParser_Parse(nc, buf, 5);
    // parse the rest..
    natsParser_Parse(nc, buf + 5, 10);
    s = natsConnection_GetLastError(nc, &le);
    testCond((s == NATS_PROTOCOL_ERROR)
                && (nc->ps->argBuf == NULL)
                && (nc->ps->msgBuf == NULL)
                && (nc->ps->ma.subject == NULL)
                && (nc->ps->ma.reply == NULL)
                && (le != NULL)
                && (strstr(le, "wrong number of arguments") != NULL));

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "MSG foo 1\r\n");
    test("Parsing MSG with not enough arguments: ")
    natsParser_Parse(nc, buf, (int) strlen(buf));
    s = natsConnection_GetLastError(nc, &le);
    testCond((s == NATS_PROTOCOL_ERROR)
                && (nc->ps->argBuf == NULL)
                && (nc->ps->msgBuf == NULL)
                && (nc->ps->ma.subject == NULL)
                && (nc->ps->ma.reply == NULL)
                && (le != NULL)
                && (strstr(le, "wrong number of arguments") != NULL));

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "MSG foo abc 2\r\n");
    test("Parsing MSG with bad sid: ")
    natsParser_Parse(nc, buf, (int) strlen(buf));
    s = natsConnection_GetLastError(nc, &le);
    testCond((s == NATS_PROTOCOL_ERROR)
                && (nc->ps->argBuf == NULL)
                && (nc->ps->msgBuf == NULL)
                && (nc->ps->ma.subject == NULL)
                && (nc->ps->ma.reply == NULL)
                && (le != NULL)
                && (strstr(le, "Bad or Missing Sid") != NULL));

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "MSG foo 1 abc\r\n");
    test("Parsing MSG with bad size: ")
    natsParser_Parse(nc, buf, (int) strlen(buf));
    s = natsConnection_GetLastError(nc, &le);
    testCond((s == NATS_PROTOCOL_ERROR)
                && (nc->ps->argBuf == NULL)
                && (nc->ps->msgBuf == NULL)
                && (nc->ps->ma.subject == NULL)
                && (nc->ps->ma.reply == NULL)
                && (le != NULL)
                && (strstr(le, "Bad or Missing Size") != NULL));

    snprintf(buf, sizeof(buf), "%s", "MSG foo 1 bar abc\r\n");
    test("Parsing MSG with bad size (with reply): ")
    natsParser_Parse(nc, buf, (int) strlen(buf));
    s = natsConnection_GetLastError(nc, &le);
    testCond((s == NATS_PROTOCOL_ERROR)
                && (nc->ps->argBuf == NULL)
                && (nc->ps->msgBuf == NULL)
                && (nc->ps->ma.subject == NULL)
                && (nc->ps->ma.reply == NULL)
                && (le != NULL)
                && (strstr(le, "Bad or Missing Size") != NULL));

    // Test extra spaces first without reply

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "MSG foo  1 2\r\n");
    test("Parsing MSG with extra space before sid: ")
    s = natsParser_Parse(nc, buf, (int) strlen(buf));
    testCond((s == NATS_OK)
                && (natsBuf_Len(nc->ps->ma.subject) == 3)
                && (strncmp(natsBuf_Data(nc->ps->ma.subject), "foo", 3) == 0)
                && (nc->ps->ma.sid == 1)
                && (nc->ps->ma.reply == NULL)
                && (nc->ps->ma.size == 2));

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "MSG bar 1  2\r\n");
    test("Parsing MSG with extra space before size: ")
    s = natsParser_Parse(nc, buf, (int) strlen(buf));
    testCond((s == NATS_OK)
                && (natsBuf_Len(nc->ps->ma.subject) == 3)
                && (strncmp(natsBuf_Data(nc->ps->ma.subject), "bar", 3) == 0)
                && (nc->ps->ma.sid == 1)
                && (nc->ps->ma.reply == NULL)
                && (nc->ps->ma.size == 2));

    // Test extra spaces first with reply

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "MSG baz  3 bat 4\r\n");
    test("Parsing MSG with extra space before sid: ")
    s = natsParser_Parse(nc, buf, (int) strlen(buf));
    testCond((s == NATS_OK)
                && (natsBuf_Len(nc->ps->ma.subject) == 3)
                && (strncmp(natsBuf_Data(nc->ps->ma.subject), "baz", 3) == 0)
                && (nc->ps->ma.sid == 3)
                && (natsBuf_Len(nc->ps->ma.reply) == 3)
                && (strncmp(natsBuf_Data(nc->ps->ma.reply), "bat", 3) == 0)
                && (nc->ps->ma.size == 4));

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "MSG boo 5  baa 6\r\n");
    test("Parsing MSG with extra space before reply: ")
    s = natsParser_Parse(nc, buf, (int) strlen(buf));
    testCond((s == NATS_OK)
                && (natsBuf_Len(nc->ps->ma.subject) == 3)
                && (strncmp(natsBuf_Data(nc->ps->ma.subject), "boo", 3) == 0)
                && (nc->ps->ma.sid == 5)
                && (natsBuf_Len(nc->ps->ma.reply) == 3)
                && (strncmp(natsBuf_Data(nc->ps->ma.reply), "baa", 3) == 0)
                && (nc->ps->ma.size == 6));

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "MSG coo 7 caa  8\r\n");
    test("Parsing MSG with extra space before size: ")
    s = natsParser_Parse(nc, buf, (int) strlen(buf));
    testCond((s == NATS_OK)
                && (natsBuf_Len(nc->ps->ma.subject) == 3)
                && (strncmp(natsBuf_Data(nc->ps->ma.subject), "coo", 3) == 0)
                && (nc->ps->ma.sid == 7)
                && (natsBuf_Len(nc->ps->ma.reply) == 3)
                && (strncmp(natsBuf_Data(nc->ps->ma.reply), "caa", 3) == 0)
                && (nc->ps->ma.size == 8));

    // Test with extra space everywhere

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "MSG  doo  8  daa   9  \r\n");
    test("Parsing MSG with extra space everywhere: ")
    s = natsParser_Parse(nc, buf, (int) strlen(buf));
    testCond((s == NATS_OK)
                && (natsBuf_Len(nc->ps->ma.subject) == 3)
                && (strncmp(natsBuf_Data(nc->ps->ma.subject), "doo", 3) == 0)
                && (nc->ps->ma.sid == 8)
                && (natsBuf_Len(nc->ps->ma.reply) == 3)
                && (strncmp(natsBuf_Data(nc->ps->ma.reply), "daa", 3) == 0)
                && (nc->ps->ma.size == 9));

    // Test HMSG

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "HMSG foo 1 bar 2 3\r\n");
    test("Parsing HMSG: ");
    s = natsParser_Parse(nc, buf, (int) strlen(buf));
    testCond((s == NATS_OK)
                && (natsBuf_Len(nc->ps->ma.subject) == 3)
                && (strncmp(natsBuf_Data(nc->ps->ma.subject), "foo", 3) == 0)
                && (nc->ps->ma.sid == 1)
                && (natsBuf_Len(nc->ps->ma.reply) == 3)
                && (strncmp(natsBuf_Data(nc->ps->ma.reply), "bar", 3) == 0)
                && (nc->ps->ma.hdr == 2)
                && (nc->ps->ma.size == 3));

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "HMSG foo 1 3\r\n");
    test("Parsing HMSG not enough args: ");
    natsParser_Parse(nc, buf, (int) strlen(buf));
    s = natsConnection_GetLastError(nc, &le);
    testCond((s == NATS_PROTOCOL_ERROR)
                && (nc->ps->argBuf == NULL)
                && (nc->ps->msgBuf == NULL)
                && (nc->ps->ma.subject == NULL)
                && (nc->ps->ma.reply == NULL)
                && (le != NULL)
                && (strstr(le, "wrong number of arguments") != NULL));

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "HMSG a b c d e f\r\n");
    test("Parsing HMSG too many args: ");
    natsParser_Parse(nc, buf, (int) strlen(buf));
    s = natsConnection_GetLastError(nc, &le);
    s = natsConnection_GetLastError(nc, &le);
    testCond((s == NATS_PROTOCOL_ERROR)
                && (nc->ps->argBuf == NULL)
                && (nc->ps->msgBuf == NULL)
                && (nc->ps->ma.subject == NULL)
                && (nc->ps->ma.reply == NULL)
                && (le != NULL)
                && (strstr(le, "wrong number of arguments") != NULL));

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "HMSG foo abc 2 4\r\n");
    test("Parsing HMSG with bad sid: ");
    natsParser_Parse(nc, buf, (int) strlen(buf));
    s = natsConnection_GetLastError(nc, &le);
    testCond((s == NATS_PROTOCOL_ERROR)
                && (nc->ps->argBuf == NULL)
                && (nc->ps->msgBuf == NULL)
                && (nc->ps->ma.subject == NULL)
                && (nc->ps->ma.reply == NULL)
                && (le != NULL)
                && (strstr(le, "Bad or Missing Sid") != NULL));

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "HMSG foo 1 baz 10\r\n");
    test("Parsing HMSG with bad header size: ");
    natsParser_Parse(nc, buf, (int) strlen(buf));
    s = natsConnection_GetLastError(nc, &le);
    testCond((s == NATS_PROTOCOL_ERROR)
                && (nc->ps->argBuf == NULL)
                && (nc->ps->msgBuf == NULL)
                && (nc->ps->ma.subject == NULL)
                && (nc->ps->ma.reply == NULL)
                && (le != NULL)
                && (strstr(le, "Bad or Missing Header Size") != NULL));

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "HMSG foo 1 bar baz 10\r\n");
    test("Parsing HMSG with bad header size (with reply): ");
    natsParser_Parse(nc, buf, (int) strlen(buf));
    s = natsConnection_GetLastError(nc, &le);
    testCond((s == NATS_PROTOCOL_ERROR)
                && (nc->ps->argBuf == NULL)
                && (nc->ps->msgBuf == NULL)
                && (nc->ps->ma.subject == NULL)
                && (nc->ps->ma.reply == NULL)
                && (le != NULL)
                && (strstr(le, "Bad or Missing Header Size") != NULL));

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "HMSG foo 1 10 4\r\n");
    test("Parsing HMSG with bad header size (out of range): ");
    natsParser_Parse(nc, buf, (int) strlen(buf));
    s = natsConnection_GetLastError(nc, &le);
    testCond((s == NATS_PROTOCOL_ERROR)
                && (nc->ps->argBuf == NULL)
                && (nc->ps->msgBuf == NULL)
                && (nc->ps->ma.subject == NULL)
                && (nc->ps->ma.reply == NULL)
                && (le != NULL)
                && (strstr(le, "Bad or Missing Header Size") != NULL));

    RECREATE_PARSER;

    snprintf(buf, sizeof(buf), "%s", "HMSG foo 1 bar 10 4\r\n");
    test("Parsing HMSG with bad header size (out of range with reply): ");
    natsParser_Parse(nc, buf, (int) strlen(buf));
    s = natsConnection_GetLastError(nc, &le);
    testCond((s == NATS_PROTOCOL_ERROR)
                && (nc->ps->argBuf == NULL)
                && (nc->ps->msgBuf == NULL)
                && (nc->ps->ma.subject == NULL)
                && (nc->ps->ma.reply == NULL)
                && (le != NULL)
                && (strstr(le, "Bad or Missing Header Size") != NULL));

    natsConnection_Destroy(nc);
}

static natsStatus
_checkPool(natsConnection *nc, char **expectedURLs, int expectedURLsCount)
{
    int     i, j, attempts;
    natsSrv *srv;
    char    *url = NULL;
    char    buf[64];
    bool    ok = false;

    natsMutex_Lock(nc->mu);
    for (attempts = 0; (!ok) && (attempts < 20); attempts++)
    {
        ok = (nc->srvPool->size == expectedURLsCount);
        for (i = 0; i < expectedURLsCount; i++)
        {
            bool foundInPool = false;
            url = expectedURLs[i];
            for (j = 0; j < nc->srvPool->size; j++)
            {
                srv = nc->srvPool->srvrs[j];
                snprintf(buf, sizeof(buf), "%s:%d", srv->url->host, srv->url->port);
                if (strcmp(buf, url))
                {
                    foundInPool = true;
                    break;
                }
            }
            if (!foundInPool)
            {
                ok = false;
                break;
            }
        }

        if (ok)
            break;

        natsMutex_Unlock(nc->mu);
        nats_Sleep(100);
        natsMutex_Lock(nc->mu);
    }

    if (!ok)
    {
        if (nc->srvPool->size != expectedURLsCount)
            printf("After 20 retries expected pool size to be %d, got %d\n", expectedURLsCount, nc->srvPool->size);
        else if (url != NULL)
            printf("After 20 retries did not find %s in pool\n", url);
    }

    natsMutex_Unlock(nc->mu);
    return ok ? NATS_OK : NATS_ERR;
}

static natsStatus
checkNewURLsAddedRandomly(natsConnection *nc, char **urlsAfterPoolSetup, int initialPoolSize)
{
    natsStatus  s;
    int         i;
    char        **currentPool = NULL;
    int         currentPoolSize = 0;

    s = natsConnection_GetServers(nc, &currentPool, &currentPoolSize);
    if (s == NATS_OK)
    {
        // Reset status to error by default. If we find a new URL
        // before the end of the initial list, we consider success
        // and return.
        s = NATS_ERR;
        for (i= 0; i < initialPoolSize; i++)
        {
            // If one of the position in initial list is occupied
            // by a new URL, we are ok.
            if (strcmp(urlsAfterPoolSetup[i], currentPool[i]))
            {
                s = NATS_OK;
                break;
            }
        }
    }
    if (currentPool != NULL)
    {
        for (i=0; i<currentPoolSize; i++)
            free(currentPool[i]);
        free(currentPool);
    }
    return s;
}

void test_AsyncINFO(void)
{
    natsConnection  *nc = NULL;
    natsOptions     *opts = NULL;
    natsStatus      s;
    char            buf[2048];
    int             i;
    const char      *good[] = {"INFO {}\r\n", "INFO  {}\r\n", "INFO {} \r\n",
                               "INFO { \"server_id\": \"test\"  }   \r\n",
                               "INFO {\"connect_urls\":[]}\r\n"};
    const char      *wrong[] = {"IxNFO {}\r\n", "INxFO {}\r\n", "INFxO {}\r\n",
                                "INFOx {}\r\n", "INFO{}\r\n", "INFO {}"};
    const char      *lastErr = NULL;

    s = natsOptions_Create(&opts);
    IFOK(s, natsConn_create(&nc, opts));
    IFOK(s, natsParser_Create(&(nc->ps)));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    snprintf(buf, sizeof(buf), "%s", "INFO {\"test\":\"abcde\"x\r\n");
    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf, 9);
    testCond((s == NATS_OK)
                && (nc->ps->state == INFO_ARG)
                && (nc->ps->argBuf != NULL));

    PARSER_START_TEST;
    natsParser_Parse(nc, buf+9, 30);
    lastErr = nats_GetLastError(&s);
    testCond((s == NATS_ERR)
                && (lastErr != NULL)
                && (strstr(lastErr, "missing") != NULL));
    nats_clearLastError();

    snprintf(buf, sizeof(buf), "%s", "INFO {}\r\n");

    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_I));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf+1, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_IN));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf+2, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_INF));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf+3, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_INFO));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf+4, 1);
    testCond((s == NATS_OK) && (nc->ps->state == OP_INFO_SPC));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf+5, (int)strlen(buf)-5);
    testCond((s == NATS_OK) && (nc->ps->state == OP_START));

    // All at once
    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    testCond((s == NATS_OK) && (nc->ps->state == OP_START));

    // Server pool was setup in natsConn_create()

    // Partials requiring argBuf
    snprintf(buf, sizeof(buf), "INFO {\"server_id\":\"%s\", \"host\":\"%s\", \"port\": %d, " \
            "\"auth_required\":%s, \"tls_required\": %s, \"max_payload\":%d}\r\n",
            "test", "localhost", 4222, "true", "true", 2*1024*1024);

    PARSER_START_TEST;
    testCond((s == NATS_OK) && (nc->ps->state == OP_START));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf, 9);
    testCond((s == NATS_OK)
                && (nc->ps->state == INFO_ARG)
                && (nc->ps->argBuf != NULL));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf+9, 2);
    testCond((s == NATS_OK)
                && (nc->ps->state == INFO_ARG)
                && (nc->ps->argBuf != NULL));

    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf+11, (int)strlen(buf)-11);
    testCond((s == NATS_OK)
                && (nc->ps->state == OP_START)
                && (nc->ps->argBuf == NULL));

    test("Check INFO is correct: ");
    testCond((s == NATS_OK)
                && (strcmp(nc->info.id, "test") == 0)
                && (strcmp(nc->info.host, "localhost") == 0)
                && (nc->info.port == 4222)
                && nc->info.authRequired
                && nc->info.tlsRequired
                && (nc->info.maxPayload == 2*1024*1024));

    // Destroy parser, it will be recreated in the loops.
    natsParser_Destroy(nc->ps);
    nc->ps = NULL;

    // Good INFOs
    for (i=0; i<(int) (sizeof(good)/sizeof(char*)); i++)
    {
        snprintf(buf, sizeof(buf), "Test with good INFO proto number %d: ", (i+1));
        test(buf);
        s = natsParser_Create(&(nc->ps));
        IFOK(s, natsParser_Parse(nc, (char*) good[i], (int)strlen(good[i])));
        testCond((s == NATS_OK)
                    && (nc->ps->state == OP_START)
                    && (nc->ps->argBuf == NULL));
        natsParser_Destroy(nc->ps);
        nc->ps = NULL;
    }

    // Wrong INFOs
    for (i=0; i<(int) (sizeof(wrong)/sizeof(char*)); i++)
    {
        snprintf(buf, sizeof(buf), "Test with wrong INFO proto number %d: ", (i+1));
        test(buf);
        s = natsParser_Create(&(nc->ps));
        IFOK(s, natsParser_Parse(nc, (char*) wrong[i], (int)strlen(wrong[i])));
        testCond(!((s == NATS_OK) && (nc->ps->state == OP_START)));
        natsParser_Destroy(nc->ps);
        nc->ps = NULL;
    }
    nats_clearLastError();

    // Now test the decoding of "connect_urls"

    // Destroy, we create a new one
    natsConnection_Destroy(nc);
    nc = NULL;

    s = natsOptions_Create(&opts);
    IFOK(s, natsConn_create(&nc, opts));
    IFOK(s, natsParser_Create(&(nc->ps)));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    snprintf(buf, sizeof(buf), "%s", "INFO {\"connect_urls\":[\"localhost:4222\",\"localhost:5222\"]}\r\n");
    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    if (s == NATS_OK)
    {
        // Pool now should contain localhost:4222 (the default URL) and localhost:5222
        const char *urls[] = {"localhost:4222", "localhost:5222"};

        s = _checkPool(nc, (char**)urls, (int)(sizeof(urls)/sizeof(char*)));
    }
    testCond((s == NATS_OK) && (nc->ps->state == OP_START));

    // Make sure that if client receives the same, it is not added again.
    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    if (s == NATS_OK)
    {
        // Pool should still contain localhost:4222 (the default URL) and localhost:5222
        const char *urls[] = {"localhost:4222", "localhost:5222"};

        s = _checkPool(nc, (char**)urls, (int)(sizeof(urls)/sizeof(char*)));
    }
    testCond((s == NATS_OK) && (nc->ps->state == OP_START));

    // Receive a new URL
    snprintf(buf, sizeof(buf), "%s", "INFO {\"connect_urls\":[\"localhost:4222\",\"localhost:5222\",\"localhost:6222\"]}\r\n");
    PARSER_START_TEST;
    s = natsParser_Parse(nc, buf, (int)strlen(buf));
    if (s == NATS_OK)
    {
        // Pool now should contain localhost:4222 (the default URL) localhost:5222 and localhost:6222
        const char *urls[] = {"localhost:4222", "localhost:5222", "localhost:6222"};

        s = _checkPool(nc, (char**)urls, (int)(sizeof(urls)/sizeof(char*)));
    }
    testCond((s == NATS_OK) && (nc->ps->state == OP_START));

    natsConnection_Destroy(nc);
    nc = NULL;

    // Check that new URLs are added at random places in the pool
    if (s == NATS_OK)
    {
        int  initialPoolSize = 0;
        char **urlsAfterPoolSetup = NULL;
        const char *newURLs = "\"impA:4222\", \"impB:4222\", \"impC:4222\", "\
            "\"impD:4222\", \"impE:4222\", \"impF:4222\", \"impG:4222\", "\
            "\"impH:4222\", \"impI:4222\", \"impJ:4222\"";

        s = natsOptions_Create(&opts);
        IFOK(s, natsOptions_SetNoRandomize(opts, false));
        IFOK(s, natsOptions_SetServers(opts, testServers, sizeof(testServers)/sizeof(char*)));
        IFOK(s, natsConn_create(&nc, opts));
        IFOK(s, natsParser_Create(&(nc->ps)));
        // Capture the pool sequence after randomization
        IFOK(s, natsConnection_GetServers(nc, &urlsAfterPoolSetup, &initialPoolSize));
        if (s != NATS_OK)
            FAIL("Unable to setup test");

        // Add new urls
        snprintf(buf, sizeof(buf), "INFO {\"connect_urls\":[%s]}\r\n", newURLs);
        test("New URLs are added randomly: ");
        s = natsParser_Parse(nc, buf, (int)strlen(buf));
        IFOK(s, checkNewURLsAddedRandomly(nc, urlsAfterPoolSetup, initialPoolSize));
        testCond((s == NATS_OK) && (nc->ps->state == OP_START));

        test("First URL should not have been changed: ")
        testCond((s == NATS_OK) && !strcmp(nc->srvPool->srvrs[0]->url->fullUrl, urlsAfterPoolSetup[0]));

        if (urlsAfterPoolSetup != NULL)
        {
            for (i=0; i<initialPoolSize; i++)
                free(urlsAfterPoolSetup[i]);
            free(urlsAfterPoolSetup);
        }

        natsConnection_Destroy(nc);
    }
}

static void
_parallelRequests(void *closure)
{
    natsConnection      *nc = (natsConnection*) closure;
    natsMsg             *msg;

    // Expecting this to timeout. This is to force parallel
    // requests.
    natsConnection_RequestString(&msg, nc, "foo", "test", 500);
}

void test_RequestPool(void)
{
    natsStatus          s;
    natsPid             pid = NATS_INVALID_PID;
    int                 i;
    natsConnection      *nc = NULL;
    natsSubscription    *sub = NULL;
    natsMsg             *msg = NULL;
    int                 numThreads = RESP_INFO_POOL_MAX_SIZE+5;
    natsThread          *threads[RESP_INFO_POOL_MAX_SIZE+5];

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    // For part of this test, we really need to have the requestor
    // timeout with the given timeout value (to increase number)
    // of parallel requests. So we create a sync subscription that
    // will not send a response back.
    s = natsConnection_SubscribeSync(&sub, nc, "foo");
    if (s != NATS_OK)
    {
        natsConnection_Destroy(nc);
        FAIL("Unable to setup test!");
    }

    // With current implementation, the pool should not
    // increase at all.
    test("Pool not growing: ");
    for (i=0; (i<RESP_INFO_POOL_MAX_SIZE); i++)
        natsConnection_RequestString(&msg, nc, "foo", "test", 1);
    natsMutex_Lock(nc->mu);
    testCond(nc->respPoolSize == 1);
    natsMutex_Unlock(nc->mu);

    test("Pool max size: ");
    for (i=0; i<numThreads; i++)
        threads[i] = NULL;

    s = NATS_OK;
    for (i=0; (s == NATS_OK) && (i<numThreads); i++)
        s = natsThread_Create(&threads[i], _parallelRequests, (void*) nc);

    for (i=0; i<numThreads; i++)
    {
        if (threads[i] != NULL)
        {
            natsThread_Join(threads[i]);
            natsThread_Destroy(threads[i]);
        }
    }
    natsMutex_Lock(nc->mu);
    testCond((s == NATS_OK) && (nc->respPoolSize == RESP_INFO_POOL_MAX_SIZE));
    natsMutex_Unlock(nc->mu);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    _stopServer(pid);
}

void test_NoFlusherIfSendAsap(void)
{
    natsStatus          s;
    natsPid             pid = NATS_INVALID_PID;
    natsConnection      *nc = NULL;
    natsOptions         *opts = NULL;
    natsSubscription    *sub = NULL;
    struct threadArg    arg;
    int                 i;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
        opts = _createReconnectOptions();
    if ((opts == NULL)
            || (natsOptions_SetURL(opts, "nats://127.0.0.1:4222") != NATS_OK)
            || (natsOptions_SetSendAsap(opts, true) != NATS_OK)
            || (natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg)))
    {
        FAIL("Failed to setup test");
    }
    arg.string = "test";
    arg.control = 1;

    pid = _startServer("nats://127.0.0.1:4222", "-a 127.0.0.1 -p 4222", true);
    CHECK_SERVER_STARTED(pid);

    test("Connect/subscribe ok: ");
    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    for (i=0; i<2; i++)
    {
        test("Send ok: ");
        s = natsConnection_PublishString(nc, "foo", "test");
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.msgReceived)
            s = natsCondition_TimedWait(arg.c, arg.m, 1500);
        natsMutex_Unlock(arg.m);
        testCond(s == NATS_OK);

        test("Flusher does not exist: ");
        natsMutex_Lock(nc->mu);
        s = (nc->flusherThread == NULL ? NATS_OK : NATS_ERR);
        natsMutex_Unlock(nc->mu);
        testCond(s == NATS_OK);

        if (i == 0)
        {
            _stopServer(pid);
            pid = NATS_INVALID_PID;
            pid = _startServer("nats://127.0.0.1:4222", "-a 127.0.0.1 -p 4222", true);
            CHECK_SERVER_STARTED(pid);
        }
    }

    natsSubscription_Destroy(sub);
    natsConnection_Close(nc);
    _waitForConnClosed(&arg);
    natsConnection_Destroy(nc);

    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);

    _stopServer(pid);
}

void test_HeadersAndSubPendingBytes(void)
{
    natsStatus          s;
    natsPid             pid   = NATS_INVALID_PID;
    natsConnection      *nc   = NULL;
    natsSubscription    *sub1 = NULL;
    natsSubscription    *sub2 = NULL;
    natsMsg             *msg  = NULL;
    natsMsg             *smsg = NULL;
    int                 msgs  = 0;
    int                 bytes = 0;
    struct threadArg    arg;
    int                 i;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test");
    arg.string = "test";

    pid = _startServer("nats://127.0.0.1:4222", "-a 127.0.0.1 -p 4222", true);
    CHECK_SERVER_STARTED(pid);

    test("Connect/subscribe ok: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub1, nc, "foo", _recvTestString, (void*) &arg));
    IFOK(s, natsSubscription_SetPendingLimits(sub1, 1000, 100));
    IFOK(s, natsConnection_SubscribeSync(&sub2, nc, "foo"));
    IFOK(s, natsSubscription_SetPendingLimits(sub2, 1000, 100));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Create message with header: ");
    s = natsMsg_Create(&msg, "foo", NULL, "hello", 5);
    // Set a header with "large" value (50 bytes)
    IFOK(s, natsMsgHeader_Set(msg, "Key", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
    testCond(s == NATS_OK);

    for (i=0; i<10; i++) {
        test("Publish and receive message: ");
        s = natsConnection_PublishMsg(nc, msg);
        IFOK(s, natsSubscription_NextMsg(&smsg, sub2, 1000));
        if (s == NATS_OK)
        {
            natsMutex_Lock(arg.m);
            while ((s != NATS_TIMEOUT) && !arg.msgReceived)
                s = natsCondition_TimedWait(arg.c, arg.m, 1000);
            arg.msgReceived = false;
            natsMutex_Unlock(arg.m);
        }
        natsMsg_Destroy(smsg);
        smsg = NULL;
        testCond(s == NATS_OK);
    }

    test("Check sub1's pending: ");
    s = natsSubscription_GetPending(sub1, &msgs, &bytes);
    testCond((s == NATS_OK) && (msgs == 0) && (bytes == 0));

    test("Check sub2's pending: ");
    s = natsSubscription_GetPending(sub1, &msgs, &bytes);
    testCond((s == NATS_OK) && (msgs == 0) && (bytes == 0));

    natsMsg_Destroy(msg);
    natsSubscription_Destroy(sub1);
    natsSubscription_Destroy(sub2);
    natsConnection_Destroy(nc);
    _destroyDefaultThreadArgs(&arg);
    _stopServer(pid);
}

static void
_dummyMsgHandler(natsConnection *nc, natsSubscription *sub, natsMsg *msg,
                void *closure)
{
    // do nothing

    natsMsg_Destroy(msg);
}

static int _numRunningThreads(natsDispatcherPool *pool)
{
    int i, n;
    for (n = 0, i = 0; i < pool->cap; i++)
        n += (pool->dispatchers[i] != NULL);
    return n;
}

void test_AssignSubToDispatch(void)
{
    natsStatus s;
#define MAX_SUBS 500
    natsPid serverPid = NATS_INVALID_PID;
    natsConnection *nc = NULL;
    natsSubscription *subs[MAX_SUBS];
    natsClientConfig config4 = {
        .DefaultToThreadPool = true,
        .ThreadPoolMax = 4,
    };
    natsClientConfig config50 = {
        .DefaultToThreadPool = true,
        .ThreadPoolMax = 50,
    };
    natsDispatcherPool *pool = NULL;
    natsDispatcherPool *rpool = NULL;
    natsClientConfig c;
    struct threadArg arg;
    char subSubj[32];
    char pubSubj[32];
    int i=0, n=0;

    const int numMsgs = 100;

    typedef struct
    {
        int numSubs;
        natsClientConfig *config;
        int expectedMax;
        int expectedDispatchers;
        bool expectedDefaultPool;
    } TC;

    TC tcs[] = {
        {
            .numSubs = 10,
            .expectedMax = 1,
            .expectedDispatchers = 0, // all should be dispatched with dedicated threads
        },
        {
            .config = &config4,
            .numSubs = 3,
            .expectedMax = 4,
            .expectedDispatchers = 3,
            .expectedDefaultPool = true,
        },
        {
            .config = &config4,
            .numSubs = 23,
            .expectedMax = 4,
            .expectedDispatchers = 4,
            .expectedDefaultPool = true,
        },
        {
            .numSubs = 77,
            .config = &config50,
            .expectedMax = 50,
            .expectedDispatchers = 50,
            .expectedDefaultPool = true,
        },
    };
    TC *tc, *end = tcs + (sizeof(tcs) / sizeof(tcs[0]));

    test("Check pool size not negative: ");
    s = nats_SetMessageDeliveryPoolSize(-1);
    testCond(s != NATS_OK);

    test("Check pool size not zero: ");
    s = nats_SetMessageDeliveryPoolSize(0);
    testCond(s != NATS_OK);

    // Reset stack since we know the above generated errors.
    nats_clearLastError();

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    // First, close the library and re-open, to reset things
    test("Reset the library's global state: ");
    nats_CloseAndWait(100);
    testCond(true);

    for (tc = tcs; tc < end; tc++, n++)
    {
        testf("%d subs over %d threads: Re-open, check settings, ensure no shared dispatchers created: ", tc->numSubs, tc->expectedDispatchers);
        s = nats_OpenWithConfig(tc->config);
        IFOK(s, (c = nats_lib()->config, NATS_OK));
        if (s == NATS_OK)
        {
            pool = &nats_lib()->messageDispatchers;
            rpool = &nats_lib()->replyDispatchers;
        }
        natsMutex_Lock(pool->lock);
        natsMutex_Lock(rpool->lock);
        testCond((s == NATS_OK) &&
                 (c.DefaultToThreadPool == tc->expectedDefaultPool) &&
                 (c.ThreadPoolMax == tc->expectedMax) &&
                 (c.DefaultRepliesToThreadPool == false) &&
                 (c.ReplyThreadPoolMax == 0) &&
                 (c.UseSeparatePoolForReplies == false) &&
                 (_numRunningThreads(pool) == 0) &&
                 (pool->cap == tc->expectedMax) &&
                 (pool->dispatchers != NULL) &&
                 (_numRunningThreads(rpool) == 0) &&
                 (rpool->dispatchers == NULL));
        natsMutex_Unlock(rpool->lock);
        natsMutex_Unlock(pool->lock);

        testf("%d subs over %d threads: Initial connect: ", tc->numSubs, tc->expectedDispatchers);
        s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
        testCond(s == NATS_OK);

        testf("%d subs over %d threads: Create %d subscriptions: ", tc->numSubs, tc->expectedDispatchers, tc->numSubs);
        if (tc->numSubs > MAX_SUBS)
            FAIL("Too many subscriptions for this test's buffer");

        arg.string = "test";
        arg.status = NATS_OK;
        arg.control = 12;
        arg.N = tc->numSubs * numMsgs;
        arg.sum = 0;
        snprintf(subSubj, sizeof(subSubj), "foo.%d.*", n);
        for (i = 0; (s == NATS_OK) && (i < tc->numSubs); i++)
        {
            s = natsConnection_Subscribe(&subs[i], nc, subSubj, _recvTestString, &arg);
        }
        testCond(s == NATS_OK);

        testf("%d subs over %d threads: Check shared dispatchers assigned as expected: ", tc->numSubs, tc->expectedDispatchers);
        natsMutex_Lock(pool->lock);
        natsMutex_Lock(rpool->lock);
        testCond((pool->dispatchers != NULL) &&
                 (_numRunningThreads(pool) == tc->expectedDispatchers) &&
                 (_numRunningThreads(rpool) == 0));

        if (tc->config != NULL)
        {
            testf("%d subs over %d threads: Verify that the dispatchers have been assigned: ", tc->numSubs, tc->expectedDispatchers);
            for (i = 0; (s == NATS_OK) && (i < tc->numSubs); i++)
            {
                natsSub_Lock(subs[i]);
                if (subs[i]->dispatcher != pool->dispatchers[i % tc->expectedDispatchers])
                    s = NATS_ERR;
                natsSub_Unlock(subs[i]);
            }
            testCond(s == NATS_OK);
        }
        natsMutex_Unlock(rpool->lock);
        natsMutex_Unlock(pool->lock);

        testf("%d subs over %d threads: Publish %d messages and verify received %d times: ", tc->numSubs, tc->expectedDispatchers, numMsgs, arg.N);
        for (i = 0; (s == NATS_OK) && (i < numMsgs); i++)
        {
            snprintf(pubSubj, sizeof(pubSubj), "foo.%d.%d", n, i);
            s = natsConnection_PublishString(nc, pubSubj, "test");
        }
        if (s == NATS_OK)
        {
            natsMutex_Lock(arg.m);
            while ((s != NATS_TIMEOUT) && !arg.msgReceived)
                s = natsCondition_TimedWait(arg.c, arg.m, 1500);
            arg.msgReceived = false;
            arg.status = NATS_OK;
            natsMutex_Unlock(arg.m);
            if (s == NATS_OK)
                s = arg.status;
        }
        testCond(s == NATS_OK);

        testf("%d subs over %d threads: Drain the subs: ", tc->numSubs, tc->expectedDispatchers);
        for (i = 0; (s == NATS_OK) && (i < tc->numSubs); i++)
        {
            s = natsSubscription_Drain(subs[i]);
            natsSubscription_Destroy(subs[i]);
        }
        testCond(s == NATS_OK);

        testf("%d subs over %d threads: Make sure we didn't get unexpected messages: ", tc->numSubs, tc->expectedDispatchers);
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.msgReceived)
            s = natsCondition_TimedWait(arg.c, arg.m, 50);
        natsMutex_Unlock(arg.m);
        testCond((s == NATS_TIMEOUT) && !arg.msgReceived && arg.status != NATS_ERR);

        testf("%d subs over %d threads: cleanup before next test: ", tc->numSubs, tc->expectedDispatchers);
        natsConnection_Destroy(nc);
        nats_Close();
        testCond(true);
    }
    _stopServer(serverPid);

    _destroyDefaultThreadArgs(&arg);
}

void test_DefaultConnection(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    natsOptions         *opts     = NULL;
    int                 i;
    const char          *urls[] = {
        NATS_DEFAULT_URL,
        "tcp://",
        "nats://localhost",
        ":4222",
    };

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetTimeout(opts, 500));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    test("Check connection fails without running server: ");
#ifndef _WIN32
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    if (s != NATS_OK)
#endif
        s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_NO_SERVER);

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    for (i=0; i<(int)(sizeof(urls)/sizeof(char*)); i++)
    {
        char buf[256];

        snprintf(buf, sizeof(buf), "Test connect with '%s':", urls[i]);
        test(buf);
        s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
        testCond(s == NATS_OK);
        natsConnection_Destroy(nc);
        nc = NULL;
    }

    natsOptions_Destroy(opts);

    _stopServer(serverPid);
}

void test_SimplifiedURLs(void)
{
    natsStatus          s         = NATS_OK;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
#if defined(NATS_HAS_TLS)
    natsOptions         *opts     = NULL;
#endif
    const char          *urls[] = {
        "nats://127.0.0.1:4222",
        "nats://127.0.0.1:",
        "nats://127.0.0.1",
        "127.0.0.1:",
        "127.0.0.1"
    };
    int                 urlsCount = sizeof(urls) / sizeof(char *);
    int                 i;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Test simplified URLs to non TLS server: ");
    for (i=0; ((s == NATS_OK) && (i<urlsCount)); i++)
    {
        s = natsConnection_ConnectTo(&nc, urls[i]);
        if (s == NATS_OK)
        {
            natsConnection_Destroy(nc);
            nc = NULL;
        }
    }
    testCond(s == NATS_OK);

    _stopServer(serverPid);
    serverPid = NATS_INVALID_PID;

#if defined(NATS_HAS_TLS)
    serverPid = _startServer("nats://127.0.0.1:4222", "-c tls_default_port.conf -DV", true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SkipServerVerification(opts, true));

    test("Test simplified URLs to TLS server: ");
    for (i=0; ((s == NATS_OK) && (i<urlsCount)); i++)
    {
        s = natsOptions_SetURL(opts, urls[i]);
        IFOK(s, natsConnection_Connect(&nc, opts));
        if (s == NATS_OK)
        {
            natsConnection_Destroy(nc);
            nc = NULL;
        }
    }
    testCond(s == NATS_OK);

    natsOptions_Destroy(opts);
    _stopServer(serverPid);
#endif
}

void test_IPResolutionOrder(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    natsOptions         *opts     = NULL;

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetURL(opts, "nats://localhost:4222"));
    IFOK(s, natsOptions_SetTimeout(opts, 500));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    test("Server listens to IPv4: ");
    serverPid = _startServer("nats://127.0.0.1:4222", "-a 127.0.0.1 -p 4222", true);
    CHECK_SERVER_STARTED(serverPid);
    testCond(serverPid != NATS_INVALID_PID);

    test("Order: 4: ");
    s = natsOptions_IPResolutionOrder(opts, 4);
    IFOK(s, natsConnection_Connect(&nc, opts));
    if (s == NATS_OK)
    {
        natsConnection_Destroy(nc);
        nc = NULL;
    }
    testCond(s == NATS_OK);

    test("Order: 46: ");
    s = natsOptions_IPResolutionOrder(opts, 46);
    IFOK(s, natsConnection_Connect(&nc, opts));
    if (s == NATS_OK)
    {
        natsConnection_Destroy(nc);
        nc = NULL;
    }
    testCond(s == NATS_OK);


    test("Order: 64: ");
    s = natsOptions_IPResolutionOrder(opts, 64);
    IFOK(s, natsConnection_Connect(&nc, opts));
    if (s == NATS_OK)
    {
        natsConnection_Destroy(nc);
        nc = NULL;
    }
    testCond(s == NATS_OK);

    test("Order: 0: ");
    s = natsOptions_IPResolutionOrder(opts, 0);
    IFOK(s, natsConnection_Connect(&nc, opts));
    if (s == NATS_OK)
    {
        natsConnection_Destroy(nc);
        nc = NULL;
    }
    testCond(s == NATS_OK);

    test("Order: 6: ");
    s = natsOptions_IPResolutionOrder(opts, 6);
    if (s == NATS_OK)
    {
        s = natsConnection_Connect(&nc, opts);
        if (s == NATS_OK)
        {
            // Should not have connected
            natsConnection_Destroy(nc);
            nc = NULL;
            s = NATS_ERR;
        }
        else
        {
            s = NATS_OK;
        }
    }
    testCond(s == NATS_OK);

    _stopServer(serverPid);
    serverPid = NATS_INVALID_PID;

    if (!runOnTravis)
    {
        test("Server listens to IPv6: ");
        serverPid = _startServer("nats://[::1]:4222", "-a :: -p 4222", true);
        CHECK_SERVER_STARTED(serverPid);
        testCond(serverPid != NATS_INVALID_PID);

        test("Order: 6: ");
        s = natsOptions_IPResolutionOrder(opts, 6);
        IFOK(s, natsConnection_Connect(&nc, opts));
        if (s == NATS_OK)
        {
            natsConnection_Destroy(nc);
            nc = NULL;
        }
        testCond(s == NATS_OK);

        test("Order: 46: ");
        s = natsOptions_IPResolutionOrder(opts, 46);
        IFOK(s, natsConnection_Connect(&nc, opts));
        if (s == NATS_OK)
        {
            natsConnection_Destroy(nc);
            nc = NULL;
        }
        testCond(s == NATS_OK);

        test("Order: 64: ");
        s = natsOptions_IPResolutionOrder(opts, 64);
        IFOK(s, natsConnection_Connect(&nc, opts));
        if (s == NATS_OK)
        {
            natsConnection_Destroy(nc);
            nc = NULL;
        }
        testCond(s == NATS_OK);

        test("Order: 0: ");
        s = natsOptions_IPResolutionOrder(opts, 0);
        IFOK(s, natsConnection_Connect(&nc, opts));
        if (s == NATS_OK)
        {
            natsConnection_Destroy(nc);
            nc = NULL;
        }
        testCond(s == NATS_OK);

        test("Order: 4: ");
        s = natsOptions_IPResolutionOrder(opts, 4);
        if (s == NATS_OK)
        {
            s = natsConnection_Connect(&nc, opts);
            // This should fail, but server listening to
            // [::] still accepts IPv4 connections, so be
            // tolerant of that.
            if (s == NATS_OK)
            {
                fprintf(stderr, ">>>> Server listening on [::] accepted an IPv4 connection");
                natsConnection_Destroy(nc);
                nc = NULL;
            }
            else
            {
                s = NATS_OK;
            }
        }
        testCond(s == NATS_OK);

        _stopServer(serverPid);
        serverPid = NATS_INVALID_PID;
    }

    natsOptions_Destroy(opts);
}

void test_UseDefaultURLIfNoServerSpecified(void)
{
    natsStatus          s;
    natsOptions         *opts     = NULL;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    s = natsOptions_Create(&opts);
    if (s != NATS_OK)
        FAIL("Unable to create options!");

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Check we can connect even if no server is specified: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_ConnectToWithMultipleURLs(void)
{
    natsStatus      s;
    natsConnection  *nc       = NULL;
    natsPid         serverPid = NATS_INVALID_PID;
    char            buf[256];

    buf[0] = '\0';

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Check multiple URLs work: ");
    s = natsConnection_ConnectTo(&nc, "nats://127.0.0.1:4444,nats://127.0.0.1:4443,nats://127.0.0.1:4222");
    IFOK(s, natsConnection_Flush(nc));
    IFOK(s, natsConnection_GetConnectedUrl(nc, buf, sizeof(buf)));
    testCond((s == NATS_OK)
             && (strcmp(buf, "nats://127.0.0.1:4222") == 0));
    natsConnection_Destroy(nc);

    test("Check multiple URLs work, even with spaces: ");
    s = natsConnection_ConnectTo(&nc, "nats://127.0.0.1:4444 , nats://127.0.0.1:4443  ,  nats://127.0.0.1:4222   ");
    IFOK(s, natsConnection_Flush(nc));
    IFOK(s, natsConnection_GetConnectedUrl(nc, buf, sizeof(buf)));
    testCond((s == NATS_OK)
             && (strcmp(buf, "nats://127.0.0.1:4222") == 0));
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_ConnectionToWithNullURLs(void)
{
    natsStatus      s;
    natsConnection  *nc       = NULL;
    natsPid         serverPid = NATS_INVALID_PID;
    char            buf[256];

    test("Check NULL URLs: ");
    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NULL);
    IFOK(s, natsConnection_Flush(nc));
    IFOK(s, natsConnection_GetConnectedUrl(nc, buf, sizeof(buf)));

    testCond((s == NATS_OK) && (strcmp(buf, NATS_DEFAULT_URL) == 0));
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_ConnectionWithNullOptions(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Check connect with NULL options is allowed: ");
    s = natsConnection_Connect(&nc, NULL);
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_ConnectionStatus(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    test("Test connection status should be CONNECTED: ");
    testCond((s == NATS_OK)
             && (natsConnection_Status(nc) == NATS_CONN_STATUS_CONNECTED));

    if (s == NATS_OK)
    {
        natsConnection_Close(nc);
        test("Test connection status should be CLOSED: ");
        testCond(natsConnection_Status(nc) == NATS_CONN_STATUS_CLOSED);
    }

    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_ConnClosedCB(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
        opts = _createReconnectOptions();

    if ((opts == NULL)
        || (natsOptions_SetURL(opts, NATS_DEFAULT_URL) != NATS_OK)
        || (natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg) != NATS_OK))
    {
        FAIL("Unable to setup test for ConnClosedCB!");
    }

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_Connect(&nc, opts);
    if (s == NATS_OK)
        natsConnection_Close(nc);

    test("Test connection closed CB invoked: ");

    natsMutex_Lock(arg.m);
    s = NATS_OK;
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 1000);
    natsMutex_Unlock(arg.m);

    testCond((s == NATS_OK) && arg.closed);

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_CloseDisconnectedCB(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
        opts = _createReconnectOptions();

    if ((opts == NULL)
        || (natsOptions_SetURL(opts, NATS_DEFAULT_URL) != NATS_OK)
        || (natsOptions_SetAllowReconnect(opts, false) != NATS_OK)
        || (natsOptions_SetDisconnectedCB(opts, _closedCb, (void*) &arg) != NATS_OK))
    {
        FAIL("Unable to setup test for ConnClosedCB!");
    }

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_Connect(&nc, opts);
    if (s == NATS_OK)
        natsConnection_Close(nc);

    test("Test connection disconnected CB invoked: ");

    natsMutex_Lock(arg.m);
    s = NATS_OK;
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 1000);
    natsMutex_Unlock(arg.m);

    testCond((s == NATS_OK) && arg.closed);

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_ServerStopDisconnectedCB(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
        opts = _createReconnectOptions();

    if ((opts == NULL)
        || (natsOptions_SetURL(opts, NATS_DEFAULT_URL) != NATS_OK)
        || (natsOptions_SetAllowReconnect(opts, false) != NATS_OK)
        || (natsOptions_SetDisconnectedCB(opts, _closedCb, (void*) &arg) != NATS_OK))
    {
        FAIL("Unable to setup test for ConnClosedCB!");
    }

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_Connect(&nc, opts);

    _stopServer(serverPid);

    test("Test connection disconnected CB invoked on server shutdown: ");

    natsMutex_Lock(arg.m);
    s = NATS_OK;
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);

    testCond((s == NATS_OK) && arg.closed);

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);
}

void test_ClosedConnections(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *goodsub  = NULL;
    natsSubscription    *sub      = NULL;
    natsMsg             *msg      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_SubscribeSync(&goodsub, nc, "foo"));
    if (s == NATS_OK)
        natsConnection_Close(nc);

    // Test all API endpoints do the right thing with a closed connection.

    test("Publish on closed should fail: ")
    s = natsConnection_Publish(nc, "foo", NULL, 0);
    testCond(s == NATS_CONNECTION_CLOSED);

    test("PublishMsg on closed should fail: ")
    s = natsMsg_Create(&msg, "foo", NULL, NULL, 0);
    IFOK(s, natsConnection_PublishMsg(nc, msg));
    testCond(s == NATS_CONNECTION_CLOSED);

    natsMsg_Destroy(msg);
    msg = NULL;

    test("Flush on closed should fail: ")
    s = natsConnection_Flush(nc);
    testCond(s == NATS_CONNECTION_CLOSED);

    test("Subscribe on closed should fail: ")
    s = natsConnection_Subscribe(&sub, nc, "foo", _dummyMsgHandler, NULL);
    testCond(s == NATS_CONNECTION_CLOSED);

    test("SubscribeSync on closed should fail: ")
    s = natsConnection_SubscribeSync(&sub, nc, "foo");
    testCond(s == NATS_CONNECTION_CLOSED);

    test("QueueSubscribe on closed should fail: ")
    s = natsConnection_QueueSubscribe(&sub, nc, "foo", "bar", _dummyMsgHandler, NULL);
    testCond(s == NATS_CONNECTION_CLOSED);

    test("QueueSubscribeSync on closed should fail: ")
    s = natsConnection_QueueSubscribeSync(&sub, nc, "foo", "bar");
    testCond(s == NATS_CONNECTION_CLOSED);

    test("Request on closed should fail: ")
    s = natsConnection_Request(&msg, nc, "foo", NULL, 0, 10);
    testCond(s == NATS_CONNECTION_CLOSED);

    test("NextMsg on closed should fail: ")
    s = natsSubscription_NextMsg(&msg, goodsub, 10);
    testCond(s == NATS_CONNECTION_CLOSED);

    test("Unsubscribe on closed should fail: ")
    s = natsSubscription_Unsubscribe(goodsub);
    testCond(s == NATS_CONNECTION_CLOSED);

    natsSubscription_Destroy(goodsub);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_ConnectVerboseOption(void)
{
    natsStatus          s         = NATS_OK;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    args;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s == NATS_OK)
    {
        opts = _createReconnectOptions();
        if (opts == NULL)
            s = NATS_ERR;
    }
    IFOK(s, natsOptions_SetVerbose(opts, true));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, &args));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Check connect OK with Verbose option: ");

    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_Flush(nc));

    testCond(s == NATS_OK)

    _stopServer(serverPid);
    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Check reconnect OK with Verbose option: ");

    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.reconnected)
        s = natsCondition_TimedWait(args.c, args.m, 5000);
    natsMutex_Unlock(args.m);

    IFOK(s, natsConnection_Flush(nc));

    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&args);

    _stopServer(serverPid);
}

void test_ReconnectThreadLeak(void)
{
    natsStatus          s;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    natsConnection      *nc       = NULL;
    int                 i;
    struct threadArg    arg;

    serverPid = _startServer("nats://127.0.0.1:4222", "-a 127.0.0.1 -p 4222", true);
    CHECK_SERVER_STARTED(serverPid);

    s = _createDefaultThreadArgsForCbTests(&arg);

    opts = _createReconnectOptions();
    if ((opts == NULL)
            || (natsOptions_SetURL(opts, "nats://127.0.0.1:4222") != NATS_OK)
            || (natsOptions_SetDisconnectedCB(opts, _disconnectedCb, (void*) &arg) != NATS_OK)
            || (natsOptions_SetReconnectedCB(opts, _reconnectedCb, (void*) &arg) != NATS_OK)
            || (natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg) != NATS_OK))
    {
        FAIL("Unable to setup test");
    }

    test("Connect ok: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    for (i=0; i<10; i++)
    {
        natsMutex_Lock(nc->mu);
        natsSock_Shutdown(nc->sockCtx.fd);
        natsMutex_Unlock(nc->mu);

        test("Waiting for disconnect: ");
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.disconnected)
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        arg.disconnected = false;
        natsMutex_Unlock(arg.m);
        testCond(s == NATS_OK);

        test("Waiting for reconnect: ");
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.reconnected)
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        arg.reconnected = false;
        natsMutex_Unlock(arg.m);
        testCond(s == NATS_OK);
    }

    natsConnection_Close(nc);
    _waitForConnClosed(&arg);

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_ReconnectTotalTime(void)
{
    natsStatus  s;
    natsOptions *opts = NULL;

    test("Check reconnect time: ");
    s = natsOptions_Create(&opts);
    testCond((s == NATS_OK)
             && ((opts->maxReconnect * opts->reconnectWait) >= (2 * 60 * 1000)));

    natsOptions_Destroy(opts);
}

void test_ReconnectDisallowedFlags(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect: ");
    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:22222"));
    IFOK(s, natsOptions_SetAllowReconnect(opts, false));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);

    _stopServer(serverPid);

    test("Test connection closed CB invoked: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);
}

void test_ReconnectAllowedFlags(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Create options and connect: ");
    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:22222"));
    IFOK(s, natsOptions_SetAllowReconnect(opts, true));
    IFOK(s, natsOptions_SetMaxReconnect(opts, 2));
    IFOK(s, natsOptions_SetReconnectWait(opts, 1000));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);

    _stopServer(serverPid);

    test("Test reconnecting in progress: ");

    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 500);
    natsMutex_Unlock(arg.m);

    testCond((s == NATS_TIMEOUT)
             && !arg.disconnected
             && natsConnection_IsReconnecting(nc));

    natsConnection_Close(nc);
    natsMutex_Lock(arg.m);
    s = NATS_OK;
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 500);
    natsMutex_Unlock(arg.m);

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);
}

static void
_closeConn(void *arg)
{
    natsConnection *nc = (natsConnection*) arg;

    natsConnection_Close(nc);
}

void test_ConnCloseBreaksReconnectLoop(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsThread          *t        = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
    {
        opts = _createReconnectOptions();
        if (opts == NULL)
            s = NATS_NO_MEMORY;
    }
    IFOK(s, natsOptions_SetMaxReconnect(opts, 1000));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, &arg));
    IFOK(s, natsOptions_SetDisconnectedCB(opts, _disconnectedCb, &arg));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connection close breaks out reconnect loop: ");
    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_Flush(nc));

    // Shutdown the server
    _stopServer(serverPid);

    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.disconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 3000);
    natsMutex_Unlock(arg.m);

    // Wait a bit before trying to close the connection to make sure
    // that the reconnect loop has started.
    nats_Sleep(1000);

    // Close the connection, this should break the reconnect loop.
    // Do this in a go routine since the issue was that Close()
    // would block until the reconnect loop is done.
    s = natsThread_Create(&t, _closeConn, (void*) nc);

    // Even on Windows (where a createConn takes more than a second)
    // we should be able to break the reconnect loop with the following
    // timeout.
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 3000);
    natsMutex_Unlock(arg.m);

    testCond((s == NATS_OK) && arg.closed);

    if (t != NULL)
    {
        natsThread_Join(t);
        natsThread_Destroy(t);
    }

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&arg);
}

void test_BasicReconnectFunctionality(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
    {
        arg.string = "bar";
        arg.status = NATS_OK;
    }
    if (s == NATS_OK)
        opts = _createReconnectOptions();

    if ((opts == NULL)
        || (natsOptions_SetDisconnectedCB(opts, _disconnectedCb, &arg) != NATS_OK)
        || (natsOptions_SetClosedCB(opts, _closedCb, &arg) != NATS_OK))
    {
        FAIL("Unable to create reconnect options!");
    }

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, &arg));
    IFOK(s, natsConnection_Flush(nc));

    _stopServer(serverPid);
    serverPid = NATS_INVALID_PID;

    test("Disconnected CB invoked: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.disconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 500);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && arg.disconnected);

    test("Publish message: ");
    s = natsConnection_PublishString(nc, "foo", arg.string);
    if (s == NATS_OK)
    {
        serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
        CHECK_SERVER_STARTED(serverPid);
    }
    IFOK(s, natsConnection_FlushTimeout(nc, 5000));
    testCond(s == NATS_OK);

    test("Check message received after reconnect: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.msgReceived)
        s = natsCondition_TimedWait(arg.c, arg.m, 1500);
    natsMutex_Unlock(arg.m);

    if (s == NATS_OK)
        s = arg.status;
    testCond((s == NATS_OK) && (nc->stats.reconnects == 1));

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _waitForConnClosed(&arg);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

static void
_doneCb(natsConnection *nc, natsSubscription *sub, natsMsg *msg, void *closure)
{
    struct threadArg *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);
    arg->done = true;
    natsCondition_Signal(arg->c);
    natsMutex_Unlock(arg->m);

    natsMsg_Destroy(msg);
}

void test_ExtendedReconnectFunctionality(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsSubscription    *sub2     = NULL;
    natsSubscription    *sub3     = NULL;
    natsSubscription    *sub4     = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
    {
        arg.string = "bar";
        arg.status = NATS_OK;
        arg.control=3;
    }
    if (s == NATS_OK)
        opts = _createReconnectOptions();

    if ((opts == NULL)
        || (natsOptions_SetReconnectedCB(opts, _reconnectedCb, &arg) != NATS_OK)
        || (natsOptions_SetDisconnectedCB(opts, _disconnectedCb, &arg) != NATS_OK)
        || (natsOptions_SetClosedCB(opts, _closedCb, &arg) != NATS_OK))
    {
        FAIL("Unable to create reconnect options!");
    }

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Setup: ");
    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, &arg));
    IFOK(s, natsConnection_Subscribe(&sub2, nc, "foobar", _recvTestString, &arg));
    IFOK(s, natsConnection_PublishString(nc, "foo", arg.string));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    _stopServer(serverPid);
    serverPid = NATS_INVALID_PID;

    test("Disconnected CB invoked: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.disconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && arg.disconnected);

    test("Some protos while disconnected: ");
    // Sub while disconnected
    s = natsConnection_Subscribe(&sub3, nc, "bar", _recvTestString, &arg);

    // Unsubscribe foo and bar while disconnected
    IFOK(s, natsSubscription_Unsubscribe(sub2));
    IFOK(s, natsConnection_PublishString(nc, "foo", arg.string));
    IFOK(s, natsConnection_PublishString(nc, "bar", arg.string));
    testCond(s == NATS_OK);

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    // Server is restarted, wait for reconnect
    test("Check reconnected: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.reconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && arg.reconnected);

    test("Publish more: ");
    s = natsConnection_PublishString(nc, "foobar", arg.string);
    IFOK(s, natsConnection_PublishString(nc, "foo", arg.string));
    IFOK(s, natsConnection_Subscribe(&sub4, nc, "done", _doneCb, &arg));
    IFOK(s, natsConnection_PublishString(nc, "done", "done"));
    testCond(s == NATS_OK);

    test("Done msg received: ")
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.done)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && arg.done);

    nats_Sleep(50);

    test("All msgs were received: ");
    testCond(arg.sum == 4);

    natsSubscription_Destroy(sub);
    natsSubscription_Destroy(sub2);
    natsSubscription_Destroy(sub3);
    natsSubscription_Destroy(sub4);;
    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _waitForConnClosed(&arg);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_QueueSubsOnReconnect(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub1     = NULL;
    natsSubscription    *sub2     = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
    {
        arg.string = "bar";
        arg.status = NATS_OK;
        arg.control= 6;
    }
    if (s == NATS_OK)
        opts = _createReconnectOptions();

    if ((opts == NULL)
        || (natsOptions_SetReconnectedCB(opts, _reconnectedCb, &arg) != NATS_OK))
    {
        FAIL("Unable to create reconnect options!");
    }

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_QueueSubscribe(&sub1, nc, "foo.bar", "workers",
                                          _recvTestString, &arg));
    IFOK(s, natsConnection_QueueSubscribe(&sub2, nc, "foo.bar", "workers",
                                          _recvTestString, &arg));
    IFOK(s, natsConnection_Flush(nc));

    for (int i=0; (s == NATS_OK) && (i < 10); i++)
    {
        char seq[20];

        snprintf(seq, sizeof(seq), "%d", i);
        s = natsConnection_PublishString(nc, "foo.bar", seq);
    }

    IFOK(s, natsConnection_Flush(nc));

    nats_Sleep(50);

    natsMutex_Lock(arg.m);
    for (int i=0; (s == NATS_OK) && (i<10); i++)
    {
        if (arg.results[i] != 1)
            s = NATS_ERR;
    }
    IFOK(s, arg.status);

    memset(&arg.results, 0, sizeof(arg.results));
    natsMutex_Unlock(arg.m);

    test("Base results: ");
    testCond(s == NATS_OK);

    _stopServer(serverPid);
    serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Reconnects: ")
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.reconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && arg.reconnected);

    for (int i=0; (s == NATS_OK) && (i < 10); i++)
    {
        char seq[20];

        snprintf(seq, sizeof(seq), "%d", i);
        s = natsConnection_PublishString(nc, "foo.bar", seq);
    }

    IFOK(s, natsConnection_Flush(nc));

    nats_Sleep(50);

    natsMutex_Lock(arg.m);
    for (int i=0; (s == NATS_OK) && (i<10); i++)
    {
        if (arg.results[i] != 1)
            s = NATS_ERR;
    }
    IFOK(s, arg.status);

    memset(&arg.results, 0, sizeof(arg.results));
    natsMutex_Unlock(arg.m);

    test("Reconnect results: ");
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub1);
    natsSubscription_Destroy(sub2);
    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_IsClosed(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, "nats://127.0.0.1:22222");
    test("Check IsClosed is correct: ");
    testCond((s == NATS_OK) && !natsConnection_IsClosed(nc));

    _stopServer(serverPid);
    serverPid = NATS_INVALID_PID;

    test("Check IsClosed after server shutdown: ");
    testCond((s == NATS_OK) && !natsConnection_IsClosed(nc));

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Check IsClosed after server restart: ");
    testCond((s == NATS_OK) && !natsConnection_IsClosed(nc));

    natsConnection_Close(nc);
    test("Check IsClosed after connection closed: ");
    testCond((s == NATS_OK) && natsConnection_IsClosed(nc));

    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_IsReconnectingAndStatus(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Check reconnecting state: ");
    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:22222"));
    IFOK(s, natsOptions_SetAllowReconnect(opts, true));
    IFOK(s, natsOptions_SetMaxReconnect(opts, 10000));
    IFOK(s, natsOptions_SetReconnectWait(opts, 100));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));
    IFOK(s, natsOptions_SetDisconnectedCB(opts, _disconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));

    // Connect, verify initial reconnecting state check, then stop the server
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond((s == NATS_OK) && !natsConnection_IsReconnecting(nc));

    test("Check status: ");
    testCond((s == NATS_OK) && (natsConnection_Status(nc) == NATS_CONN_STATUS_CONNECTED));

    _stopServer(serverPid);
    serverPid = NATS_INVALID_PID;

    // Wait until we get the disconnected callback
    test("Check we are disconnected: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.disconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 1000);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && arg.disconnected);

    test("Check IsReconnecting is correct: ");
    testCond(natsConnection_IsReconnecting(nc));

    test("Check Status is correct: ");
    testCond(natsConnection_Status(nc) == NATS_CONN_STATUS_RECONNECTING);

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    // Wait until we get the reconnect callback
    test("Check we are reconnected: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.reconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 1000);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && arg.reconnected);

    test("Check IsReconnecting is correct: ");
    testCond(!natsConnection_IsReconnecting(nc));

    test("Check Status is correct: ");
    testCond(natsConnection_Status(nc) == NATS_CONN_STATUS_CONNECTED);

    // Close the connection, reconnecting should still be false
    natsConnection_Close(nc);

    test("Check IsReconnecting is correct: ");
    testCond(!natsConnection_IsReconnecting(nc));

    test("Check Status is correct: ");
    testCond(natsConnection_Status(nc) == NATS_CONN_STATUS_CLOSED);

    natsMutex_Lock(arg.m);
    while (!arg.closed)
        natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_ReconnectBufSize(void)
{
    natsStatus          s         = NATS_OK;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
    {
        opts = _createReconnectOptions();
        if (opts == NULL)
            s = NATS_ERR;
    }
    if (s == NATS_OK)
        s = natsOptions_SetDisconnectedCB(opts, _disconnectedCb, &arg);

    if (s != NATS_OK)
        FAIL("Unable to setup test");

    test("Option invalid settings. NULL options: ");
    s = natsOptions_SetReconnectBufSize(NULL, 1);
    testCond(s != NATS_OK);

    test("Option invalid settings. Negative value: ");
    s = natsOptions_SetReconnectBufSize(opts, -1);
    testCond(s != NATS_OK);

    test("Option valid settings. Zero: ");
    s = natsOptions_SetReconnectBufSize(opts, 0);
    testCond(s == NATS_OK);

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    // For this test, set to a low value.
    s = natsOptions_SetReconnectBufSize(opts, 32);
    IFOK(s, natsConnection_Connect(&nc, opts));
    IFOK(s, natsConnection_Flush(nc));

    _stopServer(serverPid);

    // Wait until we get the disconnected callback
    test("Check we are disconnected: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.disconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 1000);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && arg.disconnected);

    // Publish 2 messages, they should be accepted
    test("Can publish while server is down: ");
    s = natsConnection_PublishString(nc, "foo", "abcd");
    IFOK(s, natsConnection_PublishString(nc, "foo", "abcd"));
    testCond(s == NATS_OK);

    // This publish should fail
    test("Exhausted buffer should return an error: ");
    s = natsConnection_PublishString(nc, "foo", "abcd");
    testCond(s == NATS_INSUFFICIENT_BUFFER);

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);
}

static void
_startServerForRetryOnConnect(void *closure)
{
    struct threadArg *arg = (struct threadArg*) closure;
    natsPid           pid = NATS_INVALID_PID;

    // Delay start a bit...
    nats_Sleep(300);

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    natsMutex_Lock(arg->m);
    while (!arg->done)
        natsCondition_Wait(arg->c, arg->m);
    natsMutex_Unlock(arg->m);

    _stopServer(pid);
}

static void
_connectedCb(natsConnection *nc, void* closure)
{
    struct threadArg *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);
    arg->connected = true;
    natsCondition_Broadcast(arg->c);
    natsMutex_Unlock(arg->m);
}

static int64_t
_testCustomReconnectDelayOnInitialConnect(natsConnection *nc, int attempts, void *closure)
{
    if (attempts == 10)
        natsConnection_Close(nc);
    return 50;
}

void test_RetryOnFailedConnect(void)
{
    natsStatus          s;
    natsConnection      *nc   = NULL;
    natsOptions         *opts = NULL;
    int64_t             start = 0;
    int64_t             end   = 0;
    natsThread          *t    = NULL;
    natsSubscription    *sub  = NULL;
    natsMsg             *msg  = NULL;
    natsMsg             *rmsg = NULL;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetRetryOnFailedConnect(opts, true, NULL, NULL));
    IFOK(s, natsOptions_SetMaxReconnect(opts, 10));
    IFOK(s, natsOptions_SetReconnectWait(opts, 100));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));
#ifdef _WIN32
    // Windows takes the full timeout to report connect failure, so reduce
    // timeout here.
    IFOK(s, natsOptions_SetTimeout(opts, 100));
#endif
    if (s != NATS_OK)
    {
        natsOptions_Destroy(opts);
        _destroyDefaultThreadArgs(&arg);
        FAIL("Unable to setup test");
    }

    start = nats_Now();
    test("Connect failed: ");
    s = natsConnection_Connect(&nc, opts);
    end = nats_Now();
    testCond(s == NATS_NO_SERVER);
    nats_clearLastError();

    test("Retried: ")
#ifdef _WIN32
    testCond((((end-start) >= 1000) && ((end-start) <= 2800)));
#else
    testCond((((end-start) >= 300) && ((end-start) <= 1500)));
#endif

    test("Connects ok: ");
    s = natsOptions_SetMaxReconnect(opts, 20);
    IFOK(s, natsThread_Create(&t, _startServerForRetryOnConnect, (void*) &arg));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);

    // close to avoid reconnect when shutting down server.
    natsConnection_Close(nc);

    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    natsThread_Join(t);
    natsThread_Destroy(t);
    t = NULL;

    natsConnection_Destroy(nc);
    nc = NULL;

    // Try with async connect
    test("Connect does not block: ");
    s = natsOptions_SetRetryOnFailedConnect(opts, true, _connectedCb, (void*)&arg);
    // Set disconnected/reconnected to make sure that these are not
    // invoked as part of async connect.
    IFOK(s, natsOptions_SetDisconnectedCB(opts, _disconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetMaxReconnect(opts, -1));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond((s == NATS_NOT_YET_CONNECTED) && (nc != NULL));
    nats_clearLastError();

    // Request with a message with headers does timeout as opposed to returning
    // the NATS_NO_SERVER_SUPPORT error.
    test("Request with message with headers: ");
    s = natsMsg_Create(&msg, "request.headers", NULL, "hello", 5);
    IFOK(s, natsMsgHeader_Set(msg, "some", "header"));
    IFOK(s, natsConnection_RequestMsg(&rmsg, nc, msg, 250));
    testCond(s == NATS_TIMEOUT);
    nats_clearLastError();
    natsMsg_Destroy(msg);

    test("Subscription ok: ");
    arg.control = 99;
    s = natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*)&arg);
    testCond(s == NATS_OK);

    test("Publish ok: ");
    s = natsConnection_Publish(nc, "foo", (const void*) "hello", 5);
    testCond(s == NATS_OK);

    // Start server
    arg.done = false;
    s = natsThread_Create(&t, _startServerForRetryOnConnect, (void*) &arg);

    test("Connected: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.connected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("No disconnected and reconnected callbacks: ");
    natsMutex_Lock(arg.m);
    s = ((arg.disconnected || arg.reconnected) ? NATS_ERR : NATS_OK);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Message received: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.msgReceived)
        s = natsCondition_TimedWait(arg.c, arg.m, 5000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    // Close nc to avoid reconnect when shutting down server
    natsConnection_Close(nc);

    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Broadcast(arg.c);
    natsMutex_Unlock(arg.m);

    natsThread_Join(t);
    natsThread_Destroy(t);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    nc = NULL;

    // Try the custom reconnect handler and close the connection after
    // certain number of attempts.
    test("Close in custom reconnect delay: ")
    s = natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg);
    IFOK(s, natsOptions_SetCustomReconnectDelay(opts, _testCustomReconnectDelayOnInitialConnect, NULL));
    IFOK(s, natsConnection_Connect(&nc, opts));
    if (s == NATS_NOT_YET_CONNECTED)
        s = NATS_OK;

    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    nc = NULL;
    natsOptions_Destroy(opts);
    opts = NULL;

    _destroyDefaultThreadArgs(&arg);

    // Make sure that closing the connection while not connected returns fast
    test("Create opts: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetRetryOnFailedConnect(opts, true, _connectedCb, NULL));
    IFOK(s, natsOptions_SetURL(opts, "nats://localhost:54321"));
    testCond(s == NATS_OK);

    test("Start connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_NOT_YET_CONNECTED);
    nats_clearLastError();
    s = NATS_OK;

    test("Close does not take too long: ");
    start = nats_Now();
    natsConnection_Close(nc);
    testCond(nats_Now()-start <1500);

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);
}

static void
_closeConnWithDelay(void *arg)
{
    natsConnection *nc = (natsConnection*) arg;

    nats_Sleep(200);
    natsConnection_Close(nc);
}

static void
_connectToMockupServer(void *closure)
{
    struct threadArg    *arg = (struct threadArg *) closure;
    natsConnection      *nc = NULL;
    natsOptions         *opts = arg->opts;
    natsStatus          s = NATS_OK;
    int                 control;

    // Make sure that the server is ready to accept our connection.
    nats_Sleep(100);

    if (opts == NULL)
    {
        s = natsOptions_Create(&opts);
        IFOK(s, natsOptions_SetAllowReconnect(opts, false));
    }
    IFOK(s, natsConnection_Connect(&nc, opts));

    natsOptions_Destroy(opts);

    natsMutex_Lock(arg->m);
    control = arg->control;
    natsMutex_Unlock(arg->m);

    if (control == 2)
    {
        int64_t payload = 0;

        if (s == NATS_OK)
        {
            test("Check expected max payload: ")
            payload = natsConnection_GetMaxPayload(nc);
            if (payload != 10)
                s = NATS_ERR;
            testCondNoReturn(s == NATS_OK);
        }
        if (s == NATS_OK)
        {
            test("Expect getting an error when publish more than max payload: ");
            s = natsConnection_PublishString(nc, "hello", "Hello World!");
            testCondNoReturn(s != NATS_OK);

            // reset status
            s = NATS_OK;
        }
        if (s == NATS_OK)
        {
            test("Expect success if publishing less than max payload: ");
            s = natsConnection_PublishString(nc, "hello", "a");
            testCondNoReturn(s == NATS_OK);
        }

        natsMutex_Lock(arg->m);
        arg->closed = true;
        natsCondition_Signal(arg->c);
        natsMutex_Unlock(arg->m);
    }
    else if (control == 3)
    {
        natsThread *t = NULL;

        s = natsThread_Create(&t, _closeConnWithDelay, (void*) nc);
        if (s == NATS_OK)
        {
            s = natsConnection_Flush(nc);

            natsThread_Join(t);
            natsThread_Destroy(t);
        }
    }
    else if (control == 4)
    {
        s = natsConnection_Flush(nc);
    }
    else if ((control == 5) || (control == 6))
    {
        // Wait for disconnect Cb
        natsMutex_Lock(arg->m);
        while ((s != NATS_TIMEOUT) && !arg->disconnected)
            s = natsCondition_TimedWait(arg->c, arg->m, 5000);
        natsMutex_Unlock(arg->m);

        if ((s == NATS_OK) && (control == 5))
        {
            // Should reconnect
            natsMutex_Lock(arg->m);
            while ((s != NATS_TIMEOUT) && !arg->reconnected)
                s = natsCondition_TimedWait(arg->c, arg->m, 5000);
            natsMutex_Unlock(arg->m);

            natsConnection_Close(nc);
        }
        else if (s == NATS_OK)
        {
            // Wait that we are closed, then check nc's last error.
            natsMutex_Lock(arg->m);
            while ((s != NATS_TIMEOUT) && !arg->closed)
                s = natsCondition_TimedWait(arg->c, arg->m, 5000);
            natsMutex_Unlock(arg->m);
            if (s == NATS_OK)
            {
                const char* lastErr = NULL;

                s = natsConnection_GetLastError(nc, &lastErr);
                if (strcmp(lastErr, arg->string) != 0)
                    s = NATS_ILLEGAL_STATE;
            }
        }
    }
    else if (control == 7)
    {
        natsMutex_Lock(arg->m);
        while ((s != NATS_TIMEOUT) && !arg->done)
            s = natsCondition_TimedWait(arg->c, arg->m, 5000);
        natsMutex_Unlock(arg->m);
    }

    natsConnection_Destroy(nc);

    natsMutex_Lock(arg->m);
    arg->status = s;
    natsCondition_Signal(arg->c);
    natsMutex_Unlock(arg->m);
}

static natsStatus
_startMockupServer(natsSock *serverSock, const char *host, const char *port)
{
    struct addrinfo     hints;
    struct addrinfo     *servinfo = NULL;
    int                 res;
    natsStatus          s = NATS_OK;
    natsSock            sock = NATS_SOCK_INVALID;

    memset(&hints,0,sizeof(hints));

    hints.ai_family   = AF_INET;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_protocol = IPPROTO_TCP;
    hints.ai_flags    = AI_PASSIVE;

    if ((res = getaddrinfo(host, port, &hints, &servinfo)) != 0)
    {
         hints.ai_family = AF_INET6;

         if ((res = getaddrinfo(host, port, &hints, &servinfo)) != 0)
             s = NATS_SYS_ERROR;
    }
    if (s == NATS_OK)
    {
        sock = socket(servinfo->ai_family, servinfo->ai_socktype,
                      servinfo->ai_protocol);
        if (sock == NATS_SOCK_INVALID)
            s = NATS_SYS_ERROR;

        IFOK(s, natsSock_SetCommonTcpOptions(sock));
        IFOK(s, natsSock_SetBlocking(sock, true));
    }
    if ((s == NATS_OK)
        && (bind(sock, servinfo->ai_addr, (natsSockLen) servinfo->ai_addrlen) == NATS_SOCK_ERROR))
    {
        s = NATS_SYS_ERROR;
    }

    if ((s == NATS_OK) && (listen(sock, 100) == NATS_SOCK_ERROR))
        s = NATS_SYS_ERROR;

    if (s == NATS_OK)
        *serverSock = sock;
    else
        natsSock_Close(sock);

    nats_FreeAddrInfo(servinfo);

    return s;
}

void test_ErrOnConnectAndDeadlock(void)
{
    natsStatus          s = NATS_OK;
    natsSock            sock = NATS_SOCK_INVALID;
    natsThread          *t = NULL;
    struct threadArg    arg;
    natsSockCtx         ctx;

    memset(&ctx, 0, sizeof(natsSockCtx));

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("@@ Unable to setup test!");

    arg.control = 1;

    test("Verify that bad INFO does not cause deadlock in client: ");

    // We will hand run a fake server that will timeout and not return a proper
    // INFO proto. This is to test that we do not deadlock.

    s = _startMockupServer(&sock, "localhost", "4222");

    // Start the thread that will try to connect to our server...
    IFOK(s, natsThread_Create(&t, _connectToMockupServer, (void*) &arg));

    if ((s == NATS_OK)
        && (((ctx.fd = accept(sock, NULL, NULL)) == NATS_SOCK_INVALID)
            || (natsSock_SetCommonTcpOptions(ctx.fd) != NATS_OK)))
    {
        s = NATS_SYS_ERROR;
    }

    if (s == NATS_OK)
    {
        const char* badInfo = "INFOZ \r\n";

        // Send back a mal-formed INFO.
        s = natsSock_WriteFully(&ctx, badInfo, (int) strlen(badInfo));
    }

    if (s == NATS_OK)
    {
        natsMutex_Lock(arg.m);

        while ((s != NATS_TIMEOUT) && (arg.status == NATS_OK))
            s = natsCondition_TimedWait(arg.c, arg.m, 3000);

        natsMutex_Unlock(arg.m);
    }

    if (t != NULL)
    {
        natsThread_Join(t);
        natsThread_Destroy(t);
    }

    testCond((s == NATS_OK) && (arg.status != NATS_OK));

    _destroyDefaultThreadArgs(&arg);

    natsSock_Close(ctx.fd);
    natsSock_Close(sock);
}

void test_ErrOnMaxPayloadLimit(void)
{
    natsStatus          s = NATS_OK;
    natsSock            sock = NATS_SOCK_INVALID;
    natsThread          *t = NULL;
    int                 expectedMaxPayLoad = 10;
    struct threadArg    arg;
    natsSockCtx         ctx;

    memset(&ctx, 0, sizeof(natsSockCtx));

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("@@ Unable to setup test!");

    arg.control = 2;

    s = _startMockupServer(&sock, "localhost", "4222");

    if ((s == NATS_OK) && (listen(sock, 100) == NATS_SOCK_ERROR))
        s = NATS_SYS_ERROR;

    // Start the thread that will try to connect to our server...
    IFOK(s, natsThread_Create(&t, _connectToMockupServer, (void*) &arg));

    if ((s == NATS_OK)
        && (((ctx.fd = accept(sock, NULL, NULL)) == NATS_SOCK_INVALID)
            || (natsSock_SetCommonTcpOptions(ctx.fd) != NATS_OK)))
    {
        s = NATS_SYS_ERROR;
    }
    if (s == NATS_OK)
    {
        char info[1024];

        snprintf(info, sizeof(info),
                 "INFO {\"server_id\":\"foobar\",\"version\":\"latest\",\"go\":\"latest\",\"host\":\"localhost\",\"port\":4222,\"auth_required\":false,\"tls_required\":false,\"max_payload\":%d}\r\n",
                 expectedMaxPayLoad);

        // Send INFO.
        s = natsSock_WriteFully(&ctx, info, (int) strlen(info));
        if (s == NATS_OK)
        {
            char buffer[1024];

            memset(buffer, 0, sizeof(buffer));

            // Read connect and ping commands sent from the client
            s = natsSock_ReadLine(&ctx, buffer, sizeof(buffer));
            IFOK(s, natsSock_ReadLine(&ctx, buffer, sizeof(buffer)));
        }
        // Send PONG
        IFOK(s, natsSock_WriteFully(&ctx, _PONG_PROTO_, _PONG_PROTO_LEN_));
    }

    // Wait for the client to be about to close the connection.
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 3000);
    natsMutex_Unlock(arg.m);

    natsSock_Close(ctx.fd);
    natsSock_Close(sock);

    // Wait for the client to finish.
    if (t != NULL)
    {
        natsThread_Join(t);
        natsThread_Destroy(t);
    }

    test("Test completed ok: ");
    testCond(s == NATS_OK);

    _destroyDefaultThreadArgs(&arg);
}

void test_Auth(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    natsOptions         *opts     = NULL;

    test("Server with auth on, client without should fail: ");

    serverPid = _startServer("nats://127.0.0.1:8232", "--user ivan --pass foo -p 8232", false);
    CHECK_SERVER_STARTED(serverPid);

    nats_Sleep(1000);

    s = natsConnection_ConnectTo(&nc, "nats://127.0.0.1:8232");
    testCond((s == NATS_CONNECTION_AUTH_FAILED)
             && (nats_strcasestr(nats_GetLastError(NULL), "Authorization Violation") != NULL));

    test("Server with auth on, client with proper auth should succeed: ");

    s = natsConnection_ConnectTo(&nc, "nats://ivan:foo@127.0.0.1:8232");
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    nc = NULL;

    // Use Options
    test("Connect using SetUserInfo: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:8232"));
    IFOK(s, natsOptions_SetUserInfo(opts, "ivan", "foo"));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);
    natsConnection_Destroy(nc);
    nc = NULL;

    // Verify that credentials in URL take precedence.
    test("URL takes precedence: ");
    s = natsOptions_SetURL(opts, "nats://ivan:foo@127.0.0.1:8232");
    IFOK(s, natsOptions_SetUserInfo(opts, "foo", "bar"));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _stopServer(serverPid);
}

void test_AuthFailNoDisconnectCB(void)
{
    natsStatus          s;
    natsOptions         *opts     = NULL;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    serverPid = _startServer("nats://127.0.0.1:8232", "--user ivan --pass foo -p 8232", true);
    CHECK_SERVER_STARTED(serverPid);

    opts = _createReconnectOptions();
    if (opts == NULL)
        FAIL("Unable to create options!");

    test("Connect should fail: ");
    s = natsOptions_SetDisconnectedCB(opts, _disconnectedCb, &arg);
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s != NATS_OK);

    test("DisconnectCb should not be invoked on auth failure: ");
    natsMutex_Lock(arg.m);
    s = NATS_OK;
    while ((s != NATS_TIMEOUT) && !arg.disconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 1000);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_TIMEOUT) && !arg.disconnected);

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_AuthToken(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    natsOptions         *opts     = NULL;

    serverPid = _startServer("nats://testSecret@127.0.0.1:8232", "-auth testSecret -p 8232", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Server with token authorization, client without should fail: ");
    s = natsConnection_ConnectTo(&nc, "nats://127.0.0.1:8232");
    testCond(s != NATS_OK);

    test("Server with token authorization, client with proper auth should succeed: ");
    s = natsConnection_ConnectTo(&nc, "nats://testSecret@127.0.0.1:8232");
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    nc = NULL;

    // Use Options
    test("Connect using SetUserInfo: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:8232"));
    IFOK(s, natsOptions_SetToken(opts, "testSecret"));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);
    natsConnection_Destroy(nc);
    nc = NULL;

    // Verify that token in URL take precedence.
    test("URL takes precedence: ");
    s = natsOptions_SetURL(opts, "nats://testSecret@127.0.0.1:8232");
    IFOK(s, natsOptions_SetToken(opts, "badtoken"));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _stopServer(serverPid);
}

static const char*
_tokenHandler(void* closure)
{
    return (char*) closure;
}

void test_AuthTokenHandler(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    natsOptions         *opts     = NULL;

    serverPid = _startServer("nats://testSecret@127.0.0.1:8232", "-auth testSecret -p 8232", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect using SetTokenHandler: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:8232"));
    IFOK(s, natsOptions_SetTokenHandler(opts, _tokenHandler, (char*) "testSecret"));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);
    natsConnection_Destroy(nc);
    nc = NULL;

    test("cannot set a tokenHandler when token set: ");
    s = natsOptions_SetTokenHandler(opts, NULL, NULL);
    IFOK(s, natsOptions_SetToken(opts, "token"));
    IFOK(s, natsOptions_SetTokenHandler(opts, _tokenHandler, (char*) "testSecret"));
    testCond(s == NATS_ILLEGAL_STATE);

    test("cannot set a token when tokenHandler set: ");
    s = natsOptions_SetToken(opts, NULL);
    IFOK(s, natsOptions_SetTokenHandler(opts, _tokenHandler, (char*) "testSecret"));
    IFOK(s, natsOptions_SetToken(opts, "token"));
    testCond(s == NATS_ILLEGAL_STATE);

    test("token in URL not valid with tokenHandler: ");
    s = natsOptions_SetURL(opts, "nats://testSecret@127.0.0.1:8232");
    IFOK(s, natsOptions_SetTokenHandler(opts, _dummyTokenHandler, NULL));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_ILLEGAL_STATE);

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _stopServer(serverPid);
}

static void
_permsViolationHandler(natsConnection *nc, natsSubscription *sub, natsStatus err, void *closure)
{
    struct threadArg *args      = (struct threadArg*) closure;
    const char       *lastError = NULL;

    // Technically, there is no guarantee that the connection's last error
    // be still the one that is given to this callback.
    if (err == NATS_NOT_PERMITTED)
    {
        bool ok = true;

        // So consider ok if currently not the same or if the same, the
        // error string is as expected.
        if (natsConnection_GetLastError(nc, &lastError) == NATS_NOT_PERMITTED)
            ok = (nats_strcasestr(lastError, args->string) != NULL ? true : false);

        if (ok)
        {
            natsMutex_Lock(args->m);
            args->done = true;
            natsCondition_Broadcast(args->c);
            natsMutex_Unlock(args->m);
        }
    }
}

void test_PermViolation(void)
{
    natsStatus          s;
    natsConnection      *conn = NULL;
    natsSubscription    *sub  = NULL;
    natsOptions         *opts = NULL;
    natsPid             pid   = NATS_INVALID_PID;
    int                 i;
    bool                cbCalled;
    struct threadArg    args;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s == NATS_OK)
    {
        args.string = PERMISSIONS_ERR;
        s = natsOptions_Create(&opts);
    }
    IFOK(s, natsOptions_SetURL(opts, "nats://ivan:pwd@127.0.0.1:8232"));
    IFOK(s, natsOptions_SetErrorHandler(opts, _permsViolationHandler, &args));
    if (s != NATS_OK)
        FAIL("Error setting up test");

    pid = _startServer("nats://127.0.0.1:8232", "-c permissions.conf -a 127.0.0.1 -p 8232", false);
    CHECK_SERVER_STARTED(pid);
    s = _checkStart("nats://ivan:pwd@127.0.0.1:8232", 4, 10);
    if (s != NATS_OK)
    {
        _stopServer(pid);
        FAIL("Error starting server!");
    }
    test("Check connection created: ");
    s = natsConnection_Connect(&conn, opts);
    testCond(s == NATS_OK);

    for (i=0; i<2; i++)
    {
        cbCalled = false;

        test("Should get perm violation: ");
        if (i == 0)
            s = natsConnection_PublishString(conn, "bar", "fail");
        else
            s = natsConnection_Subscribe(&sub, conn, "foo", _dummyMsgHandler, NULL);

        if (s == NATS_OK)
        {
            natsMutex_Lock(args.m);
            while (!args.done && s == NATS_OK)
                s = natsCondition_TimedWait(args.c, args.m, 2000);
            cbCalled = args.done;
            args.done = false;
            natsMutex_Unlock(args.m);
        }
        testCond((s == NATS_OK) && cbCalled);
    }

    test("Connection not closed: ");
    testCond((s == NATS_OK) && !natsConnection_IsClosed(conn));

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(conn);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&args);

    _stopServer(pid);
}

static void
_authViolationHandler(natsConnection *nc, natsSubscription *sub, natsStatus err, void *closure)
{
    struct threadArg *args      = (struct threadArg*) closure;
    const char       *lastError = NULL;

    // Technically, there is no guarantee that the connection's last error
    // be still the one that is given to this callback.
    if (err == NATS_CONNECTION_AUTH_FAILED)
    {
        bool ok = true;

        // So consider ok if currently not the same or if the same, the
        // error string is as expected.
        if (natsConnection_GetLastError(nc, &lastError) == NATS_CONNECTION_AUTH_FAILED)
            ok = (nats_strcasestr(nc->errStr, AUTHORIZATION_ERR) != NULL ? true : false);
        if (ok)
        {
            natsMutex_Lock(args->m);
            args->results[0]++;
            args->done = true;
            natsCondition_Broadcast(args->c);
            natsMutex_Unlock(args->m);
        }
    }
}

void test_AuthViolation(void)
{
    natsStatus          s = NATS_OK;
    natsSock            sock = NATS_SOCK_INVALID;
    natsThread          *t = NULL;
    struct threadArg    arg;
    natsSockCtx         ctx;

    memset(&ctx, 0, sizeof(natsSockCtx));

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&(arg.opts)));
    IFOK(s, natsOptions_SetAllowReconnect(arg.opts, false));
    IFOK(s, natsOptions_SetErrorHandler(arg.opts, _authViolationHandler, &arg));
    IFOK(s, natsOptions_SetClosedCB(arg.opts, _closedCb, &arg));
    if (s != NATS_OK)
        FAIL("@@ Unable to setup test!");

    arg.control = 7;
    arg.string  = AUTHORIZATION_ERR;

    test("Behavior of connection on Server Error: ")

    s = _startMockupServer(&sock, "localhost", "4222");

    // Start the thread that will try to connect to our server...
    IFOK(s, natsThread_Create(&t, _connectToMockupServer, (void*) &arg));

    if ((s == NATS_OK)
        && (((ctx.fd = accept(sock, NULL, NULL)) == NATS_SOCK_INVALID)
            || (natsSock_SetCommonTcpOptions(ctx.fd) != NATS_OK)))
    {
        s = NATS_SYS_ERROR;
    }
    if (s == NATS_OK)
    {
        char info[1024];

        strncpy(info,
                "INFO {\"server_id\":\"foobar\",\"version\":\"latest\",\"go\":\"latest\",\"host\":\"localhost\",\"port\":4222,\"auth_required\":false,\"tls_required\":false,\"max_payload\":1048576}\r\n",
                sizeof(info));

        // Send INFO.
        s = natsSock_WriteFully(&ctx, info, (int) strlen(info));
        if (s == NATS_OK)
        {
            char buffer[1024];

            memset(buffer, 0, sizeof(buffer));

            // Read connect and ping commands sent from the client
            s = natsSock_ReadLine(&ctx, buffer, sizeof(buffer));
            IFOK(s, natsSock_ReadLine(&ctx, buffer, sizeof(buffer)));
        }
        // Send PONG
        IFOK(s, natsSock_WriteFully(&ctx,
                                    _PONG_PROTO_, _PONG_PROTO_LEN_));

        if (s == NATS_OK)
        {
            // Wait a tiny, and simulate an error sent by the server
            nats_Sleep(50);

            snprintf(info, sizeof(info), "-ERR '%s'\r\n", arg.string);
            s = natsSock_WriteFully(&ctx, info, (int)strlen(info));
        }
    }
    if (s == NATS_OK)
    {
        // Wait for the client to process the async err
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.done)
            s = natsCondition_TimedWait(arg.c, arg.m, 5000);
        natsMutex_Unlock(arg.m);

        natsSock_Close(ctx.fd);
    }

    natsSock_Close(sock);

    // Wait for the client to finish.
    if (t != NULL)
    {
        natsThread_Join(t);
        natsThread_Destroy(t);
    }

    // Wait for closed CB
    if (s == NATS_OK)
    {
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.closed)
            s = natsCondition_TimedWait(arg.c, arg.m, 5000);
        if ((s == NATS_OK) && arg.reconnects != 0)
            s = NATS_ERR;
        natsMutex_Unlock(arg.m);
    }
    testCond(s == NATS_OK);

    _destroyDefaultThreadArgs(&arg);
}

static void
_startServerSendErrThread(void *closure)
{
    natsStatus          s = NATS_OK;
    natsSock            sock = NATS_SOCK_INVALID;
    struct threadArg    *arg = (struct threadArg*) closure;
    natsSockCtx         ctx;
    char                buffer[1024];
    int                 connect = 0;

    memset(&ctx, 0, sizeof(natsSockCtx));

    _startMockupServer(&sock, "localhost", "4222");

    for (connect=1; connect<4; connect++)
    {
        if (((ctx.fd = accept(sock, NULL, NULL)) == NATS_SOCK_INVALID)
                || (natsSock_SetCommonTcpOptions(ctx.fd) != NATS_OK))
        {
            s = NATS_SYS_ERROR;
        }
        if (s == NATS_OK)
        {
            const char info[] = "INFO {\"server_id\":\"22\",\"version\":\"latest\",\"go\":\"latest\",\"port\":4222,\"max_payload\":1048576}\r\n";

            natsMutex_Lock(arg->m);
            arg->control++;
            natsMutex_Unlock(arg->m);

            // Send INFO.
            s = natsSock_WriteFully(&ctx, info, (int) strlen(info));
            if (s == NATS_OK)
            {
                memset(buffer, 0, sizeof(buffer));

                // Read connect and ping commands sent from the client
                s = natsSock_ReadLine(&ctx, buffer, sizeof(buffer));
                IFOK(s, natsSock_ReadLine(&ctx, buffer, sizeof(buffer)));
            }
        }
        if ((s == NATS_OK) && connect == 1)
        {
            // Send PONG
            s = natsSock_WriteFully(&ctx, _PONG_PROTO_, _PONG_PROTO_LEN_);
            nats_Sleep(500);
            snprintf(buffer, sizeof(buffer), "-ERR '%s'\r\n", AUTHENTICATION_EXPIRED_ERR);
        }
        else if (s == NATS_OK)
        {
            snprintf(buffer, sizeof(buffer), "-ERR '%s'\r\n", AUTHORIZATION_ERR);
        }
        if (s == NATS_OK)
        {
            s = natsSock_WriteFully(&ctx, buffer, (int)strlen(buffer));
            nats_Sleep(200);
        }
        natsSock_Close(ctx.fd);
    }

    natsMutex_Lock(arg->m);
    while ((s != NATS_TIMEOUT) && !arg->disconnected)
        s = natsCondition_TimedWait(arg->c, arg->m, 5000);
    natsMutex_Unlock(arg->m);

    natsSock_Close(sock);
}

static void
_authExpiredHandler(natsConnection *nc, natsSubscription *sub, natsStatus err, void *closure)
{
    struct threadArg *args      = (struct threadArg*) closure;
    const char       *lastError = NULL;

    // Technically, there is no guarantee that the connection's last error
    // be still the one that is given to this callback.
    if (err == NATS_CONNECTION_AUTH_FAILED)
    {
        bool ok = true;

        natsMutex_Lock(args->m);

        // So consider ok if currently not the same or if the same, the
        // error string is as expected.
        if (natsConnection_GetLastError(nc, &lastError) == NATS_CONNECTION_AUTH_FAILED)
        {
            if (args->control == 1)
                ok = (nats_strcasestr(nc->errStr, AUTHENTICATION_EXPIRED_ERR) != NULL ? true : false);
            else
                ok = (nats_strcasestr(nc->errStr, AUTHORIZATION_ERR) != NULL ? true : false);
        }
        if (ok)
        {
            args->results[0]++;
            args->done = true;
            natsCondition_Broadcast(args->c);
        }

        natsMutex_Unlock(args->m);
    }
}

void test_AuthenticationExpired(void)
{
    natsStatus          s    = NATS_OK;
    natsConnection      *nc  = NULL;
    natsOptions         *opts= NULL;
    natsThread          *t   = NULL;
    struct threadArg    arg;
    natsSockCtx         ctx;
    int                 i;

    memset(&ctx, 0, sizeof(natsSockCtx));

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetMaxReconnect(opts, -1));
    IFOK(s, natsOptions_SetReconnectWait(opts, 25));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));
    IFOK(s, natsOptions_SetErrorHandler(opts, _authExpiredHandler, &arg));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, &arg));
    if (s != NATS_OK)
        FAIL("@@ Unable to setup test!");

    s = natsThread_Create(&t, _startServerSendErrThread, (void*) &arg);
    if (s != NATS_OK)
    {
        _destroyDefaultThreadArgs(&arg);
        FAIL("Unable to setup test");
    }

    test("Should connect ok: ");
    for (i=0; i<10; i++)
    {
        s = natsConnection_Connect(&nc, opts);
        if (s == NATS_OK)
            break;
        nats_Sleep(100);
    }
    testCond(s == NATS_OK);

    test("Should have been closed: ");
    s = _waitForConnClosed(&arg);
    testCond(s == NATS_OK);

    // First we would have gotten the async authentication expired error,
    // then 2 consecutive auth violations that causes the lib to stop
    // trying to reconnect.
    test("Should have posted 3 errors: ");
    if (s == NATS_OK)
    {
        natsMutex_Lock(arg.m);
        s = (((arg.results[0] == 3) && arg.done) ? NATS_OK : NATS_ERR);
        natsMutex_Unlock(arg.m);
    }
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    nc = NULL;

    natsMutex_Lock(arg.m);
    arg.disconnected = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    natsThread_Join(t);
    natsThread_Destroy(t);

    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&arg);
}

static void
_startServerSendErr2Thread(void *closure)
{
    natsStatus          s = NATS_OK;
    natsSock            sock = NATS_SOCK_INVALID;
    struct threadArg    *arg = (struct threadArg*) closure;
    natsSockCtx         ctx;
    char                buffer[1024];
    int                 connect = 0;
    bool                done = false;

    memset(&ctx, 0, sizeof(natsSockCtx));

    _startMockupServer(&sock, "localhost", "4222");

    for (connect=1; !done; connect++)
    {
        if (((ctx.fd = accept(sock, NULL, NULL)) == NATS_SOCK_INVALID)
                || (natsSock_SetCommonTcpOptions(ctx.fd) != NATS_OK))
        {
            s = NATS_SYS_ERROR;
        }
        if (s == NATS_OK)
        {
            const char info[] = "INFO {\"server_id\":\"22\",\"version\":\"latest\",\"go\":\"latest\",\"port\":4222,\"max_payload\":1048576}\r\n";

            natsMutex_Lock(arg->m);
            arg->control++;
            natsMutex_Unlock(arg->m);

            // Send INFO.
            s = natsSock_WriteFully(&ctx, info, (int) strlen(info));
            if (s == NATS_OK)
            {
                memset(buffer, 0, sizeof(buffer));

                // Read connect and ping commands sent from the client
                s = natsSock_ReadLine(&ctx, buffer, sizeof(buffer));
                IFOK(s, natsSock_ReadLine(&ctx, buffer, sizeof(buffer)));
            }
            if (s == NATS_OK)
            {
                // Send PONG
                s = natsSock_WriteFully(&ctx, _PONG_PROTO_, _PONG_PROTO_LEN_);
            }
        }
        if ((s == NATS_OK) && connect == 1)
        {
            nats_Sleep(500);
            snprintf(buffer, sizeof(buffer), "-ERR '%s'\r\n", AUTHENTICATION_EXPIRED_ERR);
            s = natsSock_WriteFully(&ctx, buffer, (int)strlen(buffer));
            nats_Sleep(200);
        }
        else if (s == NATS_OK)
        {
            natsMutex_Lock(arg->m);
            while ((s != NATS_TIMEOUT) && !arg->disconnected)
                s = natsCondition_TimedWait(arg->c, arg->m, 5000);
            natsMutex_Unlock(arg->m);
            done = true;
        }
        natsSock_Close(ctx.fd);
    }
    natsSock_Close(sock);
}

void test_AuthenticationExpiredReconnect(void)
{
    natsStatus          s    = NATS_OK;
    natsConnection      *nc  = NULL;
    natsOptions         *opts= NULL;
    natsThread          *t   = NULL;
    struct threadArg    arg;
    natsSockCtx         ctx;
    int                 i;
    const char          *lastErr = NULL;

    memset(&ctx, 0, sizeof(natsSockCtx));

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetMaxReconnect(opts, 2));
    IFOK(s, natsOptions_SetReconnectWait(opts, 25));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));
    IFOK(s, natsOptions_SetErrorHandler(opts, _authExpiredHandler, &arg));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, &arg));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, &arg));
    if (s != NATS_OK)
        FAIL("@@ Unable to setup test!");

    s = natsThread_Create(&t, _startServerSendErr2Thread, (void*) &arg);
    if (s != NATS_OK)
    {
        _destroyDefaultThreadArgs(&arg);
        FAIL("Unable to setup test");
    }

    test("Should connect ok: ");
    for (i=0; i<10; i++)
    {
        s = natsConnection_Connect(&nc, opts);
        if (s == NATS_OK)
            break;
        nats_Sleep(100);
    }
    testCond(s == NATS_OK);

    test("Should have posted 1 error: ");
    if (s == NATS_OK)
    {
        // Waiting for the err handler to fire
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.done && arg.results[0] != 1)
            s = natsCondition_TimedWait(arg.c, arg.m, 5000);
        natsMutex_Unlock(arg.m);
    }
    testCond(s == NATS_OK);

    test("Should have reconnected: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.reconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 5000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    // Wait a tiny bit and make sure connection is still connected
    nats_Sleep(100);
    test("Still connected: ");
    testCond(!natsConnection_IsClosed(nc));

    // Check last error is cleared
    test("Check last error cleared: ");
    s = natsConnection_GetLastError(nc, &lastErr);
    testCond((s == NATS_OK) && (lastErr[0] == '\0'));

    test("Close: ");
    natsConnection_Destroy(nc);
    s = _waitForConnClosed(&arg);
    testCond(s == NATS_OK);
    nc = NULL;

    natsMutex_Lock(arg.m);
    arg.disconnected = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    natsThread_Join(t);
    natsThread_Destroy(t);

    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&arg);
}

void test_ConnectedServer(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    char                buffer[128];

    buffer[0] = '\0';

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Verify ConnectedUrl is correct: ")
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_GetConnectedUrl(nc, buffer, sizeof(buffer)));
    testCond((s == NATS_OK)
             && (buffer[0] != '\0')
             && (strcmp(buffer, NATS_DEFAULT_URL) == 0));

    buffer[0] = '\0';

    test("Verify ConnectedServerId is not null: ")
    s = natsConnection_GetConnectedServerId(nc, buffer, sizeof(buffer));
    testCond((s == NATS_OK)  && (buffer[0] != '\0'));

    buffer[0] = '\0';

    test("Verify ConnectedUrl is empty after disconnect: ")
    natsConnection_Close(nc);
    s = natsConnection_GetConnectedUrl(nc, buffer, sizeof(buffer));
    testCond((s == NATS_OK) && (buffer[0] == '\0'));

    buffer[0] = '\0';

    test("Verify ConnectedServerId is empty after disconnect: ")
    s = natsConnection_GetConnectedServerId(nc, buffer, sizeof(buffer));
    testCond((s == NATS_OK)  && (buffer[0] == '\0'));

    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_MultipleClose(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsThread          *threads[10];
    natsPid             serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Test that multiple Close are fine: ")
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    for (int i=0; (s == NATS_OK) && (i<10); i++)
        s = natsThread_Create(&(threads[i]), _closeConn, (void*) nc);

    for (int i=0; (s == NATS_OK) && (i<10); i++)
    {
        natsThread_Join(threads[i]);
        natsThread_Destroy(threads[i]);
    }
    testCond((s == NATS_OK)
             && (nc->status == NATS_CONN_STATUS_CLOSED)
             && (nc->refs == 1));

    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_SimplePublish(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Test simple publish: ")
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_PublishString(nc, "foo", "Hello world!"));
    IFOK(s, natsConnection_Publish(nc, "foo", (const void*) "Hello world!",
                                   (int) strlen("Hello world!")));
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_SimplePublishNoData(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Test simple publish with no data: ")
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_PublishString(nc, "foo", NULL));
    IFOK(s, natsConnection_PublishString(nc, "foo", ""));
    IFOK(s, natsConnection_Publish(nc, "foo", NULL, 0));
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_PublishMsg(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
    {
        arg.string = "hello!";
        arg.status = NATS_OK;
    }
    if ( s != NATS_OK)
        FAIL("Unable to setup test!");

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Test simple publishMsg: ")
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, &arg));
    IFOK(s, natsConnection_Flush(nc));
    if (s == NATS_OK)
    {
        const char  data[] = {104, 101, 108, 108, 111, 33};
        natsMsg     *msg   = NULL;

        s = natsMsg_Create(&msg, "foo", NULL, data, sizeof(data));
        IFOK(s, natsConnection_PublishMsg(nc, msg));

        natsMsg_Destroy(msg);
    }
    IFOK(s, natsConnection_Flush(nc));

    if (s == NATS_OK)
    {
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.msgReceived)
            s = natsCondition_TimedWait(arg.c, arg.m, 1500);
        natsMutex_Unlock(arg.m);
    }
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);

    _destroyDefaultThreadArgs(&arg);
}

void test_InvalidSubsArgs(void)
{
    natsStatus          s;
    natsConnection      *nc = NULL;
    natsSubscription    *sub = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    // ASYNC Subscription

    test("Test async subscriber, invalid connection: ")
    s = natsConnection_Subscribe(&sub, NULL, "foo", _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async subscriber, invalid subject: ")
    s = natsConnection_Subscribe(&sub, nc, NULL, _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async subscriber, invalid subject (empty string): ")
    s = natsConnection_Subscribe(&sub, nc, "", _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async subscriber, invalid cb: ")
    s = natsConnection_Subscribe(&sub, nc, "foo", NULL, NULL);
    testCond(s != NATS_OK);

    test("Test async queue subscriber, invalid connection: ")
    s = natsConnection_QueueSubscribe(&sub, NULL, "foo", "group", _recvTestString, NULL);
    testCond(s != NATS_OK);

    // Async Subscription Timeout

    test("Test async subscriber timeout, invalid connection: ")
    s = natsConnection_SubscribeTimeout(&sub, NULL, "foo", 100, _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async subscriber timeout, invalid subject: ")
    s = natsConnection_SubscribeTimeout(&sub, nc, NULL, 100, _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async subscriber timeout, invalid subject (empty string): ")
    s = natsConnection_SubscribeTimeout(&sub, nc, "", 100, _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async subscriber timeout, invalid cb: ")
    s = natsConnection_SubscribeTimeout(&sub, nc, "foo", 100, NULL, NULL);
    testCond(s != NATS_OK);

    test("Test async subscriber timeout, invalid timeout (<0): ")
    s = natsConnection_SubscribeTimeout(&sub, nc, "foo", -100, _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async subscriber timeout, invalid timeout (0): ")
    s = natsConnection_SubscribeTimeout(&sub, nc, "foo", 0, _recvTestString, NULL);
    testCond(s != NATS_OK);

    // ASYNC Queue Subscription

    test("Test async queue subscriber timeout, invalid connection: ")
    s = natsConnection_QueueSubscribe(&sub, NULL, "foo", "group", _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async queue subscriber, invalid subject: ")
    s = natsConnection_QueueSubscribe(&sub, nc, NULL, "group", _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async queue subscriber, invalid subject (empty string): ")
    s = natsConnection_QueueSubscribe(&sub, nc, "", "group", _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async queue subscriber, invalid group name: ")
    s = natsConnection_QueueSubscribe(&sub, nc, "foo", NULL, _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async queue subscriber, invalid group name (empty): ")
    s = natsConnection_QueueSubscribe(&sub, nc, "foo", "", _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async queue subscriber, invalid cb: ")
    s = natsConnection_QueueSubscribe(&sub, nc, "foo", "group", NULL, NULL);
    testCond(s != NATS_OK);

    // ASYNC Queue Subscription Timeout

    test("Test async queue subscriber timeout, invalid connection: ")
    s = natsConnection_QueueSubscribeTimeout(&sub, NULL, "foo", "group", 100, _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async queue subscriber timeout, invalid subject: ")
    s = natsConnection_QueueSubscribeTimeout(&sub, nc, NULL, "group", 100, _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async queue subscriber timeout, invalid subject (empty string): ")
    s = natsConnection_QueueSubscribeTimeout(&sub, nc, "", "group", 100, _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async queue subscriber timeout, invalid group name: ")
    s = natsConnection_QueueSubscribeTimeout(&sub, nc, "foo", NULL, 100, _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async queue subscriber timeout, invalid group name (empty): ")
    s = natsConnection_QueueSubscribeTimeout(&sub, nc, "foo", "", 100, _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async queue subscriber timeout, invalid cb: ")
    s = natsConnection_QueueSubscribeTimeout(&sub, nc, "foo", "group", 100, NULL, NULL);
    testCond(s != NATS_OK);

    test("Test async queue subscriber timeout, invalid timeout (<0): ")
    s = natsConnection_QueueSubscribeTimeout(&sub, nc, "foo", "group", -100, _recvTestString, NULL);
    testCond(s != NATS_OK);

    test("Test async queue subscriber timeout, invalid timeout (0): ")
    s = natsConnection_QueueSubscribeTimeout(&sub, nc, "foo", "group", 0, _recvTestString, NULL);
    testCond(s != NATS_OK);

    // SYNC Subscription

    test("Test sync subscriber, invalid connection: ")
    s = natsConnection_SubscribeSync(&sub, NULL, "foo");
    testCond(s != NATS_OK);

    test("Test sync subscriber, invalid subject: ")
    s = natsConnection_SubscribeSync(&sub, nc, NULL);
    testCond(s != NATS_OK);

    test("Test sync subscriber, invalid subject (empty string): ")
    s = natsConnection_SubscribeSync(&sub, nc, "");
    testCond(s != NATS_OK);

    // SYNC Queue Subscription

    test("Test sync queue subscriber, invalid connection: ")
    s = natsConnection_QueueSubscribeSync(&sub, NULL, "foo", "group");
    testCond(s != NATS_OK);

    test("Test sync queue subscriber, invalid subject: ")
    s = natsConnection_QueueSubscribeSync(&sub, nc, NULL, "group");
    testCond(s != NATS_OK);

    test("Test sync queue subscriber, invalid subject (empty string): ")
    s = natsConnection_QueueSubscribeSync(&sub, nc, "", "group");
    testCond(s != NATS_OK);

    test("Test sync queue subscriber, invalid group name: ")
    s = natsConnection_QueueSubscribeSync(&sub, nc, "foo", NULL);
    testCond(s != NATS_OK);

    test("Test sync queue subscriber, invalid group name (empty): ")
    s = natsConnection_QueueSubscribeSync(&sub, nc, "foo", "");
    testCond(s != NATS_OK);

    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_AsyncSubscribe(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if ( s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.string = "Hello World";
    arg.status = NATS_OK;
    arg.control= 1;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Test async subscriber: ")
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString,
                                     (void*) &arg));
    IFOK(s, natsConnection_PublishString(nc, "foo", arg.string));

    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.msgReceived)
        s = natsCondition_TimedWait(arg.c, arg.m, 1500);
    natsMutex_Unlock(arg.m);

    IFOK(s, arg.status);
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

typedef struct __asyncTimeoutInfo
{
    struct threadArg    *arg;
    int64_t             timeout;
    int64_t             timeAfterFirstMsg;
    int64_t             timeSecondMsg;
    int64_t             timeFirstTimeout;
    int64_t             timeSecondTimeout;

} _asyncTimeoutInfo;

static void
_asyncTimeoutCb(natsConnection *nc, natsSubscription *sub, natsMsg *msg, void *closure)
{
   _asyncTimeoutInfo *ai = (_asyncTimeoutInfo*) closure;

   natsMutex_Lock(ai->arg->m);
   if (msg != NULL)
   {
       ai->arg->sum++;
       switch (ai->arg->sum)
       {
           case 1:
           {
               // Release lock for sleep...
               natsMutex_Unlock(ai->arg->m);

               // Sleep for 1.5x the timeout value
               nats_Sleep(ai->timeout+ai->timeout/2);

               natsMutex_Lock(ai->arg->m);
               ai->timeAfterFirstMsg = nats_Now();
               break;
           }
           case 2: ai->timeSecondMsg = nats_Now(); break;
           case 3:
           {
               ai->arg->done = true;
               natsSubscription_Destroy(sub);
               natsCondition_Signal(ai->arg->c);
               break;
           }
           default:
           {
               ai->arg->status = NATS_ERR;
               break;
           }
       }
       natsMsg_Destroy(msg);
   }
   else
   {
       ai->arg->timerFired++;
       switch (ai->arg->timerFired)
       {
           case 1:
           {
               ai->timeFirstTimeout = nats_Now();
               // Notify the main thread to send the second message
               // after waiting 1/2 of the timeout period.
               natsCondition_Signal(ai->arg->c);
               break;
           }
           case 2:
           {
               ai->timeSecondTimeout = nats_Now();
               // Signal that we timed-out for the 2nd time.
               ai->arg->timerStopped = 1;
               natsCondition_Signal(ai->arg->c);
               break;
           }
           default:
           {
               ai->arg->status = NATS_ERR;
               break;
           }
       }
    }
    natsMutex_Unlock(ai->arg->m);
}

void test_AsyncSubscribeTimeout(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    natsOptions         *opts     = NULL;
    struct threadArg    arg;
    bool                useLibDlv = false;
    int                 i;
    int64_t             timeout   = 100;
    _asyncTimeoutInfo   ai;

    for (i=0; i<4; i++)
    {
        memset(&ai, 0, sizeof(_asyncTimeoutInfo));
        memset(&arg, 0, sizeof(struct threadArg));

        s = natsOptions_Create(&opts);
        IFOK(s, natsOptions_UseGlobalMessageDelivery(opts, useLibDlv));
        IFOK(s, _createDefaultThreadArgsForCbTests(&arg));
        if ( s != NATS_OK)
            FAIL("Unable to setup test!");

        ai.arg = &arg;
        ai.timeout = timeout;
        arg.status = NATS_OK;

        serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
        CHECK_SERVER_STARTED(serverPid);

        testf("Test async %ssubscriber timeout%s: ",
                 ((i == 1 || i == 3) ? "queue " : ""),
                 (i > 1 ? " (lib msg delivery)" : ""));
        s = natsConnection_Connect(&nc, opts);
        if (s == NATS_OK)
        {
            if (i == 0 || i == 2)
                s = natsConnection_SubscribeTimeout(&sub, nc, "foo", timeout,
                                                    _asyncTimeoutCb, (void*) &ai);
            else
                s = natsConnection_QueueSubscribeTimeout(&sub, nc, "foo", "group",
                                                         timeout, _asyncTimeoutCb, (void*) &ai);
        }
        IFOK(s, natsConnection_PublishString(nc, "foo", "msg1"));

        // Wait to be notified that sub timed-out 2 times
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && (arg.timerFired != 1))
            s = natsCondition_TimedWait(arg.c, arg.m, 5000);
        natsMutex_Unlock(arg.m);

        // Wait half the timeout
        nats_Sleep(timeout/2);

        // Send the second message. This should reset the timeout timer.
        IFOK(s, natsConnection_PublishString(nc, "foo", "msg2"));
        IFOK(s, natsConnection_Flush(nc));

        // Wait for 2nd timeout
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && (arg.timerStopped == 0))
            s = natsCondition_TimedWait(arg.c, arg.m, 5000);
        natsMutex_Unlock(arg.m);

        // Send two more messages, only one should be received since the
        // subscription will be unsubscribed/closed when receiving the
        // first.
        IFOK(s, natsConnection_PublishString(nc, "foo", "msg3"));
        IFOK(s, natsConnection_PublishString(nc, "foo", "msg4"));
        IFOK(s, natsConnection_Flush(nc));

        // Wait for end of test
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.done)
            s = natsCondition_TimedWait(arg.c, arg.m, 5000);
        natsMutex_Unlock(arg.m);

        // Wait more than the timeout time to see if extra timeout callbacks
        // incorrectly fire
        nats_Sleep(timeout+timeout/2);

        // Check for success
        natsMutex_Lock(arg.m);
        testCond((s == NATS_OK) && (arg.status == NATS_OK)
                    && (arg.sum == 3) && (arg.timerFired == 2)
                    && (ai.timeFirstTimeout >= ai.timeAfterFirstMsg + timeout - 50)
                    && (ai.timeFirstTimeout <= ai.timeAfterFirstMsg + timeout + 50)
                    && (ai.timeSecondTimeout >= ai.timeSecondMsg + timeout - 50)
                    && (ai.timeSecondTimeout <= ai.timeSecondMsg + timeout + 50))
        natsMutex_Unlock(arg.m);

        natsConnection_Destroy(nc);
        natsOptions_Destroy(opts);

        _destroyDefaultThreadArgs(&arg);

        _stopServer(serverPid);

        if (i >= 1)
            useLibDlv = true;
    }
}

void test_SyncSubscribe(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsMsg             *msg      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    const char          *string   = "Hello World";

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Test sync subscriber: ")
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));
    IFOK(s, natsConnection_PublishString(nc, "foo", string));
    IFOK(s, natsSubscription_NextMsg(&msg, sub, 1000));
    testCond((s == NATS_OK)
             && (msg != NULL)
             && (strncmp(string, natsMsg_GetData(msg), natsMsg_GetDataLength(msg)) == 0));

    natsMsg_Destroy(msg);
    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_PubSubWithReply(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsMsg             *msg      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    const char          *string   = "Hello World";

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Test PubSub with reply: ")
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));
    IFOK(s, natsConnection_PublishRequestString(nc, "foo", "bar", string));
    IFOK(s, natsSubscription_NextMsg(&msg, sub, 1000));
    testCond((s == NATS_OK)
             && (msg != NULL)
             && (strncmp(string, natsMsg_GetData(msg), natsMsg_GetDataLength(msg)) == 0)
             && (strncmp(natsMsg_GetReply(msg), "bar", 3) == 0));

    natsMsg_Destroy(msg);
    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_NoResponders(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsMsg             *msg      = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    const char          *string   = "Hello World";
    struct threadArg    arg;

    if (!serverVersionAtLeast(2,2,0))
    {
        char txt[200];

        snprintf(txt, sizeof(txt), "Skipping since requires server version of at least 2.2.0, got %s: ", serverVersion);
        test(txt);
        testCond(true);
        return;
    }

    s = _createDefaultThreadArgsForCbTests(&arg);
    if ( s != NATS_OK)
        FAIL("Unable to setup test!");

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("No responders on NextMsg: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));
    IFOK(s, natsConnection_PublishRequestString(nc, "bar", "foo", string));
    IFOK(s, natsSubscription_NextMsg(&msg, sub, 1000));
    testCond(NATS_NO_RESPONDERS);

    natsMsg_Destroy(msg);
    natsSubscription_Destroy(sub);
    sub = NULL;

    arg.status  = NATS_ERR;
    arg.control = 10;

    test("No responders in callback: ");
    s = natsConnection_Subscribe(&sub, nc, "bar", _recvTestString, (void*)&arg);
    IFOK(s, natsConnection_PublishRequestString(nc, "foo", "bar", string));
    if (s == NATS_OK)
    {
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.msgReceived)
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        if (s == NATS_OK)
            s = arg.status;
        natsMutex_Unlock(arg.m);
    }
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    sub = NULL;
    natsConnection_Destroy(nc);
    nc = NULL;

    test("Disable no responders: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_DisableNoResponders(opts, true));
    IFOK(s, natsConnection_Connect(&nc, opts));
    IFOK(s, natsConnection_RequestString(&msg, nc, "foo", string, 500));
    testCond((s == NATS_TIMEOUT) && (msg == NULL));

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

struct flushArg
{
    natsConnection      *nc;
    natsStatus          s;
    int                 count;
    int64_t             timeout;
    int64_t             initialSleep;
    int64_t             loopSleep;
};

static void
_doFlush(void *arg)
{
    struct flushArg     *p = (struct flushArg*) arg;
    int                 i;

    nats_Sleep(p->initialSleep);

    for (i = 0; (p->s == NATS_OK) && (i < p->count); i++)
    {
        p->s = natsConnection_FlushTimeout(p->nc, p->timeout);
        if ((p->s == NATS_OK) && (p->loopSleep > 0))
            nats_Sleep(p->loopSleep);
    }
}

void test_Flush(void)
{
    natsStatus          s;
    natsOptions         *opts     = NULL;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    const char          *string   = "Hello World";
    natsThread          *threads[3] = { NULL, NULL, NULL };
    struct flushArg     args[3];
    int64_t             start = 0;
    int64_t             elapsed = 0;

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:4222"));
    IFOK(s, natsOptions_SetReconnectWait(opts, 100));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));
    IFOK(s, natsOptions_SetPingInterval(opts, 100));

    if (s != NATS_OK)
        FAIL("Unable to setup test");

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Test Flush empties buffer: ")
    s = natsConnection_Connect(&nc, opts);
    for (int i=0; (s == NATS_OK) && (i < 1000); i++)
        s = natsConnection_PublishString(nc, "flush", string);
    IFOK(s, natsConnection_Flush(nc));
    testCond((s == NATS_OK)
             && natsConnection_Buffered(nc) == 0);

    test("Check parallel Flush: ")
    for (int i=0; (s == NATS_OK) && (i < 3); i++)
    {
        args[i].nc           = nc;
        args[i].s            = NATS_OK;
        args[i].timeout      = 5000;
#ifdef _WIN32
        args[i].count        = 100;
#else
        args[i].count        = 1000;
#endif
        args[i].initialSleep = 500;
        args[i].loopSleep    = 1;
        s = natsThread_Create(&(threads[i]), _doFlush, (void*) &(args[i]));
    }

    for (int i=0; (s == NATS_OK) && (i < 10000); i++)
        s = natsConnection_PublishString(nc, "flush", "Hello world");

    for (int i=0; (i < 3); i++)
    {
        if (threads[i] == NULL)
            continue;

        natsThread_Join(threads[i]);
        natsThread_Destroy(threads[i]);

        if (args[i].s != NATS_OK)
            s = args[i].s;
    }
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    nc = NULL;

    test("Check Flush while in doReconnect: ")
    s = natsOptions_SetReconnectWait(opts, 3000);
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));
    IFOK(s, natsConnection_Connect(&nc, opts));
    if (s == NATS_OK)
    {
        // Capture the moment we connected
        start = nats_Now();

        // Stop the server
        _stopServer(serverPid);
        serverPid = NATS_INVALID_PID;

        // We can restart right away, since the client library will wait 3 sec
        // before reconnecting.
        serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
        CHECK_SERVER_STARTED(serverPid);

        // Attempt to Flush. This should wait for the reconnect to occur, then
        // proceed.
        for (int i=0; (s == NATS_OK) && (i < 3); i++)
        {
            args[i].nc           = nc;
            args[i].s            = NATS_OK;
            args[i].timeout      = 5000;
            args[i].count        = 1;
            args[i].initialSleep = 1000;
            args[i].loopSleep    = 0;
            s = natsThread_Create(&(threads[i]), _doFlush, (void*) &(args[i]));
        }
    }
    for (int i=0; (i < 3); i++)
    {
        if (threads[i] == NULL)
            continue;

        natsThread_Join(threads[i]);
        natsThread_Destroy(threads[i]);

        if ((s == NATS_OK) && (args[i].s != NATS_OK))
        {
            s = args[i].s;
            printf("t=%d s=%u\n", i, s);
        }
    }
    if (s == NATS_OK)
        elapsed = (nats_Now() - start);

    testCond((s == NATS_OK) && (elapsed >= 2500) && (elapsed <= 3200));

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_ConnCloseDoesFlush(void)
{
    natsStatus          s    = NATS_OK;
    natsPid             pid  = NATS_INVALID_PID;
    natsConnection      *nc1 = NULL;
    natsConnection      *nc2 = NULL;
    natsSubscription    *sub = NULL;
    int                 tc   = 100000;
    int                 i, iter;

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    if (valgrind)
        tc = 1000;

    test("Connection close flushes: ");
    for (iter=0; (s == NATS_OK) && (iter<10); iter++)
    {
        s = natsConnection_ConnectTo(&nc1, NATS_DEFAULT_URL);
        IFOK(s, natsConnection_SubscribeSync(&sub, nc1, "foo"));
        IFOK(s, natsSubscription_SetPendingLimits(sub, -1, -1));
        IFOK(s, natsConnection_Flush(nc1));

        IFOK(s, natsConnection_ConnectTo(&nc2, NATS_DEFAULT_URL));

        for (i=0; (s == NATS_OK) && (i<tc); i++)
            s = natsConnection_PublishString(nc2, "foo", "hello");
        if (s == NATS_OK)
            natsConnection_Close(nc2);

        for (i=0; (s == NATS_OK) && (i<tc); i++)
        {
            natsMsg *msg = NULL;
            s = natsSubscription_NextMsg(&msg, sub, 1000);
            natsMsg_Destroy(msg);
        }

        natsConnection_Destroy(nc2);
        nc2 = NULL;
        natsSubscription_Destroy(sub);
        sub = NULL;
        natsConnection_Destroy(nc1);
        nc1 = NULL;
    }
    testCond(s == NATS_OK);

    _stopServer(pid);
}

void test_QueueSubscriber(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *s1       = NULL;
    natsSubscription    *s2       = NULL;
    natsMsg             *msg      = NULL;
    uint64_t            r1        = 0;
    uint64_t            r2        = 0;
    float               v         = 1000.0 * 0.15;
    int64_t             d1;
    int64_t             d2;
    natsPid             serverPid = NATS_INVALID_PID;
    const char          *string   = "Hello World";

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Test QueueSubscriber receive correct amount: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_QueueSubscribeSync(&s1, nc, "foo", "bar"));
    IFOK(s, natsConnection_QueueSubscribeSync(&s2, nc, "foo", "bar"));
    IFOK(s, natsConnection_PublishString(nc, "foo", string));
    IFOK(s, natsConnection_Flush(nc));
    IFOK(s, natsSubscription_QueuedMsgs(s1, &r1));
    IFOK(s, natsSubscription_QueuedMsgs(s2, &r2));
    testCond((s == NATS_OK)
             && (r1 + r2 == 1));

    (void)natsSubscription_NextMsg(&msg, s1, 0);
    natsMsg_Destroy(msg);
    msg = NULL;

    (void)natsSubscription_NextMsg(&msg, s2, 0);
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Test correct amount when more messages are sent: ");
    for (int i=0; (s == NATS_OK) && (i<1000); i++)
        s = natsConnection_PublishString(nc, "foo", string);

    IFOK(s, natsConnection_Flush(nc));

    r1 = r2 = 0;

    IFOK(s, natsSubscription_QueuedMsgs(s1, &r1));
    IFOK(s, natsSubscription_QueuedMsgs(s2, &r2));

    testCond((s == NATS_OK) && (r1 + r2 == 1000));

    test("Variance acceptable: ");
    d1 = 500 - r1;
    if (d1 < 0)
        d1 *= -1;

    d2 = 500 - r1;
    if (d2 < 0)
        d2 *= -1;

    testCond((d1 <= v) && (d2 <= v));

    natsSubscription_Destroy(s1);
    natsSubscription_Destroy(s2);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_ReplyArg(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if ( s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.string = "bar";
    arg.status = NATS_OK;
    arg.control= 2;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Test for correct Reply arg in callback: ")
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString,
                                     (void*) &arg));
    IFOK(s, natsConnection_PublishRequestString(nc, "foo", "bar", "hello"));

    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.msgReceived)
        s = natsCondition_TimedWait(arg.c, arg.m, 1500);
    natsMutex_Unlock(arg.m);

    IFOK(s, arg.status);

    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_SyncReplyArg(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsMsg             *msg      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Test for correct Reply arg in msg: ")
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));
    IFOK(s, natsConnection_PublishRequestString(nc, "foo", "bar", "hello"));
    IFOK(s, natsSubscription_NextMsg(&msg, sub, 1000));

    testCond((s == NATS_OK)
             && (msg != NULL)
             && (natsMsg_GetReply(msg) != NULL)
             && (strcmp("bar", natsMsg_GetReply(msg)) == 0));

    natsMsg_Destroy(msg);
    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_Unsubscribe(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if ( s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.string = "bar";
    arg.status = NATS_OK;
    arg.control= 3;
    arg.sum    = 0;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect and create sub: ")
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg));
    testCond(s == NATS_OK);

    test("Send messages and flush: ");
    for (int i=0; (s == NATS_OK) && (i<20); i++)
        s = natsConnection_PublishString(nc, "foo", "hello");
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Unsubscribe from callback: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.msgReceived)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    IFOK(s, arg.status);
    testCond(s == NATS_OK);

    test("No more callback: ");
    nats_Sleep(250);
    natsMutex_Lock(arg.m);
    testCond((s == NATS_OK) && (arg.sum == 10));
    natsMutex_Unlock(arg.m);

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create new sub: ");
    s = natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg);
    testCond(s == NATS_OK);

    test("Unsubscribe after connection close: ");
    natsConnection_Destroy(nc);
    s = natsSubscription_Unsubscribe(sub);
    testCond(s == NATS_CONNECTION_CLOSED);

    natsSubscription_Destroy(sub);
    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_DoubleUnsubscribe(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect and create subscription: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));
    testCond(s == NATS_OK);

    test("Unsubscribe: ");
    s = natsSubscription_Unsubscribe(sub);
    testCond(s == NATS_OK);

    test("Double Unsubscribe: ");
    s = natsSubscription_Unsubscribe(sub);
    testCond(s == NATS_INVALID_SUBSCRIPTION);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_SubRemovedWhileProcessingMsg(void)
{
    natsStatus s;
    natsConnection *nc = NULL;
    natsOptions *opts = NULL;
    natsSubscription *sub = NULL;
    natsPid serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect and create sub: ")
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));
    testCond(s == NATS_OK);

    // Lock the sub so that we can remove while the connection
    // readLoop is trying to push to the sub.
    natsSub_Lock(sub);

    test("Send message: ");
    s = natsConnection_PublishString(nc, "foo", "hello");
    testCond(s == NATS_OK);

    test("Close sub: ");
    natsSub_Unlock(sub);
    natsSub_close(sub, false);
    testCond(s == NATS_OK);

    test("Check msg not given: ");
    natsSub_Lock(sub);
    testCond(sub->ownDispatcher.queue.msgs == 0);
    natsSub_Unlock(sub);

    natsSubscription_Destroy(sub);
    sub = NULL;
    natsConnection_Destroy(nc);
    nc = NULL;

    // Repeat with global message delivery option.
    test("Set global delivery option: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_UseGlobalMessageDelivery(opts, true));
    testCond(s == NATS_OK);

    test("Connect and create sub: ");
    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _dummyMsgHandler, NULL));
    testCond(s == NATS_OK);

    nats_lockSubAndDispatcher(sub);
    test("Send message: ");
    s = natsConnection_PublishString(nc, "foo", "hello");
    testCond(s == NATS_OK);

    test("Close sub: ");
    nats_unlockSubAndDispatcher(sub);
    natsSub_close(sub, false);
    testCond(s == NATS_OK);

    test("Check msg not given: ");
    nats_lockSubAndDispatcher(sub);
    testCond(sub->ownDispatcher.queue.msgs == 0);
    nats_unlockSubAndDispatcher(sub);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _stopServer(serverPid);
}

void test_RequestTimeout(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsMsg             *msg      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Test Request should timeout: ")
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_RequestString(&msg, nc, "foo", "bar", 500));
    testCond(serverVersionAtLeast(2, 2, 0) ? (s == NATS_NO_RESPONDERS) : (s == NATS_TIMEOUT));

    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_Request(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsMsg             *msg      = NULL;
    natsMsg             *req      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;
    int                 i;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if ( s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.string = "I will help you";
    arg.status = NATS_OK;
    arg.control= 4;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect and subscribe: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg));
    testCond(s == NATS_OK);

    test("Test Request: ")
    s = natsConnection_RequestString(&msg, nc, "foo", "help", 500);

    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.msgReceived)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    IFOK(s, arg.status);
    natsMutex_Unlock(arg.m);

    testCond((s == NATS_OK)
             && (msg != NULL)
             && (strncmp(arg.string,
                         natsMsg_GetData(msg),
                         natsMsg_GetDataLength(msg)) == 0));

    natsMsg_Destroy(msg);
    msg = NULL;

    test("Create req message: ");
    s = natsMsg_Create(&req, "foo", NULL, "help", 4);
    testCond(s == NATS_OK);

    test("Test RequestMsg: ");
    s = natsConnection_RequestMsg(&msg, nc, req, 500);

    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.msgReceived)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    IFOK(s, arg.status);
    natsMutex_Unlock(arg.m);

    testCond((s == NATS_OK)
             && (msg != NULL)
             && (strncmp(arg.string,
                         natsMsg_GetData(msg),
                         natsMsg_GetDataLength(msg)) == 0));

    natsMsg_Destroy(msg);
    msg = NULL;
    natsMsg_Destroy(req);
    req = NULL;

    natsMutex_Lock(arg.m);
    arg.control = 11;
    natsMutex_Unlock(arg.m);

    test("Race on timeout: ");
    for (i=0; (s == NATS_OK) && (i<100); i++)
    {
        s = natsConnection_Request(&msg, nc, "foo", "help!", 5, 1);
        // Make sure that we get either OK with msg != NULL
        // or TIMEOUT but with msg == NULL
        if (s == NATS_OK)
        {
            if (msg == NULL)
                s = NATS_ERR;
            else
            {
                natsMsg_Destroy(msg);
                msg = NULL;
            }
        }
        else if ((s == NATS_TIMEOUT) && (msg == NULL))
        {
            s = NATS_OK;
            nats_clearLastError();
        }
        // else if timeout and msg != NULL, that is a bug!
    }
    testCond(s == NATS_OK);

    // Ensure the last callback returns to avoid accessing data that has been freed.
    natsMutex_Lock(arg.m);
    s = NATS_OK;
    while ((s != NATS_TIMEOUT) && (arg.sum != 100))
        natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_RequestNoBody(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsMsg             *msg      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if ( s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.string = "I will help you";
    arg.status = NATS_OK;
    arg.control= 4;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect and subscribe: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg));
    testCond(s == NATS_OK);

    test("Test Request with no body content: ")
    s = natsConnection_RequestString(&msg, nc, "foo", NULL, 500);

    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT)
           && !arg.msgReceived)
    {
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    }
    natsMutex_Unlock(arg.m);

    IFOK(s, arg.status);

    testCond((s == NATS_OK)
             && (msg != NULL)
             && (strncmp(arg.string,
                         natsMsg_GetData(msg),
                         natsMsg_GetDataLength(msg)) == 0));

    natsMsg_Destroy(msg);
    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

static void
_serverForMuxWithMappedSubject(void *closure)
{
    natsStatus          s = NATS_OK;
    natsSock            sock = NATS_SOCK_INVALID;
    struct threadArg    *arg = (struct threadArg*) closure;
    natsSockCtx         ctx;

    memset(&ctx, 0, sizeof(natsSockCtx));

    s = _startMockupServer(&sock, "127.0.0.1", "4222");
    natsMutex_Lock(arg->m);
    arg->status = s;
    natsCondition_Signal(arg->c);
    natsMutex_Unlock(arg->m);

    if (((ctx.fd = accept(sock, NULL, NULL)) == NATS_SOCK_INVALID)
            || (natsSock_SetCommonTcpOptions(ctx.fd) != NATS_OK))
    {
        s = NATS_SYS_ERROR;
    }
    if (s == NATS_OK)
    {
        char info[1024];

        snprintf(info, sizeof(info), "%s", "INFO {\"server_id\":\"22\",\"version\":\"latest\",\"go\":\"latest\",\"port\":4222,\"max_payload\":1048576}\r\n");

        // Send INFO.
        s = natsSock_WriteFully(&ctx, info, (int) strlen(info));
    }
    if (s == NATS_OK)
    {
        char buffer[1024];

        memset(buffer, 0, sizeof(buffer));

        // Read connect and ping commands sent from the client
        s = natsSock_ReadLine(&ctx, buffer, sizeof(buffer));
        IFOK(s, natsSock_ReadLine(&ctx, buffer, sizeof(buffer)));

        // Send PONG
        IFOK(s, natsSock_WriteFully(&ctx, _PONG_PROTO_, _PONG_PROTO_LEN_));

        // Now wait for the SUB proto and the Request
        IFOK(s, natsSock_ReadLine(&ctx, buffer, sizeof(buffer)));
        IFOK(s, natsSock_ReadLine(&ctx, buffer, sizeof(buffer)));

        // Send the reply on a different subject
        IFOK(s, natsSock_WriteFully(&ctx, "MSG bar 1 2\r\nok\r\n", 17));
        if (s == NATS_OK)
        {
            // Wait for client to tell us it is done
            natsMutex_Lock(arg->m);
            while ((s != NATS_TIMEOUT) && !(arg->done))
                s = natsCondition_TimedWait(arg->c, arg->m, 10000);
            natsMutex_Unlock(arg->m);
        }
        natsSock_Close(ctx.fd);
    }
    natsSock_Close(sock);
}

void test_RequestMuxWithMappedSubject(void)
{
    natsStatus          s;
    natsConnection      *nc  = NULL;
    natsMsg             *msg = NULL;
    natsThread          *t   = NULL;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if ( s != NATS_OK)
        FAIL("Unable to setup test!");

    test("Start server: ");
    arg.status = NATS_ERR;
    s = natsThread_Create(&t, _serverForMuxWithMappedSubject, (void*) &arg);
    if (s == NATS_OK)
    {
        // Wait for server to be ready
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && (arg.status != NATS_OK))
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        s = arg.status;
        natsMutex_Unlock(arg.m);
    }
    testCond(s == NATS_OK);

    test("Connect: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    testCond(s == NATS_OK);

    test("Request: ");
    s = natsConnection_RequestString(&msg, nc, "foo", "help", 1000);
    testCond(s == NATS_OK);

    natsMsg_Destroy(msg);
    natsConnection_Destroy(nc);

    // Notify mock server we are done
    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    natsThread_Join(t);
    natsThread_Destroy(t);

    _destroyDefaultThreadArgs(&arg);
}

void test_OldRequest(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsSubscription    *sub      = NULL;
    natsMsg             *msg      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if ( s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.string = "I will help you";
    arg.status = NATS_OK;
    arg.control= 4;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Setup: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_UseOldRequestStyle(opts, true));
    IFOK(s, natsConnection_Connect(&nc, opts));
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg));
    testCond(s == NATS_OK);

    test("Test Old Request Style: ")
    s = natsConnection_RequestString(&msg, nc, "foo", "help", 500);

    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT)
           && !arg.msgReceived)
    {
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    }
    natsMutex_Unlock(arg.m);

    IFOK(s, arg.status);

    testCond((s == NATS_OK)
             && (msg != NULL)
             && (strncmp(arg.string,
                         natsMsg_GetData(msg),
                         natsMsg_GetDataLength(msg)) == 0));

    natsMsg_Destroy(msg);
    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

static void
_sendRequest(void *closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;
    natsStatus          s;
    natsMsg             *msg = NULL;

    nats_Sleep(250);

    s = natsConnection_RequestString(&msg, arg->nc, "foo", "Help!", 2000);
    natsMutex_Lock(arg->m);
    if ((s == NATS_OK)
            && (msg != NULL)
            && strncmp(arg->string,
                       natsMsg_GetData(msg),
                       natsMsg_GetDataLength(msg)) == 0)
    {
        arg->sum++;
    }
    else
    {
        arg->status = NATS_ERR;
    }
    natsMutex_Unlock(arg->m);
    natsMsg_Destroy(msg);
}

void test_SimultaneousRequest(void)
{
    natsStatus          s;
     natsConnection      *nc       = NULL;
     natsSubscription    *sub      = NULL;
     natsThread          *threads[10];
     natsPid             serverPid = NATS_INVALID_PID;
     struct threadArg    arg;
     int                 i;

     s = _createDefaultThreadArgsForCbTests(&arg);
     if ( s != NATS_OK)
         FAIL("Unable to setup test!");

     arg.string = "ok";
     arg.status = NATS_OK;
     arg.control= 4;

     serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
     CHECK_SERVER_STARTED(serverPid);

     s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
     if (s == NATS_OK)
     {
         arg.nc = nc;
         s = natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg);
     }

     for (i=0; i<10; i++)
         threads[i] = NULL;

     test("Test simultaneous requests: ")
     for (i=0; (s == NATS_OK) && (i<10); i++)
         s = natsThread_Create(&(threads[i]), _sendRequest, (void*) &arg);

     for (i=0; i<10; i++)
     {
         if (threads[i] != NULL)
         {
             natsThread_Join(threads[i]);
             natsThread_Destroy(threads[i]);
         }
     }

     natsMutex_Lock(arg.m);
     if ((s != NATS_OK)
             || ((s = arg.status) != NATS_OK)
             || (arg.sum != 10))
     {
         s = NATS_ERR;
     }
     natsMutex_Unlock(arg.m);

     testCond(s == NATS_OK);

     natsSubscription_Destroy(sub);
     natsConnection_Destroy(nc);

     _destroyDefaultThreadArgs(&arg);

     _stopServer(serverPid);
}

void test_RequestClose(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsMsg             *msg      = NULL;
    natsThread          *t        = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    // Because of no responders, we would get an immediate timeout.
    // So we need to create a sync subscriber that is simply not
    // going to send a reply back.

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    test("Test Request is kicked out with a connection close: ")
    IFOK(s, natsThread_Create(&t, _closeConnWithDelay, (void*) nc));
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));
    IFOK(s, natsConnection_RequestString(&msg, nc, "foo", "help", 2000));

    if (t != NULL)
    {
        natsThread_Join(t);
        natsThread_Destroy(t);
    }
    testCond((s == NATS_CONNECTION_CLOSED) && (msg == NULL));

    natsMsg_Destroy(msg);
    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}


void test_CustomInbox(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsSubscription    *sub1     = NULL;
    natsSubscription    *sub2     = NULL;
    natsMsg             *msg      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    const char          *badPfx[] = {"bad prefix", "bad\tprefix", "bad..prefix", "bad.*.prefix",
                                     "bad.>.prefix", "bad.prefix.*", "bad.prefix.>",
                                     "bad.prefix.", "bad.prefix.."};
    struct threadArg    arg;
    int                 i;
    int                 mode;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Create options: ");
    s = natsOptions_Create(&opts);
    testCond(s == NATS_OK);

    for (i = 0; i<(int)(sizeof(badPfx)/sizeof(char*)); i++)
    {
        char tmp[128];

        snprintf(tmp, sizeof(tmp), "Bad prefix '%s': ", badPfx[i]);
        test(tmp);
        s = natsOptions_SetCustomInboxPrefix(opts, badPfx[i]);
        testCond((s == NATS_INVALID_ARG) && (opts->inboxPfx == NULL));
        nats_clearLastError();
    }

    test("Good prefix: ");
    s = natsOptions_SetCustomInboxPrefix(opts, "my.prefix");
    testCond((s == NATS_OK) && (opts->inboxPfx != NULL)
                && (strcmp(opts->inboxPfx, "my.prefix.") == 0));

    arg.string = "I will help you";
    arg.control= 4;

    for (mode=0; mode<2; mode++)
    {
        test("Set old request style: ");
        s = natsOptions_UseOldRequestStyle(opts, true);
        testCond(s == NATS_OK);

        test("Connect and setup sub: ");
        s = natsConnection_Connect(&nc, opts);
        IFOK(s, natsConnection_SubscribeSync(&sub1, nc, "my.prefix.>"));
        IFOK(s, natsConnection_Subscribe(&sub2, nc, "foo", _recvTestString, (void*) &arg));
        testCond(s == NATS_OK);

        test("Send request: ");
        s = natsConnection_RequestString(&msg, nc, "foo", "help", 1000);
        testCond((s == NATS_OK) && (msg != NULL));
        natsMsg_Destroy(msg);
        msg = NULL;

        test("Check custom inbox: ");
        s = natsSubscription_NextMsg(&msg, sub1, 500);
        testCond((s == NATS_OK) && (msg != NULL)
                    && (strstr(natsMsg_GetSubject(msg), "my.prefix.") == natsMsg_GetSubject(msg)));
        natsMsg_Destroy(msg);
        msg = NULL;

        natsSubscription_Destroy(sub1);
        sub1 = NULL;
        natsSubscription_Destroy(sub2);
        sub2 = NULL;
        natsConnection_Destroy(nc);
        nc = NULL;
    }

    for (mode = 0; mode < 2; mode++)
    {
        test("Set option: ");
        s = natsOptions_SetCustomInboxPrefix(opts, (mode == 0 ? NULL : "my.prefix"));
        testCond(s == NATS_OK);

        test("Connect: ");
        s = natsConnection_Connect(&nc, opts);
        testCond(s == NATS_OK);

        test("Inbox init: ");
        {
            char inboxBuf[NATS_DEFAULT_INBOX_PRE_LEN+NUID_BUFFER_LEN+1];
            char *inbox = NULL;
            bool allocated = false;

            s = natsConn_initInbox(nc, inboxBuf, sizeof(inboxBuf), &inbox, &allocated);
            if (s == NATS_OK)
            {
                if (mode == 0)
                {
                    if (allocated || (inbox != inboxBuf) ||
                            (strstr(inbox, NATS_DEFAULT_INBOX_PRE) != inbox))
                    {
                        s = NATS_ERR;
                    }
                }
                else
                {
                    // Since the custom prefix "my.prefix." is more than "_INBOX.",
                    // init should have allocated memory
                    if (!allocated || (inbox == inboxBuf) ||
                            (strstr(inbox, "my.prefix.") != inbox))
                    {
                        s = NATS_ERR;
                    }
                    else if (allocated)
                        free(inbox);
                }
            }
        }
        testCond(s == NATS_OK);

        natsConnection_Destroy(nc);
        nc = NULL;
    }

    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);
    _stopServer(serverPid);
}

void test_MessageBufferPadding(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsSubscription    *sub      = NULL;
    natsMsg             *msg      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    const char          *string   = "Hello World";
    const char          *servers[] = { "nats://127.0.0.1:4222" };
    int                 serversCount = 1;
    int                 paddingSize = 32;
    bool                paddingIsZeros = true;

    serverPid = _startServer(servers[0], NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Create options: ");
    s = natsOptions_Create(&opts);
    testCond(s == NATS_OK);

    test("Setting message buffer padding: ");
    s = natsOptions_SetMessageBufferPadding(opts, paddingSize);
    testCond(s == NATS_OK);

    test("Setting servers: ");
    s = natsOptions_SetServers(opts, servers, serversCount);
    testCond(s == NATS_OK);

    test("Test generating message for subscriber: ")
    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));
    IFOK(s, natsConnection_PublishString(nc, "foo", string));
    IFOK(s, natsSubscription_NextMsg(&msg, sub, 1000));
    testCond((s == NATS_OK)
             && (msg != NULL)
             && (strncmp(string, natsMsg_GetData(msg), natsMsg_GetDataLength(msg)) == 0));

    test("Test access to memory in message buffer beyond data length: ");
    // This test can pass even if padding doesn't work as excepted.
    // But valgrind will show access to unallocated memory
    for (int i=natsMsg_GetDataLength(msg); i<natsMsg_GetDataLength(msg)+paddingSize; i++) {
        if (natsMsg_GetData(msg)[i])
            paddingIsZeros = false;
    }

    testCond(paddingIsZeros);

    natsMsg_Destroy(msg);
    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _stopServer(serverPid);
}

void test_FlushInCb(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if ( s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.status = NATS_OK;
    arg.control= 5;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Test Flush in callback: ")
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg));
    IFOK(s, natsConnection_PublishString(nc, "foo", "hello"));

    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT)
           && !arg.msgReceived)
    {
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    }
    natsMutex_Unlock(arg.m);

    IFOK(s, arg.status);

    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_ReleaseFlush(void)
{
    natsStatus          s = NATS_OK;
    natsSock            sock = NATS_SOCK_INVALID;
    natsThread          *t = NULL;
    struct threadArg    arg;
    natsSockCtx         ctx;

    memset(&ctx, 0, sizeof(natsSockCtx));

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("@@ Unable to setup test!");

    test("Check that Flush() release on connection close: ")

    arg.control = 3;

    // We will hand run a fake server that will to reply to the second PING

    s = _startMockupServer(&sock, "localhost", "4222");

    // Start the thread that will try to connect to our server...
    IFOK(s, natsThread_Create(&t, _connectToMockupServer, (void*) &arg));

    if ((s == NATS_OK)
        && (((ctx.fd = accept(sock, NULL, NULL)) == NATS_SOCK_INVALID)
            || natsSock_SetCommonTcpOptions(ctx.fd) != NATS_OK))
    {
        s = NATS_SYS_ERROR;
    }

    if (s == NATS_OK)
    {
        char buffer[1024];
        char info[1024];

        strncpy(info,
                "INFO {\"server_id\":\"foobar\",\"version\":\"latest\",\"go\":\"latest\",\"host\":\"localhost\",\"port\":4222,\"auth_required\":false,\"tls_required\":false,\"max_payload\":1048576}\r\n",
                sizeof(info));

        // Send INFO.
        s = natsSock_WriteFully(&ctx, info, (int) strlen(info));
        if (s == NATS_OK)
        {
            memset(buffer, 0, sizeof(buffer));

            // Read connect and ping commands sent from the client
            s = natsSock_ReadLine(&ctx, buffer, sizeof(buffer));
            IFOK(s, natsSock_ReadLine(&ctx, buffer, sizeof(buffer)));
        }
        // Send PONG
        IFOK(s, natsSock_WriteFully(&ctx, _PONG_PROTO_, _PONG_PROTO_LEN_));

        // Get the PING
        IFOK(s, natsSock_ReadLine(&ctx, buffer, sizeof(buffer)));
    }

    // Now wait for the client to close the connection...
    nats_Sleep(500);

    // Need to close those for the client side to unblock.
    natsSock_Close(ctx.fd);
    natsSock_Close(sock);

    // Wait for the client to finish.
    if (t != NULL)
    {
        natsThread_Join(t);
        natsThread_Destroy(t);
    }

    testCond((s == NATS_OK) && (arg.status != NATS_OK));

    _destroyDefaultThreadArgs(&arg);
}


void test_FlushErrOnDisconnect(void)
{
    natsStatus          s = NATS_OK;
    natsSock            sock = NATS_SOCK_INVALID;
    natsThread          *t = NULL;
    struct threadArg    arg;
    natsSockCtx         ctx;

    memset(&ctx, 0, sizeof(natsSockCtx));

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("@@ Unable to setup test!");

    test("Check that Flush() returns an error during a disconnect: ");

    arg.control = 4;

    // We will hand run a fake server that will to reply to the second PING

    s = _startMockupServer(&sock, "localhost", "4222");

    // Start the thread that will try to connect to our server...
    IFOK(s, natsThread_Create(&t, _connectToMockupServer, (void*) &arg));

    if ((s == NATS_OK)
        && (((ctx.fd = accept(sock, NULL, NULL)) == NATS_SOCK_INVALID)
            || (natsSock_SetCommonTcpOptions(ctx.fd) != NATS_OK)))
    {
        s = NATS_SYS_ERROR;
    }
    if (s == NATS_OK)
    {
        char info[1024];

        strncpy(info,
                "INFO {\"server_id\":\"foobar\",\"version\":\"latest\",\"go\":\"latest\",\"host\":\"localhost\",\"port\":4222,\"auth_required\":false,\"tls_required\":false,\"max_payload\":1048576}\r\n",
                sizeof(info));

        // Send INFO.
        s = natsSock_WriteFully(&ctx, info, (int) strlen(info));
        if (s == NATS_OK)
        {
            char buffer[1024];

            memset(buffer, 0, sizeof(buffer));

            // Read connect and ping commands sent from the client
            s = natsSock_ReadLine(&ctx, buffer, sizeof(buffer));
            IFOK(s, natsSock_ReadLine(&ctx, buffer, sizeof(buffer)));
        }
        // Send PONG
        IFOK(s, natsSock_WriteFully(&ctx, _PONG_PROTO_, _PONG_PROTO_LEN_));
    }

    // Wait a bit and kill the server.
    nats_Sleep(500);
    natsSock_Close(ctx.fd);
    natsSock_Close(sock);

    // Wait for the client to finish.
    if (t != NULL)
    {
        natsThread_Join(t);
        natsThread_Destroy(t);
    }

    testCond(arg.status != NATS_OK);

    if (valgrind)
        nats_Sleep(900);

    _destroyDefaultThreadArgs(&arg);
}

void test_Inbox(void)
{
    natsStatus  s;
    natsInbox   *inbox = NULL;

    test("Inbox starts with correct prefix: ");
    s = natsInbox_Create(&inbox);
    testCond((s == NATS_OK)
             && (inbox != NULL)
             && (strncmp(inbox, "_INBOX.", 7) == 0));

    natsInbox_Destroy(inbox);
}

void test_Stats(void)
{
    natsStatus          s;
    natsConnection      *nc     = NULL;
    natsStatistics      *stats  = NULL;
    natsSubscription    *s1     = NULL;
    natsSubscription    *s2     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    const char          *data = "The quick brown fox jumped over the lazy dog";
    int                 iter  = 10;
    uint64_t            outMsgs = 0;
    uint64_t            outBytes = 0;
    uint64_t            inMsgs = 0;
    uint64_t            inBytes = 0;

    test("Check invalid arg: ");
    s = natsStatistics_GetCounts(NULL, NULL, NULL, NULL, NULL, NULL);
    testCond(s == NATS_INVALID_ARG);

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);

    for (int i=0; (s == NATS_OK) && (i<iter); i++)
        s = natsConnection_PublishString(nc, "foo", data);

    IFOK(s, natsStatistics_Create(&stats));
    IFOK(s, natsConnection_GetStats(nc, stats));
    IFOK(s, natsStatistics_GetCounts(stats, NULL, NULL, &outMsgs, &outBytes, NULL));

    test("Tracking OutMsgs properly: ");
    testCond((s == NATS_OK) && (outMsgs == (uint64_t) iter));

    test("Tracking OutBytes properly: ");
    testCond((s == NATS_OK) && (outBytes == (uint64_t) (iter * strlen(data))));

    // Test both sync and async versions of subscribe.
    s = natsConnection_Subscribe(&s1, nc, "foo", _dummyMsgHandler, NULL);
    IFOK(s, natsConnection_SubscribeSync(&s2, nc, "foo"));

    for (int i=0; (s == NATS_OK) && (i<iter); i++)
        s = natsConnection_PublishString(nc, "foo", data);

    IFOK(s, natsConnection_Flush(nc));

    IFOK(s, natsConnection_GetStats(nc, stats));
    IFOK(s, natsStatistics_GetCounts(stats, &inMsgs, &inBytes, NULL, NULL, NULL));

    test("Tracking inMsgs properly: ");
    testCond((s == NATS_OK) && (inMsgs == (uint64_t)(2 * iter)));

    test("Tracking inBytes properly: ");
    testCond((s == NATS_OK) && (inBytes == (uint64_t)(2 * (iter * strlen(data)))));

    natsStatistics_Destroy(stats);
    natsSubscription_Destroy(s1);
    natsSubscription_Destroy(s2);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_BadSubject(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    testCond(s == NATS_OK);

    test("Should get an error with empty subject: ");
    s = natsConnection_PublishString(nc, "", "hello");
    testCond(s != NATS_OK);

    test("Error should be NATS_INVALID_SUBJECT: ");
    testCond(s == NATS_INVALID_SUBJECT);

    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_SubBadSubjectAndQueueName(void)
{
    natsStatus          s;
    natsConnection      *nc         = NULL;
    natsSubscription    *sub        = NULL;
    natsPid             pid         = NATS_INVALID_PID;
    const char          *badSubjs[] = {NULL, "", "foo bar", "foo..bar", ".foo", "bar.baz.", "baz\t.foo"};
    const char          *goodSubjs[]= {"foo.bar", "a.bcd", "abc.d"};
    const char          *wcSubjs[]  = {"*", "*.*", "*.foo.bar", "foo.*.bar", "foo.bar.*", ">", "foo.bar.>"};
    const char          *badWCs[]   = {">.foo", "foo.>.bar", ">.>"};
    const char          *badQueues[]= {"queue name", "queue.name ", " queue.name",
                                       "queue\tname", "\tqueue.name", "\t.queue.name", "queue.name\t", "queue.name.\t",
                                       "queue\rname", "\rqueue.name", "\r.queue.name", "queue.name\r", "queue.name.\r",
                                       "queue\nname", "\nqueue.name", "\n.queue.name", "queue.name\n", "queue.name.\n"};
    char                buf[256];
    int                 i;

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Connect ok: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    testCond(s == NATS_OK);

    for (i=0; i<(int) (sizeof(badSubjs)/sizeof(char*)); i++)
    {
        snprintf(buf, sizeof(buf), "test subject '%s': ", badSubjs[i]);
        test(buf)
        s = (nats_IsSubjectValid(badSubjs[i], true) ? NATS_ERR : NATS_OK);
        testCond(s == NATS_OK);
    }

    for (i=0; i<(int) (sizeof(goodSubjs)/sizeof(char*)); i++)
    {
        snprintf(buf, sizeof(buf), "test subject '%s': ", goodSubjs[i]);
        test(buf)
        s = (nats_IsSubjectValid(goodSubjs[i], true) ? NATS_OK : NATS_ERR);
        testCond(s == NATS_OK);
    }

    for (i=0; i<(int) (sizeof(badQueues)/sizeof(char*)); i++)
    {
        snprintf(buf, sizeof(buf), "test queue '%s': ", badQueues[i]);
        test(buf)
        s = natsConnection_QueueSubscribeSync(&sub, nc, "foo", badQueues[i]);
        testCond((s == NATS_INVALID_QUEUE_NAME) && (sub == NULL));
        nats_clearLastError();
    }

    for (i=0; i<(int) (sizeof(wcSubjs)/sizeof(char*)); i++)
    {
        snprintf(buf, sizeof(buf), "test wildcard ok '%s': ", wcSubjs[i]);
        test(buf)
        s = (nats_IsSubjectValid(wcSubjs[i], true) ? NATS_OK : NATS_ERR);
        testCond(s == NATS_OK);
    }

    for (i=0; i<(int) (sizeof(wcSubjs)/sizeof(char*)); i++)
    {
        snprintf(buf, sizeof(buf), "test no wildcard allowed '%s': ", wcSubjs[i]);
        test(buf)
        s = (nats_IsSubjectValid(wcSubjs[i], false) ? NATS_ERR : NATS_OK);
        testCond(s == NATS_OK);
    }

    for (i=0; i<(int) (sizeof(badWCs)/sizeof(char*)); i++)
    {
        snprintf(buf, sizeof(buf), "bad wildcard '%s': ", badWCs[i]);
        test(buf)
        s = (nats_IsSubjectValid(badWCs[i], true) ? NATS_ERR : NATS_OK);
        testCond(s == NATS_OK);
    }

    natsConnection_Destroy(nc);

    _stopServer(pid);
}

static void
_subComplete(void *closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);
    arg->done = true;
    natsCondition_Signal(arg->c);
    natsMutex_Unlock(arg->m);
}

void test_ClientAsyncAutoUnsub(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;
    int                 checks;
    int64_t             nd = 0;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if ( s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.status = NATS_OK;
    arg.control= 9;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg));
    IFOK(s, natsSubscription_AutoUnsubscribe(sub, 10));

    for (int i=0; (s == NATS_OK) && (i<100); i++)
        s = natsConnection_PublishString(nc, "foo", "hello");

    IFOK(s, natsConnection_Flush(nc));

    // Wait for the subscription to become invalid
    checks = 0;
    while (natsSubscription_IsValid(sub)
           && (checks++ < 10))
    {
        nats_Sleep(100);
    }
    test("IsValid should be false: ");
    testCond((sub != NULL) && !natsSubscription_IsValid(sub));

    test("Received no more than max: ");
    testCond(arg.sum == 10);

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Subscribe and publish 100 msgs: ");
    s = natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg);
    IFOK(s, natsSubscription_SetOnCompleteCB(sub, _subComplete, (void*) &arg));
    for (int i=0; (s == NATS_OK) && (i<100); i++)
        s = natsConnection_PublishString(nc, "foo", "hello");
    IFOK(s, natsConnection_Flush(nc));
    checks = 0;
    while ((natsSubscription_GetDelivered(sub, &nd) == NATS_OK)
           && (checks++ < 10))
    {
        nats_Sleep(100);
    }
    testCond((s == NATS_OK) && (nd == 100));

    test("Auto-unsub with 10, should close the sub right away: ");
    s = natsSubscription_AutoUnsubscribe(sub, 10);
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.done)
        s = natsCondition_TimedWait(arg.c, arg.m, 1000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_ClientSyncAutoUnsub(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsMsg             *msg      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    int                 received  = 0;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));
    IFOK(s, natsSubscription_AutoUnsubscribe(sub, 10));

    for (int i=0; (s == NATS_OK) && (i<100); i++)
        s = natsConnection_PublishString(nc, "foo", "hello");

    IFOK(s, natsConnection_Flush(nc));

    test("Get correct error: ");
    for (int i=0; (s == NATS_OK) && (i<100); i++)
    {
        s = natsSubscription_NextMsg(&msg, sub, 10);
        if (s == NATS_OK)
        {
            received++;
            natsMsg_Destroy(msg);
        }
    }
    testCond(s == NATS_MAX_DELIVERED_MSGS);

    test("Received no more than max: ");
    testCond(received == 10);

    test("IsValid should be false: ");
    testCond((sub != NULL) && !natsSubscription_IsValid(sub));

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Subscribe and publish 100 msgs: ");
    s = natsConnection_SubscribeSync(&sub, nc, "foo");
    for (int i=0; (s == NATS_OK) && (i<100); i++)
        s = natsConnection_PublishString(nc, "foo", "hello");
    IFOK(s, natsConnection_Flush(nc));
    for (int i=0; (s == NATS_OK) && (i<100); i++)
    {
        s = natsSubscription_NextMsg(&msg, sub, 1000);
        received++;
        natsMsg_Destroy(msg);
    }
    testCond(s == NATS_OK);

    test("Auto-unsub with 10, should close the sub right away: ");
    s = natsSubscription_AutoUnsubscribe(sub, 10);
    testCond(!natsSubscription_IsValid(sub));

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_ClientAutoUnsubAndReconnect(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    opts = _createReconnectOptions();
    if ((opts == NULL)
        || ((s = _createDefaultThreadArgsForCbTests(&arg)) != NATS_OK))
    {
        FAIL("Unable to setup test!");
    }

    arg.status = NATS_OK;
    arg.control= 9;

    s = natsOptions_SetReconnectedCB(opts, _reconnectedCb, &arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg));
    IFOK(s, natsSubscription_AutoUnsubscribe(sub, 10));

    // Send less than the max
    for (int i=0; (s == NATS_OK) && (i<5); i++)
        s = natsConnection_PublishString(nc, "foo", "hello");
    IFOK(s, natsConnection_Flush(nc));

    // Restart the server
    _stopServer(serverPid);
    serverPid = _startServer("nats://127.0.0.1:22222", "-p 22222", true);
    CHECK_SERVER_STARTED(serverPid);

    // Wait for reconnect
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.reconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 5000);
    natsMutex_Unlock(arg.m);

    // Now send more than the max
    for (int i=0; (s == NATS_OK) && (i<50); i++)
        s = natsConnection_PublishString(nc, "foo", "hello");
    IFOK(s, natsConnection_Flush(nc));

    nats_Sleep(10);

    test("Received no more than max: ");
    natsMutex_Lock(arg.m);
    int sum = arg.sum;
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && (sum == 10));

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

static void
_autoUnsub(natsConnection *nc, natsSubscription *sub, natsMsg *msg, void *closure)
{
    struct threadArg *args = (struct threadArg*) closure;

    natsMsg_Destroy(msg);
    natsSubscription_Destroy(sub);
    natsMutex_Lock(args->m);
    args->done = true;
    natsCondition_Signal(args->c);
    natsMutex_Unlock(args->m);
}


void test_AutoUnsubNoUnsubOnDestroy(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    natsBuffer          *buf      = NULL;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if ( s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.status = NATS_OK;
    arg.control= 9;

    serverPid = _startServer("nats://127.0.0.1:4222", "-DV", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Auto-unsub: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _autoUnsub, (void*) &arg));
    IFOK(s, natsSubscription_AutoUnsubscribe(sub, 1));
    IFOK(s, natsConnection_PublishString(nc, "foo", "hello"));
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.done)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);

    test("Read logfile: ");
    s = nats_ReadFile(&buf, 1024, LOGFILE_NAME);
    testCond(s == NATS_OK);

    test("Check UNSUB not sent: ");
    s = (strstr(natsBuf_Data(buf), "[UNSUB 1 ]") == NULL ? NATS_OK : NATS_ERR);
    testCond(s == NATS_OK);

    natsBuf_Destroy(buf);
}

void test_NextMsgOnClosedSub(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsMsg             *msg      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Setup: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));
    IFOK(s, natsSubscription_Unsubscribe(sub));
    testCond(s == NATS_OK);

    test("Get correct error: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond(s == NATS_INVALID_SUBSCRIPTION);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

static void
_nextMsgKickedOut(void *closure)
{
    natsSubscription    *sub = (natsSubscription*) closure;
    natsMsg             *msg = NULL;

    (void) natsSubscription_NextMsg(&msg, sub, 10000);
}

void test_CloseSubRelease(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsThread          *t        = NULL;
    natsThread          *subs[3];
    natsPid             serverPid = NATS_INVALID_PID;
    int64_t             start, end;
    int                 i;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));

    for (i=0; i<3; i++)
        s = natsThread_Create(&(subs[i]), _nextMsgKickedOut, (void*) sub);

    start = nats_Now();

    IFOK(s, natsThread_Create(&t, _closeConnWithDelay, (void*) nc));

    for (i=0; i<3; i++)
    {
        if (subs[i] != NULL)
        {
            natsThread_Join(subs[i]);
            natsThread_Destroy(subs[i]);
        }
    }

    end = nats_Now();

    test("Test that NexMsg was kicked out properly: ");
    testCond((s != NATS_TIMEOUT)
             && ((end - start) <= 1000));

    natsThread_Join(t);
    natsThread_Destroy(t);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_IsValidSubscriber(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsMsg             *msg      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));

    test("Sub is valid: ");
    testCond((s == NATS_OK) && natsSubscription_IsValid(sub));

    test("Publish some msgs: ");
    for (int i=0; (s == NATS_OK) && (i<10); i++)
        s = natsConnection_PublishString(nc, "foo", "hello");

    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Received msg ok: ")
    s = natsSubscription_NextMsg(&msg, sub, 200);
    testCond((s == NATS_OK) && (msg != NULL));

    natsMsg_Destroy(msg);

    test("Unsub: ");
    s = natsSubscription_Unsubscribe(sub);
    testCond(s == NATS_OK);

    test("Received msg should fail after unsubscribe: ")
    s = natsSubscription_NextMsg(&msg, sub, 200);
    testCond(s != NATS_OK);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_SlowSubscriber(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsOptions         *opts     = NULL;
    natsMsg             *msg      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    int                 total     = 100;
    int64_t             start, end;

    s = natsOptions_Create(&opts);
    if (s == NATS_OK)
        s = natsOptions_SetMaxPendingMsgs(opts, total);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Setup: ");
    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));

    for (int i=0;
        (s == NATS_OK) && (i < total + 100); i++)
    {
        s = natsConnection_PublishString(nc, "foo", "hello");
    }
    testCond(s == NATS_OK);

    test("Check flush returns before timeout: ");
    start = nats_Now();

    (void) natsConnection_FlushTimeout(nc, 5000);

    end = nats_Now();

    testCond((end - start) < 5000);

    test("NextMsg should report error: ");
    // Make sure NextMsg returns an error to indicate slow consumer
    s = natsSubscription_NextMsg(&msg, sub, 200);
    testCond(s != NATS_OK);

    natsMsg_Destroy(msg);
    natsSubscription_Destroy(sub);
    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_SlowAsyncSubscriber(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsOptions         *opts     = NULL;
    const char          *lastErr  = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    int                 total     = 100;
    int64_t             start, end;
    struct threadArg    arg;

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetMaxPendingMsgs(opts, total));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    s = _createDefaultThreadArgsForCbTests(&arg);
    if ( s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.status = NATS_OK;
    arg.control= 7;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo.*", _recvTestString, (void*) &arg));

    for (int i=0;
        (s == NATS_OK) && (i < (2 * total)); i++)
    {
        char subj[32];
        snprintf(subj, sizeof(subj), "foo.%d", i);
        s = natsConnection_PublishString(nc, subj, "hello");
    }

    test("Check Publish does not fail due to SlowConsumer: ");
    testCond(s == NATS_OK);

    test("Check flush returns before timeout: ");
    start = nats_Now();

    s = natsConnection_FlushTimeout(nc, 5000);

    end = nats_Now();

    testCond((end - start) < 5000);

    test("Flush should not report an error: ");
    testCond(s == NATS_OK);

    // Make sure the callback blocks before checking for slow consumer
    natsMutex_Lock(arg.m);

    while ((s != NATS_TIMEOUT) && !(arg.msgReceived))
        s = natsCondition_TimedWait(arg.c, arg.m, 5000);

    natsMutex_Unlock(arg.m);

    test("Last Error should be SlowConsumer: ");
    testCond((s == NATS_OK)
             && natsConnection_GetLastError(nc, &lastErr) == NATS_SLOW_CONSUMER);

    // Release the sub
    natsMutex_Lock(arg.m);

    // And destroy the subscription here so that the next msg callback
    // is not invoked.
    natsSubscription_Destroy(sub);

    // Unblock the wait, let the callback finish
    test("Unblock and wait for the callback to finish: ");
    arg.closed = true;
    natsCondition_Signal(arg.c);
    arg.msgReceived = false;
    natsMutex_Unlock(arg.m);

    natsMutex_Lock(arg.m);
    while (!arg.msgReceived)
        natsCondition_TimedWait(arg.c, arg.m, 5000);
    testCond(arg.msgReceived);
    natsMutex_Unlock(arg.m);

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    if (valgrind)
        nats_Sleep(900);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

static void
_slowConsErrCB(natsConnection *nc, natsSubscription *sub, natsStatus err, void *closure)
{
    struct threadArg *arg    = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);
    if (err == NATS_SLOW_CONSUMER)
    {
        arg->sum++;
        natsCondition_Signal(arg->c);
    }
    natsMutex_Unlock(arg->m);
}

void test_SlowConsumerCB(void)
{
    natsStatus          s;
    natsConnection      *nc     = NULL;
    natsSubscription    *sub    = NULL;
    natsOptions         *opts   = NULL;
    natsPid             pid     = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetMaxPendingMsgs(opts, 1));
    IFOK(s, natsOptions_SetErrorHandler(opts, _slowConsErrCB, (void*) &arg));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    test("Create sub: ")
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"))
    testCond(s == NATS_OK);

    test("Publish 2 messages: ");
    IFOK(s, natsConnection_PublishString(nc, "foo", "msg1"));
    IFOK(s, natsConnection_PublishString(nc, "foo", "msg2"));
    testCond(s == NATS_OK);

    test("Error handler invoked: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && (arg.sum != 1))
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Produce 1 message: ");
    IFOK(s, natsConnection_PublishString(nc, "foo", "msg3"));
    testCond(s == NATS_OK);

    test("Check handler is not invoked: ");
    nats_Sleep(50);
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && (arg.sum != 1))
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    natsSubscription_Destroy(sub);
    natsOptions_Destroy(opts);
    _stopServer(pid);
    _destroyDefaultThreadArgs(&arg);
}

void test_PendingLimitsDeliveredAndDropped(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    const char          *lastErr  = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    int                 total     = 100;
    int                 sent      = total + 20;
    int                 msgsLimit = 0;
    int                 bytesLimit= 0;
    int                 msgs      = 0;
    int                 bytes     = 0;
    int64_t             dropped   = 0;
    int64_t             delivered = 0;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.status = NATS_OK;
    arg.control= 7;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg));

    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    test("Settings, invalid args, NULL sub: ");
    s = natsSubscription_SetPendingLimits(NULL, 1, 1);
    testCond(s != NATS_OK);

    test("Settings, invalid args, zero msgs: ");
    s = natsSubscription_SetPendingLimits(sub, 0, 1);
    testCond(s != NATS_OK);

    test("Settings, invalid args, zero bytes: ");
    s = natsSubscription_SetPendingLimits(sub, 1, 0);
    testCond(s != NATS_OK);

    test("Check pending limits, NULL sub: ");
    s = natsSubscription_GetPendingLimits(NULL, &msgsLimit, &bytesLimit);
    testCond(s != NATS_OK);

    test("Check pending limits, other params NULL are OK: ");
    s = natsSubscription_GetPendingLimits(sub, NULL, NULL);
    testCond(s == NATS_OK);

    test("Check pending limits, msgsLimit NULL is OK: ");
    s = natsSubscription_GetPendingLimits(sub, NULL, &bytesLimit);
    testCond((s == NATS_OK) && (bytesLimit == NATS_OPTS_DEFAULT_MAX_PENDING_MSGS * 1024));

    test("Check pending limits, msgsLibytesLimitmit NULL is OK: ");
    s = natsSubscription_GetPendingLimits(sub, &msgsLimit, NULL);
    testCond((s == NATS_OK) && (msgsLimit == NATS_OPTS_DEFAULT_MAX_PENDING_MSGS));

    test("Set negative value for msgs OK: ");
    s = natsSubscription_SetPendingLimits(sub, -1, 100);
    testCond(s == NATS_OK);

    test("Set negative value for bytes OK: ");
    s = natsSubscription_SetPendingLimits(sub, 100, -1);
    testCond(s == NATS_OK);

    test("Set negative values OK: ");
    s = natsSubscription_SetPendingLimits(sub, -10, -10);
    testCond(s == NATS_OK);

    test("Get pending with negative values returned OK: ");
    s = natsSubscription_GetPendingLimits(sub, &msgsLimit, &bytesLimit);
    testCond((s == NATS_OK) && (msgsLimit == -10) && (bytesLimit == -10));

    msgsLimit = 0;
    bytesLimit = 0;

    test("Set valid values: ");
    s = natsSubscription_SetPendingLimits(sub, total, total * 1024);
    testCond(s == NATS_OK);

    test("Check pending limits: ");
    s = natsSubscription_GetPendingLimits(sub, &msgsLimit, &bytesLimit);
    testCond((s == NATS_OK) && (msgsLimit == total) && (bytesLimit == total * 1024));

    for (int i=0;
        (s == NATS_OK) && (i < sent); i++)
    {
        s = natsConnection_PublishString(nc, "foo", "hello");
    }
    IFOK(s, natsConnection_Flush(nc));

    // Make sure the callback blocks before checking for slow consumer
    natsMutex_Lock(arg.m);

    while ((s != NATS_TIMEOUT) && !arg.msgReceived)
        s = natsCondition_TimedWait(arg.c, arg.m, 5000);

    natsMutex_Unlock(arg.m);

    test("Last Error should be SlowConsumer: ");
    testCond((s == NATS_OK)
             && natsConnection_GetLastError(nc, &lastErr) == NATS_SLOW_CONSUMER);

    // Check the pending values
    test("Check pending values, NULL sub: ");
    s = natsSubscription_GetPending(NULL, &msgs, &bytes);
    testCond(s != NATS_OK);

    test("Check pending values, NULL msgs: ");
    s = natsSubscription_GetPending(sub, NULL, &bytes);
    testCond(s == NATS_OK);

    test("Check pending values, NULL bytes: ");
    s = natsSubscription_GetPending(sub, &msgs, NULL);
    testCond(s == NATS_OK);

    msgs = 0;
    bytes = 0;

    test("Check pending values: ");
    s = natsSubscription_GetPending(sub, &msgs, &bytes);
    testCond((s == NATS_OK)
             && ((msgs == total) || (msgs == total - 1))
             && ((bytes == total * 5) || (bytes == (total - 1) * 5)));

    test("Check dropped: NULL sub: ");
    s = natsSubscription_GetDropped(NULL, &dropped);
    testCond(s != NATS_OK);

    test("Check dropped, NULL msgs: ");
    s = natsSubscription_GetDropped(sub, NULL);
    testCond(s != NATS_OK);

    msgs = 0;
    test("Check dropped: ");
    s = natsSubscription_GetDropped(sub, &dropped);
    testCond((s == NATS_OK)
            && ((dropped == (int64_t)(sent - total))
                || (dropped == (int64_t)(sent - total - 1))));

    test("Check delivered: NULL sub: ");
    s = natsSubscription_GetDelivered(NULL, &delivered);
    testCond(s != NATS_OK);

    test("Check delivered: NULL msgs: ");
    s = natsSubscription_GetDelivered(sub, NULL);
    testCond(s != NATS_OK);

    msgs = 0;
    test("Check delivered: ");
    s = natsSubscription_GetDelivered(sub, &delivered);
    testCond((s == NATS_OK) && (delivered == 1));

    test("Check get stats pending: ");
    s = natsSubscription_GetStats(sub, &msgs, &bytes, NULL, NULL, NULL, NULL);
    testCond((s == NATS_OK)
              && ((msgs == total) || (msgs == total - 1))
              && ((bytes == total * 5) || (bytes == (total - 1) * 5)));

    test("Check get stats max pending: ");
    s = natsSubscription_GetStats(sub, NULL, NULL, &msgs, &bytes, NULL, NULL);
    testCond((s == NATS_OK)
             && (msgs >= total - 1) && (msgs <= total)
             && (bytes >= (total - 1) * 5) && (bytes <= total * 5));

    test("Check get stats delivered: ");
    s = natsSubscription_GetStats(sub, NULL, NULL, NULL, NULL, &delivered, NULL);
    testCond((s == NATS_OK) && (delivered == 1));

    test("Check get stats dropped: ");
    s = natsSubscription_GetStats(sub, NULL, NULL, NULL, NULL, NULL, &dropped);
    testCond((s == NATS_OK)
             && ((dropped == (int64_t) (sent - total))
                 || (dropped == (int64_t) (sent - total - 1))));

    test("Check get stats all NULL: ");
    s = natsSubscription_GetStats(sub, NULL, NULL, NULL, NULL, NULL, NULL);
    testCond(s == NATS_OK);

    // Release the sub
    natsMutex_Lock(arg.m);

    // Unblock the wait
    arg.closed = true;

    // And close the subscription here so that the next msg callback
    // is not invoked.
    natsSubscription_Unsubscribe(sub);

    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    // All these calls should fail with a closed subscription
    test("SetPendingLimit on closed sub: ");
    s = natsSubscription_SetPendingLimits(sub, 1, 1);
    testCond(s != NATS_OK);

    test("GetPendingLimit on closed sub: ");
    s = natsSubscription_GetPendingLimits(sub, NULL, NULL);
    testCond(s != NATS_OK);

    test("GetPending on closed sub: ");
    s = natsSubscription_GetPending(sub, &msgs, &bytes);
    testCond(s != NATS_OK);

    test("GetDelivered on closed sub: ");
    s = natsSubscription_GetDelivered(sub, &delivered);
    testCond(s != NATS_OK);

    test("GetDropped on closed sub: ");
    s = natsSubscription_GetDropped(sub, &dropped);
    testCond(s != NATS_OK);

    test("Check get stats on closed sub: ");
    s = natsSubscription_GetStats(sub, NULL, NULL, NULL, NULL, NULL, NULL);
    testCond(s != NATS_OK);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_PendingLimitsWithSyncSub(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsMsg             *msg      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    int                 msgsLimit = 0;
    int                 bytesLimit= 0;
    int                 msgs      = 0;
    int                 bytes     = 0;
    int64_t             dropped   = 0;
    int64_t             delivered = 0;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));
    IFOK(s, natsSubscription_SetPendingLimits(sub, 10000, 10));

    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    test("Check pending limits: ");
    s = natsSubscription_GetPendingLimits(sub, &msgsLimit, &bytesLimit);
    testCond((s == NATS_OK) && (msgsLimit == 10000) && (bytesLimit == 10));

    test("Can publish: ");
    s = natsConnection_PublishString(nc, "foo", "abcde");
    IFOK(s, natsConnection_PublishString(nc, "foo", "abcdefghijklmnopqrstuvwxyz"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    // Check the pending values
    test("Check pending values: ");
    s = natsSubscription_GetPending(sub, &msgs, &bytes);
    testCond((s == NATS_OK) && (msgs == 1) && (bytes == 5));

    msgs = 0;
    test("Check dropped: ");
    s = natsSubscription_GetDropped(sub, &dropped);
    testCond((s == NATS_OK) && (dropped == 1));

    test("Can publish small: ");
    s = natsConnection_PublishString(nc, "foo", "abc");
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Receive first msg: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK)
             && (msg != NULL)
             && (strcmp(natsMsg_GetData(msg), "abcde") == 0));

    msgs = 0;
    test("Check delivered: ");
    s = natsSubscription_GetDelivered(sub, &delivered);
    testCond((s == NATS_OK) && (delivered == 1));

    natsMsg_Destroy(msg);
    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_AsyncSubscriptionPending(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    int                 total     = 100;
    int                 msgs      = 0;
    int                 bytes     = 0;
    int                 mlen      = 10;
    int                 totalSize = total * mlen;
    uint64_t            queuedMsgs= 0;
    int                 i;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.status = NATS_OK;
    arg.control= 7;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg));

    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    // Check with invalid args
    test("Call MaxPending with invalid args: NULL sub: ");
    s = natsSubscription_GetMaxPending(NULL, &msgs, &bytes);
    testCond(s != NATS_OK);

    test("Call MaxPending with invalid args: other NULL params OK: ");
    s = natsSubscription_GetMaxPending(sub, NULL, &bytes);
    IFOK(s, natsSubscription_GetMaxPending(sub, &msgs, NULL));
    IFOK(s, natsSubscription_GetMaxPending(sub, NULL, NULL));
    testCond(s == NATS_OK);

    for (i = 0; (s == NATS_OK) && (i < total); i++)
        s = natsConnection_PublishString(nc, "foo", "0123456789");

    IFOK(s, natsConnection_Flush(nc));

    // Wait that a message is received, so checks are safe
    natsMutex_Lock(arg.m);

    while ((s != NATS_TIMEOUT) && !arg.msgReceived)
        s = natsCondition_TimedWait(arg.c, arg.m, 5000);

    natsMutex_Unlock(arg.m);

    // Test old way
    test("Test queued msgs old way: ");
    s = natsSubscription_QueuedMsgs(sub, &queuedMsgs);
    testCond((s == NATS_OK)
            && (((int)queuedMsgs == total) || ((int)queuedMsgs == total - 1)));

    // New way, make sure the same and check bytes.
    test("Test new way: ");
    s = natsSubscription_GetPending(sub, &msgs, &bytes);
    testCond((s == NATS_OK)
             && ((msgs == total) || (msgs == total - 1))
             && ((bytes == totalSize) || (bytes == totalSize - mlen)));


    // Make sure max has been set. Since we block after the first message is
    // received, MaxPending should be >= total - 1 and <= total
    test("Check max pending: ");
    s = natsSubscription_GetMaxPending(sub, &msgs, &bytes);
    testCond((s == NATS_OK)
             && ((msgs <= total) && (msgs >= total-1))
             && ((bytes <= totalSize) && (bytes >= totalSize-mlen)));

    test("Check ClearMaxPending: ");
    s = natsSubscription_ClearMaxPending(sub);
    if (s == NATS_OK)
        s = natsSubscription_GetMaxPending(sub, &msgs, &bytes);
    testCond((s == NATS_OK) && (msgs == 0) && (bytes == 0));

    natsMutex_Lock(arg.m);
    arg.closed = true;
    natsSubscription_Unsubscribe(sub);
    natsCondition_Signal(arg.c);
    arg.msgReceived = false;
    natsMutex_Unlock(arg.m);

    // These calls should fail once the subscription is closed.
    test("Check MaxPending on closed sub: ");
    s = natsSubscription_GetMaxPending(sub, &msgs, &bytes);
    testCond(s != NATS_OK);

    test("Check ClearMaxPending on closed sub: ");
    s = natsSubscription_ClearMaxPending(sub);
    testCond(s != NATS_OK);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    natsMutex_Lock(arg.m);
    while (!arg.msgReceived)
        natsCondition_TimedWait(arg.c, arg.m, 5000);
    natsMutex_Unlock(arg.m);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_AsyncSubscriptionPendingDrain(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    int                 total     = 100;
    int                 msgs      = 0;
    int                 bytes     = 0;
    int64_t             delivered = 0;
    int                 i;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.status = NATS_OK;
    arg.string = "0123456789";
    arg.control= 1;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg));

    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    for (i = 0; (s == NATS_OK) && (i < total); i++)
        s = natsConnection_PublishString(nc, "foo", arg.string);

    IFOK(s, natsConnection_Flush(nc));

    test("Wait for all delivered: ");
    msgs = 0;
    for (i=0; (s == NATS_OK) && (i<500); i++)
    {
        s = natsSubscription_GetDelivered(sub, &delivered);
        if ((s == NATS_OK) && (delivered == (int64_t) total))
            break;

        nats_Sleep(10);
    }
    testCond((s == NATS_OK) && (delivered == (int64_t) total));

    test("Check pending: ");
    s = natsSubscription_GetPending(sub, &msgs, &bytes);
    testCond((s == NATS_OK) && (msgs == 0) && (bytes == 0));

    natsSubscription_Unsubscribe(sub);

    test("Check Delivered on closed sub: ");
    s = natsSubscription_GetDelivered(sub, &delivered);
    testCond(s != NATS_OK);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_SyncSubscriptionPending(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsMsg             *msg      = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    int                 total     = 100;
    int                 msgs      = 0;
    int                 bytes     = 0;
    int                 mlen      = 10;
    int                 totalSize = total * mlen;
    uint64_t            queuedMsgs= 0;
    int                 i;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));

    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    // Check with invalid args
    test("Call MaxPending with invalid args: NULL sub: ");
    s = natsSubscription_GetMaxPending(NULL, &msgs, &bytes);
    testCond(s != NATS_OK);

    test("Call MaxPending with invalid args: other NULL params OK: ");
    s = natsSubscription_GetMaxPending(sub, NULL, &bytes);
    IFOK(s, natsSubscription_GetMaxPending(sub, &msgs, NULL));
    IFOK(s, natsSubscription_GetMaxPending(sub, NULL, NULL));
    testCond(s == NATS_OK);

    for (i = 0; (s == NATS_OK) && (i < total); i++)
        s = natsConnection_PublishString(nc, "foo", "0123456789");

    IFOK(s, natsConnection_Flush(nc));

    // Test old way
    test("Test queued msgs old way: ");
    s = natsSubscription_QueuedMsgs(sub, &queuedMsgs);
    testCond((s == NATS_OK)
            && (((int)queuedMsgs == total) || ((int)queuedMsgs == total - 1)));

    // New way, make sure the same and check bytes.
    test("Test new way: ");
    s = natsSubscription_GetPending(sub, &msgs, &bytes);
    testCond((s == NATS_OK)
             && ((msgs == total) || (msgs == total - 1))
             && ((bytes == totalSize) || (bytes == totalSize - mlen)));


    // Make sure max has been set. Since we block after the first message is
    // received, MaxPending should be >= total - 1 and <= total
    test("Check max pending: ");
    s = natsSubscription_GetMaxPending(sub, &msgs, &bytes);
    testCond((s == NATS_OK)
             && ((msgs <= total) && (msgs >= total-1))
             && ((bytes <= totalSize) && (bytes >= totalSize-mlen)));

    test("Check ClearMaxPending: ");
    s = natsSubscription_ClearMaxPending(sub);
    IFOK(s, natsSubscription_GetMaxPending(sub, &msgs, &bytes));
    testCond((s == NATS_OK) && (msgs == 0) && (bytes == 0));

    // Drain all but one
    for (i=0; (s == NATS_OK) && (i<total-1); i++)
    {
        s = natsSubscription_NextMsg(&msg, sub, 1000);
        if (s == NATS_OK)
            natsMsg_Destroy(msg);
    }

    test("Check pending: ");
    s = natsSubscription_GetPending(sub, &msgs, &bytes);
    testCond((s == NATS_OK) && (msgs == 1) && (bytes == mlen));

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_SyncSubscriptionPendingDrain(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsMsg             *msg      = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    int                 total     = 100;
    int                 msgs      = 0;
    int                 bytes     = 0;
    int64_t             delivered = 0;
    int                 i;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));

    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    for (i = 0; (s == NATS_OK) && (i < total); i++)
        s = natsConnection_PublishString(nc, "foo", "0123456789");

    IFOK(s, natsConnection_Flush(nc));

    test("Wait for all delivered: ");
    do
    {
        do
        {
            s = natsSubscription_NextMsg(&msg, sub, 10);
            if (s == NATS_OK)
                natsMsg_Destroy(msg);
        }
        while (s == NATS_OK);

        s = natsSubscription_GetDelivered(sub, &delivered);
        if ((s == NATS_OK) && (delivered == (int64_t) total))
            break;

        nats_Sleep(100);
        i++;
    }
    while ((s == NATS_OK) && (i < 50));
    testCond((s == NATS_OK) && (delivered == (int64_t) total));

    test("Check pending: ");
    s = natsSubscription_GetPending(sub, &msgs, &bytes);
    testCond((s == NATS_OK) && (msgs == 0) && (bytes == 0));

    natsSubscription_Unsubscribe(sub);

    test("Check Delivered on closed sub: ");
    s = natsSubscription_GetDelivered(sub, &delivered);
    testCond(s != NATS_OK);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

static void
_asyncErrCb(natsConnection *nc, natsSubscription *sub, natsStatus err, void* closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);

    if (arg->sum == 1)
    {
        natsMutex_Unlock(arg->m);
        return;
    }

    arg->sum = 1;

    if (sub != arg->sub)
        arg->status = NATS_ERR;

    if ((arg->status == NATS_OK) && (err != NATS_SLOW_CONSUMER))
        arg->status = NATS_ERR;

    arg->closed = true;
    arg->done = true;
    natsCondition_Broadcast(arg->c);

    natsMutex_Unlock(arg->m);
}

void test_AsyncErrHandlerMaxPendingMsgs(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.status = NATS_OK;
    arg.control= 7;

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetURL(opts, NATS_DEFAULT_URL));
    IFOK(s, natsOptions_SetMaxPendingMsgs(opts, 10));
    IFOK(s, natsOptions_SetErrorHandler(opts, _asyncErrCb, (void*) &arg));

    if (s != NATS_OK)
        FAIL("Unable to create options for test AsyncErrHandler");

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "async_test", _recvTestString, (void*) &arg));

    natsMutex_Lock(arg.m);
    arg.sub = sub;
    natsMutex_Unlock(arg.m);

    for (int i=0;
        (s == NATS_OK) && (i < (opts->maxPendingMsgs + 100)); i++)
    {
        s = natsConnection_PublishString(nc, "async_test", "hello");
    }
    IFOK(s, natsConnection_Flush(nc));

    // Wait for async err callback
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.done)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);

    test("Aync fired properly, and all checks are good: ");
    testCond((s == NATS_OK)
             && arg.done
             && arg.closed
             && (arg.status == NATS_OK));

    natsOptions_Destroy(opts);
    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_AsyncErrHandlerMaxPendingBytes(void)
{
    natsStatus          s;
    natsConnection* nc = NULL;
    natsOptions* opts = NULL;
    natsSubscription* sub = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;
    int                 data_len = 10;
    const char          msg[] = { 0,1,2,3,4,5,6,7,8,9 }; //10 bytes long message
    int64_t             pendingBytesLimit = 100;
    int                 i = 0;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.status = NATS_OK;
    arg.control = 7;

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetURL(opts, NATS_DEFAULT_URL));
    IFOK(s, natsOptions_SetMaxPendingBytes(opts, pendingBytesLimit));
    IFOK(s, natsOptions_SetErrorHandler(opts, _asyncErrCb, (void*)&arg));

    if (s != NATS_OK)
        FAIL("Unable to create options for test AsyncErrHandler");

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "async_test", _recvTestString, (void*)&arg));

    natsMutex_Lock(arg.m);
    arg.sub = sub;
    natsMutex_Unlock(arg.m);
    for (i=0;
        (s == NATS_OK) && (i < (pendingBytesLimit + 100)); i+=data_len) //increment by 10 (message size) each iteration
    {
        s = natsConnection_Publish(nc, "async_test", msg, data_len);
    }
    IFOK(s, natsConnection_Flush(nc));

    // Wait for async err callback
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.done)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);

    test("Aync fired properly, and all checks are good: ");
    testCond((s == NATS_OK)
        && arg.done
        && arg.closed
        && (arg.status == NATS_OK));

    natsOptions_Destroy(opts);
    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

static void
_asyncErrBlockingCb(natsConnection *nc, natsSubscription *sub, natsStatus err, void* closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);

    arg->sum++;

    while ((arg->sum == 1) && !arg->closed)
        natsCondition_Wait(arg->c, arg->m);

    if (sub != arg->sub)
        arg->status = NATS_ERR;

    if ((arg->status == NATS_OK) && (err != NATS_SLOW_CONSUMER))
        arg->status = NATS_ERR;

    if (arg->status == NATS_OK)
    {
        // Call some subscription API to make sure that the pointer has not been freed.
        arg->current = natsSubscription_IsValid(sub);
    }

    arg->done = true;
    natsCondition_Signal(arg->c);

    natsMutex_Unlock(arg->m);
}

void test_AsyncErrHandlerSubDestroyed(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    natsMsg             *msg      = NULL;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetURL(opts, NATS_DEFAULT_URL));
    IFOK(s, natsOptions_SetMaxPendingMsgs(opts, 1));
    IFOK(s, natsOptions_SetErrorHandler(opts, _asyncErrBlockingCb, (void*) &arg));

    if (s != NATS_OK)
        FAIL("Unable to create options for test AsyncErrHandler");

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    test("Create sync sub: ");
    s = natsConnection_SubscribeSync(&sub, nc, "foo");
    testCond(s == NATS_OK);

    natsMutex_Lock(arg.m);
    arg.sub = sub;
    arg.current = true;
    natsMutex_Unlock(arg.m);

    test("Cause error: ");
    s = natsConnection_PublishString(nc, "foo", "msg1");
    IFOK(s, natsConnection_PublishString(nc, "foo", "msg2"));
    testCond(s == NATS_OK);

    // Wait a bit to make sure that we have a slow consumer.
    nats_Sleep(250);

    test("Next should be error: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_SLOW_CONSUMER) && (msg == NULL));
    nats_clearLastError();
    s = NATS_OK;

    test("Consume 1: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Cause error again: ");
    s = natsConnection_PublishString(nc, "foo", "msg3");
    IFOK(s, natsConnection_PublishString(nc, "foo", "msg4"));
    testCond(s == NATS_OK);

    test("Wait a bit for async error to be posted: ");
    nats_Sleep(200);
    testCond(true);

    test("Destroy subscription: ");
    natsSubscription_Destroy(sub);
    testCond(true);

    test("Wait for async error callback to return: ");
    natsMutex_Lock(arg.m);
    // First unblock the first instance
    arg.closed = true;
    natsCondition_Signal(arg.c);
    // Now wait for the callback to have processed both and be done.
    while ((s != NATS_TIMEOUT) && !arg.done)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    // If ok, arg.current should be false since the subscription should
    // not be valid (closed) at the second callback iteration.
    if ((s == NATS_OK) && arg.current)
        s = NATS_ERR;
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

static void
_responseCb(natsConnection *nc, natsSubscription *sub, natsMsg *msg, void *closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);

    arg->closed = true;
    arg->done = true;
    natsCondition_Signal(arg->c);

    natsMutex_Unlock(arg->m);

    natsMsg_Destroy(msg);
}

static void
_startCb(natsConnection *nc, natsSubscription *sub, natsMsg *msg, void *closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;
    natsInbox           *response = NULL;
    natsStatus          s;

    natsMutex_Lock(arg->m);

    s = natsInbox_Create(&response);
    IFOK(s, natsConnection_Subscribe(&(arg->sub), nc, response, _responseCb, (void*) arg));
    IFOK(s, natsConnection_PublishRequestString(nc, "helper", response, "Help Me!"));

    if (s != NATS_OK)
        arg->status = s;

    // We need to destroy the inbox. It has been copied by the
    // natsConnection_Subscribe() call.
    natsInbox_Destroy(response);

    natsMutex_Unlock(arg->m);

    natsMsg_Destroy(msg);
}

void test_AsyncSubscriberStarvation(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsSubscription    *sub2     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.status = NATS_OK;
    arg.control= 4;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "helper",
                                     _recvTestString, (void*) &arg));
    IFOK(s, natsConnection_Subscribe(&sub2, nc, "start",
                                     _startCb, (void*) &arg));

    IFOK(s, natsConnection_PublishString(nc, "start", "Begin"));
    IFOK(s, natsConnection_Flush(nc));

    // Wait for end of test
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.done)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);

    test("Test not stalled in cb waiting for other cb: ");
    testCond((s == NATS_OK)
             && arg.done
             && (arg.status == NATS_OK));

    natsSubscription_Destroy(arg.sub);
    natsSubscription_Destroy(sub);
    natsSubscription_Destroy(sub2);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_AsyncSubscriberOnClose(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsSubscription    *sub2     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    int                 seen      = 0;
    int                 checks    = 0;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.status = NATS_OK;
    arg.control= 8;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo",
                                     _recvTestString, (void*) &arg));

    for (int i=0; (s == NATS_OK) && (i < 10); i++)
        s = natsConnection_PublishString(nc, "foo", "Hello World");

    IFOK(s, natsConnection_Flush(nc));

    // Wait to receive the first message
    test("Wait for first message: ");
    natsMutex_Lock(arg.m);
    while ((s == NATS_OK) && (arg.sum != 1))
    {
        natsMutex_Unlock(arg.m);
        nats_Sleep(100);
        natsMutex_Lock(arg.m);
        if (checks++ > 10)
            s = NATS_ILLEGAL_STATE;
    }
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    natsConnection_Close(nc);

    // Release callbacks
    natsMutex_Lock(arg.m);
    arg.closed = true;
    natsCondition_Broadcast(arg.c);
    natsMutex_Unlock(arg.m);

    // Wait for some time
    nats_Sleep(100);

    natsMutex_Lock(arg.m);
    seen = arg.sum;
    natsMutex_Unlock(arg.m);

    test("Make sure only one callback fired: ");
    testCond(seen == 1);

    natsSubscription_Destroy(sub);
    natsSubscription_Destroy(sub2);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_NextMsgCallOnAsyncSub(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsMsg             *msg      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Setup: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, NULL))
    testCond(s == NATS_OK);

    test("NextMsg should fail for async sub: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s != NATS_OK) && (msg == NULL));

    natsSubscription_Destroy(sub);

    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

static void
testSubOnComplete(void *closure)
{
    struct threadArg *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);
    arg->status = (arg->control == 2 ? NATS_OK : NATS_ERR);
    arg->done = true;
    natsCondition_Signal(arg->c);
    natsMutex_Unlock(arg->m);
}

static void
testOnCompleteMsgHandler(natsConnection *nc, natsSubscription *sub, natsMsg *msg, void *closure)
{
    struct threadArg *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);
    arg->control = 1;
    natsCondition_Signal(arg->c);
    natsMutex_Unlock(arg->m);

    // Sleep here so that main thread invokes Unsubscribe() and we make sure that the
    // onComplete is not invoked until this function returns.
    nats_Sleep(500);

    natsMutex_Lock(arg->m);
    arg->control = 2;
    natsMutex_Unlock(arg->m);

    natsMsg_Destroy(msg);
}

void test_SubOnComplete(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsSubscription    *sub      = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Invalid arg: ");
    s = natsSubscription_SetOnCompleteCB(NULL, testSubOnComplete, NULL);
    testCond(s == NATS_INVALID_ARG);

    test("Connect + sub: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));
    testCond(s == NATS_OK);

    test("Invalid sub (NULL): ");
    s= natsSubscription_SetOnCompleteCB(sub, testSubOnComplete, NULL);
    testCond(s == NATS_INVALID_SUBSCRIPTION);

    natsSubscription_Unsubscribe(sub);
    test("Invalid sub (sync): ");
    s = natsSubscription_SetOnCompleteCB(sub, testSubOnComplete, NULL);
    testCond(s == NATS_INVALID_SUBSCRIPTION);

    natsSubscription_Destroy(sub);
    sub = NULL;
    arg.status = NATS_ERR;
    test("SetOnCompleteCB ok: ");
    s = natsConnection_Subscribe(&sub, nc, "foo", testOnCompleteMsgHandler, &arg);
    IFOK(s, natsSubscription_SetOnCompleteCB(sub, testSubOnComplete, &arg));
    testCond(s == NATS_OK);
    test("SetOnCompleteCB to NULL ok: ");
    s = natsSubscription_SetOnCompleteCB(sub, NULL, NULL);
    if (s == NATS_OK)
    {
        natsSub_Lock(sub);
        s = (((sub->onCompleteCB == NULL) && (sub->onCompleteCBClosure == NULL)) ? NATS_OK : NATS_ERR);
        natsSub_Unlock(sub);
    }
    testCond(s == NATS_OK);
    test("OnComplete invoked after last message: ");
    s = natsSubscription_SetOnCompleteCB(sub, testSubOnComplete, &arg);
    IFOK(s, natsConnection_PublishString(nc, "foo", "hello"));
    IFOK(s, natsConnection_Flush(nc));
    if (s == NATS_OK)
    {
        // Wait for message handler to be invoked
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && (arg.control != 1))
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        natsMutex_Unlock(arg.m);
    }
    IFOK(s, natsSubscription_Unsubscribe(sub));
    if (s == NATS_OK)
    {
        // Now wait for the onComplete to return.
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.done)
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);

        // This will be OK if complete callback was invoked after msg handler returned,
        // otherwise will be NATS_ERR;
        s = arg.status;
        natsMutex_Unlock(arg.m);
    }
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);
}

void test_ServersOption(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    char                buffer[128];
    int                 serversCount;

    serversCount = sizeof(testServers) / sizeof(char *);

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetNoRandomize(opts, true));
#if _WIN32
    IFOK(s, natsOptions_SetTimeout(opts, 250));
#endif

    if (s != NATS_OK)
        FAIL("Unable to create options for test ServerOptions");

    test("Connect should fail with NATS_NO_SERVER: ");
    s = natsConnection_Connect(&nc, opts);
    testCond((nc == NULL) && (s == NATS_NO_SERVER));

    test("Connect with list of servers should fail with NATS_NO_SERVER: ");
    s = natsOptions_SetServers(opts, testServers, serversCount);
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond((nc == NULL) && (s == NATS_NO_SERVER));

    serverPid = _startServer("nats://127.0.0.1:1222", "-p 1222", true);
    CHECK_SERVER_STARTED(serverPid);

    buffer[0] = '\0';
    test("Can connect to first: ")
    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_GetConnectedUrl(nc, buffer, sizeof(buffer)));
    testCond((s == NATS_OK)
             && (buffer[0] != '\0')
             && (strcmp(buffer, testServers[0]) == 0));

    natsConnection_Destroy(nc);
    nc = NULL;

    _stopServer(serverPid);
    serverPid = NATS_INVALID_PID;

    // Make sure we can connect to a non first server if running
    serverPid = _startServer("nats://127.0.0.1:1223", "-p 1223", true);
    CHECK_SERVER_STARTED(serverPid);

    buffer[0] = '\0';
    test("Can connect to second: ")
    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_GetConnectedUrl(nc, buffer, sizeof(buffer)));
    testCond((s == NATS_OK)
             && (buffer[0] != '\0')
             && (strcmp(buffer, testServers[1]) == 0));

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
}

void test_AuthServers(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid1= NATS_INVALID_PID;
    natsPid             serverPid2= NATS_INVALID_PID;
    char                buffer[128];
    const char          *plainServers[] = {"nats://127.0.0.1:1222",
                                           "nats://127.0.0.1:1224"};
    const char          *authServers[] = {"nats://127.0.0.1:1222",
                                          "nats://ivan:foo@127.0.0.1:1224"};
    int serversCount = 2;

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetNoRandomize(opts, true));
    IFOK(s, natsOptions_SetServers(opts, plainServers, serversCount));

    if (s != NATS_OK)
        FAIL("Unable to create options for test ServerOptions");

    serverPid1 = _startServer("nats://127.0.0.1:1222", "-p 1222 --user ivan --pass foo", false);
    CHECK_SERVER_STARTED(serverPid1);

    serverPid2 = _startServer("nats://127.0.0.1:1224", "-p 1224 --user ivan --pass foo", false);
    if (serverPid2 == NATS_INVALID_PID)
        _stopServer(serverPid1);
    CHECK_SERVER_STARTED(serverPid2);

    nats_Sleep(500);

    test("Connect fails due to auth error: ");
    s = natsConnection_Connect(&nc, opts);
    testCond((s == NATS_CONNECTION_AUTH_FAILED) && (nc == NULL));

    buffer[0] = '\0';
    test("Connect succeeds with correct servers list: ")
    s = natsOptions_SetServers(opts, authServers, serversCount);
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond((s == NATS_OK)
             && (nc != NULL)
             && (natsConnection_GetConnectedUrl(nc, buffer, sizeof(buffer)) == NATS_OK)
             // Even though we are using the url nats://127.0.0.1:1222, the library
             // will use the user/pwd info found in the second url. So we should have
             // connected OK to the first (no randomization option set at beginning of test).
             && (strcmp(buffer, authServers[0]) == 0));

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _stopServer(serverPid1);
    _stopServer(serverPid2);
}

void test_AuthFailToReconnect(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid1= NATS_INVALID_PID;
    natsPid             serverPid2= NATS_INVALID_PID;
    natsPid             serverPid3= NATS_INVALID_PID;
    char                buffer[64];
    const char          *servers[] = {"nats://127.0.0.1:22222",
                                      "nats://127.0.0.1:22223",
                                      "nats://127.0.0.1:22224"};
    struct threadArg    args;

    int serversCount = 3;

    s = _createDefaultThreadArgsForCbTests(&args);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetNoRandomize(opts, true));
    IFOK(s, natsOptions_SetServers(opts, servers, serversCount));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, (void*) &args));
    IFOK(s, natsOptions_SetMaxReconnect(opts, 10));
    IFOK(s, natsOptions_SetReconnectWait(opts, 100));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));

    if (s != NATS_OK)
        FAIL("Unable to setup test");

    serverPid1 = _startServer("nats://127.0.0.1:22222", "-p 22222", false);
    CHECK_SERVER_STARTED(serverPid1);

    serverPid2 = _startServer("nats://127.0.0.1:22223", "-p 22223 --user ivan --pass foo", false);
    if (serverPid2 == NATS_INVALID_PID)
        _stopServer(serverPid1);
    CHECK_SERVER_STARTED(serverPid2);

    serverPid3 = _startServer("nats://127.0.0.1:22224", "-p 22224", false);
    if (serverPid3 == NATS_INVALID_PID)
    {
        _stopServer(serverPid1);
        _stopServer(serverPid2);
    }
    CHECK_SERVER_STARTED(serverPid3);

    nats_Sleep(1000);

    test("Connect should succeed: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    // Stop the server which will trigger the reconnect
    _stopServer(serverPid1);
    serverPid1 = NATS_INVALID_PID;


    // The client will try to connect to the second server, and that
    // should fail. It should then try to connect to the third and succeed.

    // Wait for the reconnect CB.
    test("Reconnect callback should be triggered: ")
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT)
           && !(args.reconnected))
    {
        s = natsCondition_TimedWait(args.c, args.m, 5000);
    }
    natsMutex_Unlock(args.m);
    testCond((s == NATS_OK) && args.reconnected);

    test("Connection should not be closed: ");
    testCond(natsConnection_IsClosed(nc) == false);

    buffer[0] = '\0';
    test("Should have connected to third server: ");
    s = natsConnection_GetConnectedUrl(nc, buffer, sizeof(buffer));
    testCond((s == NATS_OK) && (buffer[0] != '\0')
             && (strcmp(buffer, servers[2]) == 0));

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&args);

    _stopServer(serverPid2);
    _stopServer(serverPid3);
}

void test_BasicClusterReconnect(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid1= NATS_INVALID_PID;
    natsPid             serverPid2= NATS_INVALID_PID;
    char                buffer[128];
    int                 serversCount;
    int64_t             reconnectTimeStart, reconnectTime;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    serversCount = sizeof(testServers) / sizeof(char*);

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_IPResolutionOrder(opts, 4));
    IFOK(s, natsOptions_SetTimeout(opts, 500));
    IFOK(s, natsOptions_SetNoRandomize(opts, true));
    IFOK(s, natsOptions_SetServers(opts, testServers, serversCount));
    IFOK(s, natsOptions_SetDisconnectedCB(opts, _disconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));
    IFOK(s, natsOptions_SetReconnectWait(opts, 100));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));

    if (s != NATS_OK)
        FAIL("Unable to create options for test ServerOptions");

    serverPid1 = _startServer("nats://127.0.0.1:1222", "-p 1222", true);
    CHECK_SERVER_STARTED(serverPid1);

    serverPid2 = _startServer("nats://127.0.0.1:1224", "-p 1224", true);
    if (serverPid2 == NATS_INVALID_PID)
        _stopServer(serverPid1);
    CHECK_SERVER_STARTED(serverPid2);

    test("Check connected to the right server: ");
    s = natsConnection_Connect(&nc, opts);

    _stopServer(serverPid1);

    // wait for disconnect
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.disconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);

    reconnectTimeStart = nats_Now();

    // wait for reconnect
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.reconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 3000);
    natsMutex_Unlock(arg.m);

    testCond((s == NATS_OK)
             && (natsConnection_GetConnectedUrl(nc, buffer, sizeof(buffer)) == NATS_OK)
             && (strcmp(buffer, testServers[2]) == 0));

    // Make sure we did not wait on reconnect for default time.
    // Reconnect should be fast since it will be a switch to the
    // second server and not be dependent on server restart time.
    reconnectTime = nats_Now() - reconnectTimeStart;

    test("Check reconnect time did not take too long: ");
#if _WIN32
    testCond(reconnectTime <= 1300);
#else
    testCond(reconnectTime <= 100);
#endif
    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _waitForConnClosed(&arg);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid2);
}

static const char*
_reconnectTokenHandler(void* closure)
{
    const char *token;
    struct threadArg *args      = (struct threadArg*) closure;

    natsMutex_Lock(args->m);
    token = args->tokens[args->tokenCallCount % (sizeof(args->tokens)/sizeof(char*))];
    args->tokenCallCount++;
    if (args->nc != NULL)
    {
        char        buffer[256] = {'\0'};
        natsStatus  s;

        s = natsConnection_GetConnectedUrl(args->nc, buffer, sizeof(buffer));
        if (s == NATS_OK)
        {
            if (((args->tokenCallCount == 2) && (strcmp(buffer, "nats://127.0.0.1:22223") == 0))
                || ((args->tokenCallCount == 3) && (strcmp(buffer, "nats://127.0.0.1:22224") == 0)))
            {
                args->results[0]++;
                buffer[0] = '\0';
                s = natsConnection_GetConnectedServerId(args->nc, buffer, sizeof(buffer));
                if ((s == NATS_OK) && (strlen(buffer) > 0))
                    args->results[0]++;
            }
        }
    }
    natsMutex_Unlock(args->m);

    return token;
}

void test_ReconnectWithTokenHandler(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid1= NATS_INVALID_PID;
    natsPid             serverPid2= NATS_INVALID_PID;
    natsPid             serverPid3= NATS_INVALID_PID;
    char                buffer[64];
    const char          *servers[] = {"nats://127.0.0.1:22222",
                                      "nats://127.0.0.1:22223",
                                      "nats://127.0.0.1:22224"};
    struct threadArg    args;

    int serversCount = 3;

    s = _createDefaultThreadArgsForCbTests(&args);
    args.tokenCallCount = 0;
    args.tokens[0] = "token1";
    args.tokens[1] = "badtoken";
    args.tokens[2] = "token3";

    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetNoRandomize(opts, true));
    IFOK(s, natsOptions_SetServers(opts, servers, serversCount));
    IFOK(s, natsOptions_SetTokenHandler(opts, _reconnectTokenHandler, (void*) &args));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, (void*) &args));
    IFOK(s, natsOptions_SetMaxReconnect(opts, 10));
    IFOK(s, natsOptions_SetReconnectWait(opts, 100));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));

    if (s != NATS_OK)
        FAIL("Unable to setup test");

    serverPid1 = _startServer("nats://token1@127.0.0.1:22222", "-p 22222 --auth token1", true);
    CHECK_SERVER_STARTED(serverPid1);

    serverPid2 = _startServer("nats://user:foo@127.0.0.1:22223", "-p 22223 --user ivan --pass foo", true);
    if (serverPid2 == NATS_INVALID_PID)
        _stopServer(serverPid1);
    CHECK_SERVER_STARTED(serverPid2);

    serverPid3 = _startServer("nats://token3@127.0.0.1:22224", "-p 22224 --auth token3", true);
    if (serverPid3 == NATS_INVALID_PID)
    {
        _stopServer(serverPid1);
        _stopServer(serverPid2);
    }
    CHECK_SERVER_STARTED(serverPid3);

    test("Connect should succeed: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    natsMutex_Lock(args.m);
    args.nc = nc;
    natsMutex_Unlock(args.m);

    // Stop the server which will trigger the reconnect
    _stopServer(serverPid1);
    serverPid1 = NATS_INVALID_PID;


    // The client will try to connect to the second server, and that
    // should fail. It should then try to connect to the third and succeed.

    // Wait for the reconnect CB.
    test("Reconnect callback should be triggered: ")
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT)
           && !(args.reconnected))
    {
        s = natsCondition_TimedWait(args.c, args.m, 5000);
    }
    natsMutex_Unlock(args.m);
    testCond((s == NATS_OK) && args.reconnected);

    test("Connection should not be closed: ");
    testCond(natsConnection_IsClosed(nc) == false);

    buffer[0] = '\0';
    test("Should have connected to third server: ");
    s = natsConnection_GetConnectedUrl(nc, buffer, sizeof(buffer));
    testCond((s == NATS_OK) && (buffer[0] != '\0')
             && (strcmp(buffer, servers[2]) == 0));

    test("ConnectedURL and ServerID OKs during reconnect process: ");
    natsMutex_Lock(args.m);
    s = ((args.results[0] == 4) ? NATS_OK : NATS_ERR);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&args);

    _stopServer(serverPid2);
    _stopServer(serverPid3);
}

#define NUM_CLIENTS (100)

struct hashCount
{
    int count;

};

void test_HotSpotReconnect(void)
{
    natsStatus          s;
    natsConnection      *nc[NUM_CLIENTS];
    natsOptions         *opts     = NULL;
    natsPid             serverPid1= NATS_INVALID_PID;
    natsPid             serverPid2= NATS_INVALID_PID;
    natsPid             serverPid3= NATS_INVALID_PID;
    char                buffer[128];
    int                 serversCount;
    natsStrHash         *cs = NULL;
    struct threadArg    arg;
    struct hashCount    *count = NULL;

#if _WIN32
    test("Skip when running on Windows: ");
    testCond(true);
    return;
#endif

    memset(nc, 0, sizeof(nc));

    s = natsStrHash_Create(&cs, 4);
    IFOK(s, _createDefaultThreadArgsForCbTests(&arg));
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    serversCount = sizeof(testServers) / sizeof(char*);

    serverPid1 = _startServer("nats://127.0.0.1:1222", "-p 1222", true);
    CHECK_SERVER_STARTED(serverPid1);

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetServers(opts, testServers, serversCount))
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, (void*) &arg));
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    for (int i=0; (s == NATS_OK) && (i<NUM_CLIENTS); i++)
    {
        s = natsConnection_Connect(&(nc[i]), opts);
        IFOK(s, natsConnection_GetConnectedUrl(nc[i], buffer, sizeof(buffer)));
        if ((s == NATS_OK)
            && (strcmp(buffer, testServers[0]) != 0))
        {
            s = NATS_ERR;
        }
    }
    if (s == NATS_OK)
    {
        serverPid2 = _startServer("nats://127.0.0.1:1224", "-p 1224", true);
        serverPid3 = _startServer("nats://127.0.0.1:1226", "-p 1226", true);

        if ((serverPid2 == NATS_INVALID_PID)
            || (serverPid3 == NATS_INVALID_PID))
        {
            _stopServer(serverPid1);
            _stopServer(serverPid2);
            _stopServer(serverPid3);
            FAIL("Unable to start or verify that the server was started!");
        }
    }

    _stopServer(serverPid1);

    // Wait on all reconnects
    test("Check all reconnected: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && (arg.reconnects != NUM_CLIENTS))
        s = natsCondition_TimedWait(arg.c, arg.m, 10000);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && arg.reconnects == NUM_CLIENTS);

    // Walk the clients and calculate how many of each..
    for (int i=0; (s == NATS_OK) && (i<NUM_CLIENTS); i++)
    {
        if (nc[i] == NULL)
        {
            s = NATS_ERR;
            break;
        }

        buffer[0] = '\0';
        s = natsConnection_GetConnectedUrl(nc[i], buffer, sizeof(buffer));
        if (s == NATS_OK)
        {
            count = (struct hashCount*) natsStrHash_Get(cs, buffer);
            if (count == NULL)
            {
                count = (struct hashCount*) calloc(1, sizeof(struct hashCount));
                if (count == NULL)
                    s = NATS_NO_MEMORY;
            }

            if (s == NATS_OK)
            {
                count->count++;
                s = natsStrHash_Set(cs, buffer, true, (void*) count, NULL);
            }
        }

        natsConnection_Close(nc[i]);
    }

    test("Check correct number of servers: ");
    testCond((s == NATS_OK) && (natsStrHash_Count(cs) == 2));

    if (s == NATS_OK)
    {
        // numClients  = 100
        // numServers  = 2
        // expected    = numClients / numServers
        // v           = expected * 0.3

        natsStrHashIter     iter;
        struct hashCount    *val;
        int                 total;
        int                 delta;
        int                 v = (int) (((float)NUM_CLIENTS / 2) * 0.30);
        void                *p = NULL;

        natsStrHashIter_Init(&iter, cs);
        while (natsStrHashIter_Next(&iter, NULL, &p))
        {
            val = (struct hashCount*) p;
            total = val->count;

            delta = ((NUM_CLIENTS / 2) - total);
            if (delta < 0)
                delta *= -1;

            if (delta > v)
                s = NATS_ERR;

            free(val);
        }

        test("Check variance: ");
        testCond(s == NATS_OK);
    }

    for (int i=0; i<NUM_CLIENTS; i++)
        natsConnection_Destroy(nc[i]);

    natsStrHash_Destroy(cs);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid2);
    _stopServer(serverPid3);
}

void test_ProperReconnectDelay(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    int                 serversCount;
    struct threadArg    arg;

#if _WIN32
    test("Skip when running on Windows: ");
    testCond(true);
    return;
#endif

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    serversCount = sizeof(testServers) / sizeof(char*);

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetNoRandomize(opts, true));
    IFOK(s, natsOptions_SetServers(opts, testServers, serversCount));
    IFOK(s, natsOptions_SetDisconnectedCB(opts, _disconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));

    if (s != NATS_OK)
        FAIL("Unable to create options for test ServerOptions");

    serverPid = _startServer("nats://127.0.0.1:1222", "-p 1222", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    _stopServer(serverPid);

    // wait for disconnect
    test("Wait for disconnect: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.disconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && arg.disconnected);

    // Wait, want to make sure we don't spin on reconnect to non-existent servers.
    nats_Sleep(1000);

    // Make sure we are still reconnecting..
    test("ClosedCB should not be invoked: ")
    natsMutex_Lock(arg.m);
    testCond(arg.closed == false);
    natsMutex_Unlock(arg.m);

    test("Should still be reconnecting: ");
    testCond(natsConnection_Status(nc) == NATS_CONN_STATUS_RECONNECTING);

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    // Now that the connection is destroyed, the callback will be invoked.
    // Make sure that we wait until then before destroying 'arg'.
    _waitForConnClosed(&arg);

    _destroyDefaultThreadArgs(&arg);
}

void test_ProperFalloutAfterMaxAttempts(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    int                 serversCount;
    struct threadArg    arg;

#if _WIN32
    test("Skip when running on Windows: ");
    testCond(true);
    return;
#endif

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    serversCount = sizeof(testServers) / sizeof(char*);

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetNoRandomize(opts, true));
    IFOK(s, natsOptions_SetMaxReconnect(opts, 5));
    IFOK(s, natsOptions_SetReconnectWait(opts, 25));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));
    IFOK(s, natsOptions_SetServers(opts, testServers, serversCount));
    IFOK(s, natsOptions_SetDisconnectedCB(opts, _disconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));

    if (s != NATS_OK)
        FAIL("Unable to create options for test ServerOptions");

    serverPid = _startServer("nats://127.0.0.1:1222", "-p 1222", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    _stopServer(serverPid);

    // wait for disconnect
    test("Wait for disconnected: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.disconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && arg.disconnected);

    // wait for closed
    test("Wait for closed: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && arg.closed);

    test("Disconnected should have been called only once: ");
    testCond((s == NATS_OK) && arg.disconnects == 1);

    test("Connection should be closed: ")
    testCond((s == NATS_OK)
             && natsConnection_IsClosed(nc));

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);
}

void test_StopReconnectAfterTwoAuthErr(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    natsPid             serverPid2= NATS_INVALID_PID;
    const char          *servers[]= {"nats://127.0.0.1:1222", "nats://127.0.0.1:1223"};
    natsStatistics      *stats    = NULL;
    int                 serversCount;
    struct threadArg    arg;

//#if _WIN32
//    test("Skip when running on Windows: ");
//    testCond(true);
//    return;
//#endif

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    serversCount = sizeof(servers) / sizeof(char*);

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetNoRandomize(opts, true));
    IFOK(s, natsOptions_SetMaxReconnect(opts, -1));
    IFOK(s, natsOptions_SetReconnectWait(opts, 25));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));
    IFOK(s, natsOptions_SetServers(opts, servers, serversCount));
    IFOK(s, natsOptions_SetDisconnectedCB(opts, _disconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));
#if _WIN32
    IFOK(s, natsOptions_SetTimeout(opts, 500));
#endif
    IFOK(s, natsStatistics_Create(&stats));

    if (s != NATS_OK)
        FAIL("Unable to create options for test ServerOptions");

    serverPid = _startServer("nats://127.0.0.1:1222", "-p 1222", true);
    CHECK_SERVER_STARTED(serverPid);

    serverPid2 = _startServer("nats://127.0.0.1:1223", "-p 1223 -user ivan -pass secret", true);
    if (serverPid == NATS_INVALID_PID)
        _stopServer(serverPid);
    CHECK_SERVER_STARTED(serverPid2);

    test("Connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    _stopServer(serverPid);

    // wait for disconnect
    test("Wait for disconnected: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.disconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && arg.disconnected);

    // wait for closed
    test("Wait for closed: ");
    s = _waitForConnClosed(&arg);
    testCond(s == NATS_OK);

    // Make sure we have not exceeded MaxReconnect
    test("Check reconnect twice: ");
    s = natsConnection_GetStats(nc, stats);
    testCond((s == NATS_OK)
             && (stats != NULL)
             && (stats->reconnects == 2));

    // Disconnect CB only from disconnect from server 1.
    test("Disconnected should have been called once: ");
    testCond((s == NATS_OK) && arg.disconnects == 1);

    test("Connection should be closed: ")
    testCond((s == NATS_OK)
             && natsConnection_IsClosed(nc));

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);
    natsStatistics_Destroy(stats);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid2);
}

void test_TimeoutOnNoServer(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    int                 serversCount;
    int64_t             startWait, timedWait;
    struct threadArg    arg;

#if _WIN32
    test("Skip when running on Windows: ");
    testCond(true);
    return;
#endif

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    serversCount = sizeof(testServers) / sizeof(char*);

    // 1000 milliseconds total time wait
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetNoRandomize(opts, true));
    IFOK(s, natsOptions_SetMaxReconnect(opts, 10));
    IFOK(s, natsOptions_SetReconnectWait(opts, 100));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));
    IFOK(s, natsOptions_SetServers(opts, testServers, serversCount));
    IFOK(s, natsOptions_SetDisconnectedCB(opts, _disconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));

    if (s != NATS_OK)
        FAIL("Unable to create options for test ServerOptions");

    serverPid = _startServer("nats://127.0.0.1:1222", "-p 1222", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    _stopServer(serverPid);

    // wait for disconnect
    test("Wait for disconnected: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.disconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && arg.disconnected);

    startWait = nats_Now();

    // wait for closed
    test("Wait for closed: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000 + serversCount*50);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && arg.closed);

    timedWait = nats_Now() - startWait;

    // The client will try to reconnect to serversCount servers.
    // It will do that for MaxReconnects==10 times.
    // For a server that has been already tried, it should sleep
    // ReconnectWait==100ms. When a server is not running, the connect
    // failure on UNIXes should be fast, still account for that.
    test("Check wait time for closed cb: ");
    testCond(timedWait <= ((opts->maxReconnect * opts->reconnectWait) + serversCount*opts->maxReconnect*50));

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);
}

void test_PingReconnect(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    int                 serversCount;
    int64_t             disconnectedAt, reconnectedAt, pingCycle;
    struct threadArg    arg;

#if _WIN32
    test("Skip when running on Windows: ");
    testCond(true);
    return;
#endif

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    arg.control = 9;

    serversCount = sizeof(testServers) / sizeof(char*);

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetNoRandomize(opts, true));
    IFOK(s, natsOptions_SetReconnectWait(opts, 200));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));
    IFOK(s, natsOptions_SetPingInterval(opts, 50));
    IFOK(s, natsOptions_SetMaxPingsOut(opts, -1));
    IFOK(s, natsOptions_SetServers(opts, testServers, serversCount));
    IFOK(s, natsOptions_SetDisconnectedCB(opts, _disconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));

    if (s != NATS_OK)
        FAIL("Unable to create options for test ServerOptions");

    serverPid = _startServer("nats://127.0.0.1:1222", "-p 1222", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    test("Pings cause reconnects: ")
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && (arg.reconnects != 4))
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && (arg.reconnects == 4));

    natsConnection_Destroy(nc);

    for (int i=0; i<(4-1); i++)
    {
        disconnectedAt = arg.disconnectedAt[i];
        reconnectedAt = arg.reconnectedAt[i];

        pingCycle = reconnectedAt - disconnectedAt;
        if (pingCycle > 2 * opts->pingInterval)
        {
            s = NATS_ERR;
            break;
        }
    }

    test("Reconnect due to ping cycle correct: ");
    testCond(s == NATS_OK);

    // Wait for connection closed before destroying arg.
    natsMutex_Lock(arg.m);
    while (!arg.closed)
        natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);

    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(serverPid);

}

void test_GetServers(void)
{
    natsStatus          s;
    natsConnection      *conn = NULL;
    natsPid             s1Pid = NATS_INVALID_PID;
    natsPid             s2Pid = NATS_INVALID_PID;
    natsPid             s3Pid = NATS_INVALID_PID;
    char                **servers = NULL;
    int                 count     = 0;

    s1Pid = _startServer("nats://127.0.0.1:4222", "-a 127.0.0.1 -p 4222 -cluster nats://127.0.0.1:5222 -cluster_name abc", true);
    CHECK_SERVER_STARTED(s1Pid);

    s2Pid = _startServer("nats://127.0.0.1:4223", "-a 127.0.0.1 -p 4223 -cluster nats://127.0.0.1:5223 -cluster_name abc -routes nats://127.0.0.1:5222", true);
    if (s2Pid == NATS_INVALID_PID)
        _stopServer(s1Pid);
    CHECK_SERVER_STARTED(s2Pid);

    s3Pid = _startServer("nats://127.0.0.1:4224", "-a 127.0.0.1 -p 4224 -cluster nats://127.0.0.1:5224 -cluster_name abc -routes nats://127.0.0.1:5222", true);
    if (s3Pid == NATS_INVALID_PID)
    {
        _stopServer(s1Pid);
        _stopServer(s2Pid);
    }
    CHECK_SERVER_STARTED(s3Pid);

    test("Get Servers: ");
    s = natsConnection_ConnectTo(&conn, "nats://127.0.0.1:4222");
    IFOK(s, natsConnection_GetServers(conn, &servers, &count));
    if (s == NATS_OK)
    {
        int i;

        // Be tolerant that if we were to connect to an older
        // server, we would just get 1 url back.
        if ((count != 1) && (count != 3))
            s = nats_setError(NATS_ERR, "Unexpected number of servers: %d instead of 1 or 3", count);

        for (i=0; (s == NATS_OK) && (i < count); i++)
        {
            if ((strcmp(servers[i], "nats://127.0.0.1:4222") != 0)
                    && (strcmp(servers[i], "nats://127.0.0.1:4223") != 0)
                    && (strcmp(servers[i], "nats://127.0.0.1:4224") != 0))
            {
                s = nats_setError(NATS_ERR, "Unexpected server URL: %s", servers[i]);
            }
        }

        for (i=0; i<count; i++)
            free(servers[i]);
        free(servers);
    }
    testCond(s == NATS_OK);

    natsConnection_Destroy(conn);
    conn = NULL;

    _stopServer(s3Pid);
    _stopServer(s2Pid);
    _stopServer(s1Pid);

    s1Pid = NATS_INVALID_PID;
    s1Pid = _startServer("nats://ivan:password@127.0.0.1:4222", "-a 127.0.0.1 -p 4222 -user ivan -pass password", true);
    CHECK_SERVER_STARTED(s1Pid);

    test("Get Servers does not return credentials: ");
    s = natsConnection_ConnectTo(&conn, "nats://ivan:password@127.0.0.1:4222");
    IFOK(s, natsConnection_GetServers(conn, &servers, &count));
    if (s == NATS_OK)
    {
        if (count != 1)
            s = nats_setError(NATS_ERR, "Unexpected number of servers: %d instead of 1", count);
        else if (strcmp(servers[0], "nats://127.0.0.1:4222") != 0)
            s = nats_setError(NATS_ERR, "Unexpected server URL: %s", servers[0]);

        free(servers[0]);
        free(servers);
    }
    testCond(s == NATS_OK);

    natsConnection_Destroy(conn);
    _stopServer(s1Pid);
}

void test_GetDiscoveredServers(void)
{
    natsStatus          s;
    natsConnection      *conn = NULL;
    natsPid             s1Pid = NATS_INVALID_PID;
    natsPid             s2Pid = NATS_INVALID_PID;
    char                **servers = NULL;
    int                 count     = 0;

    s1Pid = _startServer("nats://127.0.0.1:4222", "-a 127.0.0.1 -p 4222 -cluster nats://127.0.0.1:5222 -cluster_name abc", true);
    CHECK_SERVER_STARTED(s1Pid);

    s2Pid = _startServer("nats://127.0.0.1:4223", "-a 127.0.0.1 -p 4223 -cluster nats://127.0.0.1:5223 -cluster_name abc -routes nats://127.0.0.1:5222", true);
    if (s2Pid == NATS_INVALID_PID)
        _stopServer(s1Pid);
    CHECK_SERVER_STARTED(s2Pid);

    test("GetDiscoveredServers: ");
    s = natsConnection_ConnectTo(&conn, "nats://127.0.0.1:4222");
    IFOK(s, natsConnection_GetDiscoveredServers(conn, &servers, &count));
    if (s == NATS_OK)
    {
        int i;

        // Be tolerant that if we were to connect to an older
        // server, we would get nothing
        if (count > 1)
            s = nats_setError(NATS_ERR, "Unexpected number of servers: %d instead of 1 or 0", count);

        for (i=0; (s == NATS_OK) && (i < count); i++)
        {
            if (strcmp(servers[i], "nats://127.0.0.1:4223") != 0)
                s = nats_setError(NATS_ERR, "Unexpected server URL: %s", servers[i]);
        }

        for (i=0; i<count; i++)
            free(servers[i]);
        free(servers);
    }
    testCond(s == NATS_OK);

    natsConnection_Destroy(conn);
    conn = NULL;

    _stopServer(s2Pid);
    _stopServer(s1Pid);
}

static void
_discoveredServersCb(natsConnection *nc, void *closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);
    arg->sum++;
    natsCondition_Signal(arg->c);
    natsMutex_Unlock(arg->m);
}

void test_DiscoveredServersCb(void)
{
    natsStatus          s;
    natsConnection      *conn = NULL;
    natsPid             s1Pid = NATS_INVALID_PID;
    natsPid             s2Pid = NATS_INVALID_PID;
    natsPid             s3Pid = NATS_INVALID_PID;
    natsOptions         *opts = NULL;
    struct threadArg    arg;
    int                 invoked = 0;

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:4222"));
    IFOK(s, natsOptions_SetDiscoveredServersCB(opts, _discoveredServersCb, &arg));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    s1Pid = _startServer("nats://127.0.0.1:4222", "-a 127.0.0.1 -p 4222 -cluster nats-route://127.0.0.1:5222 -cluster_name abc", true);
    CHECK_SERVER_STARTED(s1Pid);

    s2Pid = _startServer("nats://127.0.0.1:4223", "-a 127.0.0.1 -p 4223 -cluster nats-route://127.0.0.1:5223 -cluster_name abc -routes nats-route://127.0.0.1:5222", true);
    if (s2Pid == NATS_INVALID_PID)
        _stopServer(s1Pid);
    CHECK_SERVER_STARTED(s2Pid);

    test("DiscoveredServersCb not triggered on initial connect: ");
    s = natsConnection_Connect(&conn, opts);
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && (arg.sum == 0))
        s = natsCondition_TimedWait(arg.c, arg.m, 500);
    invoked = arg.sum;
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_TIMEOUT) && (invoked == 0));
    s = NATS_OK;

    s3Pid = _startServer("nats://127.0.0.1:4224", "-a 127.0.0.1 -p 4224 -cluster nats-route://127.0.0.1:5224 -cluster_name abc -routes nats-route://127.0.0.1:5222", true);
    if (s3Pid == NATS_INVALID_PID)
    {
        _stopServer(s1Pid);
        _stopServer(s2Pid);
    }
    CHECK_SERVER_STARTED(s3Pid);

    test("DiscoveredServersCb triggered on new server joining the cluster: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && (arg.sum == 0))
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    invoked = arg.sum;
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && (invoked == 1));

    natsConnection_Destroy(conn);
    natsOptions_Destroy(opts);

    _stopServer(s3Pid);
    _stopServer(s2Pid);
    _stopServer(s1Pid);

    _destroyDefaultThreadArgs(&arg);
}

void test_IgnoreDiscoveredServers(void)
{
   natsStatus          s;
    natsConnection      *conn = NULL;
    natsPid             s1Pid = NATS_INVALID_PID;
    natsPid             s2Pid = NATS_INVALID_PID;
    natsOptions         *opts = NULL;
    struct threadArg    arg;
    int                 invoked = 0;

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:4222"));
    IFOK(s, natsOptions_SetIgnoreDiscoveredServers(opts, true));
    IFOK(s, natsOptions_SetDiscoveredServersCB(opts, _discoveredServersCb, &arg));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    s1Pid = _startServer("nats://127.0.0.1:4222", "-a 127.0.0.1 -p 4222 -cluster nats-route://127.0.0.1:5222 -cluster_name abc", true);
    CHECK_SERVER_STARTED(s1Pid);

    test("Connect: ");
    s = natsConnection_Connect(&conn, opts);
    testCond(s == NATS_OK);

    test("Start new server: ");
    s2Pid = _startServer("nats://127.0.0.1:4223", "-a 127.0.0.1 -p 4223 -cluster nats-route://127.0.0.1:5223 -cluster_name abc -routes nats-route://127.0.0.1:5222", true);
    CHECK_SERVER_STARTED(s2Pid);
    testCond(true);

    test("Check discovered ignored: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && (arg.sum == 0))
        s = natsCondition_TimedWait(arg.c, arg.m, 1000);
    invoked = arg.sum;
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_TIMEOUT) && (invoked == 0));
    nats_clearLastError();

    test("Check server pool: ");
    natsConn_Lock(conn);
    s = (natsSrvPool_GetSize(conn->srvPool) == 1 ? NATS_OK : NATS_ERR);
    natsConn_Unlock(conn);
    testCond(s == NATS_OK);

    natsConnection_Destroy(conn);
    natsOptions_Destroy(opts);

    _stopServer(s2Pid);
    _stopServer(s1Pid);

    _destroyDefaultThreadArgs(&arg);
}

static void
_serverSendsINFOAfterPONG(void *closure)
{
    struct threadArg    *arg    = (struct threadArg*) closure;
    natsStatus          s       = NATS_OK;
    natsSock            sock    = NATS_SOCK_INVALID;
    natsSockCtx         ctx;

    memset(&ctx, 0, sizeof(natsSockCtx));

    // We will hand run a fake server that will send an INFO protocol
    // right after sending the initial PONG.

    s = _startMockupServer(&sock, "127.0.0.1", "4222");

    natsMutex_Lock(arg->m);
    arg->status = s;
    arg->done = true;
    natsCondition_Signal(arg->c);
    natsMutex_Unlock(arg->m);

    if ((s == NATS_OK)
            && (((ctx.fd = accept(sock, NULL, NULL)) == NATS_SOCK_INVALID)
                    || (natsSock_SetCommonTcpOptions(ctx.fd) != NATS_OK)))
    {
        s = NATS_SYS_ERROR;
    }
    if (s == NATS_OK)
    {
        const char* info = "INFO {}\r\n";

        s = natsSock_WriteFully(&ctx, info, (int) strlen(info));
    }
    if (s == NATS_OK)
    {
        char buffer[1024];

        memset(buffer, 0, sizeof(buffer));

        // Read connect and ping commands sent from the client
        s = natsSock_ReadLine(&ctx, buffer, sizeof(buffer));
        IFOK(s, natsSock_ReadLine(&ctx, buffer, sizeof(buffer)));
    }
    // Send PONG + INFO
    if (s == NATS_OK)
    {
        char buffer[1024];

        snprintf(buffer, sizeof(buffer), "PONG\r\nINFO {\"connect_urls\":[\"127.0.0.1:4222\",\"me:1\"]}\r\n");

        s = natsSock_WriteFully(&ctx, buffer, (int) strlen(buffer));
    }

    // Wait for a signal from the client thread.
    natsMutex_Lock(arg->m);
    while (!arg->closed)
        natsCondition_Wait(arg->c, arg->m);
    arg->status = s;
    natsMutex_Unlock(arg->m);

    natsSock_Close(ctx.fd);
    natsSock_Close(sock);
}

void test_ReceiveINFORightAfterFirstPONG(void)
{
    natsStatus          s       = NATS_OK;
    natsThread          *t      = NULL;
    natsConnection      *nc     = NULL;
    natsOptions         *opts   = NULL;
    struct threadArg arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:4222"));
    IFOK(s, natsOptions_SetAllowReconnect(opts, false));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    test("Verify that INFO right after PONG is ok: ");

    s = natsThread_Create(&t, _serverSendsINFOAfterPONG, (void*) &arg);
    if (s == NATS_OK)
    {
        natsMutex_Lock(arg.m);
        while (!arg.done)
            natsCondition_Wait(arg.c, arg.m);
        s = arg.status;
        natsMutex_Unlock(arg.m);
    }
    IFOK(s, natsConnection_Connect(&nc, opts));
    if (s == NATS_OK)
    {
        int     i, j;
        char    **servers    = NULL;
        int     serversCount = 0;
        bool    ok           = false;

        for (i = 0; i < 100; i++)
        {
            s = natsConnection_GetDiscoveredServers(nc, &servers, &serversCount);
            if (s != NATS_OK)
                break;

            ok = ((serversCount == 1)
                    && (strcmp(servers[0], "nats://me:1") == 0));

            for (j = 0; j < serversCount; j++)
                free(servers[j]);
            free(servers);

            if (ok)
                break;

            nats_Sleep(15);
            s = NATS_ERR;
        }
    }
    if (t != NULL)
    {
        natsMutex_Lock(arg.m);
        arg.closed = true;
        natsCondition_Signal(arg.c);
        natsMutex_Unlock(arg.m);

        natsThread_Join(t);
        natsThread_Destroy(t);

        natsMutex_Lock(arg.m);
        if ((s == NATS_OK) && (arg.status != NATS_OK))
            s = arg.status;
        natsMutex_Unlock(arg.m);
    }
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);
}

void test_ServerPoolUpdatedOnClusterUpdate(void)
{
    natsStatus          s;
    natsConnection      *conn = NULL;
    natsPid             s1Pid = NATS_INVALID_PID;
    natsPid             s2Pid = NATS_INVALID_PID;
    natsPid             s3Pid = NATS_INVALID_PID;
    natsOptions         *opts = NULL;
    struct threadArg    arg;
    int                 invoked = 0;
    bool                restartS2 = false;

    if (!serverVersionAtLeast(1,0,7))
    {
        char txt[200];

        snprintf(txt, sizeof(txt), "Skipping since requires server version of at least 1.0.7, got %s: ", serverVersion);
        test(txt);
        testCond(true);
        return;
    }

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
        opts = _createReconnectOptions();
    if ((opts == NULL)
            || (natsOptions_SetURL(opts, "nats://127.0.0.1:4222") != NATS_OK)
            || (natsOptions_SetDiscoveredServersCB(opts, _discoveredServersCb, &arg))
            || (natsOptions_SetReconnectedCB(opts, _reconnectedCb, &arg) != NATS_OK)
            || (natsOptions_SetClosedCB(opts, _closedCb, &arg) != NATS_OK))
    {
        FAIL("Unable to create reconnect options!");
    }

    s1Pid = _startServer("nats://127.0.0.1:4222", "-a 127.0.0.1 -p 4222 -cluster nats://127.0.0.1:6222 -cluster_name abc -routes nats://127.0.0.1:6223,nats://127.0.0.1:6224", true);
    CHECK_SERVER_STARTED(s1Pid);

    test("Connect ok: ");
    s = natsConnection_Connect(&conn, opts);
    testCond(s == NATS_OK);

    s2Pid = _startServer("nats://127.0.0.1:4223", "-a 127.0.0.1 -p 4223 -cluster nats://127.0.0.1:6223 -cluster_name abc -routes nats://127.0.0.1:6222,nats://127.0.0.1:6224", true);
    if (s2Pid == NATS_INVALID_PID)
        _stopServer(s1Pid);
    CHECK_SERVER_STARTED(s2Pid);

    test("DiscoveredServersCb triggered: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && (arg.sum == 0))
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    invoked = arg.sum;
    arg.sum = 0;
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && (invoked == 1));

    {
        const char *urls[] = {"127.0.0.1:4222", "127.0.0.1:4223"};

        test("Check pool: ");
        s = _checkPool(conn, (char**)urls, (int)(sizeof(urls)/sizeof(char*)));
        testCond(s == NATS_OK);
    }

    s3Pid = _startServer("nats://127.0.0.1:4224", "-a 127.0.0.1 -p 4224 -cluster nats://127.0.0.1:6224 -cluster_name abc -routes nats://127.0.0.1:6222,nats://127.0.0.1:6223", true);
    if (s3Pid == NATS_INVALID_PID)
    {
        _stopServer(s1Pid);
        _stopServer(s2Pid);
    }
    CHECK_SERVER_STARTED(s3Pid);

    test("DiscoveredServersCb triggered: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && (arg.sum == 0))
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    invoked = arg.sum;
    arg.sum = 0;
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && (invoked == 1));

    {
        const char *urls[] = {"127.0.0.1:4222", "127.0.0.1:4223", "127.0.0.1:4224"};
        test("Check pool: ");
        s = _checkPool(conn, (char**)urls, (int)(sizeof(urls)/sizeof(char*)));
        testCond(s == NATS_OK);
    }

    // Stop s1. Since this was passed to the Connect() call, this one should
    // still be present.
    _stopServer(s1Pid);
    s1Pid = NATS_INVALID_PID;

    test("Wait for reconnect: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.reconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    arg.reconnected = false;
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    {
        const char *urls[] = {"127.0.0.1:4222", "127.0.0.1:4223", "127.0.0.1:4224"};

        test("Check pool: ");
        s = _checkPool(conn, (char**)urls, (int)(sizeof(urls)/sizeof(char*)));
        testCond(s == NATS_OK);
    }

    {
        const char *urls[] = {"127.0.0.1:4222", ""};
        int port = 0;

        // Check the server we reconnected to.
        natsMutex_Lock(conn->mu);
        port = conn->cur->url->port;
        natsMutex_Unlock(conn->mu);

        if (port == 4223)
        {
            urls[1] = "127.0.0.1:4224";
            _stopServer(s2Pid);
            s2Pid = NATS_INVALID_PID;
            restartS2 = true;
        }
        else
        {
            urls[1] = "127.0.0.1:4223";
            _stopServer(s3Pid);
            s3Pid = NATS_INVALID_PID;
        }

        test("Wait for reconnect: ");
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.reconnected)
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        arg.reconnected = false;
        natsMutex_Unlock(arg.m);
        testCond(s == NATS_OK);

        // The implicit server that we just shutdown should have been removed from the pool
        if (s == NATS_OK)
        {
            test("Check pool: ");
            s = _checkPool(conn, (char**)urls, (int)(sizeof(urls)/sizeof(char*)));
            testCond(s == NATS_OK);
        }
    }

    {
        const char *urls[] = {"127.0.0.1:4222", "127.0.0.1:4223", "127.0.0.1:4224"};

        if (restartS2)
        {
            s2Pid = _startServer("nats://127.0.0.1:4223", "-a 127.0.0.1 -p 4223 -cluster nats://127.0.0.1:6223 -cluster_name abc -routes nats://127.0.0.1:6222,nats://127.0.0.1:6224", true);
            if (s2Pid == NATS_INVALID_PID)
                _stopServer(s3Pid);
            CHECK_SERVER_STARTED(s2Pid);
        }
        else
        {
            s3Pid = _startServer("nats://127.0.0.1:4224", "-a 127.0.0.1 -p 4224 -cluster nats://127.0.0.1:6224 -cluster_name abc -routes nats://127.0.0.1:6222,nats://127.0.0.1:6223", true);
            if (s3Pid == NATS_INVALID_PID)
                _stopServer(s2Pid);
            CHECK_SERVER_STARTED(s3Pid);
        }
        // Since this is not a "new" server, the DiscoveredServersCB won't be invoked.

        // Checking the pool may fail for a while.
        test("Check pool: ");
        s = _checkPool(conn, (char**)urls, (int)(sizeof(urls)/sizeof(char*)));
        testCond(s == NATS_OK);
    }

    natsConnection_Close(conn);
    _waitForConnClosed(&arg);
    natsConnection_Destroy(conn);
    conn = NULL;

    // Restart s1
    s1Pid = _startServer("nats://127.0.0.1:4222", "-a 127.0.0.1 -p 4222 -cluster nats://127.0.0.1:6222 -cluster_name abc -routes nats://127.0.0.1:6223,nats://127.0.0.1:6224", true);
    if (s1Pid == NATS_INVALID_PID)
    {
        _stopServer(s2Pid);
        _stopServer(s3Pid);
    }
    CHECK_SERVER_STARTED(s1Pid);

    // We should have all 3 servers running now...
    test("Connect ok: ");
    s = natsConnection_Connect(&conn, opts);
    testCond(s == NATS_OK);

    {
        int     i;
        natsSrv *srvrs[3];

        test("Server pool size should be 3: ");
        natsMutex_Lock(conn->mu);
        s = (conn->srvPool->size == 3 ? NATS_OK : NATS_ERR);
        natsMutex_Unlock(conn->mu);
        testCond(s == NATS_OK);

        // Save references to servers from pool
        natsMutex_Lock(conn->mu);
        for (i=0; i<3; i++)
            srvrs[i] = conn->srvPool->srvrs[i];
        natsMutex_Unlock(conn->mu);

        for (i=0; (s == NATS_OK) && (i<9); i++)
        {
            natsMutex_Lock(conn->mu);
            natsSock_Shutdown(conn->sockCtx.fd);
            natsMutex_Unlock(conn->mu);

            test("Wait for reconnect: ");
            natsMutex_Lock(arg.m);
            while ((s != NATS_TIMEOUT) && !arg.reconnected)
                s = natsCondition_TimedWait(arg.c, arg.m, 2000);
            arg.reconnected = false;
            natsMutex_Unlock(arg.m);
            testCond(s == NATS_OK);
        }

        {
            int j;

            natsMutex_Lock(conn->mu);
            test("Server pool size should be 3: ");
            s = (conn->srvPool->size == 3 ? NATS_OK : NATS_ERR);
            natsMutex_Unlock(conn->mu);
            testCond(s == NATS_OK);

            test("Servers in pool have not been replaced: ");
            natsMutex_Lock(conn->mu);
            for (i=0; (s == NATS_OK) && (i<3); i++)
            {
                natsSrv *srv = conn->srvPool->srvrs[i];

                s = NATS_ERR;
                for (j=0; j<3; j++)
                {
                    if (srvrs[j] == srv)
                    {
                        s = NATS_OK;
                        break;
                    }
                }
            }
            natsMutex_Unlock(conn->mu);
            testCond(s == NATS_OK);
        }

        natsConnection_Close(conn);
        _waitForConnClosed(&arg);
    }

    natsConnection_Destroy(conn);
    natsOptions_Destroy(opts);

    _stopServer(s3Pid);
    _stopServer(s2Pid);
    _stopServer(s1Pid);

    _destroyDefaultThreadArgs(&arg);
}

void test_ReconnectJitter(void)
{
    natsStatus          s       = NATS_OK;
    natsConnection      *nc     = NULL;
    natsOptions         *opts   = NULL;
    natsPid             pid     = NATS_INVALID_PID;
    int64_t             start   = 0;
    int64_t             dur     = 0;
    struct threadArg arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    test("Default jitter values: ");
    natsOptions_lock(opts);
    s = (((opts->reconnectJitter == NATS_OPTS_DEFAULT_RECONNECT_JITTER)
            && (opts->reconnectJitterTLS == NATS_OPTS_DEFAULT_RECONNECT_JITTER_TLS)) ? NATS_OK : NATS_ERR);
    natsOptions_unlock(opts);
    testCond(s == NATS_OK);

    s = natsOptions_SetURL(opts, "nats://127.0.0.1:4222");
    IFOK(s, natsOptions_SetMaxReconnect(opts, -1));
    IFOK(s, natsOptions_SetReconnectWait(opts, 50));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 500, 0));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, (void*) &arg));

    pid = _startServer("nats://127.0.0.1:4222", "-p 4222", true);
    CHECK_SERVER_STARTED(pid);

    test("Connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    // Shutdown server
    _stopServer(pid);
    pid = NATS_INVALID_PID;

    // Wait for several iterations of failed attempts and make
    // sure that overall wait time is a bit more than just
    // the number of reconnect attempts.
    start = nats_Now();
    nats_Sleep(400);
    pid = _startServer("nats://127.0.0.1:4222", "-p 4222", true);
    CHECK_SERVER_STARTED(pid);

    test("Check jitter: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.reconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    if (s == NATS_OK)
        dur = nats_Now() - start;
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && (dur >=500));

    natsConnection_Destroy(nc);
    nc = NULL;

    // Use a long reconnect wait
    s = natsOptions_SetReconnectWait(opts, 10*60*1000); // 10 minutes
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    test("Connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    // Cause a disconnect
    _stopServer(pid);
    pid = NATS_INVALID_PID;
    // Wait a bit for the reconnect loop to go into wait mode.
    nats_Sleep(50);
    pid = _startServer("nats://127.0.0.1:4222", "-p 4222", true);
    CHECK_SERVER_STARTED(pid);
    // Now close and expect the reconnect thread to return..
    natsConnection_Close(nc);

    test("Wait for closed: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Check reconnect thread done: ");
    natsConn_Lock(nc);
    s = (nc->reconnectThread == NULL ? NATS_OK : NATS_ERR);
    natsConn_Unlock(nc);
    testCond(s == NATS_OK)

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);
    _stopServer(pid);
    _destroyDefaultThreadArgs(&arg);
}

static int64_t
_customReconnectDelayCB(natsConnection *nc, int attempts, void *closure)
{
    struct threadArg *arg  = (struct threadArg*) closure;
    int64_t          delay = 0;

    natsMutex_Lock(arg->m);
    if (attempts != arg->control)
    {
        arg->status = NATS_ERR;
        natsCondition_Signal(arg->c);
    }
    else
    {
        arg->control++;
        if (attempts <= 4)
            delay = 100;
        else
            natsConnection_Close(nc);
    }
    natsMutex_Unlock(arg->m);
    return delay;
}

void test_CustomReconnectDelay(void)
{
    natsStatus          s       = NATS_OK;
    natsConnection      *nc     = NULL;
    natsOptions         *opts   = NULL;
    natsPid             pid     = NATS_INVALID_PID;
    int64_t             start   = 0;
    int64_t             dur     = 0;
    struct threadArg arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:4222"));
#if _WIN32
    IFOK(s, natsOptions_SetTimeout(opts, 100));
#endif
    IFOK(s, natsOptions_SetMaxReconnect(opts, -1));
    IFOK(s, natsOptions_SetCustomReconnectDelay(opts, _customReconnectDelayCB, (void*) &arg));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    arg.control = 1;
    arg.status  = NATS_OK;

    pid = _startServer("nats://127.0.0.1:4222", "-p 4222", true);
    CHECK_SERVER_STARTED(pid);

    test("Connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    // Cause disconnect
    _stopServer(pid);
    pid = NATS_INVALID_PID;

    // We should be trying to reconnect 4 times
    start = nats_Now();

    // Wait on error or completion of test.
    test("Check custom delay cb: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed && (arg.status == NATS_OK))
        s = natsCondition_TimedWait(arg.c, arg.m, 5000);
    IFOK(s, arg.status);
    if (s == NATS_OK)
        dur = nats_Now()-start;
    natsMutex_Unlock(arg.m);
#if _WIN32
    testCond((s == NATS_OK) && (dur <= 1000));
#else
    testCond((s == NATS_OK) && (dur <= 600));
#endif

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);
}

static void
_lameDuckDiscoveredCb(natsConnection *nc, void *closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;
    natsStatus          s;
    char                **servers = NULL;
    int                 count     = 0;

    natsMutex_Lock(arg->m);
    s = natsConnection_GetDiscoveredServers(nc, &servers, &count);
    if (s == NATS_OK)
    {
        int i;

        if ((count != 1) || (strcmp(servers[0], "nats://127.0.0.1:1234") != 0))
            arg->status = NATS_ERR;

        for (i=0; i<count; i++)
            free(servers[i]);
        free(servers);
    }
    // Use this flag to indicate that discovered cb was invoked.
    arg->done = true;
    natsCondition_Signal(arg->c);
    natsMutex_Unlock(arg->m);
}

static void
_lameDuckCb(natsConnection *nc, void *closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);
    // Use this flag to indicate that LDM cb was invoked.
    arg->disconnected = true;
    natsCondition_Signal(arg->c);
    natsMutex_Unlock(arg->m);
}

static void
_lameDuckMockupServerThread(void *closure)
{
    natsStatus          s = NATS_OK;
    natsSock            sock = NATS_SOCK_INVALID;
    struct threadArg    *arg = (struct threadArg*) closure;
    char                buffer[1024];
    const char          *ldm[] = {"INFO {\"ldm\":true}\r\n", "INFO {\"connect_urls\":[\"127.0.0.1:1234\"],\"ldm\":true}\r\n"};
    natsSockCtx         ctx;
    int                 i;

    memset(&ctx, 0, sizeof(natsSockCtx));

    s = _startMockupServer(&sock, "127.0.0.1", "4222");
    natsMutex_Lock(arg->m);
    arg->status = s;
    natsCondition_Signal(arg->c);
    natsMutex_Unlock(arg->m);

    for (i=0; (s == NATS_OK) && (i<2); i++)
    {
        if (((ctx.fd = accept(sock, NULL, NULL)) == NATS_SOCK_INVALID)
                || (natsSock_SetCommonTcpOptions(ctx.fd) != NATS_OK))
        {
            s = NATS_SYS_ERROR;
        }
        if (s == NATS_OK)
        {
            const char *info = "INFO {\"server_id\":\"foobar\"}\r\n";

            // Send INFO.
            s = natsSock_WriteFully(&ctx, info, (int) strlen(info));
            if (s == NATS_OK)
            {
                memset(buffer, 0, sizeof(buffer));

                // Read connect and ping commands sent from the client
                s = natsSock_ReadLine(&ctx, buffer, sizeof(buffer));
                IFOK(s, natsSock_ReadLine(&ctx, buffer, sizeof(buffer)));
            }
            // Send PONG
            IFOK(s, natsSock_WriteFully(&ctx,_PONG_PROTO_, _PONG_PROTO_LEN_));
            if (s == NATS_OK)
            {
                // Wait a bit and then send a INFO with LDM
                nats_Sleep(100);
                s = natsSock_WriteFully(&ctx, ldm[i], (int) strlen(ldm[i]));
                // Wait for client to close
                if (s == NATS_OK)
                    natsSock_ReadLine(&ctx, buffer, sizeof(buffer));
            }
            natsSock_Close(ctx.fd);
        }
    }

    natsSock_Close(sock);
}

void test_LameDuckMode(void)
{
    natsStatus          s       = NATS_OK;
    natsConnection      *nc     = NULL;
    natsOptions         *opts   = NULL;
    natsThread          *t      = NULL;
    int                 i;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:4222"));
    IFOK(s, natsOptions_SetMaxReconnect(opts, -1));
    IFOK(s, natsOptions_SetDiscoveredServersCB(opts, _lameDuckDiscoveredCb, (void*) &arg));
    IFOK(s, natsOptions_SetLameDuckModeCB(opts, _lameDuckCb, (void*) &arg));
    // IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    // Set this to error, the mock server should set it to OK
    // if it can start successfully.
    arg.status = NATS_ERR;
    s = natsThread_Create(&t, _lameDuckMockupServerThread, (void*) &arg);
    if (s == NATS_OK)
    {
        // Wait for server to be ready
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && (arg.status != NATS_OK))
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        natsMutex_Unlock(arg.m);
    }

    for (i=0; i<2; i++)
    {
        test("Connect: ");
        s = natsConnection_Connect(&nc, opts);
        testCond(s == NATS_OK);

        test("Lame duck callback invoked: ");
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.disconnected)
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        natsMutex_Unlock(arg.m);
        testCond(s == NATS_OK);

        if (i == 0)
        {
            test("Discovered not invoked: ");
            natsMutex_Lock(arg.m);
            while ((s != NATS_TIMEOUT) && !arg.done)
                s = natsCondition_TimedWait(arg.c, arg.m, 200);
            if ((arg.status == NATS_OK) && (s == NATS_TIMEOUT))
                s = NATS_OK;
            natsMutex_Unlock(arg.m);
            testCond(s == NATS_OK);
        }
        else
        {
            test("Discovered servers ok: ");
            natsMutex_Lock(arg.m);
            while ((s != NATS_TIMEOUT) && !arg.done)
                s = natsCondition_TimedWait(arg.c, arg.m, 2000);
            IFOK(s, arg.status);
            natsMutex_Unlock(arg.m);
            testCond(s == NATS_OK);
        }

        natsConnection_Destroy(nc);
        nc = NULL;
    }

    if (t != NULL)
    {
        natsThread_Join(t);
        natsThread_Destroy(t);
    }

    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);
}

void test_Version(void)
{
    const char *str = NULL;

    test("Compatibility: ");
    testCond(nats_CheckCompatibility() == true);

    test("Version string: ");
    str = nats_GetVersion();
    testCond((str != NULL)
             && (strcmp(str, LIB_NATS_VERSION_STRING) == 0));

    test("Version number: ");
    testCond(nats_GetVersionNumber() == LIB_NATS_VERSION_NUMBER);
}

void test_VersionMatchesTag(void)
{
    natsStatus  s = NATS_OK;
    const char  *tag;

    tag = getenv("TRAVIS_TAG");
    if ((tag == NULL) || (tag[0] == '\0'))
    {
        test("Skipping test since no tag detected: ");
        testCond(true);
        return;
    }
    test("Check tag and version match: ");
    // We expect a tag of the form vX.Y.Z. If that's not the case,
    // we need someone to have a look. So fail if first letter is not
    // a `v`
    if (tag[0] != 'v')
        s = NATS_ERR;
    else
    {
        // Strip the `v` from the tag for the version comparison.
        s = (strcmp(nats_GetVersion(), tag+1) == 0 ? NATS_OK : NATS_ERR);
    }
    testCond(s == NATS_OK);
}

static void
_openCloseAndWaitMsgCB(natsConnection *nc, natsSubscription *sub, natsMsg *msg, void *closure)
{
    struct threadArg *arg = (struct threadArg*) closure;

    nats_Sleep(300);
    natsMsg_Destroy(msg);
    natsMutex_Lock(arg->m);
    arg->results[0]++;
    natsMutex_Unlock(arg->m);
}

static void
_openCloseAndWaitConnClosedCB(natsConnection *nc, void *closure)
{
    struct threadArg *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);
    arg->sum++;
    natsMutex_Unlock(arg->m);
}

static void
_openCloseAndWaitCloseFromThread(void *closure)
{
    struct threadArg *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);
    arg->status = nats_CloseAndWait(0);
    arg->done   = true;
    natsCondition_Signal(arg->c);
    natsMutex_Unlock(arg->m);
}

static void
_openCloseAndWaitThread(void *closure)
{
    nats_Sleep(300);
    natsLib_Release();
}

void test_OpenCloseAndWait(void)
{
    natsStatus          s;
    natsConnection      *nc  = NULL;
    natsOptions         *opts= NULL;
    natsSubscription    *sub = NULL;
    natsPid             pid  = NATS_INVALID_PID;
    int                 i;
    natsThread          *t   = NULL;
    struct threadArg    arg;

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    if (_createDefaultThreadArgsForCbTests(&arg) != NATS_OK)
        FAIL("Unable to setup test");

    // First close the library since it is opened in main
    test("Close to prepare for test: ");
    s = nats_CloseAndWait(0);
    testCond(s == NATS_OK);

    test("Open/Close in loop: ");
    for (i=0;i<2;i++)
    {
        s = nats_Open(-1);
        IFOK(s, natsOptions_Create(&opts));
        IFOK(s, natsOptions_SetClosedCB(opts, _openCloseAndWaitConnClosedCB, (void*)&arg));
        IFOK(s, natsConnection_Connect(&nc, opts));
        IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _openCloseAndWaitMsgCB, (void*)&arg));
        IFOK(s, natsConnection_PublishString(nc, "foo", "hello"));
        IFOK(s, natsConnection_Flush(nc));
        if (s == NATS_OK)
        {
            for (;;)
            {
                natsMutex_Lock(arg.m);
                if (arg.results[0] == (i+1))
                {
                    natsMutex_Unlock(arg.m);
                    break;
                }
                natsMutex_Unlock(arg.m);
                nats_Sleep(100);
            }

            natsSubscription_Destroy(sub);
            natsConnection_Destroy(nc);
            natsOptions_Destroy(opts);
            nats_CloseAndWait(0);
        }
    }
    testCond(s == NATS_OK);

    test("Check async cb count: ");
    natsMutex_Lock(arg.m);
    testCond(arg.sum == 2);
    natsMutex_Unlock(arg.m);

    test("Check msgs count: ");
    natsMutex_Lock(arg.m);
    testCond(arg.results[0] == 2);
    natsMutex_Unlock(arg.m);

    test("Close while not opened returns error: ");
    s = nats_CloseAndWait(0);
    testCond(s == NATS_NOT_INITIALIZED);

    // Re-open for rest of test
    nats_Open(-1);

    test("Check Close from thread returns error: ");
    s = natsThread_Create(&t, _openCloseAndWaitCloseFromThread, (void*)&arg);
    if (s == NATS_OK)
    {
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.done)
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        s = (arg.status == NATS_ILLEGAL_STATE ? NATS_OK : NATS_ERR);
        natsMutex_Unlock(arg.m);

        natsThread_Join(t);
        natsThread_Destroy(t);
        t = NULL;
    }
    testCond(s == NATS_OK);

    test("No timeout: ");
    natsLib_Retain();
    s = natsThread_Create(&t, _openCloseAndWaitThread, NULL);
    IFOK(s, nats_CloseAndWait(0));
    testCond(s == NATS_OK);

    natsThread_Join(t);
    natsThread_Destroy(t);
    t = NULL;
    nats_Open(-1);

    test("Timeout: ");
    natsLib_Retain();
    s = natsThread_Create(&t, _openCloseAndWaitThread, NULL);
    IFOK(s, nats_CloseAndWait(100));
    testCond(s == NATS_TIMEOUT);

    // Now wait for thread to exit
    natsThread_Join(t);
    natsThread_Destroy(t);

    _destroyDefaultThreadArgs(&arg);
    _stopServer(pid);
}

static void
_testGetLastErrInThread(void *arg)
{
    natsStatus  getLastErrSts;
    natsOptions *opts = NULL;
    const char  *getLastErr = NULL;

    test("Check that new thread has get last err clear: ");
    getLastErr = nats_GetLastError(&getLastErrSts);
    testCond((getLastErr == NULL) && (getLastErrSts == NATS_OK));

    natsOptions_Destroy(opts);
}

void test_GetLastError(void)
{
    natsStatus  s, getLastErrSts;
    natsOptions *opts = NULL;
    const char  *getLastErr = NULL;
    natsThread  *t = NULL;
    char        stackBuf[256];
    FILE        *stackFile = NULL;

    stackBuf[0] = '\0';

    test("Check GetLastError returns proper status: ");
    s = natsOptions_SetAllowReconnect(NULL, false);

    getLastErr = nats_GetLastError(&getLastErrSts);

    testCond((s == getLastErrSts)
             && (getLastErr != NULL)
             && (strstr(getLastErr, "Invalid") != NULL));

    test("Check GetLastErrorStack with invalid args: ");
    s = nats_GetLastErrorStack(NULL, 10);
    if (s != NATS_OK)
        s = nats_GetLastErrorStack(stackBuf, 0);
    testCond(s == NATS_INVALID_ARG);

    test("Check GetLastErrorStack returns proper insufficient buffer: ");
    s = nats_GetLastErrorStack(stackBuf, 10);
    testCond(s == NATS_INSUFFICIENT_BUFFER);

    test("Check GetLastErrorStack: ");
    s = nats_GetLastErrorStack(stackBuf, sizeof(stackBuf));
    testCond((s == NATS_OK)
             && (strlen(stackBuf) > 0)
             && (strstr(stackBuf, "natsOptions_SetAllowReconnect") != NULL));

    test("Check PrintStack: ");
    stackBuf[0] = '\0';
    stackFile = fopen("stack.txt", "w");
    if (stackFile == NULL)
        FAIL("Unable to create a file for print stack test");

    s = NATS_OK;
    nats_PrintLastErrorStack(stackFile);
    fclose(stackFile);
    stackFile = fopen("stack.txt", "r");
    s = (fgets(stackBuf, sizeof(stackBuf), stackFile) == NULL ? NATS_ERR : NATS_OK);
    if ((s == NATS_OK)
        && ((strlen(stackBuf) == 0)
            || (strstr(stackBuf, "Invalid Argument") == NULL)))
    {
        s = NATS_ERR;
    }
    s = (fgets(stackBuf, sizeof(stackBuf), stackFile) == NULL ? NATS_ERR : NATS_OK);
    if (s == NATS_OK)
        s = (fgets(stackBuf, sizeof(stackBuf), stackFile) == NULL ? NATS_ERR : NATS_OK);
    if ((s == NATS_OK)
        && ((strlen(stackBuf) == 0)
            || (strstr(stackBuf, "natsOptions_SetAllowReconnect") == NULL)))
    {
        s = NATS_ERR;
    }
    testCond(s == NATS_OK);
    fclose(stackFile);
    remove("stack.txt");

    test("Check the error not cleared until next error occurs: ");
    s = natsOptions_Create(&opts);

    getLastErr = nats_GetLastError(&getLastErrSts);

    testCond((s == NATS_OK)
             && (getLastErrSts != NATS_OK)
             && (getLastErr != NULL)
             && (strstr(getLastErr, "Invalid") != NULL));

    s = natsThread_Create(&t, _testGetLastErrInThread, NULL);
    if (s == NATS_OK)
    {
        natsThread_Join(t);
        natsThread_Destroy(t);
    }

    natsOptions_Destroy(opts);

    nats_clearLastError();
    stackBuf[0] = '\0';

    test("Check stack not updated when asked: ");
    nats_doNotUpdateErrStack(true);
    s = natsConnection_Publish(NULL, NULL, NULL, 0);
    nats_GetLastErrorStack(stackBuf, sizeof(stackBuf));
    testCond((s != NATS_OK)
             && (stackBuf[0] == '\0'));

    test("Check call reentrant: ");
    nats_doNotUpdateErrStack(true);
    nats_doNotUpdateErrStack(false);
    s = natsConnection_Publish(NULL, NULL, NULL, 0);
    nats_GetLastErrorStack(stackBuf, sizeof(stackBuf));
    testCond((s != NATS_OK)
             && (stackBuf[0] == '\0'));

    nats_doNotUpdateErrStack(false);

    test("Check stack updates again: ");
    s = natsConnection_Publish(NULL, NULL, NULL, 0);
    nats_GetLastErrorStack(stackBuf, sizeof(stackBuf));
    testCond((s != NATS_OK)
             && (stackBuf[0] != '\0'));

    nats_clearLastError();
}

void test_StaleConnection(void)
{
    natsStatus          s = NATS_OK;
    natsSock            sock = NATS_SOCK_INVALID;
    natsThread          *t = NULL;
    struct threadArg    arg;
    natsSockCtx         ctx;
    int                 i;
    const char          *stale_conn_err = "-ERR 'Stale Connection'\r\n";

    memset(&ctx, 0, sizeof(natsSockCtx));

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&(arg.opts)));
    IFOK(s, natsOptions_SetReconnectWait(arg.opts, 20));
    IFOK(s, natsOptions_SetReconnectJitter(arg.opts, 0, 0));
    IFOK(s, natsOptions_SetMaxReconnect(arg.opts, 100));
    IFOK(s, natsOptions_SetDisconnectedCB(arg.opts, _disconnectedCb, &arg));
    IFOK(s, natsOptions_SetReconnectedCB(arg.opts, _reconnectedCb, &arg));
    IFOK(s, natsOptions_SetClosedCB(arg.opts, _closedCb, &arg));
    if (s != NATS_OK)
        FAIL("@@ Unable to setup test!");

    arg.control = 5;

    test("Behavior of connection on Stale Connection: ")

    s = _startMockupServer(&sock, "localhost", "4222");

    // Start the thread that will try to connect to our server...
    IFOK(s, natsThread_Create(&t, _connectToMockupServer, (void*) &arg));

    for (i = 0; (i < 2) && (s == NATS_OK); i++)
    {
        if (((ctx.fd = accept(sock, NULL, NULL)) == NATS_SOCK_INVALID)
                || (natsSock_SetCommonTcpOptions(ctx.fd) != NATS_OK))
        {
            s = NATS_SYS_ERROR;
        }
        if (s == NATS_OK)
        {
            char info[1024];

            strncpy(info,
                    "INFO {\"server_id\":\"foobar\",\"version\":\"latest\",\"go\":\"latest\",\"host\":\"localhost\",\"port\":4222,\"auth_required\":false,\"tls_required\":false,\"max_payload\":1048576}\r\n",
                    sizeof(info));

            // Send INFO.
            s = natsSock_WriteFully(&ctx, info, (int) strlen(info));
            if (s == NATS_OK)
            {
                char buffer[1024];

                memset(buffer, 0, sizeof(buffer));

                // Read connect and ping commands sent from the client
                s = natsSock_ReadLine(&ctx, buffer, sizeof(buffer));
                IFOK(s, natsSock_ReadLine(&ctx, buffer, sizeof(buffer)));
            }
            // Send PONG
            IFOK(s, natsSock_WriteFully(&ctx, _PONG_PROTO_, _PONG_PROTO_LEN_));

            if ((s == NATS_OK) && (i == 0))
            {
                // Wait a tiny, and simulate a Stale Connection
                nats_Sleep(50);
                s = natsSock_WriteFully(&ctx, stale_conn_err,
                                        (int)strlen(stale_conn_err));

                // The client should try to reconnect. When getting the
                // disconnected callback, wait for the disconnect cb
                natsMutex_Lock(arg.m);
                while ((s != NATS_TIMEOUT) && !(arg.disconnected))
                    s = natsCondition_TimedWait(arg.c, arg.m, 5000);
                natsMutex_Unlock(arg.m);
            }
            else if (s == NATS_OK)
            {
                natsMutex_Lock(arg.m);
                // We should check on !arg.closed here, but unfortunately,
                // on Windows, the thread calling natsConnection_Close()
                // would be blocked waiting for the readLoop thread to
                // exit, which it would not because fd shutdown is not
                // causing a socket error if the server is not closing
                // its side.
                while ((s != NATS_TIMEOUT) && (arg.disconnects != 2))
                    s = natsCondition_TimedWait(arg.c, arg.m, 5000);
                natsMutex_Unlock(arg.m);
            }

            natsSock_Close(ctx.fd);
        }
    }
    natsSock_Close(sock);

    // Wait for the client to finish.
    if (t != NULL)
    {
        natsThread_Join(t);
        natsThread_Destroy(t);
    }

    natsMutex_Lock(arg.m);
    IFOK(s, arg.status);
    if (s == NATS_OK)
    {
        // Wait for closed CB
        while ((s != NATS_TIMEOUT) && !(arg.closed))
            s = natsCondition_TimedWait(arg.c, arg.m, 5000);
        IFOK(s, arg.status);
    }
    natsMutex_Unlock(arg.m);

    testCond((s == NATS_OK)
             && (arg.disconnects == 2)
             && (arg.reconnects == 1)
             && (arg.closed));

    _destroyDefaultThreadArgs(&arg);
}

void test_ServerErrorClosesConnection(void)
{
    natsStatus          s = NATS_OK;
    natsSock            sock = NATS_SOCK_INVALID;
    natsThread          *t = NULL;
    struct threadArg    arg;
    natsSockCtx         ctx;

    memset(&ctx, 0, sizeof(natsSockCtx));

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&(arg.opts)));
    IFOK(s, natsOptions_SetReconnectWait(arg.opts, 20));
    IFOK(s, natsOptions_SetReconnectJitter(arg.opts, 0, 0));
    IFOK(s, natsOptions_SetMaxReconnect(arg.opts, 100));
    IFOK(s, natsOptions_SetDisconnectedCB(arg.opts, _disconnectedCb, &arg));
    IFOK(s, natsOptions_SetReconnectedCB(arg.opts, _reconnectedCb, &arg));
    IFOK(s, natsOptions_SetClosedCB(arg.opts, _closedCb, &arg));
    if (s != NATS_OK)
        FAIL("@@ Unable to setup test!");

    arg.control = 6;
    arg.string  = "Any Error";

    test("Behavior of connection on Server Error: ")

    s = _startMockupServer(&sock, "localhost", "4222");

    // Start the thread that will try to connect to our server...
    IFOK(s, natsThread_Create(&t, _connectToMockupServer, (void*) &arg));

    if ((s == NATS_OK)
        && (((ctx.fd = accept(sock, NULL, NULL)) == NATS_SOCK_INVALID)
            || (natsSock_SetCommonTcpOptions(ctx.fd) != NATS_OK)))
    {
        s = NATS_SYS_ERROR;
    }
    if (s == NATS_OK)
    {
        char info[1024];

        strncpy(info,
                "INFO {\"server_id\":\"foobar\",\"version\":\"latest\",\"go\":\"latest\",\"host\":\"localhost\",\"port\":4222,\"auth_required\":false,\"tls_required\":false,\"max_payload\":1048576}\r\n",
                sizeof(info));

        // Send INFO.
        s = natsSock_WriteFully(&ctx, info, (int) strlen(info));
        if (s == NATS_OK)
        {
            char buffer[1024];

            memset(buffer, 0, sizeof(buffer));

            // Read connect and ping commands sent from the client
            s = natsSock_ReadLine(&ctx, buffer, sizeof(buffer));
            IFOK(s, natsSock_ReadLine(&ctx, buffer, sizeof(buffer)));
        }
        // Send PONG
        IFOK(s, natsSock_WriteFully(&ctx, _PONG_PROTO_, _PONG_PROTO_LEN_));

        if (s == NATS_OK)
        {
            // Wait a tiny, and simulate an error sent by the server
            nats_Sleep(50);

            snprintf(info, sizeof(info), "-ERR '%s'\r\n", arg.string);
            s = natsSock_WriteFully(&ctx, info, (int)strlen(info));
        }

        // Wait for the client to be done.
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !(arg.closed))
            s = natsCondition_TimedWait(arg.c, arg.m, 5000);
        natsMutex_Unlock(arg.m);

        natsSock_Close(ctx.fd);
    }
    natsSock_Close(sock);

    // Wait for the client to finish.
    if (t != NULL)
    {
        natsThread_Join(t);
        natsThread_Destroy(t);
    }

    natsMutex_Lock(arg.m);
    if (s == NATS_OK)
    {
        // Wait for closed CB
        while ((s != NATS_TIMEOUT) && !(arg.closed))
            s = natsCondition_TimedWait(arg.c, arg.m, 5000);
        IFOK(s, arg.status);
    }
    natsMutex_Unlock(arg.m);

    testCond((s == NATS_ERR)
             && (arg.disconnects == 1)
             && (arg.reconnects == 0)
             && (arg.closed));

    _destroyDefaultThreadArgs(&arg);
}

void test_NoEcho(void)
{
    natsStatus          s;
    natsOptions         *opts = NULL;
    natsConnection      *conn = NULL;
    natsSubscription    *sub  = NULL;
    natsPid             pid   = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:4222"));
    IFOK(s, natsOptions_SetNoEcho(opts, true));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    arg.control = 0;
    arg.string = "test";
    test("Setup: ");
    s = natsConnection_Connect(&conn, opts);
    IFOK(s, natsConnection_Subscribe(&sub, conn, "foo", _recvTestString, (void*)&arg));
    IFOK(s, natsConnection_PublishString(conn, "foo", arg.string));
    IFOK(s, natsConnection_Flush(conn));
    // repeat
    IFOK(s, natsConnection_Flush(conn));
    testCond(s == NATS_OK);

    test("NoEcho: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.msgReceived)
        s = natsCondition_TimedWait(arg.c, arg.m, 500);
    natsMutex_Unlock(arg.m);
    // Message should not be received.
    testCond(s == NATS_TIMEOUT);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(conn);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(pid);
}

static void
_startMockupServerThread(void *closure)
{
    natsStatus          s = NATS_OK;
    natsSock            sock = NATS_SOCK_INVALID;
    struct threadArg    *arg = (struct threadArg*) closure;
    natsSockCtx         ctx;
    testCheckInfoCB     checkInfoCB = NULL;

    memset(&ctx, 0, sizeof(natsSockCtx));

    s = _startMockupServer(&sock, "localhost", "4222");
    natsMutex_Lock(arg->m);
    arg->status = s;
    natsCondition_Signal(arg->c);
    checkInfoCB = arg->checkInfoCB;
    natsMutex_Unlock(arg->m);

    if (((ctx.fd = accept(sock, NULL, NULL)) == NATS_SOCK_INVALID)
            || (natsSock_SetCommonTcpOptions(ctx.fd) != NATS_OK))
    {
        s = NATS_SYS_ERROR;
    }
    if (s == NATS_OK)
    {
        char info[1024];

        snprintf(info, sizeof(info), "%s", arg->string);

        // Send INFO.
        s = natsSock_WriteFully(&ctx, info, (int) strlen(info));
        if (s == NATS_OK)
        {
            char buffer[1024];

            memset(buffer, 0, sizeof(buffer));

            // Read connect and ping commands sent from the client
            s = natsSock_ReadLine(&ctx, buffer, sizeof(buffer));
            if ((s == NATS_OK) && (checkInfoCB != NULL))
                s = (*checkInfoCB)(buffer);
            IFOK(s, natsSock_ReadLine(&ctx, buffer, sizeof(buffer)));
        }
        // Send PONG
        IFOK(s, natsSock_WriteFully(&ctx, _PONG_PROTO_, _PONG_PROTO_LEN_));

        if (s == NATS_OK)
        {
            // Wait for client to tell us it is done
            natsMutex_Lock(arg->m);
            while ((s != NATS_TIMEOUT) && !(arg->done))
                s = natsCondition_TimedWait(arg->c, arg->m, 10000);
            natsMutex_Unlock(arg->m);
        }
        natsSock_Close(ctx.fd);
    }

    natsSock_Close(sock);
}

void test_NoEchoOldServer(void)
{
    natsStatus          s;
    natsConnection      *conn = NULL;
    natsOptions         *opts = NULL;
    natsThread          *t    = NULL;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetNoEcho(opts, true));
    if (s == NATS_OK)
    {
        // Set this to error, the mock server should set it to OK
        // if it can start successfully.
        arg.status = NATS_ERR;
        arg.string = "INFO {\"server_id\":\"22\",\"version\":\"latest\",\"go\":\"latest\",\"port\":4222,\"max_payload\":1048576}\r\n";
        s = natsThread_Create(&t, _startMockupServerThread, (void*) &arg);
    }
    if (s == NATS_OK)
    {
        // Wait for server to be ready
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && (arg.status != NATS_OK))
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        natsMutex_Unlock(arg.m);
    }
    if (s != NATS_OK)
    {
        if (t != NULL)
        {
            natsThread_Join(t);
            natsThread_Destroy(t);
        }
        natsOptions_Destroy(opts);
        _destroyDefaultThreadArgs(&arg);
        FAIL("Unable to setup test");
    }

    test("NoEcho with old server: ");
    s = natsConnection_Connect(&conn, opts);
    testCond(s == NATS_NO_SERVER_SUPPORT);

    // Notify mock server we are done
    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    natsOptions_Destroy(opts);
    natsThread_Join(t);
    natsThread_Destroy(t);

    _destroyDefaultThreadArgs(&arg);
}

void test_DrainSub(void)
{
    natsStatus          s;
    natsConnection      *nc = NULL;
    natsSubscription    *sub= NULL;
    natsSubscription    *sub2 = NULL;
    natsSubscription    *sub3 = NULL;
    natsOptions         *opts = NULL;
    natsPid             pid = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    arg.control = 8;

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Connect and create subscriptions: ");
    s = natsConnection_ConnectTo(&nc, "nats://127.0.0.1:4222");
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg));
    IFOK(s, natsConnection_SubscribeSync(&sub2, nc, "foo"));
    IFOK(s, natsConnection_SubscribeSync(&sub3, nc, "foo"));
    IFOK(s, natsSubscription_AutoUnsubscribe(sub3, 2));
    testCond(s == NATS_OK);

    test("WaitForDrainCompletion returns invalid arg: ");
    s = natsSubscription_WaitForDrainCompletion(NULL, 2000);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("WaitForDrainCompletion returns illegal state: ");
    s = natsSubscription_WaitForDrainCompletion(sub, 2000);
    testCond(s == NATS_ILLEGAL_STATE);
    nats_clearLastError();

    test("Send 2 messages: ");
    s = natsConnection_PublishString(nc, "foo", "msg");
    IFOK(s, natsConnection_PublishString(nc, "foo", "msg"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Call Drain on subscription: ");
    // Pass 0 or negative value to represent "for ever" timeout.
    s = natsSubscription_DrainTimeout(sub, -1);
    testCond(s == NATS_OK);

    test("Call Drain a second time is ok: ");
    s = natsSubscription_Drain(sub);
    testCond(s == NATS_OK);

    test("Drain sync subs: ");
    s = natsSubscription_Drain(sub2);
    IFOK(s, natsSubscription_Drain(sub3));
    testCond(s == NATS_OK);

    test("Wait for Drain times out: ");
    s = natsSubscription_WaitForDrainCompletion(sub, 10);
    if (s == NATS_TIMEOUT)
        s = natsSubscription_WaitForDrainCompletion(sub2, 10);
    testCond(s == NATS_TIMEOUT);
    nats_clearLastError();

    test("Send 1 more message: ");
    s = natsConnection_PublishString(nc, "foo", "msg");
    testCond(s == NATS_OK);

    // Unblock the callback.
    natsMutex_Lock(arg.m);
    arg.closed = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    test("Wait for Drain to complete: ");
    s = natsSubscription_WaitForDrainCompletion(sub, -1);
    testCond(s == NATS_OK);

    // Wait a bit and make sure that we did not receive the 3rd msg
    test("Third message not received: ");
    nats_Sleep(100);
    natsMutex_Lock(arg.m);
    if ((s == NATS_OK) && (arg.sum != 2))
        s = NATS_ERR;
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Drain on closed sub fails: ");
    s = natsSubscription_Drain(sub);
    testCond(s == NATS_INVALID_SUBSCRIPTION);
    nats_clearLastError();

    test("Consume sync messages: ");
    {
        natsMsg *msg = NULL;
        int     i;

        s = NATS_OK;
        for (i=0; (s == NATS_OK) && (i<2); i++)
        {
            s = natsSubscription_NextMsg(&msg, sub2, 2000);
            natsMsg_Destroy(msg);
            msg = NULL;
        }
        for (i=0; (s == NATS_OK) && (i<2); i++)
        {
            s = natsSubscription_NextMsg(&msg, sub3, 2000);
            natsMsg_Destroy(msg);
            msg = NULL;
        }
    }
    testCond(s == NATS_OK);

    test("Wait for drain to complete: ");
    s = natsSubscription_WaitForDrainCompletion(sub2, 1000);
    IFOK(s, natsSubscription_WaitForDrainCompletion(sub3, 1000));
    testCond(s == NATS_OK);

    // Repeat async test with auto-unsubscribe
    natsSubscription_Destroy(sub);
    sub = NULL;
    natsMutex_Lock(arg.m);
    arg.sum    = 0;
    arg.closed = false;
    natsMutex_Unlock(arg.m);

    test("Async sub with auto-unsub: ");
    s = natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*)&arg);
    IFOK(s, natsSubscription_AutoUnsubscribe(sub, 2));
    testCond(s == NATS_OK);

    test("Send 2 messages: ");
    s = natsConnection_PublishString(nc, "foo", "msg");
    IFOK(s, natsConnection_PublishString(nc, "foo", "msg"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Check drain status with invalid arg: ");
    s = natsSubscription_DrainCompletionStatus(NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Check drain status fails: ");
    s = natsSubscription_DrainCompletionStatus(sub);
    testCond(s == NATS_ILLEGAL_STATE);

    test("Call Drain on subscription: ");
    s = natsSubscription_Drain(sub);
    testCond(s == NATS_OK);

    test("Send 1 more message: ");
    s = natsConnection_PublishString(nc, "foo", "msg");
    testCond(s == NATS_OK);

    // Unblock the callback.
    natsMutex_Lock(arg.m);
    arg.closed = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    test("Wait for Drain to complete: ");
    s = natsSubscription_WaitForDrainCompletion(sub, -1);
    testCond(s == NATS_OK);

    test("Check drain status: ");
    s = natsSubscription_DrainCompletionStatus(sub);
    testCond(s == NATS_OK);

    // Wait a bit and make sure that we did not receive the 3rd msg
    test("Third message not received: ");
    nats_Sleep(100);
    natsMutex_Lock(arg.m);
    s = (arg.sum == 2 ? NATS_OK : NATS_ERR);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    // Close connection
    natsConnection_Close(nc);

    test("Drain on closed conn fails: ");
    s = natsSubscription_Drain(sub);
    if (s == NATS_CONNECTION_CLOSED)
        s = natsSubscription_Drain(sub2);
    if (s == NATS_CONNECTION_CLOSED)
        s = natsSubscription_Drain(sub3);
    testCond(s == NATS_CONNECTION_CLOSED);

    natsSubscription_Destroy(sub);
    sub = NULL;
    natsSubscription_Destroy(sub2);
    sub2 = NULL;
    natsSubscription_Destroy(sub3);
    sub3 = NULL;
    natsConnection_Destroy(nc);
    nc = NULL;

    natsMutex_Lock(arg.m);
    arg.sum    = 0;
    arg.closed = false;
    natsMutex_Unlock(arg.m);

    test("Connect and create sub: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetDisconnectedCB(opts, _disconnectedCb, (void*) &arg));
    IFOK(s, natsConnection_Connect(&nc, opts));
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg));
    testCond(s == NATS_OK);

    test("Send 2 messages: ");
    s = natsConnection_PublishString(nc, "foo", "msg");
    IFOK(s, natsConnection_PublishString(nc, "foo", "msg"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Disconnect: ");
    _stopServer(pid);
    testCond(s == NATS_OK);

    test("Wait for disconnect: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.disconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Call Drain on subscriptions: ");
    s = natsSubscription_DrainTimeout(sub, 500);
    testCond(s == NATS_OK);

    // Unblock the callback.
    natsMutex_Lock(arg.m);
    arg.closed = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Wait for Drain to complete: ");
    s = natsSubscription_WaitForDrainCompletion(sub, -1);
    testCond(s == NATS_OK);

    test("Check drain status: ");
    s = natsSubscription_DrainCompletionStatus(sub);
    // Since the flush has timed-out, this is what the status will be.
    testCond(s == NATS_TIMEOUT);
    s = NATS_OK;

    natsSubscription_Destroy(sub);
    sub = NULL;
    natsConnection_Destroy(nc);
    nc = NULL;
    natsOptions_Destroy(opts);
    opts = NULL;

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    natsMutex_Lock(arg.m);
    arg.sum    = 0;
    arg.closed = false;
    natsMutex_Unlock(arg.m);

    test("Create options for global msg delivery: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_UseGlobalMessageDelivery(opts, true));
    testCond(s == NATS_OK);

    test("Connect and create sub: ");
    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*)&arg));
    IFOK(s, natsConnection_Subscribe(&sub2, nc, "foo", _recvTestString, (void*)&arg));
    IFOK(s, natsSubscription_AutoUnsubscribe(sub, 2));
    testCond(s == NATS_OK);

    test("Send 2 messages: ");
    s = natsConnection_PublishString(nc, "foo", "msg");
    IFOK(s, natsConnection_PublishString(nc, "foo", "msg"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Call Drain on subscriptions: ");
    s = natsSubscription_Drain(sub);
    IFOK(s, natsSubscription_Drain(sub2));
    testCond(s == NATS_OK);

    // Unblock the callback.
    nats_Sleep(250);
    natsMutex_Lock(arg.m);
    arg.closed = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    test("Wait for Drain to complete: ");
    s = natsSubscription_WaitForDrainCompletion(sub, -1);
    testCond(s == NATS_OK);

    test("Check drain status: ");
    s = natsSubscription_DrainCompletionStatus(sub);
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    natsSubscription_Destroy(sub2);
    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(pid);
}

static void
_msgCBForDrainSubTest(natsConnection *nc, natsSubscription *sub, natsMsg *msg, void *closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;

    natsMsg_Destroy(msg);
    natsMutex_Lock(arg->m);
    if (++arg->sum == 1)
    {
        // Signal that we got the message
        natsCondition_Signal(arg->c);

        // Wait for main thread to switch to drain mode
        while (!arg->done)
            natsCondition_Wait(arg->c, arg->m);

        // Report unsubscribe status
        arg->status = natsSubscription_Unsubscribe(sub);
    }
    natsMutex_Unlock(arg->m);
}

static void
_drainSubCompleteCB(void *closure)
{
    struct threadArg *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);
    if (arg->sum == 1)
    {
        arg->closed = true;
        natsCondition_Signal(arg->c);
    }
    natsMutex_Unlock(arg->m);
}

void test_DrainSubStops(void)
{
    natsStatus          s;
    natsConnection      *nc = NULL;
    natsSubscription    *sub= NULL;
    natsPid             pid = NATS_INVALID_PID;
    struct threadArg    arg;
    int                 i;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Connect and create subscriptions: ");
    s = natsConnection_ConnectTo(&nc, "nats://127.0.0.1:4222");
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _msgCBForDrainSubTest, (void*) &arg));
    IFOK(s, natsSubscription_SetOnCompleteCB(sub, _drainSubCompleteCB, (void*) &arg))
    testCond(s == NATS_OK);

    test("Send 10 messages: ");
    for (i=0; (s == NATS_OK) && (i<10); i++)
        s = natsConnection_PublishString(nc, "foo", "msg");
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Wait for 1st message to be received: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && (arg.sum != 1))
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Drain subscription: ");
    s = natsSubscription_Drain(sub);
    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Wait for drain completion: ");
    s = natsSubscription_WaitForDrainCompletion(sub, 0);
    testCond(s == NATS_OK);

    test("Check drain status: ");
    s = natsSubscription_DrainCompletionStatus(sub);
    testCond(s == NATS_INVALID_SUBSCRIPTION);

    test("Check that drain stopped on unsubscribe: ");
    natsMutex_Lock(arg.m);
    s = NATS_OK;
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    // Get the status from unsubscribe in the callback
    IFOK(s, arg.status);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    sub = NULL;

    natsMutex_Lock(arg.m);
    arg.done    = false;
    arg.closed  = false;
    arg.sum     = 0;
    natsMutex_Unlock(arg.m);

    test("Create subscription: ");
    s = natsConnection_Subscribe(&sub, nc, "foo", _msgCBForDrainSubTest, (void*) &arg);
    IFOK(s, natsSubscription_SetOnCompleteCB(sub, _drainSubCompleteCB, (void*) &arg))
    testCond(s == NATS_OK);

    test("Send 10 messages: ");
    for (i=0; (s == NATS_OK) && (i<10); i++)
        s = natsConnection_PublishString(nc, "foo", "msg");
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Wait for 1st message to be received: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && (arg.sum != 1))
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Drain connection: ");
    s = natsConnection_Drain(nc);
    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Wait for Drain to complete: ");
    s = natsSubscription_WaitForDrainCompletion(sub, 0);
    testCond(s == NATS_OK);

    test("Check that drain stopped on unsubscribe: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    // Get the status from unsubscribe in the callback
    IFOK(s, arg.status);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(pid);
}

void test_DrainSubRaceOnAutoUnsub(void)
{
    natsStatus          s;
    natsConnection      *nc = NULL;
    natsSubscription    *sub= NULL;
    natsPid             pid = NATS_INVALID_PID;
    int                 i;

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Connect: ");
    s = natsConnection_ConnectTo(&nc, "nats://127.0.0.1:4222");
    testCond(s == NATS_OK);

    testDrainAutoUnsubRace = true;

    test("Drain with auto-unsub race: ");
    for (i=0; (s == NATS_OK) && (i<500); i++)
    {
        s = natsConnection_Subscribe(&sub, nc, "foo", _dummyMsgHandler, NULL);
        IFOK(s, natsSubscription_AutoUnsubscribe(sub, 1));
        IFOK(s, natsConnection_PublishString(nc, "foo", "msg"));
        nats_Sleep(1);
        if (s == NATS_OK)
        {
            s = natsSubscription_Drain(sub);
            // Here, it is possible that the subscription is already
            // invalid. In which case, don't attempt to wait for completion.
            if (s == NATS_INVALID_SUBSCRIPTION)
            {
                s = NATS_OK;
                nats_clearLastError();
            }
            else
            {
                IFOK(s, natsSubscription_WaitForDrainCompletion(sub, -1));
                IFOK(s, natsSubscription_DrainCompletionStatus(sub));
            }
        }
        natsSubscription_Destroy(sub);
        sub = NULL;
    }
    testCond(s == NATS_OK);

    testDrainAutoUnsubRace = false;

    natsConnection_Destroy(nc);
    _stopServer(pid);
}

void test_DrainSubNotResentOnReconnect(void)
{
    natsStatus          s;
    natsConnection      *nc   = NULL;
    natsSubscription    *sub  = NULL;
    natsOptions         *opts = NULL;
    natsPid             pid   = NATS_INVALID_PID;
    natsStatistics      stats;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetMaxReconnect(opts, -1));
    IFOK(s, natsOptions_SetReconnectWait(opts, 10));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    natsMutex_Lock(arg.m);
    arg.control = 8;
    natsMutex_Unlock(arg.m);

    test("Connect and create subscription: ");
    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg));
    testCond(s == NATS_OK);

    test("Send 1 message: ");
    s = natsConnection_PublishString(nc, "foo", "msg");
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Wait for message to be received: ");
    nats_Sleep(150);
    testCond(s == NATS_OK);

    test("Drain subscription: ");
    s = natsSubscription_Drain(sub);
    testCond(s == NATS_OK);

    test("Disconnect: ");
    nats_Sleep(250);
    _stopServer(pid);
    testCond(s == NATS_OK);

    test("Restart server: ");
    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);
    testCond(s == NATS_OK);

    test("Wait for reconnect: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.reconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Release cb: ");
    natsMutex_Lock(arg.m);
    arg.closed = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Wait for drain completion: ");
    s = natsSubscription_WaitForDrainCompletion(sub, 0);
    testCond(s == NATS_OK);

    test("Check drain status: ");
    s = natsSubscription_DrainCompletionStatus(sub);
    testCond(s == NATS_OK);

    test("Send new message: ");
    s = natsConnection_PublishString(nc, "foo", "msg");
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Msg not received by connection: ");
    s = natsConnection_GetStats(nc, &stats);
    IFOK(s, (stats.inMsgs == 1 ? NATS_OK : NATS_ERR));
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);
    _stopServer(pid);
}

static void
_drainConnBarSub(natsConnection *nc, natsSubscription *sub, natsMsg *msg, void *closure)
{
    struct threadArg *args = (struct threadArg*) closure;

    natsMutex_Lock(args->m);
    args->results[1]++;
    if (args->results[1] == args->results[0])
    {
        args->done = true;
        natsCondition_Broadcast(args->c);
    }
    natsMutex_Unlock(args->m);
    natsMsg_Destroy(msg);
}

static void
_drainConnFooSub(natsConnection *nc, natsSubscription *sub, natsMsg *msg, void *closure)
{
    struct threadArg *args = (struct threadArg*) closure;

    nats_Sleep(10);
    natsMutex_Lock(args->m);
    args->sum++;
    if (args->status == NATS_OK)
        args->status = natsConnection_PublishString(nc, natsMsg_GetReply(msg), "Stop bugging me");
    natsMutex_Unlock(args->m);
    natsMsg_Destroy(msg);
}

static void
_drainConnErrHandler(natsConnection *nc, natsSubscription *sub, natsStatus err, void *closure)
{
    struct threadArg *args = (struct threadArg*) closure;
    const char       *lastError = NULL;
    natsStatus       s = NATS_OK;

    natsMutex_Lock(args->m);
    // Since error handler is async, there is no guarantee that
    // natsConnection_GetLastError() returns the error we expect.
    // Only check if the error matches `err`.
    if (err == NATS_TIMEOUT)
    {
        s = natsConnection_GetLastError(nc, &lastError);
        if ((s != NATS_TIMEOUT)
                || (strstr(lastError, args->string) != NULL))
        {
            args->done = true;
            natsCondition_Broadcast(args->c);
        }
    }
    natsMutex_Unlock(args->m);
}

void test_DrainConn(void)
{
    natsStatus          s;
    natsConnection      *nc     = NULL;
    natsOptions         *opts   = NULL;
    natsSubscription    *sub    = NULL;
    natsConnection      *nc2    = NULL;
    natsSubscription    *sub2   = NULL;
    natsSubscription    *sub3   = NULL;
    natsPid             pid     = NATS_INVALID_PID;
    int                 expected= 50;
    int64_t             start   = 0;
    int                 i;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*)&arg));
    IFOK(s, natsOptions_SetErrorHandler(opts, _drainConnErrHandler, (void*) &arg));
    if (s != NATS_OK)
    {
        _destroyDefaultThreadArgs(&arg);
        natsOptions_Destroy(opts);
        FAIL("Unable to setup test");
    }

    arg.results[0] = expected;
    arg.string = "Drain error";

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Drain with invalid NULL: ");
    s = natsConnection_Drain(NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    test("Drain with no sub/pub ok: ");
    s = natsConnection_Drain(nc);
    testCond(s == NATS_OK);

    test("Closed CB invoked: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    arg.closed = false;
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("No async error reported: ");
    natsMutex_Lock(arg.m);
    s = (arg.done == false ? NATS_OK : NATS_ERR);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    nc = NULL;

    test("Connect: ");
    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_ConnectTo(&nc2, "nats://127.0.0.1:4222"));
    testCond(s == NATS_OK);

    test("Create listener for responses on bar: ");
    s = natsConnection_Subscribe(&sub2, nc2, "bar", _drainConnBarSub, (void*) &arg);
    testCond(s == NATS_OK);

    test("Create slow consumer for responder: ");
    s = natsConnection_Subscribe(&sub, nc, "foo", _drainConnFooSub, (void*) &arg);
    testCond(s == NATS_OK);

    test("Send messages: ");
    for (i=0; (s == NATS_OK) && (i<expected); i++)
        s = natsConnection_PublishRequestString(nc, "foo", "bar", "Slow Slow");
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Drain connection: ");
    start = nats_Now();
    // 0 or Negative means "wait for ever".
    s = natsConnection_DrainTimeout(nc, -1);
    testCond(s == NATS_OK);

    test("Check IsDraining: ");
    s = (natsConnection_IsDraining(nc) ? NATS_OK : NATS_ERR);
    testCond(s == NATS_OK);

    test("Second drain ok: ");
    s = natsConnection_Drain(nc);
    testCond(s == NATS_OK);

    test("Cannot create new subs: ");
    s = natsConnection_Subscribe(&sub3, nc, "foo", _dummyMsgHandler, NULL);
    testCond(s == NATS_DRAINING);
    nats_clearLastError();

    test("Publish should be ok: ");
    s = natsConnection_PublishString(nc, "baz", "should work");
    testCond(s == NATS_OK);

    test("Closed CB should be invoked: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Drain took as expected: ");
    s = ((nats_Now() - start) >= 10*expected ? NATS_OK : NATS_ERR);
    testCond(s == NATS_OK);

    test("Received all messages: ");
    natsMutex_Lock(arg.m);
    s = ((arg.sum == expected) ? NATS_OK : NATS_ERR);
    if (s == NATS_OK)
        s = arg.status;
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("All responses received: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.done)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    if ((s == NATS_OK) && (arg.results[1] != expected))
        s = NATS_ERR;
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Check sub drain status: ");
    s = natsSubscription_DrainCompletionStatus(sub);
    testCond(s == NATS_OK);

    test("Check IsDraining: ");
    s = (natsConnection_IsDraining(nc) ? NATS_ERR : NATS_OK);
    testCond(s == NATS_OK);

    test("Drain after closed should fail: ");
    s = natsConnection_DrainTimeout(nc, 1);
    testCond(s == NATS_CONNECTION_CLOSED);
    nats_clearLastError();

    natsSubscription_Destroy(sub);
    sub = NULL;
    natsConnection_Destroy(nc);
    nc = NULL;

    natsMutex_Lock(arg.m);
    arg.done   = false;
    arg.sum    = 0;
    arg.string = "timeout";
    natsMutex_Unlock(arg.m);

    test("Connect and subscribe: ");
    s = natsConnection_Connect(&nc, opts);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _drainConnFooSub, (void*) &arg));
    testCond(s == NATS_OK);

    test("Publish: ");
    for (i=0;(s==NATS_OK) && i<25;i++)
        s = natsConnection_PublishString(nc, "foo", "hello");
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Drain timeout: ");
    s = natsConnection_DrainTimeout(nc, 10);
    if (s == NATS_OK)
    {
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && !arg.done)
            s = natsCondition_TimedWait(arg.c, arg.m, 1000);
        natsMutex_Unlock(arg.m);
    }
    testCond(s == NATS_OK);

    test("Wait for subscription to drain: ");
    s = natsSubscription_WaitForDrainCompletion(sub, -1);
    testCond(s == NATS_OK);

    test("Check sub drain status: ");
    s = natsSubscription_DrainCompletionStatus(sub);
    // Since the connection drain timed-out, we should report as a timeout,
    // not as the connection closed.
    testCond(s == NATS_TIMEOUT);

    natsSubscription_Destroy(sub);
    sub = NULL;
    natsSubscription_Destroy(sub2);
    natsSubscription_Destroy(sub3);
    natsConnection_Destroy(nc);
    nc = NULL;
    natsConnection_Destroy(nc2);
    nc2 = NULL;
    natsOptions_Destroy(opts);

    natsMutex_Lock(arg.m);
    arg.closed = false;
    arg.sum    = 0;
    arg.control= 8;
    natsMutex_Unlock(arg.m);

    test("Connect and create sub: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_Subscribe(&sub, nc, "foo", _recvTestString, (void*) &arg));
    IFOK(s, natsConnection_ConnectTo(&nc2, NATS_DEFAULT_URL));
    testCond(s == NATS_OK);

    test("Send messages: ");
    s = natsConnection_PublishString(nc, "foo", "msg1");
    IFOK(s, natsConnection_PublishString(nc, "foo", "msg2"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Drain: ");
    s = natsConnection_DrainTimeout(nc, 10000);
    testCond(s == NATS_OK);

    test("Drain sub directly should fail: ");
    s = natsSubscription_Drain(sub);
    testCond(s == NATS_DRAINING);
    nats_clearLastError();
    s = NATS_OK;

    test("Disconnect: ");
    _stopServer(pid);
    testCond(s == NATS_OK);

    nats_Sleep(100);

    test("Drain while disconnected fails: ");
    s = natsConnection_Drain(nc2);
    testCond(s == NATS_ILLEGAL_STATE);
    nats_clearLastError();
    s = NATS_OK;

    test("Release cb: ");
    natsMutex_Lock(arg.m);
    arg.closed = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Wait for completion: ");
    s = natsSubscription_WaitForDrainCompletion(sub, 1000);
    testCond(s == NATS_OK);

    test("Check drain status: ");
    s = natsSubscription_DrainCompletionStatus(sub);
    testCond(s == NATS_CONNECTION_CLOSED);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    natsConnection_Destroy(nc2);

    // Since the drain timed-out and closed the connection,
    // the subscription will be closed but there is no guarantee
    // that the callback is not in the middle of execution at that
    // time. So to avoid valgrind reports, sleep a bit before
    // destroying sub's closure.
    nats_Sleep(100);
    _destroyDefaultThreadArgs(&arg);
}

static void
_noDoubleCloseCb(natsConnection *nc, void *closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);
    arg->sum++;
    arg->closed = true;
    natsCondition_Signal(arg->c);
    natsMutex_Unlock(arg->m);
}

static void
_noDoubleCbSubCb(natsConnection *nc, natsSubscription *sub, natsMsg *msg, void *closure)
{
    nats_Sleep(200);
    natsMsg_Destroy(msg);
}

void test_NoDoubleConnClosedOnDrain(void)
{
    natsStatus          s;
    natsConnection      *nc     = NULL;
    natsOptions         *opts   = NULL;
    natsSubscription    *sub    = NULL;
    natsPid             pid     = NATS_INVALID_PID;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetClosedCB(opts, _noDoubleCloseCb, (void*)&arg));
    if (s != NATS_OK)
    {
        _destroyDefaultThreadArgs(&arg);
        natsOptions_Destroy(opts);
        FAIL("Unable to setup test");
    }

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    test("Create sub: ");
    s = natsConnection_Subscribe(&sub, nc, "foo", _noDoubleCbSubCb, (void*)&arg);
    testCond(s == NATS_OK);

    test("Publish msg: ");
    s = natsConnection_PublishString(nc, "foo", "hello");
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Drain: ");
    s = natsConnection_Drain(nc);
    testCond(s == NATS_OK);

    nats_Sleep(200);
    test("Closing: ");
    natsConnection_Close(nc);
    testCond(s == NATS_OK);

    test("Wait for close CB: ");
    s = _waitForConnClosed(&arg);
    testCond(s == NATS_OK);

    // Now wait for connection close and make sure it was invoked once.
    test("Check closeCb invoked once: ")
    nats_Sleep(300);
    natsMutex_Lock(arg.m);
    s = (arg.sum == 1 ? NATS_OK : NATS_ERR);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);
    _stopServer(pid);
}

void test_GetClientID(void)
{
    natsStatus          s;
    natsPid             pid1    = NATS_INVALID_PID;
    natsPid             pid2    = NATS_INVALID_PID;
    natsConnection      *nc1    = NULL;
    natsConnection      *nc2    = NULL;
    natsOptions         *opts   = NULL;
    uint64_t            cid     = 0;
    uint64_t            newcid  = 0;
    natsThread          *t      = NULL;
    struct threadArg    arg;

    if (!serverVersionAtLeast(1,2,0))
    {
        char txt[200];

        snprintf(txt, sizeof(txt), "Skipping since requires server version of at least 1.2.0, got %s: ", serverVersion);
        test(txt);
        testCond(true);
        return;
    }
    pid1 = _startServer("nats://127.0.0.1:4222", "-a 127.0.0.1 -p 4222 -cluster nats://127.0.0.1:6222 -cluster_name abc", true);
    CHECK_SERVER_STARTED(pid1);

    test("Create nc1: ");
    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:4222"));
    IFOK(s, natsOptions_SetReconnectWait(opts, 50));
    IFOK(s, natsOptions_SetTimeout(opts, 250));
    IFOK(s, natsOptions_SetDiscoveredServersCB(opts, _discoveredServersCb, (void*)&arg));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, (void*)&arg));
    IFOK(s, natsConnection_Connect(&nc1, opts));
    testCond(s == NATS_OK);

    test("GetClientID for nc1: ");
    s = natsConnection_GetClientID(nc1, &cid);
    testCond((s == NATS_OK) && (cid != 0));

    test("Wait for discovered callback: ");
    pid2 = _startServer("nats://127.0.0.1:4223", "-a 127.0.0.1 -p 4223 -cluster nats://127.0.0.1:6223 -cluster_name abc -routes nats://127.0.0.1:6222", true);
    CHECK_SERVER_STARTED(pid2);

    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && (arg.sum != 1))
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    s = (arg.sum == 1 ? NATS_OK: NATS_ERR);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Check CID same: ");
    s = natsConnection_GetClientID(nc1, &newcid);
    testCond((s == NATS_OK) && (newcid == cid));

    test("Connect to server 2: ");
    s = natsConnection_ConnectTo(&nc2, "nats://127.0.0.1:4223");
    testCond(s == NATS_OK);

    test("Stop server 1: ");
    _stopServer(pid1);
    testCond(s == NATS_OK);

    test("Wait for nc1 to reconnect: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.reconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 4000);
    s = (arg.reconnected ? NATS_OK : NATS_ERR);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Check CID is different: ");
    s = natsConnection_GetClientID(nc1, &newcid);
    testCond((s == NATS_OK) && (newcid != cid));

    // Stop clients and remaining server
    natsConnection_Destroy(nc1);
    natsConnection_Destroy(nc2);
    natsOptions_Destroy(opts);
    _stopServer(pid2);

    // Now have dummy server that returns no CID and check we get expected error.
    nc1 = NULL;
    arg.status = NATS_ERR;
    arg.string = "INFO {\"server_id\":\"22\",\"version\":\"latest\",\"go\":\"latest\",\"port\":4222,\"max_payload\":1048576}\r\n";
    s = natsThread_Create(&t, _startMockupServerThread, (void*) &arg);
    if (s == NATS_OK)
    {
        // Wait for server to be ready
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && (arg.status != NATS_OK))
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        s = arg.status;
        natsMutex_Unlock(arg.m);
    }
    if (s != NATS_OK)
    {
        if (t != NULL)
        {
            natsThread_Join(t);
            natsThread_Destroy(t);
        }
        natsOptions_Destroy(opts);
        _destroyDefaultThreadArgs(&arg);
        FAIL("Unable to setup test");
    }

    test("CID not supported: ");
    s = natsConnection_ConnectTo(&nc1, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_GetClientID(nc1, &cid));
    testCond((s == NATS_NO_SERVER_SUPPORT) && (cid == 0));

    // Notify mock server we are done
    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    natsConnection_Destroy(nc1);

    natsThread_Join(t);
    natsThread_Destroy(t);

    _destroyDefaultThreadArgs(&arg);
}

void test_GetClientIP(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    char                *ip       = NULL;
    natsThread          *t    = NULL;
    struct threadArg    arg;

    test("Check server version: ");
    if (!serverVersionAtLeast(2,1,6))
    {
        char txt[200];

        snprintf(txt, sizeof(txt), "Skipping since requires server version of at least 2.1.6, got %s: ", serverVersion);
        test(txt);
        testCond(true);
        return;
    }
    testCond(true);

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect: ");
    s = natsConnection_ConnectTo(&nc, "nats://127.0.0.1:4222");
    testCond(s == NATS_OK);

    test("Get client IP - no conn: ");
    s = natsConnection_GetClientIP(NULL, &ip);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Get client IP - no ip loc: ");
    s = natsConnection_GetClientIP(nc, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Get client IP: ");
    s = natsConnection_GetClientIP(nc, &ip);
    testCond((s == NATS_OK) && (strcmp(ip, "127.0.0.1")==0));
    free(ip);
    ip = NULL;

    natsConnection_Close(nc);
    test("Get client IP after conn closed: ");
    s = natsConnection_GetClientIP(nc, &ip);
    testCond((s == NATS_CONNECTION_CLOSED) && (ip == NULL));
    nats_clearLastError();

    natsConnection_Destroy(nc);
    nc = NULL;

    _stopServer(serverPid);

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
    {
        // Set this to error, the mock server should set it to OK
        // if it can start successfully.
        arg.status = NATS_ERR;
        arg.string = "INFO {\"server_id\":\"22\",\"version\":\"latest\",\"go\":\"latest\",\"port\":4222,\"max_payload\":1048576}\r\n";
        s = natsThread_Create(&t, _startMockupServerThread, (void*) &arg);
    }
    if (s == NATS_OK)
    {
        // Wait for server to be ready
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && (arg.status != NATS_OK))
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        natsMutex_Unlock(arg.m);
    }
    if (s != NATS_OK)
    {
        if (t != NULL)
        {
            natsThread_Join(t);
            natsThread_Destroy(t);
        }
        _destroyDefaultThreadArgs(&arg);
        FAIL("Unable to setup test");
    }

    test("Connect: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    testCond(s == NATS_OK);

    test("Get client IP with old server: ");
    s = natsConnection_GetClientIP(nc, &ip);
    testCond ((s == NATS_NO_SERVER_SUPPORT) && (ip == NULL));
    nats_clearLastError();

    // Notify mock server we are done
    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    natsConnection_Close(nc);
    natsConnection_Destroy(nc);

    natsThread_Join(t);
    natsThread_Destroy(t);

    _destroyDefaultThreadArgs(&arg);
}

void test_GetRTT(void)
{
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    int64_t             rtt       = 0;

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetReconnectWait(opts, 10));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);

    test("Get RTT - no conn: ");
    s = natsConnection_GetRTT(NULL, &rtt);
    testCond(s == NATS_INVALID_ARG);

    test("Get RTT - no rtt loc: ");
    s = natsConnection_GetRTT(nc, NULL);
    testCond(s == NATS_INVALID_ARG);

    test("Get RTT: ");
    s = natsConnection_GetRTT(nc, &rtt);
    // Check that it is below 500ms...
    testCond((s == NATS_OK) && (rtt/1000000 <= 500));

    _stopServer(serverPid);

    test("Get RTT while not connected: ");
    s = natsConnection_GetRTT(nc, &rtt);
    testCond(s == NATS_CONNECTION_DISCONNECTED);

    natsConnection_Close(nc);
    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);
}

void test_GetLocalIPAndPort(void)
{
    natsStatus          s;
    natsConnection      *nc     = NULL;
    natsOptions         *opts   = NULL;
    natsPid             pid     = NATS_INVALID_PID;
    char                *ip     = NULL;
    int                 port    = 0;
    struct threadArg    arg;

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Connect: ");
    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:4222"));
    IFOK(s, natsOptions_SetDisconnectedCB(opts, _disconnectedCb, &arg));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);

    test("Get Local IP and Port - no conn: ");
    s = natsConnection_GetLocalIPAndPort(NULL, &ip, &port);
    testCond(s == NATS_INVALID_ARG);

    test("Get Local IP and Port - no ip loc: ");
    s = natsConnection_GetLocalIPAndPort(nc, NULL, &port);
    testCond(s == NATS_INVALID_ARG);

    test("Get Local IP and Port - no port loc: ");
    s = natsConnection_GetLocalIPAndPort(nc, &ip, NULL);
    testCond(s == NATS_INVALID_ARG);

    nats_clearLastError();
    test("Get Local IP and Port: ");
    s = natsConnection_GetLocalIPAndPort(nc, &ip, &port);
    testCond((s == NATS_OK)
                && ((ip != NULL) && (strcmp(ip, "127.0.0.1") == 0))
                && (port != 0));
    free(ip);

    test("Wait for disconnect: ");
    s = NATS_OK;
    _stopServer(pid);
    natsMutex_Lock(arg.m);
    while ((s == NATS_OK) && !arg.disconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Get Local IP and Port while disconnected: ");
    s = natsConnection_GetLocalIPAndPort(nc, &ip, &port);
    testCond(s == NATS_CONNECTION_DISCONNECTED);
    nats_clearLastError();

    // Close connection
    natsConnection_Close(nc);
    test("Get Local IP and Port with closed connection: ");
    s = natsConnection_GetLocalIPAndPort(nc, &ip, &port);
    testCond(s == NATS_CONNECTION_CLOSED);

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);
}

static natsStatus
_userJWTCB(char **userJWT, char **customErrTxt, void *closure)
{
    struct threadArg *arg = (struct threadArg*) closure;

    if (closure != NULL)
    {
        bool done = true;

        natsMutex_Lock(arg->m);
        if (arg->string != NULL)
            *customErrTxt = strdup(arg->string);
        else if (arg->nc != NULL)
            natsConnection_Destroy(arg->nc);
        else
            done = false;
        natsMutex_Unlock(arg->m);

        if (done)
        {
            if (*customErrTxt != NULL)
                return NATS_ERR;
            return NATS_OK;
        }
    }

    *userJWT = strdup("some user jwt");

    return NATS_OK;
}

static natsStatus
_sigCB(char **customErrTxt, unsigned char **psig, int *sigLen, const char* nonce, void *closure)
{
    const unsigned char correctSign[] = {
            155, 157,   8, 183,  93, 154,  78,   7,
            219,  39,  11,  16, 134, 231,  46, 142,
            168,  87, 110, 202, 187, 180, 179,  62,
             49, 255, 225,  74,  48,  80, 176, 111,
            248, 162, 121, 188, 203, 101, 100, 195,
            162,  70, 213, 182, 220,  14,  71, 113,
             93, 239, 141, 131,  66, 190, 237, 127,
            104, 191, 138, 217, 227,   1,  92,  14,
    };
    unsigned char *sig = NULL;

    if (closure != NULL)
    {
        struct threadArg *arg = (struct threadArg*) closure;
        bool             done = true;

        natsMutex_Lock(arg->m);
        if (arg->string != NULL)
            *customErrTxt = strdup(arg->string);
        else if (arg->nc != NULL)
            natsConnection_Destroy(arg->nc);
        else
            done = false;
        natsMutex_Unlock(arg->m);

        if (done)
        {
            if (*customErrTxt != NULL)
                return NATS_ERR;

            return NATS_OK;
        }
    }

    sig = malloc(NATS_CRYPTO_SIGN_BYTES);
    memcpy(sig, correctSign, NATS_CRYPTO_SIGN_BYTES);
    *psig = sig;
    if (sigLen != NULL)
        *sigLen = NATS_CRYPTO_SIGN_BYTES;

    return NATS_OK;
}

static natsStatus
_checkJWTAndSigCB(char *buffer)
{
    // UserJWT callback should have returned this...
    if (strstr(buffer, "some user jwt") == NULL)
        return NATS_ERR;

    // The server is sending the nonce "nonce" and we
    // use a seed that should have produced a signature
    // that converted to base64 should be:
    if (strstr(buffer, "m50It12aTgfbJwsQhucujqhXbsq7tLM-Mf_hSjBQsG_4onm8y2Vkw6JG1bbcDkdxXe-Ng0K-7X9ov4rZ4wFcDg") == NULL)
        return NATS_ERR;

    return NATS_OK;
}

void test_UserCredsCallbacks(void)
{
    natsStatus          s;
    natsConnection      *nc     = NULL;
    natsOptions         *opts   = NULL;
    natsOptions         *opts2  = NULL;
    natsPid             pid     = NATS_INVALID_PID;
    natsThread          *t      = NULL;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    if (s != NATS_OK)
        FAIL("Unable to create options for test UserCredsCallbacks");

    test("Invalid arg 1: ");
    s = natsOptions_SetUserCredentialsCallbacks(NULL, _dummyUserJWTCb, NULL, _dummySigCb, NULL);
    testCond(s == NATS_INVALID_ARG);

    test("Invalid arg 2: ");
    s = natsOptions_SetUserCredentialsCallbacks(opts, NULL, NULL, _dummySigCb, NULL);
    testCond(s == NATS_INVALID_ARG);

    test("Clone: ");
    s = natsOptions_SetUserCredentialsCallbacks(opts, _dummyUserJWTCb, (void*) 1, _dummySigCb, (void*) 2);
    if (s == NATS_OK)
    {
        opts2 = natsOptions_clone(opts);
        if (opts2 == NULL)
            s = NATS_NO_MEMORY;
    }
    IFOK(s, natsOptions_SetUserCredentialsCallbacks(opts, NULL, NULL, NULL, NULL));
    testCond((s == NATS_OK)
                && (opts2->userJWTHandler == _dummyUserJWTCb)
                && (opts2->userJWTClosure == (void*) 1)
                && (opts2->sigHandler == _dummySigCb)
                && (opts2->sigClosure == (void*) 2));

    natsOptions_Destroy(opts2);

    // We first check that we get error when callbacks do return error.
    // For that part, we don't need that the server sends the nonce.

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    // Create a connection. The user JWT callback is going to return
    // an error, ensure connection fails.
    test("UserJWTCB returns error: ");
    natsMutex_Lock(arg.m);
    arg.string = "some jwt error";
    arg.nc = NULL;
    natsMutex_Unlock(arg.m);
    s = natsOptions_SetUserCredentialsCallbacks(opts, _userJWTCB, (void*) &arg, _sigCB, NULL);
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond((s == NATS_ERR)
                && (strstr(nats_GetLastError(NULL), "some jwt error") != NULL));

    s = NATS_OK;
    nats_clearLastError();
    test("SignatureCB returns error: ");
    natsMutex_Lock(arg.m);
    arg.string = "some sig error";
    arg.nc = NULL;
    natsMutex_Unlock(arg.m);
    s = natsOptions_SetUserCredentialsCallbacks(opts, _userJWTCB, NULL, _sigCB, (void*) &arg);
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond((s == NATS_ERR)
                && (strstr(nats_GetLastError(NULL), "some sig error") != NULL));

    s = NATS_OK;
    nats_clearLastError();
    test("UserJWTCB destroys connection: ");
    natsMutex_Lock(arg.m);
    arg.string = NULL;
    arg.nc = NULL;
    arg.closed = false;
    natsMutex_Unlock(arg.m);
    s = natsOptions_SetUserCredentialsCallbacks(opts, _userJWTCB, (void*) &arg, _sigCB, NULL);
    IFOK(s, natsOptions_SetReconnectWait(opts, 100));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));
    IFOK(s, natsConnection_Connect(&nc, opts));
    if (s == NATS_OK)
    {
        natsMutex_Lock(arg.m);
        arg.nc = nc;
        natsMutex_Unlock(arg.m);

        _stopServer(pid);
        pid = _startServer("nats://127.0.0.1:4222", NULL, true);
        CHECK_SERVER_STARTED(pid);
    }
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && (!arg.closed))
        s = natsCondition_TimedWait(arg.c, arg.m, 5000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    nc = NULL;

    s = NATS_OK;
    nats_clearLastError();
    test("SigCB destroys connection: ");
    natsMutex_Lock(arg.m);
    arg.string = NULL;
    arg.nc = NULL;
    arg.closed = false;
    natsMutex_Unlock(arg.m);
    s = natsOptions_SetUserCredentialsCallbacks(opts, _userJWTCB, NULL, _sigCB, (void*) &arg);
    IFOK(s, natsOptions_SetReconnectWait(opts, 100));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));
    IFOK(s, natsConnection_Connect(&nc, opts));
    if (s == NATS_OK)
    {
        natsMutex_Lock(arg.m);
        arg.nc = nc;
        natsMutex_Unlock(arg.m);

        _stopServer(pid);
        pid = _startServer("nats://127.0.0.1:4222", NULL, true);
        CHECK_SERVER_STARTED(pid);
    }
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && (!arg.closed))
        s = natsCondition_TimedWait(arg.c, arg.m, 5000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    nc = NULL;

    _stopServer(pid);

    // Start fake server that will send predefined "nonce" so we can check
    // that connection is sending appropriate jwt and signature.

    // Set this to error, the mock server should set it to OK
    // if it can start successfully.
    arg.status = NATS_ERR;
    arg.checkInfoCB = _checkJWTAndSigCB;
    arg.string = "INFO {\"server_id\":\"22\",\"version\":\"latest\",\"go\":\"latest\",\"port\":4222,\"max_payload\":1048576,\"nonce\":\"nonce\"}\r\n";
    s = natsThread_Create(&t, _startMockupServerThread, (void*) &arg);
    if (s == NATS_OK)
    {
        // Wait for server to be ready
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && (arg.status != NATS_OK))
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        natsMutex_Unlock(arg.m);
    }
    if (s != NATS_OK)
    {
        if (t != NULL)
        {
            natsThread_Join(t);
            natsThread_Destroy(t);
        }
        natsOptions_Destroy(opts);
        _destroyDefaultThreadArgs(&arg);
        FAIL("Unable to setup test");
    }

    test("Connect sends proper JWT and Signature: ");
    natsOptions_Destroy(opts);
    opts = NULL;
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetUserCredentialsCallbacks(opts, _userJWTCB, NULL, _sigCB, NULL));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);

    // Notify mock server we are done
    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);
    natsThread_Join(t);
    natsThread_Destroy(t);
    _destroyDefaultThreadArgs(&arg);
}

void test_UserCredsFromMemory(void)
{
    natsStatus          s       = NATS_OK;
    natsConnection      *nc     = NULL;
    natsOptions         *opts   = NULL;
    natsOptions         *opts2  = NULL;
    natsPid             pid     = NATS_INVALID_PID;
    natsThread          *t      = NULL;
    struct threadArg    arg;

    const char          *jwtAndSeed     = "-----BEGIN NATS USER JWT----\nsome user jwt\n-----END NATS USER JWT-----\n\n-----BEGIN USER NKEY SEED-----\nSUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY\n-----END USER NKEY SEED-----\n";
    const char          *jwtWithoutSeed = "-----BEGIN NATS USER JWT----\nsome user jwt\n-----END NATS USER JWT-----\n";

    s = natsOptions_Create(&opts);
    if (s != NATS_OK)
        FAIL("Unable to create options for test UserCredsFromFiles");

    test("Clone: ");
    s = natsOptions_SetUserCredentialsFromMemory(opts, jwtAndSeed);
    if (s == NATS_OK)
    {
        opts2 = natsOptions_clone(opts);
        if (opts2 == NULL)
            s = NATS_NO_MEMORY;
    }
    IFOK(s, natsOptions_SetUserCredentialsFromMemory(opts, NULL));
    testCond((s == NATS_OK)
                && (opts2->userCreds != NULL)
                && (opts2->userJWTHandler == natsConn_userCreds)
                && (opts2->userJWTClosure == (void*) opts2->userCreds)
                && (opts2->sigHandler == natsConn_signatureHandler)
                && (opts2->sigClosure == (void*) opts2->userCreds));
    natsOptions_Destroy(opts2);

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("invalidCreds provided: ");
    s = natsOptions_SetUserCredentialsFromMemory(opts, "invalidCreds");
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_NOT_FOUND);

    // Use a file that contains no seed
    test("jwtAndSeed string has no seed: ");
    s = natsOptions_SetUserCredentialsFromMemory(opts, jwtWithoutSeed);
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_NOT_FOUND);

    nc = NULL;
    _stopServer(pid);

    // Start fake server that will send predefined "nonce" so we can check
    // that connection is sending appropriate jwt and signature.
    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
    {
        // Set this to error, the mock server should set it to OK
        // if it can start successfully.
        arg.done = false;
        arg.status = NATS_ERR;
        arg.checkInfoCB = _checkJWTAndSigCB;
        arg.string = "INFO {\"server_id\":\"22\",\"version\":\"latest\",\"go\":\"latest\",\"port\":4222,\"max_payload\":1048576,\"nonce\":\"nonce\"}\r\n";
        s = natsThread_Create(&t, _startMockupServerThread, (void*) &arg);
    }
    if (s == NATS_OK)
    {
        // Wait for server to be ready
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && (arg.status != NATS_OK))
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        natsMutex_Unlock(arg.m);
    }
    if (s != NATS_OK)
    {
        if (t != NULL)
        {
            natsThread_Join(t);
            natsThread_Destroy(t);
        }
        natsOptions_Destroy(opts);
        _destroyDefaultThreadArgs(&arg);
        FAIL("Unable to setup test");
    }

    s = NATS_OK;
    test("Connect with jwtAndSeed string: ");
    s = natsOptions_SetUserCredentialsFromMemory(opts, jwtAndSeed);
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);

    // Notify mock server we are done
    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    natsConnection_Destroy(nc);
    nc = NULL;

    natsThread_Join(t);
    natsThread_Destroy(t);
    t = NULL;

    _destroyDefaultThreadArgs(&arg);

    natsOptions_Destroy(opts);
}

void test_UserCredsFromFiles(void)
{
    natsStatus          s       = NATS_OK;
    natsConnection      *nc     = NULL;
    natsOptions         *opts   = NULL;
    natsOptions         *opts2  = NULL;
    natsPid             pid     = NATS_INVALID_PID;
    natsThread          *t      = NULL;
    int                 i;
    struct threadArg    arg;
    const char          *ucfn   = "user.creds";
    const char          *sfn    = "seed.txt";
    const char          *snhfn  = "seednh.txt";
    const char          *nusfn  = "nouors.txt";
    FILE                *f      = NULL;

    f = fopen(ucfn, "w");
    if (f == NULL)
        s = NATS_ERR;
    else
    {
        int res = fputs("-----BEGIN NATS USER JWT----\nsome user jwt\n-----END NATS USER JWT-----\n\n-----BEGIN USER NKEY SEED-----\nSUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY\n-----END USER NKEY SEED-----\n", f);
        if (res < 0)
            s = NATS_ERR;

        if (fclose(f) != 0)
            s = NATS_ERR;
        f = NULL;
    }
    if (s == NATS_OK)
    {
        f = fopen(sfn, "w");
        if (f == NULL)
            s = NATS_ERR;
        else
        {
            int res = fputs("-----BEGIN USER NKEY SEED-----\nSUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY\n-----END USER NKEY SEED-----\n", f);
            if (res < 0)
                s = NATS_ERR;

            if (fclose(f) != 0)
                s = NATS_ERR;
            f = NULL;
        }
    }
    if (s == NATS_OK)
    {
        f = fopen(snhfn, "w");
        if (f == NULL)
            s = NATS_ERR;
        else
        {
            int res = fputs("This file does not have the proper header\nand also has spaces before the seed:\n  \tSUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY\nthis should work\n", f);
            if (res < 0)
                s = NATS_ERR;

            if (fclose(f) != 0)
                s = NATS_ERR;
            f = NULL;
        }
    }
    if (s == NATS_OK)
    {
        f = fopen(nusfn, "w");
        if (f == NULL)
            s = NATS_ERR;
        else
        {
            int res = fputs("This file does not have a jwt\nnor a valid seed\n", f);
            if (res < 0)
                s = NATS_ERR;

            if (fclose(f) != 0)
                s = NATS_ERR;
            f = NULL;
        }
    }
    if (s != NATS_OK)
        FAIL("Unable to create creds test files");

    s = natsOptions_Create(&opts);
    if (s != NATS_OK)
        FAIL("Unable to create options for test UserCredsFromFiles");

    test("Invalid arg 1: ");
    s = natsOptions_SetUserCredentialsFromFiles(NULL, "foo", "bar");
    testCond(s == NATS_INVALID_ARG);

    test("Invalid arg 2: ");
    s = natsOptions_SetUserCredentialsFromFiles(opts, NULL, "bar");
    testCond(s == NATS_INVALID_ARG);

    test("Invalid arg 3: ");
    s = natsOptions_SetUserCredentialsFromFiles(opts, "", "bar");
    testCond(s == NATS_INVALID_ARG);

    test("Clone: ");
    s = natsOptions_SetUserCredentialsFromFiles(opts, "foo", "bar");
    if (s == NATS_OK)
    {
        opts2 = natsOptions_clone(opts);
        if (opts2 == NULL)
            s = NATS_NO_MEMORY;
    }
    IFOK(s, natsOptions_SetUserCredentialsFromFiles(opts, NULL, NULL));
    testCond((s == NATS_OK)
                && (opts2->userCreds != NULL)
                && (opts2->userJWTHandler == natsConn_userCreds)
                && (opts2->userJWTClosure == (void*) opts2->userCreds)
                && (opts2->sigHandler == natsConn_signatureHandler)
                && (opts2->sigClosure == (void*) opts2->userCreds));
    natsOptions_Destroy(opts2);

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("UserOrChainedFile not found: ");
    s = natsOptions_SetUserCredentialsFromFiles(opts, "userCredsNotFound", NULL);
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond((s == NATS_ERR)
                && (strstr(nats_GetLastError(NULL), "error opening file 'userCredsNotFound'") != NULL));

    // Use a file that contains no userJWT..
    test("UserOrChainedFile has no JWT: ");
    s = natsOptions_SetUserCredentialsFromFiles(opts, "list_test.txt", NULL);
    IFOK(s, natsConnection_Connect(&nc, opts));
    // Since we return the whole content of the file when we don't find
    // the key for the user, but we don't for seed, the error we'll get
    // is about no user seed found.
    testCond((s == NATS_ERR)
                && (strstr(nats_GetLastError(NULL), "no nkey user seed found") != NULL));

    test("SeedFile not found: ");
    s = natsOptions_SetUserCredentialsFromFiles(opts, ucfn, "seedFileNotFound");
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond((s == NATS_ERR)
                && (strstr(nats_GetLastError(NULL), "error opening file 'seedFileNotFound'") != NULL));

    // Use a seed file that contains no seed..
    test("SeedFile has no seed: ");
    s = natsOptions_SetUserCredentialsFromFiles(opts, ucfn, "list_test.txt");
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond((s == NATS_ERR)
                && (strstr(nats_GetLastError(NULL), "no nkey user seed found") != NULL));

    _stopServer(pid);

    for (i=0; i<3; i++)
    {
        // Start fake server that will send predefined "nonce" so we can check
        // that connection is sending appropriate jwt and signature.
        s = _createDefaultThreadArgsForCbTests(&arg);
        if (s == NATS_OK)
        {
            // Set this to error, the mock server should set it to OK
            // if it can start successfully.
            arg.done = false;
            arg.status = NATS_ERR;
            arg.checkInfoCB = _checkJWTAndSigCB;
            arg.string = "INFO {\"server_id\":\"22\",\"version\":\"latest\",\"go\":\"latest\",\"port\":4222,\"max_payload\":1048576,\"nonce\":\"nonce\"}\r\n";
            s = natsThread_Create(&t, _startMockupServerThread, (void*) &arg);
        }
        if (s == NATS_OK)
        {
            // Wait for server to be ready
            natsMutex_Lock(arg.m);
            while ((s != NATS_TIMEOUT) && (arg.status != NATS_OK))
                s = natsCondition_TimedWait(arg.c, arg.m, 2000);
            natsMutex_Unlock(arg.m);
        }
        if (s != NATS_OK)
        {
            if (t != NULL)
            {
                natsThread_Join(t);
                natsThread_Destroy(t);
            }
            natsOptions_Destroy(opts);
            _destroyDefaultThreadArgs(&arg);
            FAIL("Unable to setup test");
        }

        s = NATS_OK;
        if (i == 0)
        {
            test("Connect with chained file: ");
            s = natsOptions_SetUserCredentialsFromFiles(opts, ucfn, NULL);
        }
        else if (i == 1)
        {
            test("Connect with user and seed files: ");
            s = natsOptions_SetUserCredentialsFromFiles(opts, ucfn, sfn);
        }
        else if (i == 2)
        {
            test("Connect with user and seed files (seed does not contain header): ");
            s = natsOptions_SetUserCredentialsFromFiles(opts, ucfn, snhfn);
        }
        IFOK(s, natsConnection_Connect(&nc, opts));
        testCond(s == NATS_OK);

        // Notify mock server we are done
        natsMutex_Lock(arg.m);
        arg.done = true;
        natsCondition_Signal(arg.c);
        natsMutex_Unlock(arg.m);

        natsConnection_Destroy(nc);
        nc = NULL;

        natsThread_Join(t);
        natsThread_Destroy(t);
        t = NULL;

        _destroyDefaultThreadArgs(&arg);
    }
    natsOptions_Destroy(opts);

    remove(ucfn);
    remove(sfn);
    remove(snhfn);
    remove(nusfn);
}

static natsStatus
_checkNKeyAndSig(char *buffer)
{
    // NKey should have been included
    if (strstr(buffer, "pubKey") == NULL)
        return NATS_ERR;

    // The server is sending the nonce "nonce" and we
    // use a seed that should have produced a signature
    // that converted to base64 should be:
    if (strstr(buffer, "m50It12aTgfbJwsQhucujqhXbsq7tLM-Mf_hSjBQsG_4onm8y2Vkw6JG1bbcDkdxXe-Ng0K-7X9ov4rZ4wFcDg") == NULL)
        return NATS_ERR;

    return NATS_OK;
}

void test_NKey(void)
{
    natsStatus          s;
    natsOptions         *opts  = NULL;
    natsOptions         *opts2 = NULL;
    natsConnection      *nc    = NULL;
    natsThread          *t     = NULL;
    struct threadArg    arg;

    s = natsOptions_Create(&opts);
    if (s != NATS_OK)
        FAIL("Failed to setup test");

    test("Invalid arg 1: ");
    s = natsOptions_SetNKey(NULL, "pubkey", _dummySigCb, (void*) 1);
    testCond(s == NATS_INVALID_ARG);

    test("Invalid arg 2: ");
    s = natsOptions_SetNKey(opts, "pubkey", NULL, NULL);
    testCond(s == NATS_INVALID_ARG);

    test("Clone: ");
    s = natsOptions_SetNKey(opts, "pubkey", _dummySigCb, (void*) 1);
    if (s == NATS_OK)
    {
        opts2 = natsOptions_clone(opts);
        if (opts2 == NULL)
            s = NATS_NO_MEMORY;
    }
    IFOK(s, natsOptions_SetNKey(opts, NULL, NULL, NULL));
    testCond((s == NATS_OK)
                && (opts2->nkey != NULL)
                && (strcmp(opts2->nkey, "pubkey") == 0)
                && (opts2->sigHandler == _dummySigCb)
                && (opts2->sigClosure == (void*) 1));
    natsOptions_Destroy(opts2);

    test("NKey erase JWT: ");
    s = natsOptions_SetUserCredentialsFromFiles(opts, "foo", "bar");
    IFOK(s, natsOptions_SetNKey(opts, "pubkey2", _dummySigCb, (void*) 2));
    testCond((s == NATS_OK)
                && (opts->nkey != NULL)
                && (strcmp(opts->nkey, "pubkey2") == 0)
                && (opts->userJWTHandler == NULL)
                && (opts->userJWTClosure == NULL)
                && (opts->sigHandler == _dummySigCb)
                && (opts->sigClosure == (void*) 2));

    test("UserCreds erase NKey: ");
    s = natsOptions_SetUserCredentialsFromFiles(opts, "foo", "bar");
    testCond((s == NATS_OK)
                && (opts->nkey == NULL)
                && (opts->userJWTHandler == natsConn_userCreds)
                && (opts->userJWTClosure == (void*) opts->userCreds)
                && (opts->sigHandler == natsConn_signatureHandler)
                && (opts->sigClosure == (void*) opts->userCreds));

    // Start fake server that will send predefined "nonce" so we can check
    // that connection is sending appropriate jwt and signature.
    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
    {
        // Set this to error, the mock server should set it to OK
        // if it can start successfully.
        arg.done = false;
        arg.status = NATS_ERR;
        arg.checkInfoCB = _checkNKeyAndSig;
        arg.string = "INFO {\"server_id\":\"22\",\"version\":\"latest\",\"go\":\"latest\",\"port\":4222,\"max_payload\":1048576,\"nonce\":\"nonce\"}\r\n";
        s = natsThread_Create(&t, _startMockupServerThread, (void*) &arg);
    }
    if (s == NATS_OK)
    {
        // Wait for server to be ready
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && (arg.status != NATS_OK))
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        natsMutex_Unlock(arg.m);
    }
    if (s != NATS_OK)
    {
        if (t != NULL)
        {
            natsThread_Join(t);
            natsThread_Destroy(t);
        }
        natsOptions_Destroy(opts);
        _destroyDefaultThreadArgs(&arg);
        FAIL("Unable to setup test");
    }

    test("NKey works ok: ");
    s = natsOptions_SetNKey(opts, "pubKey", _sigCB, NULL);
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);

    // Notify mock server we are done
    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    natsConnection_Destroy(nc);
    nc = NULL;

    natsThread_Join(t);
    natsThread_Destroy(t);
    t = NULL;

    _destroyDefaultThreadArgs(&arg);

    natsOptions_Destroy(opts);
}

static natsStatus
_checkNKeyFromSeed(char *buffer)
{
    // NKey should have been included
    if (strstr(buffer, "UDXU4RCSJNZOIQHZNWXHXORDPRTGNJAHAHFRGZNEEJCPQTT2M7NLCNF4") == NULL)
        return NATS_ERR;

    // The server is sending the nonce "nonce" and we
    // use a seed that should have produced a signature
    // that converted to base64 should be:
    if (strstr(buffer, "AVfpO7Pw3rc56hoO1OJcFxXUCfBmO2qouchBchSlL45Fuur9zS15UzytEI1QC5wwVG7uiHIdqyfmOS6uPrwqCg") == NULL)
        return NATS_ERR;

    return NATS_OK;
}

void test_NKeyFromSeed(void)
{
    natsStatus          s;
    natsOptions         *opts  = NULL;
    natsOptions         *opts2 = NULL;
    natsConnection      *nc    = NULL;
    natsThread          *t     = NULL;
    FILE                *f     = NULL;
    struct threadArg    arg;

    s = natsOptions_Create(&opts);
    if (s != NATS_OK)
        FAIL("Failed to setup test");

    test("Invalid arg 1: ");
    s = natsOptions_SetNKeyFromSeed(NULL, "pubkey", "seed.file");
    testCond(s == NATS_INVALID_ARG);

    test("Invalid arg 2: ");
    s = natsOptions_SetNKeyFromSeed(opts, "pubkey", NULL);
    testCond(s == NATS_INVALID_ARG);

    nats_clearLastError();

    test("Clone: ");
    s = natsOptions_SetNKeyFromSeed(opts, "pubkey", "seed.file");
    if (s == NATS_OK)
    {
        opts2 = natsOptions_clone(opts);
        if (opts2 == NULL)
            s = NATS_NO_MEMORY;
    }
    IFOK(s, natsOptions_SetNKeyFromSeed(opts, NULL, NULL));
    testCond((s == NATS_OK)
                && (opts2->nkey != NULL)
                && (strcmp(opts2->nkey, "pubkey") == 0)
                && (opts2->sigHandler == natsConn_signatureHandler)
                && (opts2->sigClosure == (void*) opts2->userCreds)
                && (opts2->userCreds != NULL)
                && (opts2->userCreds->seedFile != NULL)
                && (strcmp(opts2->userCreds->seedFile, "seed.file") == 0));
    natsOptions_Destroy(opts2);

    test("NKeyFromSeed erase JWT: ");
    s = natsOptions_SetUserCredentialsFromFiles(opts, "foo", "bar");
    IFOK(s, natsOptions_SetNKeyFromSeed(opts, "pubkey2", "seed.file"));
    testCond((s == NATS_OK)
                && (opts->nkey != NULL)
                && (strcmp(opts->nkey, "pubkey2") == 0)
                && (opts->userJWTHandler == NULL)
                && (opts->userJWTClosure == NULL)
                && (opts->sigHandler == natsConn_signatureHandler)
                && (opts->sigClosure == (void*) opts->userCreds)
                && (opts->userCreds != NULL)
                && (opts->userCreds->seedFile != NULL)
                && (strcmp(opts->userCreds->seedFile, "seed.file") == 0));

    test("UserCreds erase NKeyFromSeed: ");
    s = natsOptions_SetUserCredentialsFromFiles(opts, "foo", NULL);
    testCond((s == NATS_OK)
                && (opts->nkey == NULL)
                && (opts->userJWTHandler == natsConn_userCreds)
                && (opts->userJWTClosure == (void*) opts->userCreds)
                && (opts->sigHandler == natsConn_signatureHandler)
                && (opts->sigClosure == (void*) opts->userCreds)
                && (opts->userCreds != NULL)
                && (opts->userCreds->seedFile == NULL));

    // Start fake server that will send predefined "nonce" so we can check
    // that connection is sending appropriate jwt and signature.
    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
    {
        // Set this to error, the mock server should set it to OK
        // if it can start successfully.
        arg.done = false;
        arg.status = NATS_ERR;
        arg.checkInfoCB = _checkNKeyFromSeed;
        arg.string = "INFO {\"server_id\":\"22\",\"version\":\"latest\",\"go\":\"latest\",\"port\":4222,\"max_payload\":1048576,\"nonce\":\"nonce\"}\r\n";
        s = natsThread_Create(&t, _startMockupServerThread, (void*) &arg);
    }
    if (s == NATS_OK)
    {
        // Wait for server to be ready
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && (arg.status != NATS_OK))
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        natsMutex_Unlock(arg.m);
    }
    if (s != NATS_OK)
    {
        if (t != NULL)
        {
            natsThread_Join(t);
            natsThread_Destroy(t);
        }
        natsOptions_Destroy(opts);
        _destroyDefaultThreadArgs(&arg);
        FAIL("Unable to setup test");
    }

    test("NKeyFromSeed works ok: ");
    f = fopen("seed.file", "w");
    if (f == NULL)
        s = NATS_ERR;
    else
    {
        fputs("SUACSSL3UAHUDXKFSNVUZRF5UHPMWZ6BFDTJ7M6USDXIEDNPPQYYYCU3VY\n", f);
        fclose(f);
        f = NULL;
    }
    IFOK(s, natsOptions_SetNKeyFromSeed(opts, "UDXU4RCSJNZOIQHZNWXHXORDPRTGNJAHAHFRGZNEEJCPQTT2M7NLCNF4", "seed.file"));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);

    // Notify mock server we are done
    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    natsConnection_Destroy(nc);
    nc = NULL;

    natsThread_Join(t);
    natsThread_Destroy(t);
    t = NULL;

    _destroyDefaultThreadArgs(&arg);

    natsOptions_Destroy(opts);

    remove("seed.file");
}

void test_ConnSign(void)
{
    natsStatus          s;
    natsConnection      *nc     = NULL;
    natsOptions         *opts   = NULL;
    natsPid             pid     = NATS_INVALID_PID;
    const char          *ucfn   = "user.creds";
    FILE                *f      = NULL;
    unsigned char       sig[64];
    const unsigned char expected[] = {
            155, 157,   8, 183,  93, 154,  78,   7,
            219,  39,  11,  16, 134, 231,  46, 142,
            168,  87, 110, 202, 187, 180, 179,  62,
             49, 255, 225,  74,  48,  80, 176, 111,
            248, 162, 121, 188, 203, 101, 100, 195,
            162,  70, 213, 182, 220,  14,  71, 113,
             93, 239, 141, 131,  66, 190, 237, 127,
            104, 191, 138, 217, 227,   1,  92,  14,
    };

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Connect ok: ");
    s = natsConnection_ConnectTo(&nc, "nats://127.0.0.1:4222");
    testCond(s == NATS_OK);

    test("Can't sign without user creds: ");
    s = natsConnection_Sign(nc, (const unsigned char*) "payload", 7, sig);
    testCond((s == NATS_ERR)
                && (nats_GetLastError(NULL) != NULL)
                && (strstr(nats_GetLastError(NULL), "unable to sign") != NULL));

    natsConnection_Destroy(nc);
    nc = NULL;

    s = NATS_OK;
    test("Set proper option: ");
    f = fopen(ucfn, "w");
    if (f == NULL)
        s = NATS_ERR;
    else
    {
        int res = fputs("SUAMK2FG4MI6UE3ACF3FK3OIQBCEIEZV7NSWFFEW63UXMRLFM2XLAXK4GY\n", f);
        if (res < 0)
            s = NATS_ERR;

        if (fclose(f) != 0)
            s = NATS_ERR;
        f = NULL;
    }
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetUserCredentialsFromFiles(opts, ucfn, ucfn));
    testCond(s == NATS_OK);

    test("Connect ok: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    test("Sign with NULL message: ");
    s = natsConnection_Sign(nc, NULL, 0, sig);
    testCond(s == NATS_OK);

    test("Sign message: ");
    s = natsConnection_Sign(nc, (const unsigned char*) "nonce", 5, sig);
    testCond((s == NATS_OK)
                && (sig != NULL)
                && (memcmp((void*) sig, expected, sizeof(expected)) == 0));

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _stopServer(pid);

    remove(ucfn);
}

void test_WriteDeadline(void)
{
    natsStatus          s;
    natsOptions         *opts = NULL;
    natsConnection      *nc   = NULL;
    natsThread          *t    = NULL;
    char                data[1024] = {0};
    struct threadArg    arg;

    test("Create options: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetAllowReconnect(opts, false));
    testCond(s == NATS_OK);

    test("Set invalid write deadline: ");
    s = natsOptions_SetWriteDeadline(opts, -1);
    testCond(s == NATS_INVALID_ARG);

    test("Start mock server: ");
    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
    {
        arg.status = NATS_ERR;
        arg.string = "INFO {\"server_id\":\"22\",\"version\":\"latest\",\"go\":\"latest\",\"port\":4222,\"max_payload\":1048576}\r\n";
        s = natsThread_Create(&t, _startMockupServerThread, (void*) &arg);
    }
    if (s == NATS_OK)
    {
        // Wait for server to be ready
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && (arg.status != NATS_OK))
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        natsMutex_Unlock(arg.m);
    }
    testCond(s == NATS_OK);

    test("Write deadline kicks publish out: ");
    s = natsOptions_SetIOBufSize(opts, 100);
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, &arg));
    IFOK(s, natsOptions_SetWriteDeadline(opts, 1));
    IFOK(s, natsConnection_Connect(&nc, opts));
    while (s == NATS_OK)
        s = natsConnection_Publish(nc, "foo", data, sizeof(data));
    testCond(s == NATS_TIMEOUT);

    test("Caused a disconnect: ");
    // Since we are not allowing for reconnect, we should
    // get the closed CB.
    natsMutex_Lock(arg.m);
    s = NATS_OK;
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    natsThread_Join(t);
    natsThread_Destroy(t);

    _destroyDefaultThreadArgs(&arg);
}

static void
_publish(void *arg)
{
    natsConnection  *nc = (natsConnection*) arg;
    char            data[1024] = {0};
    int             i;
    natsStatus      s = NATS_OK;

    for (i=0; ((s == NATS_OK) && (i<1000)); i++)
        s = natsConnection_Publish(nc, "foo", data, sizeof(data));

}

void test_NoPartialOnReconnect(void)
{
    natsStatus          s;
    natsOptions         *opts = NULL;
    natsConnection      *nc   = NULL;
    natsThread          *t    = NULL;
    natsThread          *t2   = NULL;
    natsPid             pid   = NATS_INVALID_PID;
    struct threadArg    arg;
    const char          *servers[2] = {"nats://127.0.0.1:4222", "nats://127.0.0.1:4223"};

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("unable to setup test");

    test("Create options: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetAllowReconnect(opts, true));
    IFOK(s, natsOptions_SetReconnectWait(opts, 10));
    IFOK(s, natsOptions_SetReconnectJitter(opts, 0, 0));
    IFOK(s, natsOptions_SetMaxReconnect(opts, 10000));
    IFOK(s, natsOptions_SetServers(opts, servers, 2));
    IFOK(s, natsOptions_SetNoRandomize(opts, true));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, &arg));
    testCond(s == NATS_OK);

    test("Start real backup server: ");
    pid = _startServer("nats://127.0.0.1:4223", "-p 4223", true);
    CHECK_SERVER_STARTED(pid);
    testCond(true);

    test("Start mock server: ");
    if (s == NATS_OK)
    {
        arg.status = NATS_ERR;
        arg.string = "INFO {\"server_id\":\"22\",\"version\":\"latest\",\"go\":\"latest\",\"port\":4222,\"max_payload\":1048576}\r\n";
        s = natsThread_Create(&t, _startMockupServerThread, (void*) &arg);
    }
    if (s == NATS_OK)
    {
        // Wait for server to be ready
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && (arg.status != NATS_OK))
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        natsMutex_Unlock(arg.m);
    }
    testCond(s == NATS_OK);

    test("Connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK)

    test("Start Publish: ");
    s = natsThread_Create(&t2, _publish, (void*) nc);
    testCond(s == NATS_OK);

    nats_Sleep(1000);

    test("Kill server: ");
    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Wait for reconnect: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !(arg.reconnected))
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    if (t2 != NULL)
    {
        natsThread_Join(t2);
        natsThread_Destroy(t2);
    }

    test("Check no proto error: ");
    {
        const char *le = NULL;
        s = natsConnection_GetLastError(nc, &le);
    }
    testCond(s == NATS_OK);

    if (t != NULL)
    {
        natsThread_Join(t);
        natsThread_Destroy(t);
    }

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(pid);
}

void test_ForcedReconnect(void)
{
    natsStatus s;
    struct threadArg    arg;
    natsOptions *opts = NULL;
    natsConnection *nc = NULL;
    natsSubscription *sub = NULL;
    natsMsg *msg = NULL;
    natsPid pid = NATS_INVALID_PID;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("unable to setup test");

    test("Start server, connect, subscribe: ");
    pid = _startServer("nats://127.0.0.1:4222", "-p 4222", true);
    CHECK_SERVER_STARTED(pid);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, &arg));
    IFOK(s, natsOptions_SetReconnectWait(opts, 100));
    IFOK(s, natsConnection_Connect(&nc, opts));
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "foo"));
    testCond(s == NATS_OK);

    test("Send a message to foo: ");
    IFOK(s, natsConnection_PublishString(nc, "foo", "bar"));
    testCond(s == NATS_OK);

    test("Receive the message: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK) && (msg != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Forced reconnect: ");
    s = natsConnection_Reconnect(nc);
    testCond(s == NATS_OK);

    test("Waiting for reconnect: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.reconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 5000);
    arg.reconnected = false;
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Send a message to foo: ");
    IFOK(s, natsConnection_PublishString(nc, "foo", "bar"));
    testCond(s == NATS_OK);

    test("Receive the message: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK) && (msg != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Reconnect again with allowReconnect false, the call succeeds: ");
    natsMutex_Lock(nc->mu);
    nc->opts->allowReconnect = false;
    natsMutex_Unlock(nc->mu);
    s = natsConnection_Reconnect(nc);
    testCond(s == NATS_OK);

    // On MacOS this returns NATS_CONNECTION_CLOSED, on Ubuntu we get a
    // NATS_TIMEOUT.
    test("But the connection is closed: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond(((s == NATS_CONNECTION_CLOSED) || (s = NATS_TIMEOUT)) && (msg == NULL));

    natsConnection_Close(nc);
    test("Reconect on a close connection errors: ");
    s = natsConnection_Reconnect(nc);
    testCond(s == NATS_CONNECTION_CLOSED);

    test("Reconect on a NULL connection errors: ");
    s = natsConnection_Reconnect(NULL);
    testCond(s == NATS_INVALID_ARG);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);
}

static void
_stopServerInThread(void *closure)
{
    natsPid pid = *((natsPid*) closure);

    nats_Sleep(150);
    _stopServer(pid);
}

void test_ReconnectFailsPendingRequest(void)
{
    natsStatus          s;
    natsOptions         *opts = NULL;
    natsConnection      *nc   = NULL;
    natsSubscription    *sub  = NULL;
    natsMsg             *msg  = NULL;
    natsThread          *t    = NULL;
    natsPid             pid   = NATS_INVALID_PID;
    bool                failr = false;
    int                 iter;

    for (iter=1; iter<=2; iter++)
    {
        failr = (iter == 2 ? true : false);

        test("Create options: ");
        s = natsOptions_Create(&opts);
        IFOK(s, natsOptions_SetFailRequestsOnDisconnect(opts, failr));
        testCond(s == NATS_OK);

        test("Start server: ");
        pid = _startServer("nats://127.0.0.1:4222", "-p 4222", true);
        CHECK_SERVER_STARTED(pid);
        testCond(true);

        test("Connect: ");
        s = natsConnection_Connect(&nc, opts);
        testCond(s == NATS_OK);

        test("Create service provider: ");
        s = natsConnection_SubscribeSync(&sub, nc, "requests");
        testCond(s == NATS_OK);

        test("Start thread that will stop server: ");
        s = natsThread_Create(&t, _stopServerInThread, (void*) &pid);
        testCond(s == NATS_OK);

        test((failr ? "Fails due to disconnect: " : "Fails due to timeout: "));
        s = natsConnection_RequestString(&msg, nc, "requests", "help", 300);
        testCond(s == (failr ? NATS_CONNECTION_DISCONNECTED : NATS_TIMEOUT));

        natsThread_Join(t);
        natsThread_Destroy(t);
        t = NULL;
        natsSubscription_Destroy(sub);
        sub = NULL;
        natsConnection_Destroy(nc);
        nc = NULL;
        natsOptions_Destroy(opts);
        opts = NULL;
    }
}

void test_HeadersNotSupported(void)
{
    natsStatus          s;
    natsConnection      *conn = NULL;
    natsMsg             *msg  = NULL;
    natsMsg             *reply= NULL;
    natsThread          *t    = NULL;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
    {
        // Set this to error, the mock server should set it to OK
        // if it can start successfully.
        arg.status = NATS_ERR;
        arg.string = "INFO {\"server_id\":\"22\",\"version\":\"latest\",\"go\":\"latest\",\"port\":4222,\"max_payload\":1048576}\r\n";
        s = natsThread_Create(&t, _startMockupServerThread, (void*) &arg);
    }
    if (s == NATS_OK)
    {
        // Wait for server to be ready
        natsMutex_Lock(arg.m);
        while ((s != NATS_TIMEOUT) && (arg.status != NATS_OK))
            s = natsCondition_TimedWait(arg.c, arg.m, 2000);
        natsMutex_Unlock(arg.m);
    }
    if (s != NATS_OK)
    {
        if (t != NULL)
        {
            natsThread_Join(t);
            natsThread_Destroy(t);
        }
        _destroyDefaultThreadArgs(&arg);
        FAIL("Unable to setup test");
    }

    test("Headers not supported with old server: ");
    s = natsConnection_ConnectTo(&conn, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_HasHeaderSupport(conn));
    testCond(s == NATS_NO_SERVER_SUPPORT);

    test("Create msg with heades: ");
    s = natsMsg_Create(&msg, "foo", NULL, "body", 4);
    IFOK(s, natsMsgHeader_Set(msg, "Header", "Hello Headers!"));
    testCond(s == NATS_OK);

    test("Publish fails: ");
    s = natsConnection_PublishMsg(conn, msg);
    testCond(s == NATS_NO_SERVER_SUPPORT);

    test("Request fails: ");
    s = natsConnection_RequestMsg(&reply, conn, msg, 1000);
    testCond((s == NATS_NO_SERVER_SUPPORT) && (reply == NULL));

    natsConnection_Destroy(conn);

    // Notify mock server we are done
    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Signal(arg.c);
    natsMutex_Unlock(arg.m);

    natsThread_Join(t);
    natsThread_Destroy(t);

    natsMsg_Destroy(msg);
    natsMsg_Destroy(reply);

    _destroyDefaultThreadArgs(&arg);
}

void test_HeadersBasic(void)
{
    natsStatus          s;
    natsConnection      *nc     = NULL;
    natsPid             pid     = NATS_INVALID_PID;
    natsMsg             *msg    = NULL;
    natsMsg             *rmsg   = NULL;
    natsSubscription    *sub    = NULL;
    const char          *val    = NULL;

    if (!serverVersionAtLeast(2, 2, 0))
    {
        char txt[200];

        snprintf(txt, sizeof(txt), "Skipping since requires server version of at least 2.2.0, got %s: ", serverVersion);
        test(txt);
        testCond(true);
        return;
    }

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Connect ok: ");
    s = natsConnection_ConnectTo(&nc, "nats://127.0.0.1:4222");
    testCond(s == NATS_OK);

    test("Headers supported: ");
    s = natsConnection_HasHeaderSupport(nc);
    testCond(s == NATS_OK);

    test("Create sub: ");
    s = natsConnection_SubscribeSync(&sub, nc, "foo");
    testCond(s == NATS_OK);

    test("Create msg with headers: ");
    s = natsMsg_Create(&msg, "foo", NULL, "body", 4);
    IFOK(s, natsMsgHeader_Set(msg, "Headers", "Hello Headers!"))
    testCond(s == NATS_OK);

    test("Publish with headers ok: ");
    s = natsConnection_PublishMsg(nc, msg);
    testCond(s == NATS_OK);

    test("Receive msg: ")
    s = natsSubscription_NextMsg(&rmsg, sub, 1000);
    testCond((s == NATS_OK) && (rmsg != NULL));

    test("Resend msg without lift: ");
    s = natsConnection_PublishMsg(nc, rmsg);
    testCond(s == NATS_OK);
    natsMsg_Destroy(rmsg);
    rmsg = NULL;

    test("Receive msg: ")
    s = natsSubscription_NextMsg(&rmsg, sub, 1000);
    testCond((s == NATS_OK) && (rmsg != NULL));

    test("Check headers: ");
    s = natsMsgHeader_Get(rmsg, "Headers", &val);
    testCond((s == NATS_OK)
                && (val != NULL) && (strcmp(val, "Hello Headers!") == 0)
                && (natsMsg_GetDataLength(rmsg) == 4)
                && (strncmp(natsMsg_GetData(msg), "body", 4) == 0));

    natsMsg_Destroy(rmsg);
    rmsg = NULL;
    test("Value with CRLFs replaced with spaces: ");
    s = natsMsgHeader_Set(msg, "Headers", "value1\r\nvalue2\r\nvalue3");
    IFOK(s, natsConnection_PublishMsg(nc, msg));
    IFOK(s, natsSubscription_NextMsg(&rmsg, sub, 1000));
    IFOK(s, natsMsgHeader_Get(rmsg, "Headers", &val));
    testCond((s == NATS_OK)
                && (val != NULL) && (strcmp(val, "value1  value2  value3") == 0)
                && (natsMsg_GetDataLength(rmsg) == 4)
                && (strncmp(natsMsg_GetData(msg), "body", 4) == 0));

    natsMsg_Destroy(msg);
    natsMsg_Destroy(rmsg);
    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    _stopServer(pid);
}

static void
_msgFilterNoOp(natsConnection *nc, natsMsg **msg, void *closure) { }

static void
_msgFilterAlterMsg(natsConnection *nc, natsMsg **msg, void *closure)
{
    natsStatus  s;
    natsMsg     *nm = NULL;

    s = natsMsg_Create(&nm, natsMsg_GetSubject(*msg), NULL, "replaced", 8);
    if (s != NATS_OK)
        nats_PrintLastErrorStack(stderr);

    natsMsg_Destroy(*msg);
    *msg = nm;
}

static void
_msgFilterDropMsg(natsConnection *nc, natsMsg **msg, void *closure)
{
    natsMsg_Destroy(*msg);
    *msg = NULL;
    natsConn_setFilter(nc, NULL);
}

void test_natsMsgsFilter(void)
{
    natsStatus          s;
    natsConnection      *nc     = NULL;
    natsPid             pid     = NATS_INVALID_PID;
    natsMsg             *msg    = NULL;
    natsSubscription    *sub    = NULL;

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Connect ok: ");
    s = natsConnection_ConnectTo(&nc, "nats://127.0.0.1:4222");
    testCond(s == NATS_OK);

    test("Create sub: ");
    s = natsConnection_SubscribeSync(&sub, nc, "foo");
    testCond(s == NATS_OK);

    test("Add no-op filter: ");
    natsConn_setFilter(nc, _msgFilterNoOp);
    s = natsConnection_PublishString(nc, "foo", "original");
    IFOK(s, natsSubscription_NextMsg(&msg, sub, 1000));
    testCond((s == NATS_OK) && (msg != NULL)
                && (strcmp(natsMsg_GetData(msg), "original") == 0));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Add alter-msg filter: ");
    natsConn_setFilter(nc, _msgFilterAlterMsg);
    s = natsConnection_PublishString(nc, "foo", "original");
    IFOK(s, natsSubscription_NextMsg(&msg, sub, 1000));
    testCond((s == NATS_OK) && (msg != NULL)
                && (strcmp(natsMsg_GetData(msg), "replaced") == 0));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Add drop-msg filter: ");
    natsConn_setFilter(nc, _msgFilterDropMsg);
    s = natsConnection_PublishString(nc, "foo", "will be dropped");
    IFOK(s, natsSubscription_NextMsg(&msg, sub, 100));
    testCond((s == NATS_TIMEOUT) && (msg == NULL));
    nats_clearLastError();

    test("Filter is removed from previous filter: ");
    s = natsConnection_PublishString(nc, "foo", "got it");
    IFOK(s, natsSubscription_NextMsg(&msg, sub, 1000));
    testCond((s == NATS_OK) && (msg != NULL)
                && (strcmp(natsMsg_GetData(msg), "got it") == 0));
    natsMsg_Destroy(msg);
    msg = NULL;

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    _stopServer(pid);
}

static natsStatus
_evLoopAttach(void **userData, void *loop, natsConnection *nc, natsSock socket)
{
    struct threadArg *arg = (struct threadArg *) loop;

    natsMutex_Lock(arg->m);
    *userData = arg;
    arg->nc = nc;
    arg->sock = socket;
    arg->attached++;
    arg->doRead = true;
    natsCondition_Broadcast(arg->c);
    natsMutex_Unlock(arg->m);

    return NATS_OK;
}

static natsStatus
_evLoopRead(void *userData, bool add)
{
    struct threadArg *arg = (struct threadArg *) userData;

    natsMutex_Lock(arg->m);
    arg->doRead = add;
    if (!add)
    {
        arg->closed = true;
        arg->sock = NATS_SOCK_INVALID;
    }
    natsCondition_Broadcast(arg->c);
    natsMutex_Unlock(arg->m);

    return NATS_OK;
}

static natsStatus
_evLoopWrite(void *userData, bool add)
{
    struct threadArg *arg = (struct threadArg *) userData;

    natsMutex_Lock(arg->m);
    arg->doWrite = add;
    natsCondition_Broadcast(arg->c);
    natsMutex_Unlock(arg->m);

    return NATS_OK;
}

static natsStatus
_evLoopDetach(void *userData)
{
    struct threadArg *arg = (struct threadArg *) userData;

    natsMutex_Lock(arg->m);
    arg->detached++;
    natsCondition_Broadcast(arg->c);
    natsMutex_Unlock(arg->m);

    return NATS_OK;
}

static void
_eventLoop(void *closure)
{
    struct threadArg *arg = (struct threadArg *) closure;
    natsConnection   *nc  = NULL;
    bool             read = false;
    bool             write= false;
    bool             stop = false;
    bool             close= false;
    natsSockCtx      ctx;

    natsSock_Init(&ctx);

    while (!stop)
    {
        natsMutex_Lock(arg->m);
        while (!arg->done && !arg->closed && (arg->sock == NATS_SOCK_INVALID))
            natsCondition_Wait(arg->c, arg->m);
        stop = arg->done;
        nc = arg->nc;
        read = arg->doRead;
        write = arg->doWrite;
        close = arg->closed;
        arg->closed = false;
        if ((ctx.fd == NATS_SOCK_INVALID) && (arg->sock != NATS_SOCK_INVALID))
            ctx.fd = arg->sock;
        natsMutex_Unlock(arg->m);

        if (!stop)
        {
            if (read)
            {
                natsStatus  s;

                natsSock_InitDeadline(&ctx, 50);
                s = natsSock_WaitReady(WAIT_FOR_READ, &ctx);
                if (s == NATS_OK)
                    natsConnection_ProcessReadEvent(nc);
            }
            if (write)
                natsConnection_ProcessWriteEvent(nc);
            if (close)
            {
                natsConnection_ProcessCloseEvent(&ctx.fd);
                close = false;
            }
        }
    }
}

void test_EventLoop(void)
{
    natsStatus          s;
    natsConnection      *nc         = NULL;
    natsOptions         *opts       = NULL;
    natsSubscription    *sub        = NULL;
    natsMsg             *msg        = NULL;
    natsPid             pid         = NATS_INVALID_PID;
    struct threadArg    arg;

    test("Set options: ");
    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetMaxReconnect(opts, 100));
    IFOK(s, natsOptions_SetReconnectWait(opts, 50));
    IFOK(s, natsOptions_SetEventLoop(opts, (void*) &arg,
                                     _evLoopAttach,
                                     _evLoopRead,
                                     _evLoopWrite,
                                     _evLoopDetach));
    IFOK(s, natsOptions_SetDisconnectedCB(opts, _disconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));
    testCond(s == NATS_OK);

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Start event loop: ");
    natsMutex_Lock(arg.m);
    arg.sock = NATS_SOCK_INVALID;
    natsMutex_Unlock(arg.m);
    s = natsThread_Create(&arg.t, _eventLoop, (void*) &arg);
    testCond(s == NATS_OK);

    test("Connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK)

    test("Create sub: ");
    s = natsConnection_SubscribeSync(&sub, nc, "foo");
    testCond(s == NATS_OK);

    test("Stop server and wait for disconnect: ");
    _stopServer(pid);
    pid = NATS_INVALID_PID;
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.disconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Restart server: ");
    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);
    testCond(s == NATS_OK);

    test("Wait for reconnect: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.reconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    arg.reconnected = false;
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Publish: ");
    s = natsConnection_PublishString(nc, "foo", "bar");
    testCond(s == NATS_OK);

    test("Check msg received: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Explicit reconnect: ");
    s = natsConnection_Reconnect(nc);
    testCond(s == NATS_OK);

    test("Wait for reconnect: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.reconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    arg.reconnected = false;
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Publish: ");
    s = natsConnection_PublishString(nc, "foo", "bar");
    testCond(s == NATS_OK);

    test("Check msg received: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Close and wait for close cb: ");
    natsConnection_Close(nc);
    s = _waitForConnClosed(&arg);
    testCond(s == NATS_OK);

    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Broadcast(arg.c);
    natsMutex_Unlock(arg.m);

    natsThread_Join(arg.t);
    natsThread_Destroy(arg.t);

    test("Check ev loop: ");
    natsMutex_Lock(arg.m);
    if (arg.attached != 3 || !arg.detached || (arg.sock != NATS_SOCK_INVALID))
        s = NATS_ERR;
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(pid);
}

void test_EventLoopRetryOnFailedConnect(void)
{
    natsStatus          s;
    natsConnection      *nc         = NULL;
    natsOptions         *opts       = NULL;
    natsSubscription    *sub        = NULL;
    natsMsg             *msg        = NULL;
    natsPid             pid         = NATS_INVALID_PID;
    struct threadArg    arg;

    test("Set options: ");
    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetMaxReconnect(opts, 100));
    IFOK(s, natsOptions_SetReconnectWait(opts, 50));
    IFOK(s, natsOptions_SetRetryOnFailedConnect(opts,
                                                true,
                                                _connectedCb,
                                                (void*) &arg));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));
    IFOK(s, natsOptions_SetEventLoop(opts, (void*) &arg,
                                     _evLoopAttach,
                                     _evLoopRead,
                                     _evLoopWrite,
                                     _evLoopDetach));
    testCond(s == NATS_OK);

    test("Start event loop: ");
    natsMutex_Lock(arg.m);
    arg.sock = NATS_SOCK_INVALID;
    natsMutex_Unlock(arg.m);
    s = natsThread_Create(&arg.t, _eventLoop, (void*) &arg);
    testCond(s == NATS_OK);

    test("Start connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_NOT_YET_CONNECTED);
    if (s == NATS_NOT_YET_CONNECTED)
        s = NATS_OK;

    test("Create sub: ");
    s = natsConnection_SubscribeSync(&sub, nc, "foo");
    testCond(s == NATS_OK);

    test("Start server: ");
    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);
    testCond(s == NATS_OK);

    test("Check connected: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.connected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Publish: ");
    s = natsConnection_PublishString(nc, "foo", "bar");
    testCond(s == NATS_OK);

    test("Check msg received: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);

    test("Close and wait for close cb: ");
    natsConnection_Close(nc);
    s = _waitForConnClosed(&arg);
    testCond(s == NATS_OK);

    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Broadcast(arg.c);
    natsMutex_Unlock(arg.m);

    natsThread_Join(arg.t);
    natsThread_Destroy(arg.t);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(pid);
}

void test_EventLoopTLS(void)
{
#if defined(NATS_HAS_TLS)
    natsStatus          s;
    natsConnection      *nc         = NULL;
    natsOptions         *opts       = NULL;
    natsPid             pid         = NATS_INVALID_PID;
    struct threadArg    arg;

    test("Set options: ");
    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetURL(opts, "nats://localhost:4443"));
    IFOK(s, natsOptions_SkipServerVerification(opts, true));
    IFOK(s, natsOptions_SetSecure(opts, true));
    IFOK(s, natsOptions_SetMaxReconnect(opts, 100));
    IFOK(s, natsOptions_SetReconnectWait(opts, 50));
    IFOK(s, natsOptions_SetDisconnectedCB(opts, _disconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, (void*) &arg));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg));
    IFOK(s, natsOptions_SetEventLoop(opts, (void*) &arg,
                                     _evLoopAttach,
                                     _evLoopRead,
                                     _evLoopWrite,
                                     _evLoopDetach));
    testCond(s == NATS_OK);

    test("Start server: ");
    pid = _startServer("nats://127.0.0.1:4443", "-config tls.conf", true);
    CHECK_SERVER_STARTED(pid);
    testCond(s == NATS_OK);

    test("Start event loop: ");
    natsMutex_Lock(arg.m);
    arg.sock = NATS_SOCK_INVALID;
    natsMutex_Unlock(arg.m);
    s = natsThread_Create(&arg.t, _eventLoop, (void*) &arg);
    testCond(s == NATS_OK);

    test("Connect: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    test("Disconnect: ");
    _stopServer(pid);
    pid = NATS_INVALID_PID;
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.disconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Restart server: ");
    pid = _startServer("nats://127.0.0.1:4443", "-config tls.conf", true);
    CHECK_SERVER_STARTED(pid);
    testCond(s == NATS_OK);

    test("Check reconnected: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.reconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    arg.reconnected = false;
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Explicit reconnect: ");
    s = natsConnection_Reconnect(nc);
    testCond(s == NATS_OK);

    test("Wait for reconnect: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.reconnected)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    arg.reconnected = false;
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Close and wait for close cb: ");
    natsConnection_Close(nc);
    s = _waitForConnClosed(&arg);
    testCond(s == NATS_OK);

    test("Shutdown evLoop: ");
    natsMutex_Lock(arg.m);
    arg.done = true;
    natsCondition_Broadcast(arg.c);
    natsMutex_Unlock(arg.m);
    natsThread_Join(arg.t);
    natsThread_Destroy(arg.t);
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&arg);

    _stopServer(pid);
#else
    test("Skipped when built with no SSL support: ");
    testCond(true);
#endif
}

void test_SSLBasic(void)
{
#if defined(NATS_HAS_TLS)
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    args;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s == NATS_OK)
        opts = _createReconnectOptions();
    if (opts == NULL)
        FAIL("Unable to setup test!");

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tls.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Check that connect switches to TLS automatically: ");
    s = natsOptions_SetURL(opts, "nats://localhost:4443");
    // For this test skip server verification
    IFOK(s, natsOptions_SkipServerVerification(opts, true));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    nc = NULL;

    test("Check connects OK with SSL options: ");
    s = natsOptions_SetSecure(opts, true);
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, &args));

    // For test purposes, we provide the CA trusted certs
    IFOK(s, natsOptions_LoadCATrustedCertificates(opts, "certs/ca.pem"));
    IFOK(s, natsConnection_Connect(&nc, opts));
    IFOK(s, natsConnection_PublishString(nc, "foo", "test"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK)

    test("Check reconnects OK: ");
    _stopServer(serverPid);

    nats_Sleep(100);

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tls.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !(args.reconnected))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);

    IFOK(s, natsConnection_PublishString(nc, "foo", "test"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK)

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&args);

    _stopServer(serverPid);
#else
    test("Skipped when built with no SSL support: ");
    testCond(true);
#endif
}

void test_SSLVerify(void)
{
#if defined(NATS_HAS_TLS)
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    args;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s == NATS_OK)
        opts = _createReconnectOptions();
    if (opts == NULL)
        FAIL("Unable to create reconnect options!");

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tlsverify.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Check that connect fails if no SSL certs: ");
    s = natsOptions_SetURL(opts, "nats://localhost:4443");
    IFOK(s, natsOptions_SetSecure(opts, true));
    // For test purposes, we provide the CA trusted certs
    IFOK(s, natsOptions_LoadCATrustedCertificates(opts, "certs/ca.pem"));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, &args));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s != NATS_OK);

    test("Check that connect succeeds with proper certs: ");
    s = natsOptions_LoadCertificatesChain(opts,
                                          "certs/client-cert.pem",
                                          "certs/client-key.pem");
    IFOK(s, natsConnection_Connect(&nc, opts));
    IFOK(s, natsConnection_PublishString(nc, "foo", "test"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Check reconnects OK: ");
    _stopServer(serverPid);

    nats_Sleep(100);

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tlsverify.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !(args.reconnected))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);

    IFOK(s, natsConnection_PublishString(nc, "foo", "test"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK)

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&args);

    _stopServer(serverPid);
#else
    test("Skipped when built with no SSL support: ");
    testCond(true);
#endif
}

void test_SSLLoadCAFromMemory(void)
{
#if defined(NATS_HAS_TLS)
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    natsBuffer          *certBuf  = NULL;
    struct threadArg    args;

    s = nats_ReadFile(&certBuf, 10000, "certs/ca.pem");
    IFOK(s, _createDefaultThreadArgsForCbTests(&args));
    if (s == NATS_OK)
        opts = _createReconnectOptions();
    if (opts == NULL)
        FAIL("Unable to create reconnect options!");

    test("Check NULL certs: ");
    s = natsOptions_SetCATrustedCertificates(opts, NULL);
    testCond(s == NATS_INVALID_ARG);

    test("Check empty certs: ");
    s = natsOptions_SetCATrustedCertificates(opts, "");
    testCond(s == NATS_INVALID_ARG);

    test("Check invalid cert: ");
    s = natsOptions_SetCATrustedCertificates(opts, "invalid");
    testCond(s == NATS_SSL_ERROR);

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tlsverify.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Check that connect succeeds with proper certs: ");
    s = natsOptions_SetReconnectedCB(opts, _reconnectedCb, &args);
    IFOK(s, natsOptions_SetURL(opts, "nats://localhost:4443"));
    IFOK(s, natsOptions_SetSecure(opts, true));
    // For test purposes, we provide the CA trusted certs
    if (s == NATS_OK)
    {
        s = natsOptions_SetCATrustedCertificates(opts, (const char*) natsBuf_Data(certBuf));
        // Demonstrate that we can free the memory after this call..
        natsBuf_Destroy(certBuf);
    }
    IFOK(s, natsOptions_LoadCertificatesChain(opts,
                                              "certs/client-cert.pem",
                                              "certs/client-key.pem"));
    IFOK(s, natsConnection_Connect(&nc, opts));
    IFOK(s, natsConnection_PublishString(nc, "foo", "test"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Check reconnects OK: ");
    _stopServer(serverPid);

    nats_Sleep(100);

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tlsverify.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !(args.reconnected))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);

    IFOK(s, natsConnection_PublishString(nc, "foo", "test"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK)

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&args);

    _stopServer(serverPid);
#else
    test("Skipped when built with no SSL support: ");
    testCond(true);
#endif
}

void test_SSLCertAndKeyFromMemory(void)
{
#if defined(NATS_HAS_TLS)
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    natsBuffer          *certBuf  = NULL;
    natsBuffer          *keyBuf   = NULL;
    struct threadArg    args;

    s = nats_ReadFile(&certBuf, 10000, "certs/client-cert.pem");
    IFOK(s, nats_ReadFile(&keyBuf, 10000, "certs/client-key.pem"));
    IFOK(s, _createDefaultThreadArgsForCbTests(&args));
    if (s == NATS_OK)
        opts = _createReconnectOptions();
    if (opts == NULL)
        FAIL("Unable to create reconnect options!");

    test("Check NULL cert: ");
    s = natsOptions_SetCertificatesChain(opts, NULL, (const char*) natsBuf_Data(keyBuf));
    testCond(s == NATS_INVALID_ARG);

    test("Check empty cert: ");
    s = natsOptions_SetCertificatesChain(opts, "", (const char*) natsBuf_Data(keyBuf));
    testCond(s == NATS_INVALID_ARG);

    test("Check NULL key: ");
    s = natsOptions_SetCertificatesChain(opts, (const char*) natsBuf_Data(certBuf), NULL);
    testCond(s == NATS_INVALID_ARG);

    test("Check empty key: ");
    s = natsOptions_SetCertificatesChain(opts, (const char*) natsBuf_Data(certBuf), "");
    testCond(s == NATS_INVALID_ARG);

    test("Check invalid cert: ");
    s = natsOptions_SetCertificatesChain(opts, "invalid", (const char*) natsBuf_Data(keyBuf));
    testCond(s == NATS_SSL_ERROR);

    test("Check invalid key: ");
    s = natsOptions_SetCertificatesChain(opts, (const char*) natsBuf_Data(certBuf), "invalid");
    testCond(s == NATS_SSL_ERROR);

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tlsverify.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Check that connect succeeds with proper certs: ");
    s = natsOptions_SetReconnectedCB(opts, _reconnectedCb, &args);
    IFOK(s, natsOptions_SetURL(opts, "nats://localhost:4443"));
    IFOK(s, natsOptions_SetSecure(opts, true));
    // For test purposes, we provide the CA trusted certs
    IFOK(s, natsOptions_LoadCATrustedCertificates(opts, "certs/ca.pem"));
    IFOK(s, natsOptions_SetCertificatesChain(opts,
                                             (const char*) natsBuf_Data(certBuf),
                                             (const char*) natsBuf_Data(keyBuf)));
    // Demonstrate that we can free the memory after this call..
    natsBuf_Destroy(certBuf);
    natsBuf_Destroy(keyBuf);

    IFOK(s, natsConnection_Connect(&nc, opts));
    IFOK(s, natsConnection_PublishString(nc, "foo", "test"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Check reconnects OK: ");
    _stopServer(serverPid);

    nats_Sleep(100);

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tlsverify.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !(args.reconnected))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);

    IFOK(s, natsConnection_PublishString(nc, "foo", "test"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK)

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&args);

    _stopServer(serverPid);
#else
    test("Skipped when built with no SSL support: ");
    testCond(true);
#endif
}

void test_SSLVerifyHostname(void)
{
#if defined(NATS_HAS_TLS)
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    args;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s == NATS_OK)
        opts = _createReconnectOptions();
    if (opts == NULL)
        FAIL("Unable to create reconnect options!");

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tls_noip.conf", true);
    CHECK_SERVER_STARTED(serverPid);

#if defined(NATS_FORCE_HOST_VERIFICATION)
    test("Check that connect fails if url is IP: ");
#else
    test("Check that connect succeeds if url is IP: ");
#endif
    s = natsOptions_SetURL(opts, "nats://127.0.0.1:4443");
    IFOK(s, natsOptions_SetSecure(opts, true));
    IFOK(s, natsOptions_LoadCATrustedCertificates(opts, "certs/ca.pem"));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, &args));
    IFOK(s, natsConnection_Connect(&nc, opts));
#if defined(NATS_FORCE_HOST_VERIFICATION)
    testCond(s == NATS_SSL_ERROR);
    nats_clearLastError();
#else
    testCond(s == NATS_OK);
    natsConnection_Destroy(nc);
    nc = NULL;
#endif

    test("Check that connect fails if wrong expected hostname: ");
    s = natsOptions_SetURL(opts, "nats://localhost:4443");
    IFOK(s, natsOptions_SetExpectedHostname(opts, "foo"));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_SSL_ERROR);
    nats_clearLastError();

    test("Check that connect succeeds if hostname ok and no expected hostname set: ");
    s = natsOptions_SetURL(opts, "nats://localhost:4443");
    IFOK(s, natsOptions_SetExpectedHostname(opts, NULL));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK)
    natsConnection_Destroy(nc);
    nc = NULL;

    test("Check that connect succeeds with proper expected hostname: ");
    s = natsOptions_SetURL(opts, "nats://127.0.0.1:4443");
    IFOK(s, natsOptions_SetExpectedHostname(opts, "localhost"));
    IFOK(s, natsConnection_Connect(&nc, opts));
    IFOK(s, natsConnection_PublishString(nc, "foo", "test"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK);

    test("Check reconnects OK: ");
    _stopServer(serverPid);

    nats_Sleep(100);

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tls.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !(args.reconnected))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);

    IFOK(s, natsConnection_PublishString(nc, "foo", "test"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK)

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&args);

    _stopServer(serverPid);
#else
    test("Skipped when built with no SSL support: ");
    testCond(true);
#endif
}

void test_SSLSkipServerVerification(void)
{
#if defined(NATS_HAS_TLS)
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    args;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s == NATS_OK)
        opts = _createReconnectOptions();
    if (opts == NULL)
        FAIL("Unable to create reconnect options!");

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tls.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Check that connect fails due to server verification: ");
    s = natsOptions_SetURL(opts, "nats://127.0.0.1:4443");
    IFOK(s, natsOptions_SetSecure(opts, true));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_SSL_ERROR);

    test("Check that connect succeeds with server verification disabled: ");
    s = natsOptions_SkipServerVerification(opts, true);
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, &args));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);

    test("Check reconnects OK: ");
    _stopServer(serverPid);

    nats_Sleep(100);

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tls.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !(args.reconnected))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);

    IFOK(s, natsConnection_PublishString(nc, "foo", "test"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK)

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&args);

    _stopServer(serverPid);
#else
    test("Skipped when built with no SSL support: ");
    testCond(true);
#endif
}

void test_SSLCiphers(void)
{
#if defined(NATS_HAS_TLS)
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    args;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s == NATS_OK)
        opts = _createReconnectOptions();
    if (opts == NULL)
        FAIL("Unable to setup test!");

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tls.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    test("SetCipherSuites requires OpenSSL 1.1: ");
    s = natsOptions_SetCipherSuites(opts, "TLS_AES_128_GCM_SHA256");
#if defined(NATS_USE_OPENSSL_1_1)
    testCond(s == NATS_OK);
#else
    testCond(s == NATS_ERR);
#endif

    test("Check that connect fails if improper ciphers: ");
    s = natsOptions_SetURL(opts, "nats://localhost:4443");
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, &args));
    IFOK(s, natsOptions_SetSecure(opts, true));
    // For test purposes, we provide the CA trusted certs
    IFOK(s, natsOptions_LoadCATrustedCertificates(opts, "certs/ca.pem"));
    IFOK(s, natsOptions_SetCiphers(opts, "-ALL:RSA"));
#if defined(NATS_USE_OPENSSL_1_1)
    IFOK(s, natsOptions_SetCipherSuites(opts, ""));
#endif
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s != NATS_OK);

    test("Check connects OK with proper ciphers: ");
    s = natsOptions_SetCiphers(opts, "-ALL:HIGH");
#if defined(NATS_USE_OPENSSL_1_1)
    IFOK(s, natsOptions_SetCipherSuites(opts, "TLS_AES_128_GCM_SHA256"));
#endif
    IFOK(s, natsConnection_Connect(&nc, opts));
    IFOK(s, natsConnection_PublishString(nc, "foo", "test"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK)

    test("Check reconnects OK: ");
    _stopServer(serverPid);

    nats_Sleep(100);

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tls.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !(args.reconnected))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);

    IFOK(s, natsConnection_PublishString(nc, "foo", "test"));
    IFOK(s, natsConnection_Flush(nc));
    testCond(s == NATS_OK)

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&args);

    _stopServer(serverPid);
#else
    test("Skipped when built with no SSL support: ");
    testCond(true);
#endif
}

#if defined(NATS_HAS_TLS)
static void
_sslMT(void *closure)
{
    struct threadArg    *args = (struct threadArg*) closure;
    natsStatus          s = NATS_OK;
    natsConnection      *nc = NULL;
    natsSubscription    *sub = NULL;
    natsMsg             *msg = NULL;
    char                subj[64];
    int                 i;
    const char          *msgPayload = "this is a test payload";
    int                 count = 50;

    natsMutex_Lock(args->m);
    snprintf(subj, sizeof(subj), "foo.%d", ++(args->sum));
    while (!(args->current) && (s == NATS_OK))
        s = natsCondition_TimedWait(args->c, args->m, 2000);
    natsMutex_Unlock(args->m);

    if (valgrind)
        count = 10;

    for (i=0; (s == NATS_OK) && (i < count); i++)
    {
        s = natsConnection_Connect(&nc, args->opts);
        IFOK(s, natsConnection_SubscribeSync(&sub, nc, subj));
        IFOK(s, natsConnection_PublishString(nc, subj, msgPayload));
        IFOK(s, natsSubscription_NextMsg(&msg, sub, 2000));
        if (s == NATS_OK)
        {
            if (strcmp(natsMsg_GetData(msg), msgPayload) != 0)
                s = NATS_ERR;
        }

        natsMsg_Destroy(msg);
        msg = NULL;
        natsSubscription_Destroy(sub);
        sub = NULL;
        natsConnection_Destroy(nc);
        nc = NULL;
    }

    if (s != NATS_OK)
    {
        natsMutex_Lock(args->m);
        if (args->status == NATS_OK)
            args->status = s;
        natsMutex_Unlock(args->m);
    }
}

#define SSL_THREADS (3)
#endif

void test_SSLMultithreads(void)
{
#if defined(NATS_HAS_TLS)
    natsStatus          s;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    natsThread          *t[SSL_THREADS];
    int                 i;
    struct threadArg    args;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s == NATS_OK)
        s = natsOptions_Create(&opts);
    if (opts == NULL)
        FAIL("Unable to setup test!");

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tls.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Create options: ");
    s = natsOptions_SetURL(opts, "nats://127.0.0.1:4443");
    IFOK(s, natsOptions_SetSecure(opts, true));
    // For test purposes, we provide the CA trusted certs
    IFOK(s, natsOptions_LoadCATrustedCertificates(opts, "certs/ca.pem"));
    IFOK(s, natsOptions_SetExpectedHostname(opts, "localhost"));
    testCond(s == NATS_OK);

    args.opts = opts;

    for (i=0; (s == NATS_OK) && (i<SSL_THREADS); i++)
        s = natsThread_Create(&t[i], _sslMT, &args);

    test("Create connections from multiple threads using same ssl ctx: ");
    natsMutex_Lock(args.m);
    args.current = true;
    natsCondition_Broadcast(args.c);
    natsMutex_Unlock(args.m);

    for (i=0; i<SSL_THREADS; i++)
    {
        if (t[i] == NULL)
            continue;

        natsThread_Join(t[i]);
        natsThread_Destroy(t[i]);
    }

    natsMutex_Lock(args.m);
    IFOK(s, args.status);
    natsMutex_Unlock(args.m);

    testCond(s == NATS_OK);

    natsOptions_Destroy(opts);

    if (valgrind)
        nats_Sleep(900);

    _destroyDefaultThreadArgs(&args);

    _stopServer(serverPid);
#else
    test("Skipped when built with no SSL support: ");
    testCond(true);
#endif
}

void test_SSLConnectVerboseOption(void)
{
#if defined(NATS_HAS_TLS)
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    struct threadArg    args;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s == NATS_OK)
    {
        opts = _createReconnectOptions();
        if (opts == NULL)
            s = NATS_ERR;
    }
    IFOK(s, natsOptions_SetVerbose(opts, true));
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, &args));
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tls.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    s = natsOptions_SetURL(opts, "nats://127.0.0.1:4443");
    IFOK(s, natsOptions_SetSecure(opts, true));
    // For test purposes, we provide the CA trusted certs
    IFOK(s, natsOptions_LoadCATrustedCertificates(opts, "certs/ca.pem"));
    IFOK(s, natsOptions_SetExpectedHostname(opts, "localhost"));

    test("Check that SSL connect OK when Verbose set: ");

    IFOK(s, natsConnection_Connect(&nc, opts));
    IFOK(s, natsConnection_Flush(nc));

    testCond(s == NATS_OK);

    _stopServer(serverPid);
    serverPid = _startServer("nats://127.0.0.1:4443", "-config tls.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Check that SSL reconnect OK when Verbose set: ");

    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.reconnected)
        s = natsCondition_TimedWait(args.c, args.m, 5000);
    natsMutex_Unlock(args.m);

    IFOK(s, natsConnection_Flush(nc));

    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    if (valgrind)
        nats_Sleep(900);

    _destroyDefaultThreadArgs(&args);

    _stopServer(serverPid);
#else
    test("Skipped when built with no SSL support: ");
    testCond(true);
#endif
}

void test_SSLHandshakeFirst(void)
{
#if defined(NATS_HAS_TLS)
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    if (!serverVersionAtLeast(2, 10, 0))
    {
        char txt[200];

        snprintf(txt, sizeof(txt), "Skipping since requires server version of at least 2.10.0, got %s: ", serverVersion);
        test(txt);
        testCond(true);
        return;
    }

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tlsfirst.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Set options: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:4443"));
    IFOK(s, natsOptions_SetSecure(opts, true));
    IFOK(s, natsOptions_SkipServerVerification(opts, true));
    IFOK(s, natsOptions_SetTimeout(opts, 500));
    testCond(s == NATS_OK);

    test("Check that connect fails if option not set: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s != NATS_OK);
    nats_clearLastError();

    test("Set TLSHandshakeFirst option error: ");
    s = natsOptions_TLSHandshakeFirst(NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Set TLSHandshakeFirst option: ");
    s = natsOptions_TLSHandshakeFirst(opts);
    testCond(s == NATS_OK);

    test("Set TLSHandshakeFirst option without setting secure: ");
    {
        // we start with a new natsOptions struct so that we can test
        // that it does not crash with a minimal config
        natsOptions *no_secure_opts = NULL;
        s = natsOptions_Create(&no_secure_opts);
        IFOK(s, natsOptions_SetURL(no_secure_opts, "nats://127.0.0.1:4443"));
        IFOK(s, natsOptions_SetTimeout(no_secure_opts, 500));
        IFOK(s, natsOptions_TLSHandshakeFirst(no_secure_opts));
        IFOK(s, natsConnection_Connect(&nc, no_secure_opts));
        // expecting an error because cert valiation will fail; the goal here is to avoid a crash
        testCond(s == NATS_SSL_ERROR);
        natsOptions_Destroy(no_secure_opts);
        nats_clearLastError();
    }

    test("Check that connect succeeds: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);
    natsConnection_Destroy(nc);
    nc = NULL;

    _stopServer(serverPid);
    serverPid = _startServer("nats://127.0.0.1:4443", "-config tls.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    test("Check that connect fails if option is set but not in the server: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s != NATS_OK);
    nats_clearLastError();

    natsOptions_Destroy(opts);

#else
    test("Skipped when built with no SSL support: ");
    testCond(true);
#endif
}

void test_SSLServerNameIndication(void)
{
#if defined(NATS_HAS_TLS)
    natsStatus          s = NATS_OK;
    natsSock            sock = NATS_SOCK_INVALID;
    natsThread          *t = NULL;
    struct threadArg    arg;
    natsSockCtx         ctx;
    static const char *server = "tls://localhost:4222";

    memset(&ctx, 0, sizeof(natsSockCtx));

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&(arg.opts)));
    IFOK(s, natsOptions_SetSecure(arg.opts, true));
    IFOK(s, natsOptions_TLSHandshakeFirst(arg.opts));
    IFOK(s, natsOptions_SetServers(arg.opts, &server, 1));
    if (s != NATS_OK)
        FAIL("@@ Unable to setup test!");

    test("Start server and connect client: ")

    arg.control = 3;

    s = _startMockupServer(&sock, "localhost", "4222");

    // Start the thread that will try to connect to our server...
    IFOK(s, natsThread_Create(&t, _connectToMockupServer, (void*) &arg));

    if ((s == NATS_OK)
        && (((ctx.fd = accept(sock, NULL, NULL)) == NATS_SOCK_INVALID)
            || natsSock_SetCommonTcpOptions(ctx.fd) != NATS_OK))
    {
        s = NATS_SYS_ERROR;
    }

    testCond((s == NATS_OK) && (ctx.fd > 0));

    test("Read ClientHello from client: ");
    char buffer[1024];
    memset(buffer, 0, sizeof(buffer));

    int size = recv(ctx.fd, buffer, sizeof(buffer), 0);
    testCond(size > 0);

    // remove all null chars to allow the use of strstr on the result
    for (int i = 0; i < size; ++i) {
        if (buffer[i] == 0)
            buffer[i] = '0';
    }

    test("Check hostname is found in ClientHello: ");
    bool found = strstr(buffer, "localhost");
#if defined(NATS_USE_OPENSSL_1_1)
    testCond(found == true);
#else
    testCond(found == false);
#endif

    // Need to close those for the client side to unblock.
    natsSock_Close(ctx.fd);
    natsSock_Close(sock);

    // Wait for the client to finish.
    if (t != NULL)
    {
        natsThread_Join(t);
        natsThread_Destroy(t);
    }

    _destroyDefaultThreadArgs(&arg);

#else
    test("Skipped when built with no SSL support: ");
    testCond(true);
#endif
}

#if defined(NATS_HAS_TLS)
static natsStatus
_elDummyAttach(void **userData, void *loop, natsConnection *nc, natsSock socket) { return NATS_OK; }

static natsStatus
_elDummyRead(void *userData, bool add) { return NATS_OK; }

static natsStatus
_elDummyWrite(void *userData, bool add) { return NATS_OK; }

static natsStatus
_elDummyDetach(void *userData) { return NATS_OK; }
#endif

void test_SSLSocketLeakWithEventLoop(void)
{
#if defined(NATS_HAS_TLS)
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;

    s = natsOptions_Create(&opts);
    if (s == NATS_OK)
    {
        // Not really using a real event loop, just setting the option.
        s = natsOptions_SetEventLoop(opts, (void*) 1,
                                     _elDummyAttach,
                                     _elDummyRead,
                                     _elDummyWrite,
                                     _elDummyDetach);
    }
    IFOK(s, natsOptions_SetURL(opts, "nats://127.0.0.1:4443"));
    IFOK(s, natsOptions_SetSecure(opts, true));
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    serverPid = _startServer("nats://127.0.0.1:4443", "-config tls.conf", true);
    CHECK_SERVER_STARTED(serverPid);

    // The options have not been set properly for a successful TLS connection.
    test("Check that SSL fails: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s != NATS_OK);

    // Valgrind will tell us if we have leaked socket.

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _stopServer(serverPid);
#else
    test("Skipped when built with no SSL support: ");
    testCond(true);
#endif
}

void test_SSLReconnectWithAuthError(void)
{
#if defined(NATS_HAS_TLS)
    natsStatus          s;
    natsConnection      *nc     = NULL;
    natsOptions         *opts   = NULL;
    natsPid             pid1    = NATS_INVALID_PID;
    natsPid             pid2    = NATS_INVALID_PID;
    struct threadArg    args;

    s = _createDefaultThreadArgsForCbTests(&args);
    IFOK(s, natsOptions_Create(&opts));
    IFOK(s, natsOptions_SetSecure(opts, true));
    IFOK(s, natsOptions_SkipServerVerification(opts, true));
    IFOK(s, natsOptions_SetTimeout(opts, 250));
    IFOK(s, natsOptions_SetMaxReconnect(opts, 1000));
    IFOK(s, natsOptions_SetReconnectWait(opts, 100));
    IFOK(s, natsOptions_SetClosedCB(opts, _closedCb, (void*) &args));
    IFOK(s, natsOptions_SetServers(opts, (const char*[2]){"tls://user:pwd@127.0.0.1:4443", "tls://bad:pwd@127.0.0.1:4444"}, 2));
    if (opts == NULL)
        FAIL("Unable to create reconnect options!");

    pid1 = _startServer("nats://127.0.0.1:4443", "-a 127.0.0.1 -p 4443 -cluster_name abc -cluster nats://127.0.0.1:6222 -tls -tlscert certs/server-cert.pem -tlskey certs/server-key.pem -tlscacert certs/ca.pem -user user -pass pwd", true);
    CHECK_SERVER_STARTED(pid1);

    pid2 = _startServer("nats://127.0.0.1:4444", "-a 127.0.0.1 -p 4444 -cluster_name abc -cluster nats://127.0.0.1:6223 -routes nats://127.0.0.1:6222 -tls -tlscert certs/server-cert.pem -tlskey certs/server-key.pem -tlscacert certs/ca.pem -user user -pass pwd", true);
    CHECK_SERVER_STARTED(pid2);

    test("Connect to server1: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    test("Stop server1: ");
    _stopServer(pid1);
    testCond(true);

    test("Check that client stops after auth errors: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.closed)
        s = natsCondition_TimedWait(args.c, args.m, 5000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&args);

    _stopServer(pid2);
#else
    test("Skipped when built with no SSL support: ");
    testCond(true);
#endif
}

void test_SSLAvailable(void)
{
#if defined(NATS_HAS_TLS)
    natsStatus          s;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    char conf[256];
    char cmdLine[1024];

    _createConfFile(conf, sizeof(conf), "allow_non_tls: true\n");
    test("Start server: ");
    snprintf(cmdLine, sizeof(cmdLine), "-tls -tlscert certs/server-cert.pem -tlskey certs/server-key.pem -tlscacert certs/ca.pem -c %s", conf);
    serverPid = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(serverPid);
    testCond(true);

    test("Create options: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetSecure(opts, true));
    IFOK(s, natsOptions_SkipServerVerification(opts, true));
    IFOK(s, natsOptions_SetURL(opts, "tls://localhost:4222"));
    testCond(s == NATS_OK);

    test("Connect with TLS: ");
    s = natsConnection_Connect(&nc, opts);
    testCond(s == NATS_OK);

    natsConnection_Destroy(nc);
    nc = NULL;
    natsOptions_Destroy(opts);
    opts = NULL;

    test("Connection without: ");
    s = natsConnection_ConnectTo(&nc, "nats://localhost:4222");
    testCond(s == NATS_OK);
    natsConnection_Destroy(nc);

    _stopServer(serverPid);
    remove(conf);
#else
    test("Skipped when built with no SSL support: ");
    testCond(true);
#endif
}

static void
rmtree(const char *path)
{
#ifdef _WIN32
    WIN32_FIND_DATA ffd;
    HANDLE          hFind = INVALID_HANDLE_VALUE;
    char            *dir  = NULL;

    if (nats_asprintf(&dir, "%s\\*", path) < 0)
        abort();

    hFind = FindFirstFile(dir, &ffd);
    if (hFind == INVALID_HANDLE_VALUE)
    {
        free(dir);
        return;
    }

    do
    {
        char *fullPath = NULL;

        if (!strcmp(ffd.cFileName, ".") || !strcmp(ffd.cFileName, ".."))
            continue;

        if (nats_asprintf(&fullPath, "%s\\%s", path, ffd.cFileName) < 0)
            abort();

        if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
            rmtree(fullPath);
        else
            DeleteFile(fullPath);
        free(fullPath);
    }
    while (FindNextFile(hFind, &ffd) != 0);

    FindClose(hFind);
    RemoveDirectory(path);
    free(dir);

#else
    DIR             *dir;
    struct stat     statPath, statEntry;
    struct dirent   *entry;

    memset(&statPath, 0, sizeof(struct stat));

    stat(path, &statPath);
    if (S_ISDIR(statPath.st_mode) == 0)
        return;

    if ((dir = opendir(path)) == NULL)
        return;

    while ((entry = readdir(dir)) != NULL)
    {
        char *fullPath = NULL;

        if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
            continue;

        if (nats_asprintf(&fullPath, "%s/%s", path, entry->d_name) < 0)
            abort();

        memset(&statEntry, 0, sizeof(struct stat));
        stat(fullPath, &statEntry);

        if (S_ISDIR(statEntry.st_mode) != 0)
            rmtree(fullPath);
        else
            unlink(fullPath);

        free(fullPath);
    }

    closedir(dir);
    rmdir(path);
#endif
}

static void
_makeUniqueDir(char *buf, int bufLen, const char *path)
{
    int n;

    if ((int) strlen(path) + 1 + NUID_BUFFER_LEN + 1 > bufLen)
        abort();

    n = snprintf(buf, bufLen, "%s", path);
    natsNUID_Next(buf+n, NUID_BUFFER_LEN+1);
    buf[n+NUID_BUFFER_LEN+1] = '\0';
}

static void
_createConfFile(char *buf, int bufLen, const char *content)
{
    FILE *f = NULL;

    if (5 + NUID_BUFFER_LEN + 1 > bufLen)
        abort();

    memcpy(buf, "conf_", 5);
    natsNUID_Next(buf+5, NUID_BUFFER_LEN+1);
    buf[5+NUID_BUFFER_LEN+1] = '\0';

    f = fopen(buf, "w");
    if (f == NULL)
        abort();

    fputs(content, f);
    fclose(f);
}

void test_ReconnectImplicitUserInfo(void)
{
    natsStatus          s;
    natsPid             pid1    = NATS_INVALID_PID;
    natsPid             pid2    = NATS_INVALID_PID;
    natsOptions         *o1     = NULL;
    natsConnection      *nc1 = NULL;
    natsSubscription    *sub = NULL;
    natsConnection      *nc2 = NULL;
    natsMsg             *msg = NULL;
    char conf[256];
    char cmdLine[1024];
    struct threadArg args;

    _createConfFile(conf, sizeof(conf),
    "accounts { "\
    "   A { "\
    "       users: [{user: a, password: pwd}] "\
    "   }\n"\
    "   B { "\
    "       users: [{user: b, password: pwd}] "\
    "   }\n"\
    "}\n"\
    "no_auth_user: b\n");
    test("Start server1: ");
    snprintf(cmdLine, sizeof(cmdLine), "-a 127.0.0.1 -p 4222 -cluster_name \"local\" -cluster nats://127.0.0.1:6222 -c %s", conf);
    pid1 = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(pid1);
    testCond(true);

    test("Connect1: ");
    s = _createDefaultThreadArgsForCbTests(&args);
    IFOK(s, natsOptions_Create(&o1));
    IFOK(s, natsOptions_SetDiscoveredServersCB(o1, _discoveredServersCb, (void*) &args));
    IFOK(s, natsOptions_SetURL(o1, "nats://a:pwd@127.0.0.1:4222"));
    IFOK(s, natsOptions_SetReconnectWait(o1, 100));
    IFOK(s, natsOptions_SetReconnectedCB(o1, _reconnectedCb, (void*) &args));
    IFOK(s, natsConnection_Connect(&nc1, o1));
    testCond(s == NATS_OK);

    test("Start server2: ");
    snprintf(cmdLine, sizeof(cmdLine), "-a 127.0.0.1 -p 4223 -cluster_name \"local\" -cluster nats://127.0.0.1:6223 -routes nats://127.0.0.1:6222 -c %s", conf);
    pid2 = _startServer("nats://127.0.0.1:4223", cmdLine, true);
    CHECK_SERVER_STARTED(pid2);
    testCond(true);

    test("Check s2 discovered: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum == 0))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Stop srv1: ");
    _stopServer(pid1);
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.reconnected)
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Create sub: ");
    s = natsConnection_SubscribeSync(&sub, nc1, "foo");
    testCond(s == NATS_OK);

    test("Flush: ");
    s = natsConnection_Flush(nc1);
    testCond(s == NATS_OK);

    test("Connect 2: ");
    s = natsConnection_ConnectTo(&nc2, "nats://a:pwd@127.0.0.1:4223");
    testCond(s == NATS_OK);

    test("Publish: ");
    s = natsConnection_PublishString(nc2, "foo", "msg");
    testCond(s == NATS_OK);

    test("Check received: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);

    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc1);
    natsOptions_Destroy(o1);
    natsConnection_Destroy(nc2);
    _stopServer(pid2);
    _destroyDefaultThreadArgs(&args);
    remove(conf);
}

void test_JetStreamUnmarshalAccountInfo(void)
{
    natsStatus          s;
    nats_JSON           *json = NULL;
    jsAccountInfo       *ai   = NULL;
    jsTier              *r    = NULL;
    const char          *bad[] = {
        "{\"memory\":\"abc\"}",
        "{\"storage\":\"abc\"}",
        "{\"streams\":\"abc\"}",
        "{\"consumers\":\"abc\"}",
        "{\"domain\":123}",
        "{\"api\":123}",
        "{\"api\":{\"total\":\"abc\"}}",
        "{\"api\":{\"errors\":\"abc\"}}",
        "{\"limits\":123}",
        "{\"limits\":{\"max_memory\":\"abc\"}}",
        "{\"limits\":{\"max_storage\":\"abc\"}}",
        "{\"limits\":{\"max_streams\":\"abc\"}}",
        "{\"limits\":{\"max_consumers\":\"abc\"}}",
        "{\"limits\":{\"max_ack_pending\":\"abc\"}}",
        "{\"limits\":{\"memory_max_stream_bytes\":\"abc\"}}",
        "{\"limits\":{\"storage_max_stream_bytes\":\"abc\"}}",
        "{\"limits\":{\"max_bytes_required\":\"abc\"}}",
        "{\"tier\":123}",
        "{\"tier\":{1, 2}}",
        "{\"tier\":{\"R1\":123}}",
        "{\"tier\":{\"R1\":{\"memory\":\"abc\"}}}",
    };
    char tmp[2048];
    int i;

    for (i=0; i<(int)(sizeof(bad)/sizeof(char*)); i++)
    {
        test("Bad fields: ");
        s = nats_JSONParse(&json, bad[i], (int) strlen(bad[i]));
        IFOK(s, js_unmarshalAccountInfo(json, &ai));
        testCond((s != NATS_OK) && (ai == NULL));
        nats_JSONDestroy(json);
        json = NULL;
        nats_clearLastError();
    }

    test("Account info complete: ");
    snprintf(tmp, sizeof(tmp), "{\"memory\":1000,\"storage\":2000,\"streams\":5,\"consumers\":7,"\
        "\"domain\":\"MyDomain\","\
        "\"api\":{\"total\":8,\"errors\":2},"\
        "\"limits\":{\"max_memory\":3000,\"max_storage\":4000,\"max_streams\":10,\"max_consumers\":20,"\
        "\"max_ack_pending\":100,\"memory_max_stream_bytes\":1000000,\"storage_max_stream_bytes\":2000000,\"max_bytes_required\":true},"\
        "\"tier\":{\"R1\":{\"memory\":1000,\"storage\":2000,\"streams\":5,\"consumers\":7,"\
        "\"limits\":{\"max_memory\":3000,\"max_storage\":4000,\"max_streams\":10,\"max_consumers\":20,"\
        "\"max_ack_pending\":100,\"memory_max_stream_bytes\":1000000,\"storage_max_stream_bytes\":2000000,\"max_bytes_required\":true}},"\
        "\"R2\":{\"memory\":2000,\"storage\":3000,\"streams\":8,\"consumers\":9,"\
        "\"limits\":{\"max_memory\":4000,\"max_storage\":5000,\"max_streams\":20,\"max_consumers\":30,"\
        "\"max_ack_pending\":200,\"memory_max_stream_bytes\":2000000,\"storage_max_stream_bytes\":3000000}}}}");
    s = nats_JSONParse(&json, tmp, (int) strlen(tmp));
    IFOK(s, js_unmarshalAccountInfo(json, &ai));
    testCond((s == NATS_OK) && (ai != NULL)
                && (ai->Memory == 1000)
                && (ai->Store == 2000)
                && (ai->Streams == 5)
                && (ai->Consumers == 7)
                && (ai->Domain != NULL)
                && (strcmp(ai->Domain, "MyDomain") == 0)
                && (ai->API.Total == 8)
                && (ai->API.Errors == 2)
                && (ai->Limits.MaxMemory == 3000)
                && (ai->Limits.MaxStore == 4000)
                && (ai->Limits.MaxStreams == 10)
                && (ai->Limits.MaxConsumers == 20)
                && (ai->Limits.MaxAckPending == 100)
                && (ai->Limits.MemoryMaxStreamBytes == 1000000)
                && (ai->Limits.StoreMaxStreamBytes == 2000000)
                && (ai->Limits.MaxBytesRequired)
                && (ai->Tiers != NULL)
                && (ai->TiersLen == 2));

    test("Check tier R1: ");
    if (strcmp(ai->Tiers[0]->Name, "R1") == 0)
        r = ai->Tiers[0];
    else if (strcmp(ai->Tiers[1]->Name, "R1") == 0)
        r = ai->Tiers[1];
    else
        s = NATS_ERR;
    testCond((s == NATS_OK)
                && (r->Memory == 1000)
                && (r->Store == 2000)
                && (r->Streams == 5)
                && (r->Consumers == 7)
                && (r->Limits.MaxMemory == 3000)
                && (r->Limits.MaxStore == 4000)
                && (r->Limits.MaxStreams == 10)
                && (r->Limits.MaxConsumers == 20)
                && (r->Limits.MaxAckPending == 100)
                && (r->Limits.MemoryMaxStreamBytes == 1000000)
                && (r->Limits.StoreMaxStreamBytes == 2000000)
                && r->Limits.MaxBytesRequired);

    test("Check tier R2: ");
    if (strcmp(ai->Tiers[0]->Name, "R2") == 0)
        r = ai->Tiers[0];
    else if (strcmp(ai->Tiers[1]->Name, "R2") == 0)
        r = ai->Tiers[1];
    else
        s = NATS_ERR;
    testCond((s == NATS_OK)
                && (r->Memory == 2000)
                && (r->Store == 3000)
                && (r->Streams == 8)
                && (r->Consumers == 9)
                && (r->Limits.MaxMemory == 4000)
                && (r->Limits.MaxStore == 5000)
                && (r->Limits.MaxStreams == 20)
                && (r->Limits.MaxConsumers == 30)
                && (r->Limits.MaxAckPending == 200)
                && (r->Limits.MemoryMaxStreamBytes == 2000000)
                && (r->Limits.StoreMaxStreamBytes == 3000000)
                && (!r->Limits.MaxBytesRequired));

    nats_JSONDestroy(json);
    jsAccountInfo_Destroy(ai);
}

void test_JetStreamUnmarshalStreamState(void)
{
    natsStatus          s;
    nats_JSON           *json = NULL;
    jsStreamState       state;
    const char          *bad[] = {
        "{\"state\":{\"messages\":\"abc\"}}",
        "{\"state\":{\"bytes\":\"abc\"}}",
        "{\"state\":{\"first_seq\":\"abc\"}}",
        "{\"state\":{\"first_ts\":123}}",
        "{\"state\":{\"last_seq\":\"abc\"}}",
        "{\"state\":{\"last_ts\":123}}",
        "{\"state\":{\"num_deleted\":\"abc\"}}",
        "{\"state\":{\"deleted\":\"abc\"}}",
        "{\"state\":{\"lost\":\"abc\"}}",
        "{\"state\":{\"lost\":{\"msgs\":\"abc\"}}}",
        "{\"state\":{\"lost\":{\"bytes\":\"abc\"}}}",
        "{\"state\":{\"consumer_count\":\"abc\"}}",
        "{\"state\":{\"num_subjects\":\"abc\"}}",
    };
    int i;

    for (i=0; i<(int)(sizeof(bad)/sizeof(char*)); i++)
    {
        test("Bad fields: ");
        memset(&state, 0, sizeof(jsStreamState));
        s = nats_JSONParse(&json, bad[i], (int) strlen(bad[i]));
        IFOK(s, js_unmarshalStreamState(json, "state", &state));
        testCond(s != NATS_OK);
        nats_JSONDestroy(json);
        json = NULL;
        nats_clearLastError();
    }

    test("Unmarshal: ");
    memset(&state, 0, sizeof(jsStreamState));
    s = nats_JSONParse(&json, "{\"state\":{\"messages\":1,\"bytes\":2,"\
        "\"first_seq\":3,\"first_ts\":\"2021-06-23T18:22:00.123Z\","\
        "\"last_seq\":4,\"last_ts\":\"2021-06-23T18:22:00.123456789Z\","\
        "\"num_deleted\":5,\"deleted\":[6,7,8,9,10],"\
        "\"lost\":{\"msgs\":[11,12,13],\"bytes\":14},"\
        "\"consumer_count\":15,\"num_subjects\":3}}", -1);
    IFOK(s, js_unmarshalStreamState(json, "state", &state));
    testCond((s == NATS_OK) && (json != NULL)
                && (state.Msgs == 1)
                && (state.Bytes == 2)
                && (state.FirstSeq == 3)
                && (state.FirstTime == 1624472520123000000)
                && (state.LastSeq == 4)
                && (state.LastTime == 1624472520123456789)
                && (state.NumDeleted == 5)
                && (state.Deleted != NULL)
                && (state.DeletedLen == 5)
                && (state.Deleted[0] == 6)
                && (state.Deleted[1] == 7)
                && (state.Deleted[2] == 8)
                && (state.Deleted[3] == 9)
                && (state.Deleted[4] == 10)
                && (state.Lost != NULL)
                && (state.Lost->MsgsLen == 3)
                && (state.Lost->Msgs != NULL)
                && (state.Lost->Msgs[0] == 11)
                && (state.Lost->Msgs[1] == 12)
                && (state.Lost->Msgs[2] == 13)
                && (state.Lost->Bytes == 14)
                && (state.Consumers == 15)
                && (state.NumSubjects == 3));

    test("Cleanup: ");
    js_cleanStreamState(&state);
    testCond(true);
    // Check that this is fine
    js_cleanStreamState(NULL);

    nats_JSONDestroy(json);
}

void test_JetStreamUnmarshalStreamConfig(void)
{
    natsStatus          s;
    nats_JSON           *json = NULL;
    jsStreamConfig      *sc   = NULL;
    const char          *missing[] = {
        "{\"name\":\"TEST\"}",
        "{\"name\":\"TEST\",\"retention\":\"limits\"}",
        "{\"name\":\"TEST\",\"retention\":\"limits\",\"max_consumers\":5}",
        "{\"name\":\"TEST\",\"retention\":\"limits\",\"max_consumers\":5,\"max_msgs\":10}",
        "{\"name\":\"TEST\",\"retention\":\"limits\",\"max_consumers\":5,\"max_msgs\":10,\"max_bytes\":1000}",
        "{\"name\":\"TEST\",\"retention\":\"limits\",\"max_consumers\":5,\"max_msgs\":10,\"max_bytes\":1000,\"max_age\":20000000}",
        "{\"name\":\"TEST\",\"retention\":\"limits\",\"max_consumers\":5,\"max_msgs\":10,\"max_bytes\":1000,\"max_age\":20000000,\"discard\":\"new\"}",
        "{\"name\":\"TEST\",\"retention\":\"limits\",\"max_consumers\":5,\"max_msgs\":10,\"max_bytes\":1000,\"max_age\":20000000,\"discard\":\"new\",\"storage\":\"memory\"}",
    };
    char tmp[2048];
    int i;

    for (i=0; i<(int)(sizeof(missing)/sizeof(char*)); i++)
    {
        test("Missing fields: ");
        s = nats_JSONParse(&json, missing[i], (int) strlen(missing[i]));
        IFOK(s, js_unmarshalStreamConfig(json, NULL, &sc));
        testCond((s == NATS_OK) && (sc != NULL));
        js_destroyStreamConfig(sc);
        sc = NULL;
        nats_JSONDestroy(json);
        json = NULL;
    }

    test("Stream config with all required: ");
    snprintf(tmp, sizeof(tmp), "{\"name\":\"TEST\",\"retention\":\"workqueue\","\
        "\"max_consumers\":5,\"max_msgs\":10,\"max_bytes\":1000,\"max_msgs_per_subject\":1,\"max_age\":20000000,"\
        "\"discard\":\"new\",\"storage\":\"memory\",\"num_replicas\":3}");
    s = nats_JSONParse(&json, tmp, (int) strlen(tmp));
    IFOK(s, js_unmarshalStreamConfig(json, NULL, &sc));
    testCond((s == NATS_OK) && (sc != NULL)
                && (strcmp(sc->Name, "TEST") == 0)
                && (sc->Retention == js_WorkQueuePolicy)
                && (sc->MaxConsumers == 5)
                && (sc->MaxMsgs == 10)
                && (sc->MaxBytes == 1000)
                && (sc->MaxAge == 20000000)
                && (sc->MaxMsgsPerSubject == 1)
                && (sc->Discard == js_DiscardNew)
                && (sc->Storage == js_MemoryStorage)
                && (sc->Replicas == 3));
    js_destroyStreamConfig(sc);
    sc = NULL;
    nats_JSONDestroy(json);
    json = NULL;

    test("Stream config with all: ");
    if (snprintf(tmp, sizeof(tmp), "{"
        "\"name\":\"TEST\""
        ",\"description\":\"this is my stream\""
        ",\"subjects\":[\"foo\",\"bar\"]"
        ",\"retention\":\"workqueue\""
        ",\"max_consumers\":5"
        ",\"max_msgs\":10"
        ",\"max_bytes\":1000"
        ",\"max_age\":20000000"
        ",\"max_msgs_per_subject\":1"
        ",\"max_msg_size\":1024"
        ",\"discard\":\"new\""
        ",\"storage\":\"memory\""
        ",\"num_replicas\":3"
        ",\"no_ack\":true"
        ",\"template_owner\":\"owner\""
        ",\"duplicate_window\":100000000000"
        ",\"placement\":{\"cluster\":\"cluster\",\"tags\":[\"tag1\",\"tag2\"]}"
        ",\"mirror\":{\"name\":\"TEST2\",\"opt_start_seq\":10,\"filter_subject\":\"foo\",\"external\":{\"api\":\"my_prefix\",\"deliver\":\"deliver_prefix\"}}"
        ",\"sources\":[{\"name\":\"TEST3\",\"opt_start_seq\":20,\"filter_subject\":\"bar\",\"external\":{\"api\":\"my_prefix2\",\"deliver\":\"deliver_prefix2\"}}]"
        ",\"sealed\":true"
        ",\"deny_delete\":true"
        ",\"deny_purge\":true"
        ",\"allow_rollup_hdrs\":true"
        ",\"republish\":{\"src\":\"foo\",\"dest\":\"bar\"}"
        ",\"allow_direct\":true"
        ",\"mirror_direct\":true"
        ",\"discard_new_per_subject\":true"
        ",\"metadata\":{\"foo\":\"bar\"}"
        ",\"compression\":\"s2\""
        ",\"first_seq\":9999"
        ",\"subject_transform\":{\"src\":\"foo\",\"dest\":\"bar\"}"
        ",\"consumer_limits\":{\"inactive_threshold\":1000,\"max_ack_pending\":99}"
        "}") >= (int) sizeof(tmp))
    {
        abort();
    }
    s = nats_JSONParse(&json, tmp, (int) strlen(tmp));
    IFOK(s, js_unmarshalStreamConfig(json, NULL, &sc));
    testCond((s == NATS_OK) && (sc != NULL)
                && (sc->Name != NULL)
                && (strcmp(sc->Name, "TEST") == 0)
                && (sc->Description != NULL)
                && (strcmp(sc->Description, "this is my stream") == 0)
                && (sc->Subjects != NULL)
                && (sc->SubjectsLen == 2)
                && (strcmp(sc->Subjects[0], "foo") == 0)
                && (strcmp(sc->Subjects[1], "bar") == 0)
                && (sc->MaxMsgsPerSubject == 1)
                && (sc->MaxMsgSize == 1024)
                && (sc->NoAck)
                && (strcmp(sc->Template, "owner") == 0)
                && (sc->Duplicates == 100000000000)
                && (sc->Placement != NULL)
                && (strcmp(sc->Placement->Cluster, "cluster") == 0)
                && (sc->Placement->TagsLen == 2)
                && (strcmp(sc->Placement->Tags[0], "tag1") == 0)
                && (strcmp(sc->Placement->Tags[1], "tag2") == 0)
                && (sc->Mirror != NULL)
                && (strcmp(sc->Mirror->Name, "TEST2") == 0)
                && (sc->Mirror->OptStartSeq == 10)
                && (strcmp(sc->Mirror->FilterSubject, "foo") == 0)
                && (sc->Mirror->External != NULL)
                && (strcmp(sc->Mirror->External->APIPrefix, "my_prefix") == 0)
                && (strcmp(sc->Mirror->External->DeliverPrefix, "deliver_prefix") == 0)
                && (sc->SourcesLen == 1)
                && (strcmp(sc->Sources[0]->Name, "TEST3") == 0)
                && (sc->Sources[0]->OptStartSeq == 20)
                && (strcmp(sc->Sources[0]->FilterSubject, "bar") == 0)
                && (sc->Sources[0]->External != NULL)
                && (strcmp(sc->Sources[0]->External->APIPrefix, "my_prefix2") == 0)
                && (strcmp(sc->Sources[0]->External->DeliverPrefix, "deliver_prefix2") == 0)
                && sc->Sealed && sc->DenyDelete && sc->DenyPurge && sc->AllowRollup
                && ((sc->RePublish != NULL)
                    && (sc->RePublish->Source != NULL)
                    && (strcmp(sc->RePublish->Source, "foo") == 0)
                    && (sc->RePublish->Destination != NULL)
                    && (strcmp(sc->RePublish->Destination, "bar") == 0))
                && sc->AllowDirect && sc->MirrorDirect
                && sc->DiscardNewPerSubject
                && (sc->Metadata.Count == 1)
                && (sc->Metadata.List != NULL)
                && (strcmp(sc->Metadata.List[0], "foo") == 0)
                && (strcmp(sc->Metadata.List[1], "bar") == 0)
                && (sc->Compression == js_StorageCompressionS2)
                && (sc->FirstSeq == 9999)
                && (strcmp(sc->SubjectTransform.Source, "foo") == 0)
                && (strcmp(sc->SubjectTransform.Destination, "bar") == 0)
                && (sc->ConsumerLimits.InactiveThreshold == 1000)
                && (sc->ConsumerLimits.MaxAckPending == 99)
                );
    js_destroyStreamConfig(sc);
    sc = NULL;
    nats_JSONDestroy(json);
    json = NULL;
}

void test_JetStreamUnmarshalStreamInfo(void)
{
    natsStatus          s;
    nats_JSON           *json = NULL;
    jsStreamInfo        *si   = NULL;
    const char *good[] = {
        "{\"cluster\":{\"name\":\"S1\",\"leader\":\"S2\"}}",
        "{\"cluster\":{\"name\":\"S1\",\"leader\":\"S2\",\"replicas\":[{\"name\":\"S1\",\"current\":true,\"offline\":false,\"active\":123,\"lag\":456},{\"name\":\"S1\",\"current\":false,\"offline\":true,\"active\":123,\"lag\":456}]}}",
        "{\"mirror\":{\"name\":\"M\",\"lag\":123,\"active\":456}}",
        "{\"mirror\":{\"name\":\"M\",\"external\":{\"api\":\"MyApi\",\"deliver\":\"deliver.prefix\"},\"lag\":123,\"active\":456}}",
        "{\"sources\":[{\"name\":\"S1\",\"lag\":123,\"active\":456}]}",
        "{\"sources\":[{\"name\":\"S1\",\"lag\":123,\"active\":456,\"filter_subject\":\"foo\",\"subject_transforms:\":[{\"src\":\"foo\",\"dest\":\"bar\"}]}]}",
        "{\"sources\":[{\"name\":\"S1\",\"lag\":123,\"active\":456},{\"name\":\"S2\",\"lag\":123,\"active\":456}]}",
        "{\"sources\":[{\"name\":\"S1\",\"external\":{\"api\":\"MyApi\",\"deliver\":\"deliver.prefix\"},\"lag\":123,\"active\":456},{\"name\":\"S2\",\"lag\":123,\"active\":456}]}",
        "{\"alternates\":[{\"name\":\"S1\",\"domain\":\"domain\",\"cluster\":\"abc\"}]}",
        "{\"alternates\":[{\"name\":\"S1\",\"domain\":\"domain\",\"cluster\":\"abc\"},{\"name\":\"S2\",\"domain\":\"domain\",\"cluster\":\"abc\"}]}",
    };
    const char *bad[] = {
        "{\"config\":123}",
        "{\"config\":{\"retention\":\"bad_policy\"}}",
        "{\"config\":{\"discard\":\"bad_policy\"}}",
        "{\"config\":{\"storage\":\"bad_policy\"}}",
        "{\"config\":{\"placement\":{\"cluster\":123}}}",
        "{\"config\":{\"placement\":{\"tags\":123}}}",
        "{\"config\":{\"mirror\":{\"name\":123}}}",
        "{\"config\":{\"mirror\":{\"opt_start_seq\":\"abc\"}}}",
        "{\"config\":{\"mirror\":{\"opt_start_time\":123}}}",
        "{\"config\":{\"mirror\":{\"filter_subject\":123}}}",
        "{\"config\":{\"mirror\":{\"external\":123}}}",
        "{\"config\":{\"mirror\":{\"external\":{\"api\":123}}}}",
        "{\"config\":{\"mirror\":{\"external\":{\"deliver\":123}}}}",
        "{\"state\":123}",
        "{\"state\":{\"messages\":\"abc\"}}",
        "{\"state\":{\"bytes\":\"abc\"}}",
        "{\"state\":{\"first_seq\":\"abc\"}}",
        "{\"state\":{\"first_ts\":\"abc\"}}",
        "{\"state\":{\"last_seq\":\"abc\"}}",
        "{\"state\":{\"last_ts\":\"abc\"}}",
        "{\"state\":{\"num_deleted\":\"abc\"}}",
        "{\"state\":{\"deleted\":\"abc\"}}",
        "{\"state\":{\"deleted\":[\"abc\",\"def\"]}}",
        "{\"state\":{\"lost\":\"abc\"}}",
        "{\"state\":{\"lost\":{\"msgs\":\"abc\"}}}",
        "{\"state\":{\"lost\":{\"msgs\":[\"abc\",\"def\"]}}}",
        "{\"state\":{\"lost\":{\"bytes\":\"abc\"}}}",
        "{\"state\":{\"consumer_count\":\"abc\"}}",
        "{\"state\":{\"num_subjects\":\"abc\"}}",
        "{\"state\":{\"subjects\":\"abc\"}}",
        "{\"state\":{\"subjects\":{\"abc\"}}}",
        "{\"state\":{\"subjects\":{\"abc\":1,\"def\":\"ghi\"}}}",
        "{\"cluster\":123}",
        "{\"cluster\":{\"name\":123}}",
        "{\"cluster\":{\"name\":\"S1\",\"leader\":123}}",
        "{\"cluster\":{\"name\":\"S1\",\"leader\":\"S2\",\"replicas\":123}}",
        "{\"cluster\":{\"name\":\"S1\",\"leader\":\"S2\",\"replicas\":[{\"name\":123}]}}",
        "{\"mirror\":123}",
        "{\"mirror\":{\"name\":123}}",
        "{\"mirror\":{\"name\":\"S1\",\"external\":123}}",
        "{\"mirror\":{\"name\":\"S1\",\"external\":{\"api\":123}}}",
        "{\"sources\":123}",
        "{\"sources\":[{\"name\":123}]}",
        "{\"sources\":[{\"name\":\"S1\",\"external\":123}]}",
        "{\"sources\":[{\"name\":\"S1\",\"external\":{\"deliver\":123}}]}",
        "{\"alternates\":123}",
        "{\"alternates\":[{\"name\":123}]}",
        "{\"alternates\":[{\"name\":\"S1\",\"domain\":123}]}",
        "{\"alternates\":[{\"name\":\"S1\",\"domain\":\"domain\",\"cluster\":123}]}",
    };
    int i;
    char tmp[64];

    for (i=0; i<(int)(sizeof(good)/sizeof(char*)); i++)
    {
        snprintf(tmp, sizeof(tmp), "Positive case %d: ", i+1);
        test(tmp);
        s = nats_JSONParse(&json, good[i], (int) strlen(good[i]));
        IFOK(s, js_unmarshalStreamInfo(json, &si));
        testCond((s == NATS_OK) && (si != NULL));
        jsStreamInfo_Destroy(si);
        si = NULL;
        nats_JSONDestroy(json);
        json = NULL;
    }

    for (i=0; i<(int)(sizeof(bad)/sizeof(char*)); i++)
    {
        snprintf(tmp, sizeof(tmp), "Negative case %d: ", i+1);
        test(tmp);
        s = nats_JSONParse(&json, bad[i], (int) strlen(bad[i]));
        IFOK(s, js_unmarshalStreamInfo(json, &si));
        testCond((s != NATS_OK) && (si == NULL));
        nats_JSONDestroy(json);
        json = NULL;
        nats_clearLastError();
    }
}

void test_JetStreamMarshalStreamConfig(void)
{
    natsStatus          s;
    jsStreamConfig      sc;
    jsPlacement         p;
    jsStreamSource      m;
    jsExternalStream    esm;
    jsStreamSource      s1;
    jsExternalStream    esmS1;
    jsStreamSource      s2;
    jsExternalStream    esmS2;
    natsBuffer          *buf           = NULL;
    nats_JSON           *json          = NULL;
    jsStreamConfig      *rsc           = NULL;
    int64_t             optStartTime   = 1624583232123456000;
    jsRePublish         rp;

    test("init bad args: ");
    s = jsStreamConfig_Init(NULL);
    if (s == NATS_INVALID_ARG)
        s = jsPlacement_Init(NULL);
    if (s == NATS_INVALID_ARG)
        s = jsStreamSource_Init(NULL);
    if (s == NATS_INVALID_ARG)
        s = jsExternalStream_Init(NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    jsStreamConfig_Init(&sc);
    sc.Name = "MyStream";
    sc.Description = "this is my stream";
    sc.Subjects = (const char*[2]){"foo", "bar"};
    sc.SubjectsLen = 2;
    sc.Retention = js_InterestPolicy;
    sc.MaxConsumers = 1;
    sc.MaxMsgs = 2;
    sc.MaxBytes = 3;
    sc.MaxAge = 4;
    sc.MaxMsgSize = 5;
    sc.Duplicates = 6;
    sc.MaxMsgsPerSubject = 1;
    sc.Discard = js_DiscardNew;
    sc.Storage = js_MemoryStorage;
    sc.Replicas = 3;
    sc.NoAck = true;
    sc.Template = "template";

    jsPlacement_Init(&p);
    p.Cluster = "MyCluster";
    p.Tags = (const char*[2]){"tag1", "tag2"};
    p.TagsLen = 2;
    sc.Placement = &p;

    jsStreamSource_Init(&m);
    m.Name = "AStream";
    m.OptStartSeq = 100;
    m.OptStartTime = optStartTime;
    m.FilterSubject = "foo";
    jsExternalStream_Init(&esm);
    esm.APIPrefix = "mirror.prefix";
    esm.DeliverPrefix = "deliver.prefix";
    m.External = &esm;
    sc.Mirror = &m;

    jsStreamSource_Init(&s1);
    s1.Name = "StreamOne";
    s1.OptStartSeq = 10;
    s1.FilterSubject = "stream.one";
    jsExternalStream_Init(&esmS1);
    esmS1.APIPrefix = "source1.prefix";
    esmS1.DeliverPrefix = "source1.deliver.prefix";
    s1.External = &esmS1;

    jsStreamSource_Init(&s2);
    s2.Name = "StreamTwo";
    s2.OptStartSeq = 20;
    s2.FilterSubject = "stream.two";
    jsExternalStream_Init(&esmS2);
    esmS2.APIPrefix = "source2.prefix";
    esmS2.DeliverPrefix = "source2.deliver.prefix";
    s2.External = &esmS2;

    sc.Sources = (jsStreamSource *[2]){&s1, &s2};
    sc.SourcesLen = 2;

    // Seal, deny purge, etc..
    sc.Sealed = true;
    sc.DenyDelete = true;
    sc.DenyPurge = true;
    sc.AllowRollup = true;
    sc.AllowDirect = true;
    sc.MirrorDirect = true;
    sc.DiscardNewPerSubject = true;

    test("RePublish init err: ");
    s = jsRePublish_Init(NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();
    s = NATS_OK;

    // Republish
    jsRePublish_Init(&rp);
    rp.Source = ">";
    rp.Destination = "RP.>";
    rp.HeadersOnly = true;
    sc.RePublish = &rp;

    // 2.10 options: Compression, Metadata, etc.
    sc.Compression = js_StorageCompressionS2;
    sc.Metadata.List = (const char *[]){"k1", "v1", "k2", "v2"};
    sc.Metadata.Count = 2;
    sc.FirstSeq = 9999;
    sc.SubjectTransform = (jsSubjectTransformConfig) {
        .Source = "foo",
        .Destination = "bar",
    };
    sc.ConsumerLimits = (jsStreamConsumerLimits) {
        .InactiveThreshold = 1000,
        .MaxAckPending = 99,
    };

    test("Marshal stream config: ");
    s = js_marshalStreamConfig(&buf, &sc);
    testCond((s == NATS_OK) && (buf != NULL) && (natsBuf_Len(buf) > 0));

    test("Verify with unmarshal: ");
    s = nats_JSONParse(&json, natsBuf_Data(buf), natsBuf_Len(buf));
    IFOK(s, js_unmarshalStreamConfig(json, NULL, &rsc));
    testCond((s == NATS_OK) && (rsc != NULL)
                && (rsc->Name != NULL)
                && (strcmp(rsc->Name, "MyStream") == 0)
                && (rsc->Description != NULL)
                && (strcmp(rsc->Description, "this is my stream") == 0)
                && (rsc->SubjectsLen == 2)
                && (rsc->Subjects != NULL)
                && (strcmp(rsc->Subjects[0], "foo") == 0)
                && (strcmp(rsc->Subjects[1], "bar") == 0)
                && (rsc->Retention == js_InterestPolicy)
                && (rsc->MaxConsumers == 1)
                && (rsc->MaxMsgs == 2)
                && (rsc->MaxBytes == 3)
                && (rsc->MaxAge == 4)
                && (rsc->MaxMsgSize == 5)
                && (rsc->MaxMsgsPerSubject == 1)
                && (rsc->Duplicates == 6)
                && (rsc->Discard == js_DiscardNew)
                && (rsc->Storage == js_MemoryStorage)
                && (rsc->Replicas == 3)
                && rsc->NoAck
                && (strcmp(rsc->Template, "template") == 0)
                && (rsc->Placement != NULL)
                && (strcmp(rsc->Placement->Cluster, "MyCluster") == 0)
                && (rsc->Placement->TagsLen == 2)
                && (rsc->Placement->Tags != NULL)
                && (strcmp(rsc->Placement->Tags[0], "tag1") == 0)
                && (strcmp(rsc->Placement->Tags[1], "tag2") == 0)
                && (rsc->Mirror != NULL)
                && (strcmp(rsc->Mirror->Name, "AStream") == 0)
                && (rsc->Mirror->OptStartSeq == 100)
                && (rsc->Mirror->OptStartTime == optStartTime)
                && (strcmp(rsc->Mirror->FilterSubject, "foo") == 0)
                && (rsc->Mirror->External != NULL)
                && (strcmp(rsc->Mirror->External->APIPrefix, "mirror.prefix") == 0)
                && (strcmp(rsc->Mirror->External->DeliverPrefix, "deliver.prefix") == 0)
                && (rsc->SourcesLen == 2)
                && (rsc->Sources != NULL)
                && (strcmp(rsc->Sources[0]->Name, "StreamOne") == 0)
                && (rsc->Sources[0]->OptStartSeq == 10)
                && (strcmp(rsc->Sources[0]->FilterSubject, "stream.one") == 0)
                && (rsc->Sources[0]->External != NULL)
                && (strcmp(rsc->Sources[0]->External->APIPrefix, "source1.prefix") == 0)
                && (strcmp(rsc->Sources[0]->External->DeliverPrefix, "source1.deliver.prefix") == 0)
                && (strcmp(rsc->Sources[1]->Name, "StreamTwo") == 0)
                && (rsc->Sources[1]->OptStartSeq == 20)
                && (strcmp(rsc->Sources[1]->FilterSubject, "stream.two") == 0)
                && (rsc->Sources[1]->External != NULL)
                && (strcmp(rsc->Sources[1]->External->APIPrefix, "source2.prefix") == 0)
                && (strcmp(rsc->Sources[1]->External->DeliverPrefix, "source2.deliver.prefix") == 0)
                && rsc->Sealed
                && rsc->DenyDelete
                && rsc->DenyPurge
                && rsc->AllowRollup
                && (rsc->RePublish != NULL)
                && (rsc->RePublish->Source != NULL)
                && (strcmp(rsc->RePublish->Source, ">") == 0)
                && (rsc->RePublish->Destination != NULL)
                && (strcmp(rsc->RePublish->Destination, "RP.>") == 0)
                && rsc->RePublish->HeadersOnly
                && rsc->AllowDirect
                && rsc->MirrorDirect
                && rsc->DiscardNewPerSubject
                && (rsc->Compression == js_StorageCompressionS2)
                && (rsc->Metadata.Count == 2)
                && (strcmp(rsc->Metadata.List[0], "k2") == 0)
                && (strcmp(rsc->Metadata.List[1], "v2") == 0)
                && (strcmp(rsc->Metadata.List[2], "k1") == 0)
                && (strcmp(rsc->Metadata.List[3], "v1") == 0)
                && (rsc->FirstSeq == 9999)
                && (strcmp(rsc->SubjectTransform.Source, "foo") == 0)
                && (strcmp(rsc->SubjectTransform.Destination, "bar") == 0)
                && (rsc->ConsumerLimits.InactiveThreshold == 1000)
                && (rsc->ConsumerLimits.MaxAckPending == 99)
                );
    js_destroyStreamConfig(rsc);
    rsc = NULL;
    // Check that this does not crash
    js_destroyStreamConfig(NULL);
    natsBuf_Destroy(buf);
    buf = NULL;
    nats_JSONDestroy(json);
    json = NULL;

    test("WorkQueue policy: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Retention = js_WorkQueuePolicy;
    s = js_marshalStreamConfig(&buf, &sc);
    testCond((s == NATS_OK) && (buf != NULL) && (natsBuf_Len(buf) > 0));

    test("Verify with unmarshal: ");
    s = nats_JSONParse(&json, natsBuf_Data(buf), natsBuf_Len(buf));
    IFOK(s, js_unmarshalStreamConfig(json, NULL, &rsc));
    testCond((s == NATS_OK) && (rsc != NULL) && (rsc->Retention == js_WorkQueuePolicy));
    js_destroyStreamConfig(rsc);
    rsc = NULL;
    natsBuf_Destroy(buf);
    buf = NULL;
    nats_JSONDestroy(json);
    json = NULL;

    test("Bad retention: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Retention = (jsRetentionPolicy) 100;
    s = js_marshalStreamConfig(&buf, &sc);
    testCond((s == NATS_INVALID_ARG) && (buf == NULL));

    test("Bad discard: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Discard = (jsDiscardPolicy) 100;
    s = js_marshalStreamConfig(&buf, &sc);
    testCond((s == NATS_INVALID_ARG) && (buf == NULL));

    test("Bad storage: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Storage = (jsStorageType) 100;
    s = js_marshalStreamConfig(&buf, &sc);
    testCond((s == NATS_INVALID_ARG) && (buf == NULL));
}

void test_JetStreamUnmarshalConsumerInfo(void)
{
    natsStatus          s;
    jsConsumerInfo      *ci     = NULL;
    nats_JSON           *json   = NULL;
    const char          *good[] = {
        "{\"stream_name\":\"TEST\"}",
        "{\"name\":\"abc\"}",
        "{\"created\":\"2021-06-23T18:22:00.123456789Z\"}",
        "{\"delivered\":{\"consumer_seq\":1,\"stream_seq\":1,\"last_active\":\"2021-08-21T11:49:00.123456789Z\"}}",
        "{\"ack_floor\":{\"consumer_seq\":1,\"stream_seq\":1,\"last_active\":\"2021-08-21T11:49:00.123456789Z\"}}",
        "{\"num_ack_pending\":1}",
        "{\"num_redelivered\":1}",
        "{\"num_waiting\":1}",
        "{\"num_pending\":1}",
        "{\"push_bound\":true}",
        "{\"config\":{\"deliver_policy\":\"" jsDeliverAllStr "\"}}",
        "{\"config\":{\"deliver_policy\":\"" jsDeliverLastStr "\"}}",
        "{\"config\":{\"deliver_policy\":\"" jsDeliverNewStr "\"}}",
        "{\"config\":{\"deliver_policy\":\"" jsDeliverBySeqStr "\"}}",
        "{\"config\":{\"deliver_policy\":\"" jsDeliverByTimeStr "\"}}",
        "{\"config\":{\"deliver_policy\":\"" jsDeliverLastPerSubjectStr "\"}}",
        "{\"config\":{\"ack_policy\":\"" jsAckNoneStr "\"}}",
        "{\"config\":{\"ack_policy\":\"" jsAckAllStr "\"}}",
        "{\"config\":{\"ack_policy\":\"" jsAckExplictStr "\"}}",
        "{\"config\":{\"replay_policy\":\"" jsReplayInstantStr "\"}}",
        "{\"config\":{\"replay_policy\":\"" jsReplayOriginalStr "\"}}",
        "{\"config\":{\"deliver_group\":\"queue_name\"}}",
        "{\"config\":{\"headers_only\":true}}",
        "{\"config\":{\"max_batch\":1}}",
        "{\"config\":{\"max_expires\":123456789}}",
        "{\"config\":{\"max_bytes\":1024}}",
        "{\"config\":{\"inactive_threshold\":123456789}}",
        "{\"config\":{\"backoff\":[50000000,250000000]}}",
        "{\"config\":{\"num_replicas\":1}}",
        "{\"config\":{\"mem_storage\":true}}",
        "{\"config\":{\"name\":\"my_name\"}}",
        "{\"config\":{\"name\":\"my_name\",\"metadata\":{\"k1\":\"v1\",\"k2\":\"v2\"}}}",
    };
    const char          *bad[] = {
        "{\"stream_name\":123}",
        "{\"name\":123}",
        "{\"created\":123}",
        "{\"config\":123}",
        "{\"config\":{\"durable_name\":123}}",
        "{\"config\":{\"description\":123}}",
        "{\"config\":{\"deliver_subject\":123}}",
        "{\"config\":{\"deliver_policy\":\"bad_policy\"}}",
        "{\"config\":{\"opt_start_seq\":\"abc\"}}",
        "{\"config\":{\"opt_start_time\":123}}",
        "{\"config\":{\"ack_policy\":\"bad_policy\"}}",
        "{\"config\":{\"ack_wait\":\"abc\"}}",
        "{\"config\":{\"max_deliver\":\"abc\"}}",
        "{\"config\":{\"filter_subject\":123}}",
        "{\"config\":{\"replay_policy\":\"bad_policy\"}}",
        "{\"config\":{\"rate_limit_bps\":\"abc\"}}",
        "{\"config\":{\"sample_freq\":123}}",
        "{\"config\":{\"max_waiting\":\"abc\"}}",
        "{\"config\":{\"max_ack_pending\":\"abc\"}}",
        "{\"config\":{\"flow_control\":\"abc\"}}",
        "{\"config\":{\"idle_heartbeat\":\"abc\"}}",
        "{\"config\":{\"deliver_group\":123}}",
        "{\"config\":{\"headers_only\":123}}",
        "{\"config\":{\"max_batch\":\"1\"}}",
        "{\"config\":{\"max_expires\":\"123456789\"}}",
        "{\"config\":{\"max_bytes\":\"123456789\"}}",
        "{\"config\":{\"inactive_threshold\":\"123456789\"}}",
        "{\"config\":{\"backoff\":true}}",
        "{\"config\":{\"max_batch\":\"abc\"}}",
        "{\"config\":{\"max_expires\":false}}",
        "{\"config\":{\"max_bytes\":false}}",
        "{\"config\":{\"mem_storage\":\"abc\"}}",
        "{\"delivered\":123}",
        "{\"delivered\":{\"consumer_seq\":\"abc\"}}",
        "{\"delivered\":{\"stream_seq\":\"abc\"}}",
        "{\"delivered\":{\"last_active\":123}}",
        "{\"ack_floor\":123}",
        "{\"ack_floor\":{\"consumer_seq\":\"abc\"}}",
        "{\"ack_floor\":{\"stream_seq\":\"abc\"}}",
        "{\"ack_floor\":{\"last_active\":123}}",
        "{\"num_ack_pending\":\"abc\"}",
        "{\"num_redelivered\":\"abc\"}",
        "{\"num_waiting\":\"abc\"}",
        "{\"num_pending\":\"abc\"}",
        "{\"cluster\":123}",
        "{\"cluster\":{\"name\":123}}",
        "{\"push_bound\":123}",
    };
    int i;
    char tmp[64];

    for (i=0; i<(int)(sizeof(good)/sizeof(char*)); i++)
    {
        snprintf(tmp, sizeof(tmp), "Positive case %d: ", i+1);
        test(tmp);
        s = nats_JSONParse(&json, good[i], (int) strlen(good[i]));
        IFOK(s, js_unmarshalConsumerInfo(json, &ci));
        testCond((s == NATS_OK) && (ci != NULL));
        jsConsumerInfo_Destroy(ci);
        ci = NULL;
        nats_JSONDestroy(json);
        json = NULL;
    }

    for (i=0; i<(int)(sizeof(bad)/sizeof(char*)); i++)
    {
        snprintf(tmp, sizeof(tmp), "Negative case %d: ", i+1);
        test(tmp);
        s = nats_JSONParse(&json, bad[i], (int) strlen(bad[i]));
        IFOK(s, js_unmarshalConsumerInfo(json, &ci));
        testCond((s != NATS_OK) && (ci == NULL));
        nats_JSONDestroy(json);
        json = NULL;
        nats_clearLastError();
    }
}

static void
testRespParsing(void)
{
    natsStatus          s;
    nats_JSON           *json = NULL;
    natsMsg             *msg  = NULL;
    const char          *bad[] = {
        "not a JSON",
        "{\"error\":123}",
        "{\"error\":{\"code\":\"abc\"}}",
        "{\"error\":{\"err_code\":\"abc\"}}",
        "{\"error\":{\"description\":123}}",
    };
    const char          *good[] = {
        "{\"not_an_error\":123}",
        "{\"error\":{\"code\":100}}",
        "{\"error\":{\"code\":404,\"description\":\"not found\"}}",
        "{\"error\":{\"code\":404,\"err_code\":10014,\"description\":\"not found\"}}",
    };
    struct _good {
        bool        isErr;
        int         code;
        uint16_t    err_code;
        const char  *desc;
    };
    const struct _good  goodRes[] = {
        { false, 0, 0, NULL },
        { true, 100, 0, NULL },
        { true, 404, 0, "not found" },
        { true, 404, JSConsumerNotFoundErr, "not found" },
    };
    char                tmp[64];
    int                 i;
    jsApiResponse       ar;

    for (i=0; i<(int)(sizeof(bad)/sizeof(char*)); i++)
    {
        snprintf(tmp, sizeof(tmp), "Negative test %d: ", i+1);
        test(tmp);
        s = natsMsg_Create(&msg, "foo", NULL, bad[i], (int)strlen(bad[i]));
        IFOK(s, js_unmarshalResponse(&ar, &json, msg));
        testCond((s != NATS_OK) && (json == NULL));
        natsMsg_Destroy(msg);
        msg = NULL;
        js_freeApiRespContent(&ar);
    }
    for (i=0; i<(int)(sizeof(good)/sizeof(char*)); i++)
    {
        snprintf(tmp, sizeof(tmp), "Positive test %d: ", i+1);
        test(tmp);
        s = natsMsg_Create(&msg, "foo", NULL, good[i], (int)strlen(good[i]));
        IFOK(s, js_unmarshalResponse(&ar, &json, msg));
        if (s == NATS_OK)
        {
            if (!goodRes[i].isErr)
            {
                if (js_apiResponseIsErr(&ar) || (ar.Error.Code != 0)
                    || (ar.Error.ErrCode != 0)
                    || !nats_IsStringEmpty(ar.Error.Description))
                {
                    s = NATS_ERR;
                }
            }
            else
            {
                if ((goodRes[i].code != ar.Error.Code)
                    || (goodRes[i].err_code != ar.Error.ErrCode)
                    || ((goodRes[i].desc == NULL) && !nats_IsStringEmpty(ar.Error.Description))
                    || ((goodRes[i].desc != NULL) && nats_IsStringEmpty(ar.Error.Description))
                    || ((goodRes[i].desc != NULL) && strcmp(goodRes[i].desc, ar.Error.Description) != 0))
                {
                    s = NATS_ERR;
                }
            }
        }
        testCond((s == NATS_OK) && (json != NULL));
        natsMsg_Destroy(msg);
        msg = NULL;
        nats_JSONDestroy(json);
        json = NULL;
        js_freeApiRespContent(&ar);
    }
    // Check that this is ok
    js_freeApiRespContent(NULL);
}

#define ENSURE_JS_VERSION(major, minor, update) \
if (!serverVersionAtLeast((major), (minor), (update))) \
{ \
    char txt[200]; \
    snprintf(txt, sizeof(txt), "Skipping since requires server version of at least %d.%d.%d, got %s: ", \
             (major), (minor), (update), serverVersion); \
    test(txt); \
    testCond(true); \
    return; \
}

#define JS_SETUP_WITH_OPTS(major, minor, update, opts) \
natsConnection  *nc = NULL; \
jsCtx           *js = NULL; \
natsPid         pid = NATS_INVALID_PID;  \
char            datastore[256] = {'\0'}; \
char            cmdLine[1024] = {'\0'};  \
\
ENSURE_JS_VERSION((major), (minor), (update)); \
\
_makeUniqueDir(datastore, sizeof(datastore), "datastore_"); \
test("Start JS Server: ");                                  \
snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s", datastore);\
pid = _startServer("nats://127.0.0.1:4222", cmdLine, true); \
CHECK_SERVER_STARTED(pid);                                  \
testCond(true);                                             \
\
test("Connect: ");                                      \
s = natsConnection_Connect(&nc, opts);                  \
testCond(s == NATS_OK);                                 \
\
test("Get context: ");                          \
s = natsConnection_JetStream(&js, nc, NULL);    \
testCond(s == NATS_OK);

#define JS_SETUP(major, minor, update) JS_SETUP_WITH_OPTS((major), (minor), (update), NULL)

#define JS_TEARDOWN \
jsCtx_Destroy(js);          \
natsConnection_Destroy(nc); \
_stopServer(pid);           \
rmtree(datastore);

void test_JetStreamContext(void)
{
    natsStatus          s;
    natsConnection      *nc = NULL;
    jsCtx               *js = NULL;
    natsPid             pid = NATS_INVALID_PID;
    jsOptions           o;
    jsAccountInfo       *ai = NULL;
    jsErrCode           jerr = 0;
    char                datastore[256] = {'\0'};
    char                cmdLine[1024] = {'\0'};
    char                confFile[256] = {'\0'};

    ENSURE_JS_VERSION(2, 3, 3);

    test("Check response parsing:\n");
    testRespParsing();
    testCond(true);

    _makeUniqueDir(datastore, sizeof(datastore), "datastore_");

    pid = _startServer("nats://127.0.0.1:4222", "", true);
    CHECK_SERVER_STARTED(pid);

    test("Connect: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    testCond(s == NATS_OK);

    test("Check JS context invalid args: ")
    s = natsConnection_JetStream(NULL, nc, NULL);
    if (s == NATS_INVALID_ARG)
        s = natsConnection_JetStream(&js, NULL, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Init options (bad args): ");
    s = jsOptions_Init(NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Check JS context with negative wait: ")
    jsOptions_Init(&o);
    o.Wait = -10;
    s = natsConnection_JetStream(&js, nc, &o);
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), "negative") != NULL));
    o.Wait = 0;
    nats_clearLastError();

    test("Get context: ");
    s = natsConnection_JetStream(&js, nc, NULL);
    testCond(s == NATS_OK);

    test("Get account info (bad args): ");
    s = js_GetAccountInfo(NULL, js, NULL, NULL);
        s = js_GetAccountInfo(&ai, NULL, NULL, NULL);
    testCond((s == NATS_INVALID_ARG) && (ai == NULL));
    nats_clearLastError();

    test("Get account fail: ");
    s = js_GetAccountInfo(&ai, js, NULL, &jerr);
    testCond((s == NATS_NO_RESPONDERS) && (ai == NULL) && (jerr == JSNotEnabledErr));
    nats_clearLastError();
    jerr = 0;

    jsCtx_Destroy(js);
    js = NULL;
    // Check this is ok
    jsCtx_Destroy(NULL);
    natsConnection_Destroy(nc);
    nc = NULL;

    // Restart the server with JS enabled
    _stopServer(pid);
    pid = NATS_INVALID_PID;
    test("Start JS server: ");
    snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s", datastore);
    pid = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(pid);
    testCond(true);

    test("Connect: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    testCond(s == NATS_OK);

    test("Get context set unknown prefix: ");
    o.Prefix = "MyPrefix";
    s = natsConnection_JetStream(&js, nc, &o);
    testCond(s == NATS_OK);

    test("Get account info with unknown prefix: ");
    s = js_GetAccountInfo(&ai, js, NULL, &jerr);
    testCond((s == NATS_NO_RESPONDERS) && (ai == NULL) && (jerr == JSNotEnabledErr));
    nats_clearLastError();
    jerr = 0;

    test("Get account info override prefix: ");
    o.Prefix = jsDefaultAPIPrefix;
    s = js_GetAccountInfo(&ai, js, &o, &jerr);
    testCond((s == NATS_OK)
                && (jerr == 0)
                && (ai != NULL)
                && (ai->Limits.MaxMemory == -1)
                && (ai->Limits.MaxStore == -1));
    jsAccountInfo_Destroy(ai);
    ai = NULL;

    test("Prefix with trailing '.' works: ");
    o.Prefix = "$JS.API.";
    s = js_GetAccountInfo(&ai, js, &o, &jerr);
    testCond((s == NATS_OK)
                && (jerr == 0)
                && (ai != NULL)
                && (ai->Limits.MaxMemory == -1)
                && (ai->Limits.MaxStore == -1));
    jsAccountInfo_Destroy(ai);
    ai = NULL;

    jsCtx_Destroy(js);
    js = NULL;

    test("Create context with prefix with traling '.': ");
    s = natsConnection_JetStream(&js, nc, &o);
    IFOK(s, js_GetAccountInfo(&ai, js, NULL, &jerr));
    testCond((s == NATS_OK)
                && (jerr == 0)
                && (js != NULL)
                // Verify we don't touch the provided prefix
                && (strcmp(o.Prefix, "$JS.API.") == 0)
                && (ai != NULL)
                && (ai->Limits.MaxMemory == -1)
                && (ai->Limits.MaxStore == -1));
    jsAccountInfo_Destroy(ai);
    ai = NULL;
    // Check this does not crash
    jsAccountInfo_Destroy(NULL);
    jsCtx_Destroy(js);
    js = NULL;
    natsConnection_Destroy(nc);
    nc = NULL;
    _stopServer(pid);
    pid = NATS_INVALID_PID;

    _createConfFile(confFile, sizeof(confFile),
        " accounts { "\
        "   NOJS { "\
        "     users: [ "\
        "       {user: ivan, pass: pwd} "\
        "     ] "\
        "   } "\
        " }\n"
    );
    test("Start JS server: ");
    snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s -c %s", datastore, confFile);
    pid = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(pid);
    testCond(true);

    test("Connect: ");
    s = natsConnection_ConnectTo(&nc, "nats://ivan:pwd@127.0.0.1:4222");
    testCond(s == NATS_OK);

    test("Get context: ");
    s = natsConnection_JetStream(&js, nc, NULL);
    testCond(s == NATS_OK);

    test("Get account info error: ");
    s = js_GetAccountInfo(&ai, js, NULL, &jerr);
    testCond((s == NATS_ERR) && (ai == NULL) && (jerr == JSNotEnabledForAccountErr)
                && (strstr(nats_GetLastError(NULL), "not enabled") != NULL));
    nats_clearLastError();

    JS_TEARDOWN;
    remove(confFile);
}

void test_JetStreamContextDomain(void)
{
    natsStatus          s;
    natsConnection      *nc = NULL;
    jsCtx               *js = NULL;
    natsPid             pid = NATS_INVALID_PID;
    jsOptions           o;
    jsAccountInfo       *ai = NULL;
    jsErrCode           jerr = 0;
    char                datastore[256] = {'\0'};
    char                cmdLine[1024] = {'\0'};
    char                confFile[256] = {'\0'};

    ENSURE_JS_VERSION(2, 3, 3);

    _makeUniqueDir(datastore, sizeof(datastore), "datastore_");
    _createConfFile(confFile, sizeof(confFile),
        " jetstream: { domain: ABC }\n"
    );

    test("Start JS server: ");
    snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s -c %s", datastore, confFile);
    pid = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(pid);
    testCond(true);

    test("Connect: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    testCond(s == NATS_OK);

    test("Create context with domain: ");
    jsOptions_Init(&o);
    o.Domain = "ABC";
    s = natsConnection_JetStream(&js, nc, &o);
    if (s == NATS_OK)
    {
        js_lock(js);
        // The option "Domain" should create the following prefix.
        // The JS internal option should not have domain set.
        if ((strcmp(js->opts.Prefix, "$JS.ABC.API") != 0)
            || (js->opts.Domain != NULL))
        {
            s = NATS_ERR;
        }
        js_unlock(js);
    }
    testCond((s == NATS_OK) && (js != NULL));

    test("Get account: ");
    s = js_GetAccountInfo(&ai, js, NULL, &jerr);
    testCond((s == NATS_OK)
                && (jerr == 0)
                && (ai != NULL)
                && (strcmp(ai->Domain, "ABC") == 0)
                && (ai->Limits.MaxMemory == -1)
                && (ai->Limits.MaxStore == -1));
    jsAccountInfo_Destroy(ai);
    ai = NULL;

    test("Get account with wrong domain override: ");
    o.Domain = "DEF";
    s = js_GetAccountInfo(&ai, js, &o, &jerr);
    testCond((s == NATS_NO_RESPONDERS) && (ai == NULL) && (jerr == JSNotEnabledErr));
    nats_clearLastError();
    jerr = 0;

    jsCtx_Destroy(js);
    js = NULL;

    test("Create context with mix prefix/domain: ");
    jsOptions_Init(&o);
    o.Prefix = "should.not.be.used";
    o.Domain = "ABC";
    s = natsConnection_JetStream(&js, nc, &o);
    testCond((s == NATS_OK) && (js != NULL));

    test("Get account: ");
    s = js_GetAccountInfo(&ai, js, NULL, &jerr);
    testCond((s == NATS_OK)
                && (jerr == 0)
                && (ai != NULL)
                && (strcmp(ai->Domain, "ABC") == 0)
                && (ai->Limits.MaxMemory == -1)
                && (ai->Limits.MaxStore == -1));
    jsAccountInfo_Destroy(ai);
    ai = NULL;

    jsCtx_Destroy(js);
    js = NULL;

    test("Create context without domain: ");
    jsOptions_Init(&o);
    s = natsConnection_JetStream(&js, nc, &o);
    testCond((s == NATS_OK) && (js != NULL));

    test("Get account with domain override: ");
    o.Domain = "ABC";
    s = js_GetAccountInfo(&ai, js, &o, &jerr);
    testCond((s == NATS_OK)
                && (jerr == 0)
                && (ai != NULL)
                && (strcmp(ai->Domain, "ABC") == 0)
                && (ai->Limits.MaxMemory == -1)
                && (ai->Limits.MaxStore == -1));
    jsAccountInfo_Destroy(ai);
    ai = NULL;

    test("Get account with domain override (prefix option ignored): ");
    o.Prefix = "should.not.be.used";
    o.Domain = "ABC";
    s = js_GetAccountInfo(&ai, js, &o, &jerr);
    testCond((s == NATS_OK)
                && (jerr == 0)
                && (ai != NULL)
                && (strcmp(ai->Domain, "ABC") == 0)
                && (ai->Limits.MaxMemory == -1)
                && (ai->Limits.MaxStore == -1));
    jsAccountInfo_Destroy(ai);
    ai = NULL;

    JS_TEARDOWN;
    remove(confFile);
}

static void
_streamsInfoListReq(natsConnection *nc, natsMsg **msg, void *closure)
{
    int         *count      = (int*) closure;
    const char  *payload    = NULL;
    natsMsg     *newMsg     = NULL;

    if (strstr(natsMsg_GetData(*msg), "stream_list_response") == NULL)
        return;

    (*count)++;
    if (*count == 1)
    {
        // Pretend limit is 2 and send 2 simplified stream infos
        payload = "{\"type\":\"io.nats.jetstream.api.v1.stream_list_response\",\"total\":5,\"offset\":0,\"limit\":2,"\
            "\"streams\":[{\"config\":{\"name\":\"S1\"}},{\"config\":{\"name\":\"S2\"}}]}";
    }
    else if (*count == 2)
    {
        // Pretend that there is a repeat of a stream name to check
        // that we are properly replacing and not leaking memory.
        payload = "{\"type\":\"io.nats.jetstream.api.v1.stream_list_response\",\"total\":5,\"offset\":2,\"limit\":2,"\
            "\"streams\":[{\"config\":{\"name\":\"S2\"}},{\"config\":{\"name\":\"S3\"}}]}";
    }
    else if (*count == 3)
    {
        // Pretend that our next page was over the limit (say streams were removed)
        // and therefore the server returned no streams (but set offset to total)
        payload = "{\"type\":\"io.nats.jetstream.api.v1.stream_list_response\",\"total\":3,\"offset\":3,\"limit\":2,"\
            "\"streams\":[]}";
    }
    else
    {
        // Use original message
        return;
    }
    if (natsMsg_create(&newMsg, (*msg)->subject, (int) strlen((*msg)->subject), NULL, 0,
                        payload, (int) strlen(payload), 0) == NATS_OK)
    {
        natsMsg_Destroy(*msg);
        *msg = newMsg;
    }
}

static void
_streamsNamesListReq(natsConnection *nc, natsMsg **msg, void *closure)
{
    int         *count      = (int*) closure;
    const char  *payload    = NULL;
    natsMsg     *newMsg     = NULL;

    if (strstr(natsMsg_GetData(*msg), "stream_names_response") == NULL)
        return;

    (*count)++;
    if (*count == 1)
    {
        // Pretend limit is 2 and send 2 stream names
        payload = "{\"type\":\"io.nats.jetstream.api.v1.stream_names_response\",\"total\":5,\"offset\":0,\"limit\":2,"\
            "\"streams\":[\"S1\",\"S2\"]}";
    }
    else if (*count == 2)
    {
        // Pretend that there is a repeat of a stream name to check
        // that we are properly replacing and not leaking memory.
        payload = "{\"type\":\"io.nats.jetstream.api.v1.stream_names_response\",\"total\":5,\"offset\":2,\"limit\":2,"\
            "\"streams\":[\"S2\",\"S3\"]}";
    }
    else if (*count == 3)
    {
        // Pretend that our next page was over the limit (say streams were removed)
        // and therefore the server returned no streams (but set offset to total)
        payload = "{\"type\":\"io.nats.jetstream.api.v1.stream_names_response\",\"total\":3,\"offset\":3,\"limit\":2,"\
            "\"streams\":null}";
    }
    else
    {
        // Use original message
        return;
    }
    if (natsMsg_create(&newMsg, (*msg)->subject, (int) strlen((*msg)->subject), NULL, 0,
                        payload, (int) strlen(payload), 0) == NATS_OK)
    {
        natsMsg_Destroy(*msg);
        *msg = newMsg;
    }
}

void test_JetStreamMgtStreams(void)
{
    natsStatus          s;
    jsCtx               *js2= NULL;
    jsStreamInfo        *si = NULL;
    jsStreamConfig      cfg;
    jsErrCode           jerr = 0;
    natsMsg             *resp = NULL;
    natsMsg             *msg  = NULL;
    natsSubscription    *sub  = NULL;
    char                *desc = NULL;
    jsStreamInfoList    *siList = NULL;
    jsStreamNamesList   *snList = NULL;
    int                 count   = 0;
    jsStreamSource      ss;
    jsExternalStream    se;
    jsOptions           o;
    int                 i;

    JS_SETUP(2, 3, 3);

    test("Get context 2: ");
    // Set some purge options that will be saved in the context.
    // They can be overridden when invoking Purge API.
    jsOptions_Init(&o);
    o.Stream.Purge.Subject = "baz";
    o.Stream.Purge.Sequence = 10;
    s = natsConnection_JetStream(&js2, nc, &o);
    testCond(s == NATS_OK);

    test("Stream config init, bad args: ");
    s = jsStreamConfig_Init(NULL);
    testCond(s == NATS_INVALID_ARG);

    test("Stream config init: ");
    s = jsStreamConfig_Init(&cfg);
    testCond(s == NATS_OK);

    test("Add stream, bad args: ");
    s = js_AddStream(&si, NULL, &cfg, NULL, NULL);
    testCond((s == NATS_INVALID_ARG) && (si == NULL));
    nats_clearLastError();

    test("Stream name required: ");
    s = js_AddStream(&si, js, NULL, NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_AddStream(&si, js, &cfg, NULL, NULL);
    testCond((s == NATS_INVALID_ARG)
                && (si == NULL)
                && (strstr(nats_GetLastError(NULL), jsErrStreamNameRequired) != NULL));
    nats_clearLastError();

    test("Invalid stream name: ");
    cfg.Name = "invalid.stream.name";
    s = js_AddStream(&si, js, &cfg, NULL, NULL);
    testCond((s == NATS_INVALID_ARG)
                && (si == NULL)
                && (strstr(nats_GetLastError(NULL), "invalid stream name") != NULL));
    nats_clearLastError();

    test("Create basic: ");
    cfg.Name = "TEST";
    s = js_AddStream(&si, js, &cfg, NULL, &jerr);
    testCond((s == NATS_OK)
                && (si != NULL)
                && (si->Config != NULL)
                && (strcmp(si->Config->Name, "TEST") == 0)
                && (jerr == 0));
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("Destroy null stream info ok: ");
    jsStreamInfo_Destroy(NULL);
    testCond(true);

    test("Create failed because already used: ");
    cfg.Subjects = (const char*[2]){"foo", "bar"};
    cfg.SubjectsLen = 2;
    s = js_AddStream(&si, js, &cfg, NULL, &jerr);
    testCond((s == NATS_ERR)
                && (si == NULL)
                && (strstr(nats_GetLastError(NULL), "already in use") != NULL)
                && ((jerr == 0) || (jerr == JSStreamNameExistErr)));
    nats_clearLastError();

    if (serverVersionAtLeast(2, 10, 0))
    {
        test("Create stream with 2.10 server features: ");
        cfg.Name = "TEST210";
        cfg.Subjects = (const char*[]){"foo210"};
        cfg.SubjectsLen = 1;
        cfg.Metadata.List = (const char *[]){"k1", "v1", "k2", "v2"};
        cfg.Metadata.Count = 2;
        cfg.Compression = js_StorageCompressionS2;
        cfg.FirstSeq = 9999;
        cfg.SubjectTransform = (jsSubjectTransformConfig) {.Source = "foo210", .Destination = "bar210"};
        cfg.ConsumerLimits = (jsStreamConsumerLimits) {.InactiveThreshold = 1000, .MaxAckPending = 99};

        s = js_AddStream(&si, js, &cfg, NULL, &jerr);

        testCond((s == NATS_OK)
            && (si != NULL)
            && (si->Config != NULL)
            && (strcmp(si->Config->Name, "TEST210") == 0)
            && (si->Config->Metadata.Count >= 2)
            && (si->Config->Compression == js_StorageCompressionS2)
            && (si->Config->FirstSeq == 9999)
            && (strcmp(si->Config->SubjectTransform.Source, "foo210") == 0)
            && (strcmp(si->Config->SubjectTransform.Destination, "bar210") == 0)
            && (si->Config->ConsumerLimits.InactiveThreshold == 1000)
            && (si->Config->ConsumerLimits.MaxAckPending == 99)
            && (jerr == 0)
            );
        jsStreamInfo_Destroy(si);
        si = NULL;
    }

    jerr = 0;
    // Reset config
    jsStreamConfig_Init(&cfg);
    test("Add stream without getting stream info back: ");
    cfg.Name = "TEST2";
    s = js_AddStream(NULL, js, &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (si == NULL));

    test("Update stream config missing: ");
    s = js_UpdateStream(&si, js, NULL, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), jsErrStreamConfigRequired) != NULL));
    nats_clearLastError();

    test("Update stream stream name missing: ");
    cfg.Name = "";
    s = js_UpdateStream(&si, js, &cfg, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), jsErrStreamNameRequired) != NULL));
    nats_clearLastError();

    test("Update stream stream name invalid: ");
    cfg.Name = "bad.stream.name";
    s = js_UpdateStream(&si, js, &cfg, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), jsErrInvalidStreamName) != NULL));
    nats_clearLastError();
    cfg.Name = "TEST";

    test("Update stream: ");
    cfg.MaxMsgs = 1000;
    s = js_UpdateStream(&si, js, &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0)
                && (si != NULL)
                && (si->Config != NULL)
                && (si->Config->MaxMsgs == 1000));
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("Update stream (not found): ");
    cfg.Name = "NOT_FOUND";
    s = js_UpdateStream(&si, js, &cfg, NULL, &jerr);
    testCond((s == NATS_NOT_FOUND)
                && (jerr == JSStreamNotFoundErr)
                && (si == NULL));
    nats_clearLastError();

    test("Send 1 message: ");
    s = natsConnection_Request(&resp, nc, "TEST2", "hello", 5, 1000);
    testCond(s == NATS_OK);
    natsMsg_Destroy(resp);
    resp = NULL;

    test("Get stream info (bad args): ");
    s = js_GetStreamInfo(NULL, NULL, "TEST2", NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_GetStreamInfo(NULL, js, "TEST2", NULL, NULL);
    testCond(s == NATS_INVALID_ARG);

    test("Get stream info (stream name missing): ");
    s = js_GetStreamInfo(&si, js, NULL, NULL, NULL);
    testCond((s == NATS_INVALID_ARG)
                && (si == NULL)
                && (strstr(nats_GetLastError(NULL), jsErrStreamNameRequired) != NULL));
    nats_clearLastError();

    test("Get stream info (invalid stream name): ");
    s = js_GetStreamInfo(&si, js, "bad.stream.name", NULL, NULL);
    testCond((s == NATS_INVALID_ARG)
                && (si == NULL)
                && (strstr(nats_GetLastError(NULL), jsErrInvalidStreamName) != NULL));
    nats_clearLastError();

    test("Get stream info: ");
    s = js_GetStreamInfo(&si, js, "TEST2", NULL, &jerr);
    testCond((s == NATS_OK)
                && (jerr == 0)
                && (si != NULL)
                && (si->Config != NULL)
                && (strcmp(si->Config->Subjects[0], "TEST2") == 0)
                && (si->State.Msgs == 1));
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("Get stream info (not found): ");
    s = js_GetStreamInfo(&si, js, "NOT_FOUND", NULL, &jerr);
    testCond((s == NATS_NOT_FOUND)
                && (jerr == JSStreamNotFoundErr)
                && (si == NULL)
                && (nats_GetLastError(NULL) == NULL));

    test("Purge stream (bad args): ");
    s = js_PurgeStream(NULL, "TEST2", NULL, &jerr);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Purge stream (stream name missing): ");
    s = js_PurgeStream(js, NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_PurgeStream(js, "", NULL, &jerr);
    testCond((s == NATS_INVALID_ARG)
                && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrStreamNameRequired) != NULL));
    nats_clearLastError();

    test("Purge stream (stream name invalid): ");
    s = js_PurgeStream(js, "bad.stream.name", NULL, &jerr);
    testCond((s == NATS_INVALID_ARG)
                && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrInvalidStreamName) != NULL));
    nats_clearLastError();

    test("Purge stream: ");
    s = js_PurgeStream(js, "TEST2", NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Purge stream (not found): ");
    s = js_PurgeStream(js, "NOT_FOUND", NULL, &jerr);
    testCond((s == NATS_NOT_FOUND)
                && (jerr == JSStreamNotFoundErr));
    nats_clearLastError();

    test("Get stream info (verify purged): ");
    s = js_GetStreamInfo(&si, js, "TEST2", NULL, &jerr);
    testCond((s == NATS_OK)
                && (jerr == 0)
                && (si != NULL)
                && (si->Config != NULL)
                && (strcmp(si->Config->Subjects[0], "TEST2") == 0)
                && (si->State.Msgs == 0));
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("Create sub to check requests: ");
    s = natsConnection_SubscribeSync(&sub, nc, "$JS.API.STREAM.MSG.DELETE.TEST_MSG_DELETE");
    testCond((s == NATS_OK) && (sub != NULL));

    test("Create stream for delete msg: ");
    jsStreamConfig_Init(&cfg);
    cfg.Name = "TEST_MSG_DELETE";
    cfg.Subjects = (const char*[1]){"delete.msg"};
    cfg.SubjectsLen = 1;
    cfg.Storage = js_MemoryStorage;
    s = js_AddStream(NULL, js, &cfg, NULL, &jerr);
    testCond(s == NATS_OK);

    test("Send 1 message: ");
    s = natsConnection_Request(&resp, nc, "delete.msg", "hello", 5, 1000);
    testCond(s == NATS_OK);
    natsMsg_Destroy(resp);
    resp = NULL;

    test("Delete msg (stream missing): ");
    s = js_DeleteMsg(js, NULL, 2, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_DeleteMsg(js, "", 2, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (jerr == 0));
    nats_clearLastError();

    test("Delete msg: ");
    s = js_DeleteMsg(js, "TEST_MSG_DELETE", 1, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Check no_erase present: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    IFOK(s, (strstr(natsMsg_GetData(msg), "no_erase") != NULL ? NATS_OK : NATS_ERR));
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Delete msg (not found): ");
    s = js_DeleteMsg(js, "TEST_MSG_DELETE", 2, NULL, &jerr);
    testCond((s == NATS_ERR) && (jerr == JSSequenceNotFoundErr));
    nats_clearLastError();

    test("Check no_erase present: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    IFOK(s, (strstr(natsMsg_GetData(msg), "no_erase") != NULL ? NATS_OK : NATS_ERR));
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Send 1 message: ");
    s = natsConnection_Request(&resp, nc, "delete.msg", "hello", 5, 1000);
    testCond(s == NATS_OK);
    natsMsg_Destroy(resp);
    resp = NULL;

    test("Erase msg: ");
    s = js_EraseMsg(js, "TEST_MSG_DELETE", 2, &o, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Check no_erase absent: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    IFOK(s, (strstr(natsMsg_GetData(msg), "no_erase") != NULL ? NATS_ERR : NATS_OK));
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Erase msg (not found): ");
    s = js_EraseMsg(js, "TEST_MSG_DELETE", 3, &o, &jerr);
    testCond((s == NATS_ERR) && (jerr == JSSequenceNotFoundErr));
    nats_clearLastError();

    test("Check no_erase absent: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    IFOK(s, (strstr(natsMsg_GetData(msg), "no_erase") != NULL ? NATS_ERR : NATS_OK));
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Get stream info (verify deleted): ");
    s = js_GetStreamInfo(&si, js, "TEST_MSG_DELETE", NULL, &jerr);
    testCond((s == NATS_OK)
                && (jerr == 0)
                && (si != NULL)
                && (si->Config != NULL)
                && (strcmp(si->Config->Subjects[0], "delete.msg") == 0)
                && (si->State.Msgs == 0));
    jsStreamInfo_Destroy(si);
    si = NULL;

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create stream: ");
    jsStreamConfig_Init(&cfg);
    cfg.Name = "TEST3";
    cfg.Subjects = (const char*[2]){"foo", "bar"};
    cfg.SubjectsLen = 2;
    s = js_AddStream(NULL, js, &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Create sub to check purge req: ");
    s = natsConnection_SubscribeSync(&sub, nc, "$JS.API.STREAM.PURGE.TEST3");
    testCond(s == NATS_OK);

    test("Purge with options (subj+seq): ");
    jsOptions_Init(&o);
    // Will purge only messages from "foo"
    o.Stream.Purge.Subject = "foo";
    // Purge up-to but do not include this sequence.
    o.Stream.Purge.Sequence = 4;
    // We care only on the outbound request, not the result of the API call.
    js_PurgeStream(js, "TEST3", &o, NULL);
    nats_clearLastError();
    s = natsSubscription_NextMsg(&resp, sub, 1000);
    testCond((s == NATS_OK)
                && (resp != NULL)
                && (strncmp("{\"filter\":\"foo\",\"seq\":4}",
                            natsMsg_GetData(resp),
                            natsMsg_GetDataLength(resp)) == 0));
    natsMsg_Destroy(resp);
    resp = NULL;

    test("Purge without options and check context used: ");
    js_PurgeStream(js2, "TEST3", NULL, NULL);
    nats_clearLastError();
    s = natsSubscription_NextMsg(&resp, sub, 1000);
    testCond((s == NATS_OK)
                && (resp != NULL)
                && (strncmp("{\"filter\":\"baz\",\"seq\":10}",
                            natsMsg_GetData(resp),
                            natsMsg_GetDataLength(resp)) == 0));
    natsMsg_Destroy(resp);
    resp = NULL;

    test("Purge with options (seq and keep mutually exclusive): ");
    jsOptions_Init(&o);
    o.Stream.Purge.Subject = "bar";
    o.Stream.Purge.Sequence = 8;
    o.Stream.Purge.Keep = 2;
    s = js_PurgeStream(js, "TEST3", &o, &jerr);
    testCond((s == NATS_INVALID_ARG)
                && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), "exclusive") != NULL));
    nats_clearLastError();

    test("Check no request was sent: ");
    s = natsSubscription_NextMsg(&resp, sub, 500);
    testCond((s == NATS_TIMEOUT) && (resp == NULL));
    nats_clearLastError();

    test("Purge with options (subj+keep): ");
    jsOptions_Init(&o);
    o.Stream.Purge.Subject = "bar";
    // Keep 2 messages in the stream's bar subject space.
    o.Stream.Purge.Keep = 2;
    // We care only on the outbound request, not the result of the API call.
    js_PurgeStream(js, "TEST3", &o, NULL);
    nats_clearLastError();
    s = natsSubscription_NextMsg(&resp, sub, 1000);
    testCond((s == NATS_OK)
                && (resp != NULL)
                && (strncmp("{\"filter\":\"bar\",\"keep\":2}",
                            natsMsg_GetData(resp),
                            natsMsg_GetDataLength(resp)) == 0));
    natsMsg_Destroy(resp);
    resp = NULL;

    test("Delete stream (bad args): ");
    s = js_DeleteStream(NULL, "TEST2", NULL, &jerr);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Delete stream (stream name missing): ");
    s = js_DeleteStream(js, NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_DeleteStream(js, "", NULL, &jerr);
    testCond((s == NATS_INVALID_ARG)
                && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrStreamNameRequired) != NULL));
    nats_clearLastError();

    test("Delete stream (stream name invalid): ");
    s = js_DeleteStream(js, "bad.stream.name", NULL, &jerr);
    testCond((s == NATS_INVALID_ARG)
                && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrInvalidStreamName) != NULL));
    nats_clearLastError();

    test("Delete stream (not found): ");
    s = js_DeleteStream(js, "NOT_FOUND", NULL, &jerr);
    testCond((s == NATS_NOT_FOUND)
                && (jerr == JSStreamNotFoundErr));
    nats_clearLastError();

    test("Delete stream: ");
    s = js_DeleteStream(js, "TEST2", NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Get stream info (not found): ");
    s = js_GetStreamInfo(&si, js, "TEST2", NULL, &jerr);
    testCond((s == NATS_NOT_FOUND)
                && (jerr == JSStreamNotFoundErr)
                && (si == NULL));
    nats_clearLastError();

    natsSubscription_Destroy(sub);
    sub = NULL;
    test("Create sub to check stream info req: ");
    s = natsConnection_SubscribeSync(&sub, nc, "$JS.API.STREAM.INFO.TEST3");
    testCond(s == NATS_OK);

    test("StreamInfo with detailed delete: ");
    o.Stream.Info.DeletedDetails = true;
    IFOK(s, js_GetStreamInfo(&si, js, "TEST3", &o, &jerr));
    IFOK(s, natsSubscription_NextMsg(&resp, sub, 1000));
    testCond((s == NATS_OK)
                && (resp != NULL)
                && (natsMsg_GetDataLength(resp) > 0)
                && (strncmp("{\"deleted_details\":true}",
                            natsMsg_GetData(resp),
                            natsMsg_GetDataLength(resp)) == 0));
    jsStreamInfo_Destroy(si);
    si = NULL;
    natsMsg_Destroy(resp);
    resp = NULL;

    test("Create stream with description: ");
    jsStreamConfig_Init(&cfg);
    cfg.Name = "TEST_DESC";
    cfg.Description = "MyDescription";
    s = js_AddStream(&si, js, &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (si != NULL) && (jerr == 0)
                && (si->Config != NULL)
                && (si->Config->Description != NULL)
                && (strcmp(si->Config->Description, "MyDescription") == 0));
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("Create stream with description too long: ");
    jsStreamConfig_Init(&cfg);
    cfg.Name = "TEST_DESC2";
    desc = malloc(4*1024+2);
    for (i=0;i<4*1024+1;i++)
        desc[i] = 'a';
    desc[i]='\0';
    cfg.Description = (const char*) desc;
    s = js_AddStream(&si, js, &cfg, NULL, &jerr);
    testCond((s == NATS_ERR) && (si == NULL) && (jerr == JSStreamInvalidConfig));
    nats_clearLastError();

    free(desc);
    jsCtx_Destroy(js2);
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create stream with wilcards: ");
    jsStreamConfig_Init(&cfg);
    cfg.Name = "STREAM_WITH_WC";
    cfg.Subjects = (const char*[2]){"foo.>", "bar.*"};
    cfg.SubjectsLen = 2;
    s = js_AddStream(&si, js, &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) & (si != NULL) && (si->Config != NULL)
                && (si->Config->SubjectsLen == 2)
                && (strcmp(si->Config->Subjects[0], "foo.>") == 0)
                && (strcmp(si->Config->Subjects[1], "bar.*") == 0));
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("List stream infos (bad args): ");
    s = js_Streams(NULL, js, NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_Streams(&siList, NULL, NULL, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Create sub for pagination check: ");
    s = natsConnection_SubscribeSync(&sub, nc, "$JS.API.STREAM.LIST");
    testCond(s == NATS_OK);

    natsConn_setFilterWithClosure(nc, _streamsInfoListReq, (void*) &count);

    test("List stream infos: ");
    s = js_Streams(&siList, js, NULL, &jerr);
    testCond((s == NATS_OK) && (siList != NULL) && (siList->List != NULL) && (siList->Count == 3));

    natsConn_setFilter(nc, NULL);

    test("Check 1st request: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK)
                && (strstr(natsMsg_GetData(msg), "offset\":0") != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Check 2nd request: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK)
                && (strstr(natsMsg_GetData(msg), "offset\":2") != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Check 3rd request: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK)
                && (strstr(natsMsg_GetData(msg), "offset\":4") != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Destroy list: ");
    // Will see with valgrind if this is doing the right thing
    jsStreamInfoList_Destroy(siList);
    siList = NULL;
    // Check this does not crash
    jsStreamInfoList_Destroy(siList);
    testCond(true);

    test("List stream infos with filter: ");
    jsOptions_Init(&o);
    o.Stream.Info.SubjectsFilter = "TEST";
    s = js_Streams(&siList, js, &o, &jerr);
    testCond((s == NATS_OK) && (siList != NULL) && (siList->List != NULL) && (siList->Count == 1)
                && (strcmp(siList->List[0]->Config->Name, "TEST") == 0));
    jsStreamInfoList_Destroy(siList);
    siList = NULL;

    test("List stream infos with filter no match: ");
    jsOptions_Init(&o);
    o.Stream.Info.SubjectsFilter = "no.match";
    s = js_Streams(&siList, js, &o, &jerr);
    testCond((s == NATS_NOT_FOUND) && (siList == NULL));

    // Do names now

    test("List stream names (bad args): ");
    s = js_StreamNames(NULL, js, NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_StreamNames(&snList, NULL, NULL, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Create sub for pagination check: ");
    s = natsConnection_SubscribeSync(&sub, nc, "$JS.API.STREAM.NAMES");
    testCond(s == NATS_OK);

    count = 0;
    natsConn_setFilterWithClosure(nc, _streamsNamesListReq, (void*) &count);

    test("List stream names: ");
    s = js_StreamNames(&snList, js, NULL, &jerr);
    testCond((s == NATS_OK) && (snList != NULL) && (snList->List != NULL) && (snList->Count == 3));

    natsConn_setFilter(nc, NULL);

    test("Check 1st request: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK)
                && (strstr(natsMsg_GetData(msg), "offset\":0") != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Check 2nd request: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK)
                && (strstr(natsMsg_GetData(msg), "offset\":2") != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Check 3rd request: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK)
                && (strstr(natsMsg_GetData(msg), "offset\":4") != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Destroy list: ");
    // Will see with valgrind if this is doing the right thing
    jsStreamNamesList_Destroy(snList);
    snList = NULL;
    // Check this does not crash
    jsStreamNamesList_Destroy(snList);
    testCond(true);

    test("List stream names with filter: ");
    jsOptions_Init(&o);
    o.Stream.Info.SubjectsFilter = "TEST";
    s = js_StreamNames(&snList, js, &o, &jerr);
    testCond((s == NATS_OK) && (snList != NULL) && (snList->List != NULL) && (snList->Count == 1)
                && (strcmp(snList->List[0], "TEST") == 0));
    jsStreamNamesList_Destroy(snList);
    snList = NULL;

    test("List stream names with filter no match: ");
    jsOptions_Init(&o);
    o.Stream.Info.SubjectsFilter = "no.match";
    s = js_StreamNames(&snList, js, &o, &jerr);
    testCond((s == NATS_NOT_FOUND) && (snList == NULL));

    test("Mirror domain and external set error: ");
    jsStreamConfig_Init(&cfg);
    cfg.Name = "MDESET";
    jsStreamSource_Init(&ss);
    ss.Domain = "Domain";
    jsExternalStream_Init(&se);
    se.DeliverPrefix = "some.prefix";
    ss.External = &se;
    cfg.Mirror = &ss;
    s = js_AddStream(&si, js, &cfg, NULL, NULL);
    testCond((s == NATS_INVALID_ARG) && (si == NULL)
                && (strstr(nats_GetLastError(NULL), "domain and external are both set") != NULL));
    nats_clearLastError();

    test("Source domain and external set error: ");
    jsStreamConfig_Init(&cfg);
    cfg.Name = "SDESET";
    jsStreamSource_Init(&ss);
    ss.Domain = "Domain";
    jsExternalStream_Init(&se);
    se.DeliverPrefix = "some.prefix";
    ss.External = &se;
    cfg.Sources = (jsStreamSource*[1]){&ss};
    cfg.SourcesLen = 1;
    s = js_AddStream(&si, js, &cfg, NULL, NULL);
    testCond((s == NATS_INVALID_ARG) && (si == NULL)
                && (strstr(nats_GetLastError(NULL), "domain and external are both set") != NULL));
    nats_clearLastError();

    JS_TEARDOWN;
}

static void
_consumersInfoListReq(natsConnection *nc, natsMsg **msg, void *closure)
{
    int         *count      = (int*) closure;
    const char  *payload    = NULL;
    natsMsg     *newMsg     = NULL;


    if (strstr(natsMsg_GetData(*msg), "consumer_list_response") == NULL)
        return;

    (*count)++;
    if (*count == 1)
    {
        // Pretend limit is 2 and send 2 simplified consumer infos
        payload = "{\"type\":\"io.nats.jetstream.api.v1.consumer_list_response\",\"total\":5,\"offset\":0,\"limit\":2,"\
            "\"consumers\":[{\"stream_name\":\"A\",\"name\":\"a\"},{\"stream_name\":\"A\",\"name\":\"b\"}]}";
    }
    else if (*count == 2)
    {
        // Pretend that there is a repeat of a stream name to check
        // that we are properly replacing and not leaking memory.
        payload = "{\"type\":\"io.nats.jetstream.api.v1.consumer_list_response\",\"total\":5,\"offset\":2,\"limit\":2,"\
            "\"consumers\":[{\"stream_name\":\"A\",\"name\":\"b\"},{\"stream_name\":\"A\",\"name\":\"c\"}]}";
    }
    else if (*count == 3)
    {
        // Pretend that our next page was over the limit (say streams were removed)
        // and therefore the server returned no streams (but set offset to total)
        payload = "{\"type\":\"io.nats.jetstream.api.v1.consumer_list_response\",\"total\":3,\"offset\":3,\"limit\":2,"\
            "\"consumers\":[]}";
    }
    else
    {
        // Use original message
        return;
    }
    if (natsMsg_create(&newMsg, (*msg)->subject, (int) strlen((*msg)->subject), NULL, 0,
                        payload, (int) strlen(payload), 0) == NATS_OK)
    {
        natsMsg_Destroy(*msg);
        *msg = newMsg;
    }
}

static void
_consumerNamesListReq(natsConnection *nc, natsMsg **msg, void *closure)
{
    int         *count      = (int*) closure;
    const char  *payload    = NULL;
    natsMsg     *newMsg     = NULL;

    if (strstr(natsMsg_GetData(*msg), "consumer_names_response") == NULL)
        return;

    (*count)++;
    if (*count == 1)
    {
        // Pretend limit is 2 and send 2 stream names
        payload = "{\"type\":\"io.nats.jetstream.api.v1.consumer_names_response\",\"total\":5,\"offset\":0,\"limit\":2,"\
            "\"consumers\":[\"a\",\"b\"]}";
    }
    else if (*count == 2)
    {
        // Pretend that there is a repeat of a stream name to check
        // that we are properly replacing and not leaking memory.
        payload = "{\"type\":\"io.nats.jetstream.api.v1.consumer_names_response\",\"total\":5,\"offset\":0,\"limit\":2,"\
            "\"consumers\":[\"b\",\"c\"]}";
    }
    else if (*count == 3)
    {
        // Pretend that our next page was over the limit (say streams were removed)
        // and therefore the server returned no streams (but set offset to total)
        payload = "{\"type\":\"io.nats.jetstream.api.v1.consumer_names_response\",\"total\":3,\"offset\":3,\"limit\":2,"\
            "\"consumers\":[]}";
    }
    else
    {
        // Use original message
        return;
    }
    if (natsMsg_create(&newMsg, (*msg)->subject, (int) strlen((*msg)->subject), NULL, 0,
                        payload, (int) strlen(payload), 0) == NATS_OK)
    {
        natsMsg_Destroy(*msg);
        *msg = newMsg;
    }
}

void test_JetStreamMgtConsumers(void)
{
    natsStatus              s;
    jsConsumerInfo          *ci = NULL;
    jsConsumerConfig        cfg;
    jsErrCode               jerr = 0;
    natsMsg                 *resp = NULL;
    natsSubscription        *sub  = NULL;
    char                    *desc = NULL;
    jsStreamConfig          scfg;
    int                     i;
    const char              *dlvPoliciesStr[] = {
        "\"deliver_policy\":\"" jsDeliverAllStr "\"",
        "\"deliver_policy\":\"" jsDeliverLastStr "\"",
        "\"deliver_policy\":\"" jsDeliverNewStr "\"",
        "\"deliver_policy\":\"" jsDeliverBySeqStr "\"",
        "\"deliver_policy\":\"" jsDeliverByTimeStr "\"",
        "\"deliver_policy\":\"" jsDeliverLastPerSubjectStr "\"",
    };
    jsDeliverPolicy         dlvPolicies[] = {
        js_DeliverAll, js_DeliverLast, js_DeliverNew, js_DeliverByStartSequence,
        js_DeliverByStartTime, js_DeliverLastPerSubject};
    const char              *ackPoliciesStr[] = {
        "\"ack_policy\":\"" jsAckNoneStr "\"",
        "\"ack_policy\":\"" jsAckAllStr "\"",
        "\"ack_policy\":\"" jsAckExplictStr "\"",
    };
    jsAckPolicy             ackPolicies[] = {
        js_AckNone, js_AckAll, js_AckExplicit};
    const char              *replayPoliciesStr[] = {
        "\"replay_policy\":\"" jsReplayInstantStr "\"",
        "\"replay_policy\":\"" jsReplayOriginalStr "\"",
    };
    jsReplayPolicy          replayPolicies[] = {
        js_ReplayInstant, js_ReplayOriginal};
    const char              *badConsNames[] = {
        "foo.bar",
        ".foobar",
        "foobar.",
        "foo bar",
        " foobar",
        "foobar ",
        "foo*bar",
        "*foobar",
        "foobar*",
        "foo>bar",
        ">foobar",
        "foobar>",
    };
    jsConsumerInfoList  *ciList = NULL;
    jsConsumerNamesList *cnList = NULL;
    int                 count   = 0;
    natsMsg             *msg    = NULL;
    jsConsumerConfig    *cloneCfg = NULL;

    JS_SETUP(2, 9, 0);

    test("Consumer config init, bad args: ");
    s = jsConsumerConfig_Init(NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Consumer config init: ");
    s = jsConsumerConfig_Init(&cfg);
    testCond(s == NATS_OK);

    test("Add consumer, ctx missing: ");
    s = js_AddConsumer(&ci, NULL, "MY_STREAM", &cfg, NULL, NULL);
    testCond((s == NATS_INVALID_ARG) && (ci == NULL));
    nats_clearLastError();

    test("Add consumer, config missing: ");
    s = js_AddConsumer(&ci, js, "MY_STREAM", NULL, NULL, NULL);
    testCond((s == NATS_INVALID_ARG) && (ci == NULL)
                && (strstr(nats_GetLastError(NULL), jsErrConsumerConfigRequired) != NULL));
    nats_clearLastError();

    test("Add consumer, stream name required: ");
    s = js_AddConsumer(&ci, js, NULL, &cfg, NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_AddConsumer(&ci, js, "", &cfg, NULL, NULL);
    testCond((s == NATS_INVALID_ARG) && (ci == NULL)
                && (strstr(nats_GetLastError(NULL), jsErrStreamNameRequired) != NULL));
    nats_clearLastError();

    test("Add consumer, stream name invalid: ");
    s = js_AddConsumer(&ci, js, "bad.stream.name", &cfg, NULL, NULL);
    testCond((s == NATS_INVALID_ARG) && (ci == NULL)
                && (strstr(nats_GetLastError(NULL), jsErrInvalidStreamName) != NULL));
    nats_clearLastError();

    test("Add consumer, invalid durable name: ");
    cfg.Durable = "invalid.durable.name";
    s = js_AddConsumer(&ci, js, "MY_STREAM", &cfg, NULL, NULL);
    testCond((s == NATS_INVALID_ARG) && (ci == NULL)
                && (strstr(nats_GetLastError(NULL), jsErrInvalidDurableName) != NULL));
    nats_clearLastError();

    s = NATS_OK;
    test("Add consumer, invalid name: ");
    jsConsumerConfig_Init(&cfg);
    for (i=0; (s == NATS_OK) && (i<(int)(sizeof(badConsNames)/sizeof(char*))); i++)
    {
        cfg.Name = badConsNames[i];
        s = js_AddConsumer(&ci, js, "MY_STREAM", &cfg, NULL, NULL);
        if ((s == NATS_INVALID_ARG) && (ci == NULL)
                && (strstr(nats_GetLastError(NULL), jsErrInvalidConsumerName) != NULL))
        {
            nats_clearLastError();
            s = NATS_OK;
        }
    }
    testCond(s == NATS_OK);

    test("Create check sub: ");
    s = natsConnection_SubscribeSync(&sub, nc, "$JS.API.CONSUMER.CREATE.MY_STREAM");
    testCond(s == NATS_OK);

    for (i=0; i<6; i++)
    {
        test("Deliver policy: ");
        jsConsumerConfig_Init(&cfg);
        cfg.DeliverPolicy = dlvPolicies[i];
        s = js_AddConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
        testCond((s == NATS_ERR) && (jerr == JSStreamNotFoundErr) && (ci == NULL));
        nats_clearLastError();

        test("Verify config: ");
        s = natsSubscription_NextMsg(&resp, sub, 1000);
        testCond((s == NATS_OK) && (resp != NULL)
                    && (strstr(natsMsg_GetData(resp), dlvPoliciesStr[i]) != NULL));
        natsMsg_Destroy(resp);
        resp = NULL;
    }

    test("Deliver policy bad: ");
    jsConsumerConfig_Init(&cfg);
    cfg.DeliverPolicy = (jsDeliverPolicy) 100;
    s = js_AddConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (jerr == 0) && (ci == NULL));
    nats_clearLastError();

    for (i=0; i<3; i++)
    {
        test("Ack policy: ");
        jsConsumerConfig_Init(&cfg);
        cfg.AckPolicy = ackPolicies[i];
        s = js_AddConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
        testCond((s == NATS_ERR) && (jerr == JSStreamNotFoundErr) && (ci == NULL));
        nats_clearLastError();

        test("Verify config: ");
        s = natsSubscription_NextMsg(&resp, sub, 1000);
        testCond((s == NATS_OK) && (resp != NULL)
                    && (strstr(natsMsg_GetData(resp), ackPoliciesStr[i]) != NULL));
        natsMsg_Destroy(resp);
        resp = NULL;
    }

    test("Ack policy bad: ");
    jsConsumerConfig_Init(&cfg);
    cfg.AckPolicy = (jsAckPolicy) 100;
    s = js_AddConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (jerr == 0) && (ci == NULL));
    nats_clearLastError();

    for (i=0; i<2; i++)
    {
        test("Replay policy: ");
        jsConsumerConfig_Init(&cfg);
        cfg.ReplayPolicy = replayPolicies[i];
        s = js_AddConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
        testCond((s == NATS_ERR) && (jerr == JSStreamNotFoundErr) && (ci == NULL));
        nats_clearLastError();

        test("Verify config: ");
        s = natsSubscription_NextMsg(&resp, sub, 1000);
        testCond((s == NATS_OK) && (resp != NULL)
                    && (strstr(natsMsg_GetData(resp), replayPoliciesStr[i]) != NULL));
        natsMsg_Destroy(resp);
        resp = NULL;
    }

    test("Replay policy bad: ");
    jsConsumerConfig_Init(&cfg);
    cfg.ReplayPolicy = (jsReplayPolicy) 100;
    s = js_AddConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (jerr == 0) && (ci == NULL));
    nats_clearLastError();

    test("Add consumer (non durable): ");
    cfg.Durable = NULL;
    cfg.Description = "MyDescription";
    cfg.DeliverSubject = "foo";
    cfg.DeliverPolicy = js_DeliverLast;
    cfg.OptStartSeq = 100;
    cfg.OptStartTime = 1624472520123450000;
    cfg.AckPolicy = js_AckExplicit;
    cfg.AckWait = 200;
    cfg.MaxDeliver = 300;
    cfg.FilterSubject = "bar";
    cfg.ReplayPolicy = js_ReplayInstant;
    cfg.RateLimit = 400;
    cfg.SampleFrequency = "60%%";
    cfg.MaxWaiting = 500;
    cfg.MaxAckPending = 600;
    cfg.FlowControl = true;
    cfg.Heartbeat = 700;
    cfg.Replicas = 1;
    cfg.MemoryStorage = true;
    cfg.Metadata.List = (const char *[]){"key1", "val1", "key2", "val2"};
    cfg.Metadata.Count = 2;

    // We create a consumer with non existing stream, so we
    // expect this to fail. We are just checking that the config
    // is properly serialized.
    s = js_AddConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
    testCond((s == NATS_ERR) && (jerr == JSStreamNotFoundErr) && (ci == NULL));
    nats_clearLastError();

    test("Verify config: ");
    s = natsSubscription_NextMsg(&resp, sub, 1000);
    testCond((s == NATS_OK) && (resp != NULL)
                && (strncmp(natsMsg_GetData(resp),
                    "{\"stream_name\":\"MY_STREAM\","\
                    "\"config\":{\"deliver_policy\":\"last\","\
                    "\"description\":\"MyDescription\","\
                    "\"deliver_subject\":\"foo\","\
                    "\"opt_start_seq\":100,"\
                    "\"opt_start_time\":\"2021-06-23T18:22:00.12345Z\",\"ack_policy\":\"explicit\","\
                    "\"ack_wait\":200,\"max_deliver\":300,\"filter_subject\":\"bar\","\
                    "\"metadata\":{\"key1\":\"val1\",\"key2\":\"val2\"},"\
                    "\"replay_policy\":\"instant\",\"rate_limit_bps\":400,"\
                    "\"sample_freq\":\"60%%\",\"max_waiting\":500,\"max_ack_pending\":600,"\
                    "\"flow_control\":true,\"idle_heartbeat\":700,"\
                    "\"num_replicas\":1,\"mem_storage\":true}}",
                    natsMsg_GetDataLength(resp)) == 0));
    natsMsg_Destroy(resp);
    resp = NULL;

    if (serverVersionAtLeast(2, 10, 0))
    {
        test("Add consumer (non durable, filter subjects): ");
        cfg.FilterSubject = NULL;
        cfg.FilterSubjects = (const char *[]){"bar1", "bar2"};
        cfg.FilterSubjectsLen = 2;
        s = js_AddConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
        testCond((s == NATS_ERR) && (jerr == JSStreamNotFoundErr) && (ci == NULL));
        nats_clearLastError();

        test("Verify config: ");
        s = natsSubscription_NextMsg(&resp, sub, 1000);
        testCond((s == NATS_OK) && (resp != NULL) && (strncmp(natsMsg_GetData(resp), "{\"stream_name\":\"MY_STREAM\","
                                                                                     "\"config\":{\"deliver_policy\":\"last\","
                                                                                     "\"description\":\"MyDescription\","
                                                                                     "\"deliver_subject\":\"foo\","
                                                                                     "\"opt_start_seq\":100,"
                                                                                     "\"opt_start_time\":\"2021-06-23T18:22:00.12345Z\",\"ack_policy\":\"explicit\","
                                                                                     "\"ack_wait\":200,\"max_deliver\":300,\"filter_subjects\":[\"bar1\",\"bar2\"],"
                                                                                     "\"metadata\":{\"key1\":\"val1\",\"key2\":\"val2\"},"
                                                                                     "\"replay_policy\":\"instant\",\"rate_limit_bps\":400,"
                                                                                     "\"sample_freq\":\"60%%\",\"max_waiting\":500,\"max_ack_pending\":600,"
                                                                                     "\"flow_control\":true,\"idle_heartbeat\":700,"
                                                                                     "\"num_replicas\":1,\"mem_storage\":true}}",
                                                              natsMsg_GetDataLength(resp)) == 0));
        natsMsg_Destroy(resp);
        resp = NULL;
        cfg.FilterSubjects = NULL;
        cfg.FilterSubjectsLen = 0;
        cfg.FilterSubject = "bar";
    }

    test("Create check sub: ");
    natsSubscription_Destroy(sub);
    sub = NULL;
    s = natsConnection_SubscribeSync(&sub, nc, "$JS.API.CONSUMER.DURABLE.CREATE.MY_STREAM.dur");
    testCond(s == NATS_OK);

    test("Add consumer (durable): ");
    cfg.Durable = "dur";
    s = js_AddConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
    testCond((s == NATS_ERR) && (jerr == JSStreamNotFoundErr) && (ci == NULL));
    nats_clearLastError();

    test("Verify config: ");
    s = natsSubscription_NextMsg(&resp, sub, 1000);
    testCond((s == NATS_OK) && (resp != NULL)
                && (strncmp(natsMsg_GetData(resp),
                    "{\"stream_name\":\"MY_STREAM\","\
                    "\"config\":{\"deliver_policy\":\"last\","\
                    "\"description\":\"MyDescription\","\
                    "\"durable_name\":\"dur\",\"deliver_subject\":\"foo\","\
                    "\"opt_start_seq\":100,"\
                    "\"opt_start_time\":\"2021-06-23T18:22:00.12345Z\",\"ack_policy\":\"explicit\","\
                    "\"ack_wait\":200,\"max_deliver\":300,\"filter_subject\":\"bar\","\
                    "\"metadata\":{\"key1\":\"val1\",\"key2\":\"val2\"},"\
                    "\"replay_policy\":\"instant\",\"rate_limit_bps\":400,"\
                    "\"sample_freq\":\"60%%\",\"max_waiting\":500,\"max_ack_pending\":600,"\
                    "\"flow_control\":true,\"idle_heartbeat\":700,"\
                    "\"num_replicas\":1,\"mem_storage\":true}}",
                    natsMsg_GetDataLength(resp)) == 0));
    natsMsg_Destroy(resp);
    resp = NULL;
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create check sub: ");
    natsSubscription_Destroy(sub);
    sub = NULL;
    s = natsConnection_SubscribeSync(&sub, nc, "$JS.API.CONSUMER.CREATE.MY_STREAM.>");
    testCond(s == NATS_OK);

    test("Add consumer (name): ");
    cfg.Durable = NULL;
    cfg.Name = "my_name";
    s = js_AddConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
    testCond((s == NATS_ERR) && (jerr == JSStreamNotFoundErr) && (ci == NULL));
    nats_clearLastError();

    test("Verify config: ");
    s = natsSubscription_NextMsg(&resp, sub, 1000);
    testCond((s == NATS_OK) && (resp != NULL)
                && (strncmp(natsMsg_GetData(resp),
                    "{\"stream_name\":\"MY_STREAM\","\
                    "\"config\":{\"deliver_policy\":\"last\","\
                    "\"name\":\"my_name\","\
                    "\"description\":\"MyDescription\","\
                    "\"deliver_subject\":\"foo\","\
                    "\"opt_start_seq\":100,"\
                    "\"opt_start_time\":\"2021-06-23T18:22:00.12345Z\",\"ack_policy\":\"explicit\","\
                    "\"ack_wait\":200,\"max_deliver\":300,\"filter_subject\":\"bar\","\
                    "\"metadata\":{\"key1\":\"val1\",\"key2\":\"val2\"},"\
                    "\"replay_policy\":\"instant\",\"rate_limit_bps\":400,"\
                    "\"sample_freq\":\"60%%\",\"max_waiting\":500,\"max_ack_pending\":600,"\
                    "\"flow_control\":true,\"idle_heartbeat\":700,"\
                    "\"num_replicas\":1,\"mem_storage\":true}}",
                    natsMsg_GetDataLength(resp)) == 0));
    natsMsg_Destroy(resp);
    resp = NULL;
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create stream: ");
    jsStreamConfig_Init(&scfg);
    scfg.Name = "MY_STREAM";
    scfg.Subjects = (const char*[1]){"bar.>"};
    scfg.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &scfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Add consumer (name): ");
    jsConsumerConfig_Init(&cfg);
    cfg.Name = "my_name";
    cfg.DeliverSubject = "mn.foo";
    cfg.FilterSubject = "bar.>";
#define TIME_20350101 ((int64_t)2051251200L * (int64_t)1000000000L)
    if (serverVersionAtLeast(2, 11, 0))
    {
        cfg.PauseUntil = TIME_20350101;
    }
    s = js_AddConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (ci != NULL)
                && (strcmp(ci->Stream, "MY_STREAM") == 0)
                && (strcmp(ci->Name, "my_name") == 0)
                && (strcmp(ci->Config->Name, "my_name") == 0)
                && ((cfg.PauseUntil == 0) || (ci->Paused && ci->PauseRemaining > 0)));
    jsConsumerInfo_Destroy(ci);
    ci = NULL;

    if (serverVersionAtLeast(2, 11, 0))
    {
        test("Pause consumer: ");
        jsConsumerPauseResponse *cpr = NULL;
        s = js_PauseConsumer(&cpr, js, "MY_STREAM", "my_name", TIME_20350101, NULL, &jerr);
        testCond((s == NATS_OK) && (jerr == 0) && (cpr != NULL)
            && cpr->Paused
            && (cpr->PauseUntil == TIME_20350101)
            && (cpr->PauseRemaining > 0));
        jsConsumerPauseResponse_Destroy(cpr);
        cpr = NULL;

        test("Verify consumer paused with GetInfo: ");
        s = js_GetConsumerInfo(&ci, js, "MY_STREAM", "my_name", NULL, &jerr);
        testCond((s == NATS_OK) && (jerr == 0) && (ci != NULL)
            && ci->Paused
            && (ci->PauseRemaining > 0));
        jsConsumerInfo_Destroy(ci);
        ci = NULL;

        test("Unpause consumer: ");
        s = js_PauseConsumer(&cpr, js, "MY_STREAM", "my_name", 0, NULL, &jerr);
        testCond((s == NATS_OK) && (jerr == 0) && (cpr != NULL)
            && !cpr->Paused
            && (cpr->PauseUntil == 0)
            && (cpr->PauseRemaining == 0));
        jsConsumerPauseResponse_Destroy(cpr);
        cpr = NULL;
    }

    test("Add consumer (durable): ");
    jsConsumerConfig_Init(&cfg);
    cfg.Durable = "dur";
    cfg.DeliverSubject = "foo";
    cfg.FilterSubject = "bar.>";
    s = js_AddConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (ci != NULL)
                && (strcmp(ci->Stream, "MY_STREAM") == 0)
                && (strcmp(ci->Name, "dur") == 0));
    jsConsumerInfo_Destroy(ci);
    ci = NULL;

    test("Publish: ");
    s = natsConnection_Publish(nc, "bar.baz", "hello", 5);
    testCond(s == NATS_OK);

    test("Get consumer info (bad args): ");
    s = js_GetConsumerInfo(NULL, js, "MY_STREAM", "dur", NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_GetConsumerInfo(&ci, NULL, "MY_STREAM", "dur", NULL, &jerr);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Get consumer info, stream name missing: ");
    s = js_GetConsumerInfo(&ci, js, NULL, "dur", NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_GetConsumerInfo(&ci, js, "", "dur", NULL, &jerr);
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), jsErrStreamNameRequired) != NULL));
    nats_clearLastError();

    test("Get consumer info, stream name invalid: ");
    s = js_GetConsumerInfo(&ci, js, "bad.stream.name", "dur", NULL, &jerr);
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), jsErrInvalidStreamName) != NULL));
    nats_clearLastError();

    test("Get consumer info, consumer name missing: ");
    s = js_GetConsumerInfo(&ci, js, "MY_STREAM", NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_GetConsumerInfo(&ci, js, "MY_STREAM", "", NULL, &jerr);
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), jsErrConsumerNameRequired) != NULL));
    nats_clearLastError();

    test("Get consumer info, consumer name invalid: ");
    s = js_GetConsumerInfo(&ci, js, "MY_STREAM", "bad.consumer.name", NULL, &jerr);
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), jsErrInvalidConsumerName) != NULL));
    nats_clearLastError();

    test("Get consumer info: ");
    s = js_GetConsumerInfo(&ci, js, "MY_STREAM", "dur", NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (ci != NULL)
                && (strcmp(ci->Stream, "MY_STREAM") == 0)
                && (strcmp(ci->Name, "dur") == 0)
                && (ci->Config != NULL)
                && (ci->Config->FilterSubject != NULL)
                && (strcmp(ci->Config->FilterSubject, "bar.>") == 0));
    jsConsumerInfo_Destroy(ci);
    ci = NULL;

    test("Get consumer info (not found): ");
    s = js_GetConsumerInfo(&ci, js, "MY_STREAM", "dur2", NULL, &jerr);
    testCond((s == NATS_NOT_FOUND)
                && (jerr == JSConsumerNotFoundErr)
                && (ci == NULL)
                && (nats_GetLastError(NULL) == NULL));

    test("Delete consumer (bad args): ");
    s = js_DeleteConsumer(NULL, "MY_STREAM", "dur", NULL, &jerr);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Delete consumer, stream name missing: ");
    s = js_DeleteConsumer(js, NULL, "dur", NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_DeleteConsumer(js, "", "dur", NULL, &jerr);
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), jsErrStreamNameRequired) != NULL));
    nats_clearLastError();

    test("Delete consumer, stream name invalid: ");
    s = js_DeleteConsumer(js, "bad.stream.name", "dur", NULL, &jerr);
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), jsErrInvalidStreamName) != NULL));
    nats_clearLastError();

    test("Delete consumer, consumer name missing: ");
    s = js_DeleteConsumer(js, "MY_STREAM", NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_DeleteConsumer(js, "MY_STREAM", "", NULL, &jerr);
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), jsErrConsumerNameRequired) != NULL));
    nats_clearLastError();

    test("Get consumer info, consumer name invalid: ");
    s = js_DeleteConsumer(js, "MY_STREAM", "bad.consumer.name", NULL, &jerr);
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), jsErrInvalidConsumerName) != NULL));
    nats_clearLastError();

    test("Delete consumer: ");
    s = js_DeleteConsumer(js, "MY_STREAM", "dur", NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Delete consumer (not found): ");
    s = js_DeleteConsumer(js, "MY_STREAM", "dur2", NULL, &jerr);
    testCond((s == NATS_NOT_FOUND)
                && (jerr == JSConsumerNotFoundErr));
    nats_clearLastError();

    test("Create consumer with description: ");
    jsConsumerConfig_Init(&cfg);
    cfg.Description = "MyDescription";
    cfg.DeliverSubject = "desc1";
    s = js_AddConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (ci != NULL) && (jerr == 0)
                && (ci->Config != NULL)
                && (ci->Config->Description != NULL)
                && (strcmp(ci->Config->Description, "MyDescription") == 0));
    jsConsumerInfo_Destroy(ci);
    ci = NULL;

    test("Create consumer with description too long: ");
    jsConsumerConfig_Init(&cfg);
    desc = malloc(4*1024+2);
    for (i=0;i<4*1024+1;i++)
        desc[i] = 'a';
    desc[i]='\0';
    cfg.Description = (const char*) desc;
    cfg.DeliverSubject = "desc2";
    s = js_AddConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
    testCond((s == NATS_ERR) && (ci == NULL) && (jerr == JSConsumerDescriptionTooLongErr));
    nats_clearLastError();
    free(desc);

    test("Create consumer: ");
    jsConsumerConfig_Init(&cfg);
    cfg.Durable = "update_push_consumer";
    cfg.DeliverSubject = "bar";
    cfg.FilterSubject = "bar.baz";
    cfg.AckPolicy = js_AckExplicit;
    s = js_AddConsumer(NULL, js, "MY_STREAM", &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    // We will update this config and pass it to UpdateConsumer
    // and check that the result after UpdateConsumer matches...
    // Currently, server supports these fields:
    // description, ack_wait, max_deliver, sample_freq, max_ack_pending, max_waiting and headers_only
    cfg.Description = "my description";
    cfg.AckWait = NATS_SECONDS_TO_NANOS(2);
    cfg.MaxDeliver = 1;
    cfg.SampleFrequency = "30";
    cfg.MaxAckPending = 10;
    cfg.HeadersOnly = true;

    test("Update consumer, config missing: ");
    s = js_UpdateConsumer(&ci, js, "MY_STREAM", NULL, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (ci == NULL)
                && (strstr(nats_GetLastError(NULL), jsErrConsumerConfigRequired) != NULL));
    nats_clearLastError();

    test("Update consumer, stream name missing: ");
    s = js_UpdateConsumer(&ci, js, NULL, &cfg, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_UpdateConsumer(&ci, js, "", &cfg, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (ci == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrStreamNameRequired) != NULL));
    nats_clearLastError();

    test("Update consumer, stream name invalid: ");
    s = js_UpdateConsumer(&ci, js, "bad.stream.name", &cfg, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (ci == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrInvalidStreamName) != NULL));
    nats_clearLastError();

    test("Update needs durable name: ");
    cfg.Durable = NULL;
    s = js_UpdateConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
    {
        cfg.Durable = "";
        s = js_UpdateConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
    }
    testCond((s == NATS_INVALID_ARG) && (ci == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrDurRequired) != NULL));
    nats_clearLastError();
    cfg.Durable = "update_push_consumer";
    cfg.FilterSubject = "bar.bat";

    test("Update works ok: ");
    s = js_UpdateConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (ci != NULL) && (ci->Config != NULL)
                && (strcmp(ci->Config->Description, "my description") == 0)
                && (ci->Config->AckWait == NATS_SECONDS_TO_NANOS(2))
                && (ci->Config->MaxDeliver == 1)
                && (strcmp(ci->Config->SampleFrequency, "30") == 0)
                && (ci->Config->MaxAckPending == 10)
                && (ci->Config->HeadersOnly)
                && (ci->Config->FilterSubject != NULL)
                && (strcmp(ci->Config->FilterSubject, "bar.bat") == 0));
    jsConsumerInfo_Destroy(ci);
    ci = NULL;

    if (serverVersionAtLeast(2, 10, 0))
    {
        test("Update (filter subjects) works ok: ");
        cfg.FilterSubject = NULL;
        cfg.FilterSubjects = (const char *[]){"bar1", "bar2"};
        cfg.FilterSubjectsLen = 2;
        s = js_UpdateConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
        testCond((s == NATS_OK) && (jerr == 0) && (ci != NULL) && (ci->Config != NULL)
                    && (strcmp(ci->Config->Description, "my description") == 0)
                    && (ci->Config->AckWait == NATS_SECONDS_TO_NANOS(2))
                    && (ci->Config->MaxDeliver == 1)
                    && (strcmp(ci->Config->SampleFrequency, "30") == 0)
                    && (ci->Config->MaxAckPending == 10)
                    && (ci->Config->HeadersOnly)
                    && (ci->Config->FilterSubject == NULL)
                    && (ci->Config->FilterSubjectsLen == 2)
                    && (ci->Config->FilterSubjects != NULL)
                    && (strcmp(ci->Config->FilterSubjects[0], "bar1") == 0)
                    && (strcmp(ci->Config->FilterSubjects[1], "bar2") == 0));
        jsConsumerInfo_Destroy(ci);
        ci = NULL;
        cfg.FilterSubject = "bar.bat";
        cfg.FilterSubjects = NULL;
        cfg.FilterSubjectsLen = 0;
    }

    test("Add pull consumer: ");
    jsConsumerConfig_Init(&cfg);
    cfg.Durable = "update_pull_consumer";
    cfg.AckPolicy = js_AckExplicit;
    cfg.MaxWaiting = 1;
    s = js_AddConsumer(NULL, js, "MY_STREAM", &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    cfg.Description = "my description";
    cfg.AckWait = NATS_SECONDS_TO_NANOS(2);
    cfg.MaxDeliver = 1;
    cfg.SampleFrequency = "30";
    cfg.MaxAckPending = 10;
    cfg.HeadersOnly = true;
    cfg.MaxRequestBatch = 10;
    cfg.MaxRequestExpires = NATS_SECONDS_TO_NANOS(2);

    test("Update works ok: ");
    s = js_UpdateConsumer(&ci, js, "MY_STREAM", &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (ci != NULL) && (ci->Config != NULL)
                && (strcmp(ci->Config->Description, "my description") == 0)
                && (ci->Config->AckWait == NATS_SECONDS_TO_NANOS(2))
                && (ci->Config->MaxDeliver == 1)
                && (strcmp(ci->Config->SampleFrequency, "30") == 0)
                && (ci->Config->MaxAckPending == 10)
                && (ci->Config->HeadersOnly)
                && (ci->Config->MaxRequestBatch == 10)
                && (ci->Config->MaxRequestExpires == NATS_SECONDS_TO_NANOS(2)));
    jsConsumerInfo_Destroy(ci);
    ci = NULL;

    test("Create stream: ");
    jsStreamConfig_Init(&scfg);
    scfg.Name = "A";
    scfg.Subjects = (const char*[2]){"foo", "bar"};
    scfg.SubjectsLen = 2;
    s = js_AddStream(NULL, js, &scfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Create sub: ");
    s = natsConnection_SubscribeSync(&sub, nc, "$JS.API.CONSUMER.>");
    testCond(s == NATS_OK);

    test("Ephemeral with name: ");
    jsConsumerConfig_Init(&cfg);
    cfg.Name = "a";
    cfg.AckPolicy = js_AckExplicit;
    cfg.InactiveThreshold = NATS_SECONDS_TO_NANOS(1);
    s = js_AddConsumer(&ci, js, "A", &cfg, NULL, &jerr);
    testCond((s == NATS_OK)
                && (strcmp(ci->Name, "a") == 0)
                && (ci->Config->Durable == NULL)
                && (ci->Config->InactiveThreshold == NATS_SECONDS_TO_NANOS(1))
                && (jerr == 0));
    jsConsumerInfo_Destroy(ci);
    ci = NULL;

    test("Check: ");
    s = natsSubscription_NextMsg(&resp, sub, 1000);
    testCond((s == NATS_OK)
                && (resp != NULL)
                && (strcmp(natsMsg_GetSubject(resp), "$JS.API.CONSUMER.CREATE.A.a") == 0)
                && (strstr(natsMsg_GetData(resp), "\"name\":\"a\"") != NULL));
    natsMsg_Destroy(resp);
    resp = NULL;

    test("Durable: ");
    jsConsumerConfig_Init(&cfg);
    cfg.Durable = "b";
    cfg.AckPolicy = js_AckExplicit;
    s = js_AddConsumer(&ci, js, "A", &cfg, NULL, &jerr);
    testCond((s == NATS_OK)
                && (strcmp(ci->Name, "b") == 0)
                && (strcmp(ci->Config->Durable, "b") == 0)
                && (ci->Config->InactiveThreshold == 0)
                && (jerr == 0));
    jsConsumerInfo_Destroy(ci);
    ci = NULL;

    test("Check: ");
    s = natsSubscription_NextMsg(&resp, sub, 1000);
    testCond((s == NATS_OK)
                && (resp != NULL)
                && (strcmp(natsMsg_GetSubject(resp), "$JS.API.CONSUMER.DURABLE.CREATE.A.b") == 0)
                && (strstr(natsMsg_GetData(resp), "\"name\":") == NULL));
    natsMsg_Destroy(resp);
    resp = NULL;

    test("Durable and Name same: ");
    jsConsumerConfig_Init(&cfg);
    cfg.Name = "b";
    cfg.Durable = "b";
    cfg.AckPolicy = js_AckExplicit;
    s = js_AddConsumer(&ci, js, "A", &cfg, NULL, &jerr);
    testCond((s == NATS_OK)
                && (strcmp(ci->Name, "b") == 0)
                && (strcmp(ci->Config->Durable, "b") == 0)
                && (ci->Config->InactiveThreshold == 0)
                && (jerr == 0));
    jsConsumerInfo_Destroy(ci);
    ci = NULL;

    test("Check subject: ");
    s = natsSubscription_NextMsg(&resp, sub, 1000);
    testCond((s == NATS_OK)
                && (resp != NULL)
                && (strcmp(natsMsg_GetSubject(resp), "$JS.API.CONSUMER.CREATE.A.b") == 0)
                && (strstr(natsMsg_GetData(resp), "\"name\":\"b\"") != NULL));
    natsMsg_Destroy(resp);
    resp = NULL;

    test("Durable and Name different: ");
    jsConsumerConfig_Init(&cfg);
    cfg.Name = "x";
    cfg.Durable = "y";
    cfg.AckPolicy = js_AckExplicit;
    s = js_AddConsumer(&ci, js, "A", &cfg, NULL, &jerr);
    testCond((s == NATS_ERR)
                && (jerr == JSConsumerDurableNameNotMatchSubjectErr)
                && (strstr(nats_GetLastError(NULL), "consumer name in subject does not match durable name in request") != NULL));

    test("Check subject: ");
    s = natsSubscription_NextMsg(&resp, sub, 1000);
    testCond((s == NATS_OK)
                && (resp != NULL)
                && (strcmp(natsMsg_GetSubject(resp), "$JS.API.CONSUMER.CREATE.A.x") == 0)
                && (strstr(natsMsg_GetData(resp), "\"name\":\"x\"") != NULL));
    natsMsg_Destroy(resp);
    resp = NULL;

    test("Ephemeral with filter: ");
    jsConsumerConfig_Init(&cfg);
    cfg.Name = "c";
    cfg.AckPolicy = js_AckExplicit;
    cfg.FilterSubject = "bar";
    s = js_AddConsumer(&ci, js, "A", &cfg, NULL, &jerr);
    testCond((s == NATS_OK)
                && (strcmp(ci->Name, "c") == 0)
                && (ci->Config->Durable == NULL)
                && (ci->Config->InactiveThreshold != 0)
                && (jerr == 0));
    jsConsumerInfo_Destroy(ci);
    ci = NULL;

    test("Check subject: ");
    s = natsSubscription_NextMsg(&resp, sub, 1000);
    testCond((s == NATS_OK)
                && (resp != NULL)
                && (strcmp(natsMsg_GetSubject(resp), "$JS.API.CONSUMER.CREATE.A.c.bar") == 0)
                && (strstr(natsMsg_GetData(resp), "\"name\":\"c\"") != NULL));
    natsMsg_Destroy(resp);
    resp = NULL;

    test("Legacy ephemeral: ");
    jsConsumerConfig_Init(&cfg);
    cfg.AckPolicy = js_AckExplicit;
    cfg.FilterSubject = "bar";
    s = js_AddConsumer(&ci, js, "A", &cfg, NULL, &jerr);
    testCond((s == NATS_OK)
                && (ci->Name != NULL)
                && (ci->Config->Durable == NULL)
                && (ci->Config->InactiveThreshold != 0)
                && (jerr == 0));
    jsConsumerInfo_Destroy(ci);
    ci = NULL;

    test("Check subject: ");
    s = natsSubscription_NextMsg(&resp, sub, 1000);
    testCond((s == NATS_OK)
                && (resp != NULL)
                && (strcmp(natsMsg_GetSubject(resp), "$JS.API.CONSUMER.CREATE.A") == 0)
                && (strstr(natsMsg_GetData(resp), "\"name\":") == NULL));
    natsMsg_Destroy(resp);
    resp = NULL;

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("List consumer infos (bad args): ");
    s = js_Consumers(NULL, js, "A", NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_Consumers(&ciList, NULL, "A", NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_Consumers(&ciList, js, NULL, NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_Consumers(&ciList, js, "", NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_Consumers(&ciList, js, "invalid.stream.name", NULL, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("List consumer infos (unknown stream): ");
    s = js_Consumers(&ciList, js, "unknown", NULL, &jerr);
    testCond((s == NATS_NOT_FOUND) && (jerr == JSStreamNotFoundErr));
    nats_clearLastError();

    test("Create sub for pagination check: ");
    s = natsConnection_SubscribeSync(&sub, nc, "$JS.API.CONSUMER.LIST.A");
    testCond(s == NATS_OK);

    natsConn_setFilterWithClosure(nc, _consumersInfoListReq, (void*) &count);

    test("List consumers infos: ");
    s = js_Consumers(&ciList, js, "A", NULL, &jerr);
    testCond((s == NATS_OK) && (ciList != NULL) && (ciList->List != NULL) && (ciList->Count == 3));

    natsConn_setFilter(nc, NULL);

    test("Check 1st request: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK)
                && (strstr(natsMsg_GetData(msg), "offset\":0") != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Check 2nd request: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK)
                && (strstr(natsMsg_GetData(msg), "offset\":2") != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Check 3rd request: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK)
                && (strstr(natsMsg_GetData(msg), "offset\":4") != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Destroy list: ");
    // Will see with valgrind if this is doing the right thing
    jsConsumerInfoList_Destroy(ciList);
    ciList = NULL;
    // Check this does not crash
    jsConsumerInfoList_Destroy(ciList);
    testCond(true);

    // Do names now

    test("List consumer names (bad args): ");
    s = js_ConsumerNames(NULL, js, "A", NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_ConsumerNames(&cnList, NULL, "A", NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_ConsumerNames(&cnList, js, NULL, NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_ConsumerNames(&cnList, js, "", NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_ConsumerNames(&cnList, js, "invalid.stream.name", NULL, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("List consumer names (unknown stream): ");
    s = js_ConsumerNames(&cnList, js, "unknown", NULL, &jerr);
    testCond((s == NATS_NOT_FOUND) && (jerr == JSStreamNotFoundErr));
    nats_clearLastError();

    test("Create sub for pagination check: ");
    s = natsConnection_SubscribeSync(&sub, nc, "$JS.API.CONSUMER.NAMES.A");
    testCond(s == NATS_OK);

    count = 0;
    natsConn_setFilterWithClosure(nc, _consumerNamesListReq, (void*) &count);

    test("List consumer names: ");
    s = js_ConsumerNames(&cnList, js, "A", NULL, &jerr);
    testCond((s == NATS_OK) && (cnList != NULL) && (cnList->List != NULL) && (cnList->Count == 3));

    natsConn_setFilter(nc, NULL);

    test("Check 1st request: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK)
                && (strstr(natsMsg_GetData(msg), "offset\":0") != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Check 2nd request: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK)
                && (strstr(natsMsg_GetData(msg), "offset\":2") != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Check 3rd request: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK)
                && (strstr(natsMsg_GetData(msg), "offset\":4") != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Destroy list: ");
    // Will see with valgrind if this is doing the right thing
    jsConsumerNamesList_Destroy(cnList);
    cnList = NULL;
    // Check this does not crash
    jsConsumerNamesList_Destroy(cnList);
    testCond(true);

    test("Check clone: ");
    jsConsumerConfig_Init(&cfg);
    cfg.Name = "A";
    cfg.Durable = "B";
    cfg.Description = "C";
    cfg.FilterSubject = "D";
    cfg.FilterSubjects = (const char*[]){"E", "F"};
    cfg.FilterSubjectsLen = 2;
    cfg.SampleFrequency = "G";
    cfg.DeliverSubject = "H";
    cfg.DeliverGroup = "I";
    cfg.BackOff = (int64_t[]){NATS_MILLIS_TO_NANOS(50), NATS_MILLIS_TO_NANOS(250)};
    cfg.BackOffLen = 2;
    s = js_cloneConsumerConfig(&cfg, &cloneCfg);
    testCond((s == NATS_OK) && (cloneCfg != NULL)
                && (cloneCfg->Name != NULL) && (strcmp(cloneCfg->Name, "A") == 0)
                && (cloneCfg->Durable != NULL) && (strcmp(cloneCfg->Durable, "B") == 0)
                && (cloneCfg->Description != NULL) && (strcmp(cloneCfg->Description, "C") == 0)
                && (cloneCfg->FilterSubject != NULL) && (strcmp(cloneCfg->FilterSubject, "D") == 0)
                && (cloneCfg->FilterSubject != NULL) && (strcmp(cloneCfg->FilterSubject, "D") == 0)
                && (cloneCfg->FilterSubjectsLen == 2)
                && (cloneCfg->FilterSubjects != NULL)
                && (strcmp(cloneCfg->FilterSubjects[0], "E") == 0)
                && (strcmp(cloneCfg->FilterSubjects[1], "F") == 0)
                && (cloneCfg->SampleFrequency != NULL) && (strcmp(cloneCfg->SampleFrequency, "G") == 0)
                && (cloneCfg->DeliverSubject != NULL) && (strcmp(cloneCfg->DeliverSubject, "H") == 0)
                && (cloneCfg->DeliverGroup != NULL) && (strcmp(cloneCfg->DeliverGroup, "I") == 0)
                && (cloneCfg->BackOffLen == 2)
                && (cloneCfg->BackOff != NULL)
                && (cloneCfg->BackOff[0] == NATS_MILLIS_TO_NANOS(50))
                && (cloneCfg->BackOff[1] == NATS_MILLIS_TO_NANOS(250)));
    js_destroyConsumerConfig(cloneCfg);

    JS_TEARDOWN;
}

void test_JetStreamPublish(void)
{
    natsStatus          s;
    natsConnection      *nc = NULL;
    jsCtx               *js = NULL;
    natsPid             pid = NATS_INVALID_PID;
    jsStreamConfig      cfg;
    jsPubOptions        opts;
    jsErrCode           jerr = 0;
    jsPubAck            *pa = NULL;
    natsMsg             *msg = NULL;
    char                datastore[256] = {'\0'};
    char                cmdLine[1024] = {'\0'};
    char                confFile[256] = {'\0'};
    jsOptions           o;

    ENSURE_JS_VERSION(2, 3, 5);

    _makeUniqueDir(datastore, sizeof(datastore), "datastore_");
    _createConfFile(confFile, sizeof(confFile),
        " jetstream: { domain: ABC }\n"
    );

    test("Start JS server: ");
    snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s -c %s", datastore, confFile);
    pid = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(pid);
    testCond(true);

    test("Connect: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    testCond(s == NATS_OK);

    test("Get context: ");
    jsOptions_Init(&o);
    o.Domain = "ABC";
    s = natsConnection_JetStream(&js, nc, &o);
    testCond(s == NATS_OK);

    test("Stream config init: ");
    s = jsStreamConfig_Init(&cfg);
    testCond(s == NATS_OK);

    test("Add stream: ");
    cfg.Name = "TEST";
    cfg.Subjects = (const char*[2]){"foo", "bar"};
    cfg.SubjectsLen = 2;
    s = js_AddStream(NULL, js, &cfg, NULL, NULL);
    testCond(s == NATS_OK);

    test("Publish bad args: ");
    s = js_Publish(NULL, NULL, NULL, "hello", 5, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_Publish(NULL, js, NULL, "hello", 5, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_Publish(NULL, js, "", "hello", 5, NULL, &jerr);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Options init bad args: ");
    s = jsPubOptions_Init(NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Publish bad ttl option: ");
    s = jsPubOptions_Init(&opts);
    if (s == NATS_OK)
    {
        opts.MaxWait = -10;
        s = js_Publish(NULL, js, "foo", "hello", 5, &opts, &jerr);
    }
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), "negative") != NULL));
    nats_clearLastError();

    test("Publish data: ");
    opts.MaxWait = 3000;
    s = js_Publish(NULL, js, "foo", "hello", 5, &opts, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Publish data with pubAck: ");
    jsPubOptions_Init(&opts);
    opts.MsgId = "msg2";
    s = js_Publish(&pa, js, "foo", "hello", 5, &opts, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (pa != NULL)
                && (strcmp(pa->Domain, "ABC") == 0)
                && (strcmp(pa->Stream, "TEST") == 0)
                && (pa->Sequence == 2)
                && !pa->Duplicate);
    jsPubAck_Destroy(pa);
    pa = NULL;
    // Check this is ok
    jsPubAck_Destroy(NULL);

    test("Publish message with same msgID: ");
    s = js_Publish(&pa, js, "foo", "hello", 5, &opts, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (pa != NULL) && pa->Duplicate);
    jsPubAck_Destroy(pa);
    pa = NULL;

    test("Publish with wrong expected stream: ");
    jsPubOptions_Init(&opts);
    opts.ExpectStream = "WRONG";
    s = js_Publish(&pa, js, "foo", "hello", 5, &opts, &jerr);
    testCond((s == NATS_ERR) && ((jerr == 0) || (jerr == JSStreamNotMatchErr)) && (pa == NULL));
    jerr = 0;

    test("Publish with wrong expected sequence: ");
    jsPubOptions_Init(&opts);
    opts.ExpectLastSeq = 4;
    s = js_Publish(&pa, js, "foo", "hello", 5, &opts, &jerr);
    testCond((s == NATS_ERR) && ((jerr == 0) || (jerr == JSStreamWrongLastSequenceErr)) && (pa == NULL));
    jerr = 0;

    test("Publish with wrong expected message ID: ");
    jsPubOptions_Init(&opts);
    opts.ExpectLastMsgId = "WRONG";
    s = js_Publish(&pa, js, "foo", "hello", 5, &opts, &jerr);
    testCond((s == NATS_ERR) && ((jerr == 0) || (jerr == JSStreamWrongLastMsgIDErr)) && (pa == NULL));
    jerr = 0;

    test("Publish 1 msg on bar: ");
    jsPubOptions_Init(&opts);
    s = js_Publish(&pa, js, "bar", "hello", 5, &opts, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (pa != NULL)
                && (strcmp(pa->Stream, "TEST") == 0)
                && (pa->Sequence == 3)
                && !pa->Duplicate);
    jsPubAck_Destroy(pa);
    pa = NULL;

    test("Publish with wrong expected subj sequence: ");
    jsPubOptions_Init(&opts);
    // There should be 3 messages now, with "foo, 1", "foo, 2" and "bar, 3"
    // We are going to send on "foo" and say that last expected msg seq on "foo"
    // is 3, which is wrong, so should fail.
    opts.ExpectLastSubjectSeq = 3;
    s = js_Publish(&pa, js, "foo", "hello", 5, &opts, &jerr);
    testCond((s == NATS_ERR) && ((jerr == 0) || (jerr == JSStreamWrongLastSequenceErr)) && (pa == NULL));
    jerr = 0;
    nats_clearLastError();

    test("Publish with correct expected subj sequence: ");
    // Now set last expected for subject to 2, and it should be ok, and the sequence will be 4.
    opts.ExpectLastSubjectSeq = 2;
    s = js_Publish(&pa, js, "foo", "hello", 5, &opts, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (pa != NULL)
                && (strcmp(pa->Stream, "TEST") == 0)
                && (pa->Sequence == 4)
                && !pa->Duplicate);
    jsPubAck_Destroy(pa);
    pa = NULL;

    // ---- Same than above but with PublishMsg variant
    test("Recreate stream: ");
    s = js_DeleteStream(js, "TEST", NULL, NULL);
    IFOK(s, js_AddStream(NULL, js, &cfg, NULL, NULL));
    testCond(s == NATS_OK);

    test("Publish bad args: ");
    s = js_PublishMsg(NULL, NULL, NULL, NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_PublishMsg(NULL, js, NULL, NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_PublishMsg(NULL, NULL, msg, NULL, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Create msg: ");
    s = natsMsg_Create(&msg, "foo", NULL, "hello", 5);
    testCond(s == NATS_OK);

    test("Publish bad ttl option: ");
    s = jsPubOptions_Init(&opts);
    if (s == NATS_OK)
    {
        opts.MaxWait = -10;
        s = js_PublishMsg(NULL, js, msg, &opts, &jerr);
    }
    testCond((s == NATS_INVALID_ARG)
                && (strstr(nats_GetLastError(NULL), "negative") != NULL));
    nats_clearLastError();

    test("Publish data: ");
    opts.MaxWait = 3000;
    s = js_PublishMsg(NULL, js, msg, &opts, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Publish data with pubAck: ");
    jsPubOptions_Init(&opts);
    opts.MsgId = "msg2";
    s = js_PublishMsg(&pa, js, msg, &opts, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (pa != NULL)
                && (strcmp(pa->Stream, "TEST") == 0)
                && (pa->Sequence == 2)
                && !pa->Duplicate);
    jsPubAck_Destroy(pa);
    pa = NULL;
    natsMsg_Destroy(msg);
    msg = NULL;
    natsMsg_Create(&msg, "foo", NULL, "hello", 5);

    test("Publish message with same msgID: ");
    s = js_PublishMsg(&pa, js, msg, &opts, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (pa != NULL) && pa->Duplicate);
    jsPubAck_Destroy(pa);
    pa = NULL;
    natsMsg_Destroy(msg);
    msg = NULL;
    natsMsg_Create(&msg, "foo", NULL, "hello", 5);

    test("Publish with wrong expected stream: ");
    jsPubOptions_Init(&opts);
    opts.ExpectStream = "WRONG";
    s = js_PublishMsg(&pa, js, msg, &opts, &jerr);
    testCond((s == NATS_ERR) && ((jerr == 0) || (jerr == JSStreamNotMatchErr)) && (pa == NULL));
    jerr = 0;
    natsMsg_Destroy(msg);
    msg = NULL;
    natsMsg_Create(&msg, "foo", NULL, "hello", 5);

    test("Publish with wrong expected sequence: ");
    jsPubOptions_Init(&opts);
    opts.ExpectLastSeq = 4;
    s = js_PublishMsg(&pa, js, msg, &opts, &jerr);
    testCond((s == NATS_ERR) && ((jerr == 0) || (jerr == JSStreamWrongLastSequenceErr)) && (pa == NULL));
    jerr = 0;
    natsMsg_Destroy(msg);
    msg = NULL;
    natsMsg_Create(&msg, "foo", NULL, "hello", 5);

    test("Publish with wrong expected message ID: ");
    jsPubOptions_Init(&opts);
    opts.ExpectLastMsgId = "WRONG";
    s = js_PublishMsg(&pa, js, msg, &opts, &jerr);
    testCond((s == NATS_ERR) && ((jerr == 0) || (jerr == JSStreamWrongLastMsgIDErr)) && (pa == NULL));
    jerr = 0;
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Publish 1 msg on bar: ");
    jsPubOptions_Init(&opts);
    natsMsg_Create(&msg, "bar", NULL, "hello", 5);
    s = js_PublishMsg(&pa, js, msg, &opts, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (pa != NULL)
                && (strcmp(pa->Stream, "TEST") == 0)
                && (pa->Sequence == 3)
                && !pa->Duplicate);
    jsPubAck_Destroy(pa);
    pa = NULL;
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Publish with wrong expected subj sequence: ");
    jsPubOptions_Init(&opts);
    natsMsg_Create(&msg, "foo", NULL, "hello", 5);
    // There should be 3 messages now, with "foo, 1", "foo, 2" and "bar, 3"
    // We are going to send on "foo" and say that last expected msg seq on "foo"
    // is 3, which is wrong, so should fail.
    opts.ExpectLastSubjectSeq = 3;
    s = js_PublishMsg(&pa, js, msg, &opts, &jerr);
    testCond((s == NATS_ERR) && ((jerr == 0) || (jerr == JSStreamWrongLastSequenceErr)) && (pa == NULL));
    jerr = 0;
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Publish with correct expected subj sequence: ");
    natsMsg_Create(&msg, "foo", NULL, "hello", 5);
    // Now set last expected for subject to 2, and it should be ok, and the sequence will be 4.
    opts.ExpectLastSubjectSeq = 2;
    s = js_PublishMsg(&pa, js, msg, &opts, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (pa != NULL)
                && (strcmp(pa->Stream, "TEST") == 0)
                && (pa->Sequence == 4)
                && !pa->Duplicate);
    jsPubAck_Destroy(pa);
    pa = NULL;
    natsMsg_Destroy(msg);
    msg = NULL;

    JS_TEARDOWN;
    remove(confFile);
}

static void
_jsPubAckErrHandler(jsCtx *js, jsPubAckErr *pae, void *closure)
{
    struct threadArg    *args = (struct threadArg*) closure;

    natsMutex_Lock(args->m);
    args->sum++;
    if (strcmp(natsMsg_GetData(pae->Msg), "fail1") == 0)
    {
        if ((pae->Err == NATS_ERR)
            && ((pae->ErrCode == 0) || (pae->ErrCode == JSStreamWrongLastMsgIDErr))
            && (strstr(pae->ErrText, "wrong last") != NULL))
        {
            args->status = NATS_ERR;
        }
    }
    else if (strcmp(natsMsg_GetData(pae->Msg), "fail2") == 0)
    {
        // Resend only once
        if (args->sum == 2)
            js_PublishMsgAsync(js, &(pae->Msg), NULL);
    }
    else if (strcmp(natsMsg_GetData(pae->Msg), "fail3") == 0)
    {
        while (!args->done)
            natsCondition_Wait(args->c, args->m);

        // Destroy context
        jsCtx_Destroy(js);
        // Notify that we are done
        args->closed = true;
        natsCondition_Broadcast(args->c);
    }
    else if (strcmp(natsMsg_GetData(pae->Msg), "block") == 0)
    {
        while (!args->done)
            natsCondition_Wait(args->c, args->m);
        nats_Sleep(500);

        args->closed = true;
        natsCondition_Broadcast(args->c);
    }
    else if (strcmp(natsMsg_GetData(pae->Msg), "destroyed") == 0)
    {
        // Notify that we are in the callback.
        args->msgReceived = true;
        natsCondition_Broadcast(args->c);

        // Now wait to be notified that the context was destroyed.
        while (!args->closed)
            natsCondition_Wait(args->c, args->m);

        // Then access the message content again to make sure that message
        // is still valid.
        if (strcmp(natsMsg_GetData(pae->Msg), "destroyed") != 0)
            args->status = NATS_ERR;

        // Notify that we are done.
        args->done = true;
        natsCondition_Broadcast(args->c);
    }
    else if ((pae->Err == NATS_NO_RESPONDERS) || (pae->Err == NATS_TIMEOUT))
    {
        args->msgReceived = true;
        natsCondition_Broadcast(args->c);
    }
    natsMutex_Unlock(args->m);
}

void test_JetStreamPublishAsync(void)
{
    natsStatus          s;
    natsSubscription    *sub= NULL;
    jsOptions           o;
    jsStreamConfig      cfg;
    jsPubOptions        opts;
    natsMsg             *msg = NULL;
    natsMsg             *cmsg = NULL;
    natsMsg             *msg1 = NULL;
    natsMsg             *msg2 = NULL;
    const char          *val = NULL;
    const char          **keys = NULL;
    int                 keysCount = 0;
    struct threadArg    args;
    int                 i;
    bool                ok1 = false;
    bool                ok2 = false;
    natsMsgList         pending;

    JS_SETUP(2, 3, 3);
    jsCtx_Destroy(js);
    js = NULL;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    test("Create control sub: ");
    s = natsConnection_SubscribeSync(&sub, nc, "foo");
    testCond(s == NATS_OK);

    test("Prepare JS options: ");
    s = jsOptions_Init(&o);
    if (s == NATS_OK)
    {
        o.PublishAsync.ErrHandler        = _jsPubAckErrHandler;
        o.PublishAsync.ErrHandlerClosure = &args;
    }
    testCond(s == NATS_OK);

    test("Get context: ");
    s = natsConnection_JetStream(&js, nc, &o);
    testCond(s == NATS_OK);

    test("Stream config init: ");
    s = jsStreamConfig_Init(&cfg);
    testCond(s == NATS_OK);

    test("Add stream: ");
    cfg.Name = "foo";
    s = js_AddStream(NULL, js, &cfg, NULL, NULL);
    testCond(s == NATS_OK);

    test("Publish bad args: ");
    s = js_PublishAsync(NULL, NULL, "hello", 5, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_PublishAsync(js, NULL, "hello", 5, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_PublishAsync(js, "", "hello", 5, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_PublishAsync(NULL, "foo", "hello", 5, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("PublishAsyncComplete bad args: ");
    s = js_PublishAsyncComplete(NULL, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("PublishAsyncComplete with no pending: ");
    s = js_PublishAsyncComplete(js, NULL);
    testCond(s == NATS_OK);

    test("Publish data: ");
    s = js_PublishAsync(js, "foo", "ok1", 3, NULL);
    IFOK(s, js_PublishAsyncComplete(js, NULL));
    testCond(s == NATS_OK);

    test("Check pub msg no header and reply set: ");
    s = natsSubscription_NextMsg(&cmsg, sub, 1000);
    testCond((s == NATS_OK) && (cmsg != NULL)
                && !nats_IsStringEmpty(natsMsg_GetReply(cmsg))
                && (natsMsgHeader_Keys(cmsg, &keys, &keysCount) == NATS_NOT_FOUND));
    natsMsg_Destroy(cmsg);

    test("Publish msg (bad args): ");
    s = js_PublishMsgAsync(NULL, NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_PublishMsgAsync(NULL, &msg, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Publish msg: ");
    s = jsPubOptions_Init(&opts);
    if (s == NATS_OK)
        opts.MsgId = "msgID";
    IFOK(s, natsMsg_Create(&msg, "foo", NULL, "ok2", 3));
    IFOK(s, js_PublishMsgAsync(js, &msg, &opts));
    // Check that library took ownership of message by checking
    // that msg is now NULL.
    testCond((s == NATS_OK) && (msg == NULL));

    test("Check pub msg reply set: ");
    s = natsSubscription_NextMsg(&cmsg, sub, 1000);
    testCond((s == NATS_OK) && (cmsg != NULL)
                && !nats_IsStringEmpty(natsMsg_GetReply(cmsg)));

    test("Check msg ID header set: ");
    s = natsMsgHeader_Get(cmsg, jsMsgIdHdr, &val);
    testCond((s == NATS_OK) && (strcmp(val, "msgID") == 0));
    natsMsg_Destroy(cmsg);
    val = NULL;

    test("Wait for complete (bad args): ");
    opts.MaxWait = -1000;
    s = js_PublishAsyncComplete(js, &opts);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Wait for complete: ");
    opts.MaxWait = 1000;
    s = js_PublishAsyncComplete(js, &opts);
    testCond(s == NATS_OK);

    test("Send fails due to wrong last ID: ");
    args.status = NATS_OK;
    opts.MsgId = NULL;
    opts.ExpectLastMsgId = "wrong";
    s = natsMsg_Create(&msg, "foo", NULL, "fail1", 5);
    IFOK(s, js_PublishMsgAsync(js, &msg, &opts));
    testCond((s == NATS_OK) && (msg == NULL));

    test("Check pub msg reply set: ");
    s = natsSubscription_NextMsg(&cmsg, sub, 1000);
    testCond((s == NATS_OK) && (cmsg != NULL)
                && !nats_IsStringEmpty(natsMsg_GetReply(cmsg)));

    test("Check msg ID header not set: ");
    s = natsMsgHeader_Get(cmsg, jsMsgIdHdr, &val);
    testCond((s == NATS_NOT_FOUND) && (val == NULL));

    test("Check expect last msg ID header set: ");
    s = natsMsgHeader_Get(cmsg, jsExpectedLastMsgIdHdr, &val);
    testCond((s == NATS_OK) && (val != NULL) && (strcmp(val, "wrong") == 0));
    natsMsg_Destroy(cmsg);
    val = NULL;

    test("Wait for complete: ");
    s = js_PublishAsyncComplete(js, NULL);
    testCond(s == NATS_OK);

    test("Check cb got proper failure: ")
    natsMutex_Lock(args.m);
    s = ((args.status == NATS_ERR) && (args.sum == 1) ? NATS_OK : NATS_ERR);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Send new failed message, will be resent in cb: ");
    s = js_PublishAsync(js, "foo", "fail2", 5, &opts);
    testCond(s == NATS_OK);

    test("Wait complete: ")
    s = js_PublishAsyncComplete(js, NULL);
    if (s == NATS_OK)
    {
        natsMutex_Lock(args.m);
        s = (args.sum == 3 ? NATS_OK : NATS_ERR);
        natsMutex_Unlock(args.m);
    }
    testCond(s == NATS_OK);

    test("Send new failed messages which will block cb: ");
    s = js_PublishAsync(js, "foo", "fail3", 5, &opts);
    // Send another message, which should not be delivered to CB
    // since we will destroy context from CB on releasing CB
    // after fail3 msg is processed.
    IFOK(s, js_PublishAsync(js, "foo", "fail4", 5, &opts));
    testCond(s == NATS_OK);

    test("Check complete timeout: ");
    opts.MaxWait = 100;
    s = js_PublishAsyncComplete(js, &opts);
    testCond(s == NATS_TIMEOUT);
    nats_clearLastError();

    test("Release cb which will destroy context: ");
    s = NATS_OK;
    natsMutex_Lock(args.m);
    args.done = true;
    natsCondition_Broadcast(args.c);
    while ((s != NATS_TIMEOUT) && !args.closed)
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Check that last msg was not delivered to CB: ");
    natsMutex_Lock(args.m);
    // cb has seen: fail1, fail2 twice, fail3, so sum == 4
    s = (args.sum == 4 ? NATS_OK: NATS_ERR);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    js = NULL;
    jsOptions_Init(&o);
    test("Stall wait bad args: ");
    o.PublishAsync.StallWait = -10;
    s = natsConnection_JetStream(&js, nc, &o);
    testCond((s == NATS_INVALID_ARG) && (js == NULL));
    nats_clearLastError();

    test("Recreate context: ");
    o.PublishAsync.MaxPending        = 1;
    o.PublishAsync.StallWait         = 100;
    o.PublishAsync.ErrHandler        = _jsPubAckErrHandler;
    o.PublishAsync.ErrHandlerClosure = &args;
    s = natsConnection_JetStream(&js, nc, &o);
    testCond(s == NATS_OK);

    test("Block CB: ");
    natsMutex_Lock(args.m);
    args.done = false;
    args.closed = false;
    natsMutex_Unlock(args.m);
    // Pass options so that we add an expected last msg ID which will
    // cause failure.
    jsPubOptions_Init(&opts);
    opts.ExpectLastMsgId = "WRONG";
    s = js_PublishAsync(js, "foo", "block", 5, &opts);
    testCond(s == NATS_OK);

    test("Send should fail due to stall: ");
    s = js_PublishAsync(js, "foo", "stalled", 7, NULL);
    testCond((s == NATS_TIMEOUT)
                && (strstr(nats_GetLastError(NULL), "too many outstanding") != NULL));
    nats_clearLastError();

    // Release CB, which will sleep a bit before returning, so that we
    // have time to start a publish here that we will check gets unstalled.
    // Artificially increase the stallWait so that we don't flap on Travis/etc..
    natsMutex_Lock(js->mu);
    js->opts.PublishAsync.StallWait = 10000;
    natsMutex_Unlock(js->mu);
    // should unstall the publish that we are going to make here.
    natsMutex_Lock(args.m);
    args.done = true;
    natsCondition_Broadcast(args.c);
    natsMutex_Unlock(args.m);

    test("Pub will stall: ");
    s = js_PublishAsync(js, "foo", "ok", 2, NULL);
    testCond(s == NATS_OK);

    test("Wait complete: ");
    s = js_PublishAsyncComplete(js, NULL);
    testCond(s == NATS_OK);

    test("Wait for CB to return: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.closed)
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Msg needs to be destroyed on failure: ");
    natsSubscription_Destroy(sub);
    natsConnection_Destroy(nc);
    nc = NULL;
    s = natsMsg_Create(&msg, "foo", NULL, "conclosed", 9);
    IFOK(s, js_PublishMsgAsync(js, &msg, NULL));
    testCond((s == NATS_CONNECTION_CLOSED) && (msg != NULL));
    nats_clearLastError();

    test("Msg destroy: ");
    natsMsg_Destroy(msg);
    testCond(true);

    jsCtx_Destroy(js);
    js = NULL;

    test("Connect: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    testCond(s == NATS_OK);

    test("Create context: ");
    s = jsOptions_Init(&o);
    if (s == NATS_OK)
    {
        o.PublishAsync.ErrHandler        = _jsPubAckErrHandler;
        o.PublishAsync.ErrHandlerClosure = &args;
    }
    IFOK(s, natsConnection_JetStream(&js, nc, &o));
    testCond((s == NATS_OK) && (js != NULL));

    test("Publish async no responders: ");
    s = js_PublishAsync(js, "no.responder.check", "hello", 5, NULL);
    if (s == NATS_OK)
    {
        natsMutex_Lock(args.m);
        while ((s != NATS_TIMEOUT) && !args.msgReceived)
            s = natsCondition_TimedWait(args.c, args.m, 2000);
        args.msgReceived = false;
        natsMutex_Unlock(args.m);
    }
    testCond(s == NATS_OK);

    test("Enqueue message with bad subject: ");
    s = natsMsg_Create(&msg, "some.subject", NULL, "hello", 5);
    if (s == NATS_OK)
    {
        natsSubscription *rsub;

        js_lock(js);
        rsub = js->rsub;
        js_unlock(js);

        _waitSubPending(rsub, 0);

        natsSub_Lock(rsub);
        rsub->ownDispatcher.queue.head = msg;
        rsub->ownDispatcher.queue.tail = msg;
        rsub->ownDispatcher.queue.msgs = 1;
        rsub->ownDispatcher.queue.bytes = natsMsg_dataAndHdrLen(msg);
        natsCondition_Signal(rsub->ownDispatcher.cond);
        natsSub_Unlock(rsub);

        // Message is owned by subscription, do not destroy it here.
    }
    testCond(s == NATS_OK);

    test("Publish async cb received non existent pid: ");
    {
        char subj[64];
        snprintf(subj, sizeof(subj), "%sabcdefgh", js->rpre);
        s = natsConnection_Publish(nc, subj, NULL, 0);
    }
    testCond(s == NATS_OK);

    test("Produce failed message: ");
    jsPubOptions_Init(&opts);
    opts.ExpectStream = "WRONG";
    natsMutex_Lock(args.m);
    args.done        = false;
    args.closed      = false;
    args.msgReceived = false;
    args.status      = NATS_OK;
    natsMutex_Unlock(args.m);
    // Pass options with wrong expected stream, so pub will fail.
    s = js_PublishAsync(js, "foo", "destroyed", 9, &opts);
    testCond(s == NATS_OK);

    test("Wait for msg in CB: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.msgReceived)
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    args.msgReceived = false;
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Destroy context, notify CB: ");
    jsCtx_Destroy(js);
    js = NULL;
    natsMutex_Lock(args.m);
    args.closed = true;
    natsCondition_Broadcast(args.c);
    natsMutex_Unlock(args.m);
    testCond(true);

    test("Wait for CB to return: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.done)
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    if (s == NATS_OK)
        s = args.status;
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Reply subject can be set: ");
    s = natsConnection_JetStream(&js, nc, NULL);
    IFOK(s, natsMsg_Create(&msg, "bar", "baz", "bat", 3));
    IFOK(s, js_PublishMsgAsync(js, &msg, NULL));
    testCond((s == NATS_OK) && (msg == NULL));

    test("Wait complete: ");
    s = js_PublishAsyncComplete(js, NULL);
    testCond(s == NATS_OK);

    test("Publish async: ");
    _stopServer(pid);
    pid = NATS_INVALID_PID;
    s = natsMsg_Create(&msg1, "foo", NULL, "hello1", 6);
    IFOK(s, natsMsg_Create(&msg2, "foo", NULL, "hello2", 6));
    IFOK(s, js_PublishMsgAsync(js, &msg1, NULL));
    IFOK(s, js_PublishMsgAsync(js, &msg2, NULL));
    testCond((s == NATS_OK) && (msg1 == NULL) && (msg2 == NULL));

    test("Get pending (bad args): ");
    s = js_PublishAsyncGetPendingList(NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_PublishAsyncGetPendingList(&pending, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_PublishAsyncGetPendingList(NULL, js);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Get pending: ");
    s = js_PublishAsyncGetPendingList(&pending, js);
    testCond((s == NATS_OK)
                && (pending.Msgs != NULL)
                && (pending.Count == 2));

    test("Verify pending list: ");
    for (i=0; i<pending.Count; i++)
    {
        natsMsg *msg = pending.Msgs[i];

        // Cannot assume order, but we should get hello1 and hello2 in all.
        if (strncmp(natsMsg_GetData(msg), "hello1", 6) == 0)
        {
            ok1 = true;
            msg1 = msg;
            pending.Msgs[i] = NULL;
        }
        else if (strncmp(natsMsg_GetData(msg), "hello2", 6) == 0)
            ok2 = true;
    }
    testCond(ok1 && ok2 && (msg1 != NULL));

    test("Destroy list leaves msg1 valid: ");
    natsMsgList_Destroy(&pending);
    // Access something from msg1 to make sure that memory is still valid
    s = (strncmp(natsMsg_GetData(msg1), "hello1", 6) == 0 ? NATS_OK : NATS_ERR);
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg1);
    msg1 = NULL;

    test("Get pending, no msg: ");
    s = js_PublishAsyncGetPendingList(&pending, js);
    testCond((s == NATS_NOT_FOUND) && (pending.Msgs == NULL) && (pending.Count == 0));

    test("Publish async: ");
    s = natsMsg_Create(&msg1, "foo", NULL, "hello1", 6);
    IFOK(s, natsMsg_Create(&msg2, "foo", NULL, "hello2", 6));
    IFOK(s, js_PublishMsgAsync(js, &msg1, NULL));
    IFOK(s, js_PublishMsgAsync(js, &msg2, NULL));
    testCond((s == NATS_OK) && (msg1 == NULL) && (msg2 == NULL));

    test("Get pending: ");
    s = js_PublishAsyncGetPendingList(&pending, js);
    testCond((s == NATS_OK)
                && (pending.Msgs != NULL)
                && (pending.Count == 2));

    msg1 = pending.Msgs[0];
    msg2 = pending.Msgs[1];

    test("Check that if Msgs set to NULL, no crash: ");
    {
        natsMsg **l  = pending.Msgs;
        pending.Msgs = NULL;
        natsMsgList_Destroy(&pending);
        free(l);
    }
    testCond(true);

    natsMsg_Destroy(msg1);
    natsMsg_Destroy(msg2);

    test("Publish timeout: ");
    jsCtx_Destroy(js);
    js = NULL;
    o.PublishAsync.ErrHandler = _jsPubAckErrHandler;
    o.PublishAsync.ErrHandlerClosure = (void*) &args;
    s = natsConnection_JetStream(&js, nc, &o);
    if (s == NATS_OK)
    {
        opts.MaxWait = 100;
        s = js_PublishAsync(js, "foo", "timeout", 7, &opts);
    }
    if (s == NATS_OK)
    {
        natsMutex_Lock(args.m);
        while ((s != NATS_TIMEOUT) && !args.msgReceived)
            s = natsCondition_TimedWait(args.c, args.m, 2000);
        args.msgReceived = false;
        natsMutex_Unlock(args.m);
    }
    testCond(s == NATS_OK);

    JS_TEARDOWN;
    _destroyDefaultThreadArgs(&args);
}

static void
_jsPubAckHandler(jsCtx *js, natsMsg *msg, jsPubAck *pa, jsPubAckErr *pae, void *closure)
{
    struct threadArg *args = (struct threadArg*) closure;

    natsMutex_Lock(args->m);
    args->sum++;
    args->status = NATS_ERR;
    switch (args->sum) {
        case 1:
            if ((pae == NULL) && (pa != NULL)
                && ((pa->Stream != NULL) && (strcmp(pa->Stream, "TEST") == 0))
                && (pa->Sequence == 1))
            {
                // Test resending message from callback...
                args->status = js_PublishMsgAsync(js, &msg, NULL);
            }
            break;
        case 2:
            if ((pae == NULL) && (pa != NULL)
                && ((pa->Stream != NULL) && (strcmp(pa->Stream, "TEST") == 0))
                && (pa->Sequence == 1) && (pa->Duplicate))
            {
                args->status = NATS_OK;
            }
            break;
        case 3:
            if ((pa == NULL) && (pae != NULL)
                && (pae->Err == NATS_ERR) && (pae->ErrCode == JSStreamStoreFailedErr)
                && (strstr(pae->ErrText, "maximum messages exceeded") != NULL))
            {
                args->status = NATS_OK;
            }
            break;
        case 4:
            if ((msg != NULL) && (natsMsg_GetData(msg)[0] == '1')
                && (pa == NULL) && (pae != NULL)
                && (pae->Err == NATS_TIMEOUT) && (pae->ErrCode == 0))
            {
                args->status = NATS_OK;
            }
            break;
        case 5:
            if ((msg != NULL) && (natsMsg_GetData(msg)[0] == '2')
                && (pa == NULL) && (pae != NULL)
                && (pae->Err == NATS_TIMEOUT) && (pae->ErrCode == 0))
            {
                args->status = NATS_OK;
            }
            break;
        case 6:
            if ((msg != NULL) && (natsMsg_GetData(msg)[0] == '3')
                    && (pa == NULL) && (pae != NULL)
                    && (pae->Err == NATS_TIMEOUT) && (pae->ErrCode == 0))
            {
                args->status = NATS_OK;
            }
            break;
    }
    natsMsg_Destroy(msg);
    args->done = true;
    natsCondition_Broadcast(args->c);
    natsMutex_Unlock(args->m);
}

static natsStatus
_checkPubAckResult(natsStatus s, struct threadArg *args)
{
    if (s != NATS_OK)
        return s;

    natsMutex_Lock(args->m);
    while ((s != NATS_TIMEOUT) && !args->done)
        s = natsCondition_TimedWait(args->c, args->m, 1000);
    IFOK(s, args->status);
    args->done = false;
    args->status = NATS_OK;
    natsMutex_Unlock(args->m);

    return s;
}

void test_JetStreamPublishAckHandler(void)
{
    natsStatus          s;
    jsOptions           o;
    jsStreamConfig      cfg;
    jsPubOptions        opts;
    // natsMsg             *msg = NULL;
    struct threadArg    args;


    JS_SETUP(2, 3, 3);
    jsCtx_Destroy(js);
    js = NULL;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    test("Prepare JS options: ");
    s = jsOptions_Init(&o);
    if (s == NATS_OK)
    {
        o.PublishAsync.AckHandler        = _jsPubAckHandler;
        o.PublishAsync.AckHandlerClosure = &args;
    }
    testCond(s == NATS_OK);

    test("Get context: ");
    s = natsConnection_JetStream(&js, nc, &o);
    testCond(s == NATS_OK);

    test("Add stream: ");
    jsStreamConfig_Init(&cfg);
    cfg.Name = "TEST";
    cfg.Subjects = (const char*[1]){"foo"};
    cfg.SubjectsLen = 1;
    cfg.MaxMsgs = 1;
    cfg.Discard = js_DiscardNew;
    s = js_AddStream(NULL, js, &cfg, NULL, NULL);
    testCond(s == NATS_OK);

    test("Publish async ok: ");
    jsPubOptions_Init(&opts);
    opts.MsgId = "msg1";
    s = js_PublishAsync(js, "foo", (const void*) "ok", 2, &opts);
    s = _checkPubAckResult(s, &args);
    testCond(s == NATS_OK);

    test("Publish async (duplicate): ");
    // Message above is resent from the ack handler callback.
    s = _checkPubAckResult(s, &args);
    testCond(s == NATS_OK);

    test("Publish async (max msgs): ");
    s = js_PublishAsync(js, "foo", (const void*) "ok", 2, NULL);
    s = _checkPubAckResult(s, &args);
    testCond(s == NATS_OK);

    _stopServer(pid);
    pid = NATS_INVALID_PID;

    test("Publish async with timeouts: ");
    opts.MsgId = NULL;
    opts.MaxWait = 250;
    s = js_PublishAsync(js, "foo", (const void*) "2", 1, &opts);
    opts.MaxWait = 500;
    IFOK(s, js_PublishAsync(js, "foo", (const void*) "3", 1, &opts));
    opts.MaxWait = 100;
    IFOK(s, js_PublishAsync(js, "foo", (const void*) "1", 1, &opts));
    testCond(s == NATS_OK);

    test("Publish async timeout (1): ");
    s = _checkPubAckResult(s, &args);
    testCond(s == NATS_OK);

    test("Publish async timeout (2): ");
    s = _checkPubAckResult(s, &args);
    testCond(s == NATS_OK);

    test("Publish async timeout (3): ");
    s = _checkPubAckResult(s, &args);
    testCond(s == NATS_OK);

    test("Ctx destroy releases timer: ");
    opts.MaxWait = 250;
    s = js_PublishAsync(js, "foo", (const void*) "4", 1, &opts);
    IFOK(s, js_PublishAsync(js, "foo", (const void*) "5", 1, &opts));
    if (s == NATS_OK)
    {
        js_lock(js);
        js->refs++;
        nats_Sleep(300);
        jsCtx_Destroy(js);
        js_unlock(js);
    }
    testCond(s == NATS_OK);

    test("Check refs: ");
    nats_Sleep(100);
    js_lock(js);
    s = (js->refs == 1 ? NATS_OK : NATS_ERR);
    js_unlock(js);
    testCond(s == NATS_OK);

    js_release(js);
    nats_Sleep(100);
    js = NULL;

    JS_TEARDOWN;
    _destroyDefaultThreadArgs(&args);
}

static void
_jsMsgHandler(natsConnection *nc, natsSubscription *sub, natsMsg *msg, void *closure)
{
    struct threadArg *args = (struct threadArg*) closure;

    natsMutex_Lock(args->m);
    args->sum++;
    if (args->control == 1)
        natsMsg_Ack(msg, NULL);
    else if (args->control == 3)
    {
        if (args->sum == 1)
            natsMsg_Nak(msg, NULL);
        else
        {
            while (!args->done)
                natsCondition_Wait(args->c, args->m);
            args->msgReceived = true;
        }
    }
    if ((args->control != 2) || (args->sum == args->results[0]))
        natsCondition_Broadcast(args->c);
    natsMutex_Unlock(args->m);

    natsMsg_Destroy(msg);
}

static void
_jsSubComplete(void *closure)
{
    struct threadArg *args = (struct threadArg*) closure;

    natsMutex_Lock(args->m);
    args->done = true;
    natsCondition_Broadcast(args->c);
    natsMutex_Unlock(args->m);
}

static void
_jsCreateSubThread(void *closure)
{
    struct threadArg    *args = (struct threadArg*) closure;
    natsStatus          s;
    jsCtx               *js;
    jsSubOptions        so;
    natsSubscription    *sub = NULL;

    natsMutex_Lock(args->m);
    js = args->js;
    natsMutex_Unlock(args->m);

    nats_Sleep(100);

    jsSubOptions_Init(&so);
    so.Stream = "TEST_CONCURRENT";
    so.Config.Durable = "my_durable";
    s = js_Subscribe(&sub, js, "concurrent", _jsMsgHandler, (void*) args, NULL, &so, NULL);
    if (s == NATS_OK)
    {
        natsMutex_Lock(args->m);
        args->attached++;
        args->sub = sub;
        natsMutex_Unlock(args->m);
    }
    else
    {
        natsMutex_Lock(args->m);
        args->detached++;
        natsMutex_Unlock(args->m);
    }
}

static void
_jsDrainErrCb(natsConnection *nc, natsSubscription *sub, natsStatus err, void *closure)
{
    struct threadArg *args = (struct threadArg*) closure;

    natsMutex_Lock(args->m);
    if ((err == NATS_NOT_FOUND)
            && (strstr(nats_GetLastError(NULL), "delete consumer") != NULL))
    {
        args->msgReceived = true;
        natsCondition_Broadcast(args->c);
    }
    natsMutex_Unlock(args->m);
}

void test_JetStreamSubscribe(void)
{
    natsStatus          s;
    natsOptions         *ncOpts = NULL;
    natsConnection      *nc = NULL;
    natsSubscription    *sub= NULL;
    jsCtx               *js = NULL;
    natsPid             pid = NATS_INVALID_PID;
    jsErrCode           jerr= 0;
    char                datastore[256] = {'\0'};
    char                cmdLine[1024] = {'\0'};
    natsSubscription    *ackSub = NULL;
    natsMsg             *ack = NULL;
    natsSubscription    *sub2= NULL;
    jsStreamConfig      sc;
    jsConsumerConfig    cc;
    jsSubOptions        so;
    struct threadArg    args;
    int                 i;
    jsConsumerInfo      *ci = NULL;
#ifndef _WIN32
    char                longsn[256];
#endif
    natsThread          *threads[10] = {NULL};
    const char *subjectsStack[] = {"foo", "bar"};
    const char **subjects = subjectsStack;
    const int numSubjects = sizeof(subjectsStack)/sizeof(char*);

    ENSURE_JS_VERSION(2, 3, 5);

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    _makeUniqueDir(datastore, sizeof(datastore), "datastore_");

    test("Start JS server: ");
    snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s", datastore);
    pid = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(pid);
    testCond(true);

    test("Create options: ");
    s = natsOptions_Create(&ncOpts);
    IFOK(s, natsOptions_SetErrorHandler(ncOpts, _jsDrainErrCb, (void*) &args));
    testCond(s == NATS_OK);

    test("Connect: ");
    s = natsConnection_Connect(&nc, ncOpts);
    testCond(s == NATS_OK);

    test("Get context: ");
    s = natsConnection_JetStream(&js, nc, NULL);
    testCond(s == NATS_OK);

    test("Sub options init (bad args): ");
    s = jsSubOptions_Init(NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Create async sub (invalid args): ");
    s = js_Subscribe(NULL, js, "foo", NULL, NULL, NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_Subscribe(&sub, NULL, "foo", NULL, NULL, NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_Subscribe(&sub, js, NULL, NULL, NULL, NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_Subscribe(&sub, js, "", NULL, NULL, NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_Subscribe(&sub, js, "foo", NULL, NULL, NULL, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0));
    nats_clearLastError();

    test("Create, no stream exists: ");
    s = js_Subscribe(&sub, js, "foo", _dummyMsgHandler, NULL, NULL, NULL, &jerr);
    testCond((s != NATS_OK) && (sub == NULL)
                && (strstr(nats_GetLastError(NULL), jsErrNoStreamMatchesSubject) != NULL));
    nats_clearLastError();

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Subjects = subjects;
    sc.SubjectsLen = numSubjects;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Populate: ");
    s = js_Publish(NULL, js, "foo", "msg1", 4, NULL, &jerr);
    IFOK(s, js_Publish(NULL, js, "foo", "msg2", 4, NULL, &jerr));
    IFOK(s, js_Publish(NULL, js, "foo", "msg3", 4, NULL, &jerr));
    IFOK(s, js_Publish(NULL, js, "bar", "msg1", 4, NULL, &jerr));
    IFOK(s, js_Publish(NULL, js, "bar", "msg2", 4, NULL, &jerr));
    testCond(s == NATS_OK);

    test("Create sub to check lib sends ACKs: ");
    s = natsConnection_SubscribeSync(&ackSub, nc, "$JS.ACK.TEST.>");
    testCond(s == NATS_OK);

    test("Subscribe, no options: ");
    s = js_Subscribe(&sub, js, "foo", _jsMsgHandler, &args, NULL, NULL, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Check msgs received: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum != 3))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Check acks sent: ");
    // Sub should have its own autoAck CB, so usrCb should be != NULL
    natsMutex_Lock(sub->mu);
    s = (sub->jsi->usrCb != NULL ? NATS_OK : NATS_ERR);
    natsMutex_Unlock(sub->mu);
    for (i=0; (s == NATS_OK) && (i < 3); i++)
    {
        s = natsSubscription_NextMsg(&ack, ackSub, 1000);
        if (s == NATS_OK)
        {
            // Make sure this was not a sync call...
            s = (natsMsg_GetReply(ack) == NULL ? NATS_OK : NATS_ERR);
        }
        natsMsg_Destroy(ack);
        ack = NULL;
    }
    testCond(s == NATS_OK);

    test("Create sub with manual ack: ");
    natsSubscription_Destroy(sub);
    sub = NULL;
    natsMutex_Lock(args.m);
    args.control = 1;
    args.sum = 0;
    natsMutex_Unlock(args.m);
    jsSubOptions_Init(&so);
    so.ManualAck = true;
    s = js_Subscribe(&sub, js, "foo", _jsMsgHandler, (void*) &args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Check msgs received: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum != 3))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Check acks sent: ");
    // Sub should have NOT have its own autoAck CB, so usrCb should be NULL
    natsMutex_Lock(sub->mu);
    s = (sub->jsi->usrCb == NULL ? NATS_OK : NATS_ERR);
    natsMutex_Unlock(sub->mu);
    for (i=0; (s == NATS_OK) && (i < 3); i++)
    {
        s = natsSubscription_NextMsg(&ack, ackSub, 1000);
        natsMsg_Destroy(ack);
        ack = NULL;
    }
    testCond(s == NATS_OK);

    test("Check no auto-ack behavior: ");
    s = natsSubscription_NextMsg(&ack, ackSub, 150);
    testCond((s == NATS_TIMEOUT) && (ack == NULL));
    nats_clearLastError();

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create sub with auto-ack: ");
    natsMutex_Lock(args.m);
    args.control = 3;
    args.sum = 0;
    natsMutex_Unlock(args.m);
    jsSubOptions_Init(&so);
    so.Stream = "TEST";
    so.Config.DeliverPolicy = js_DeliverLast;
    s = js_Subscribe(&sub, js, "foo", _jsMsgHandler, (void*)&args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Check msgs received: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum < 1))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Check NAck sent: ");
    s = natsSubscription_NextMsg(&ack, ackSub, 1000);
    testCond(s == NATS_OK);
    natsMsg_Destroy(ack);
    ack = NULL;

    test("Check no auto-ack: ");
    s = natsSubscription_NextMsg(&ack, ackSub, 100);
    testCond((s == NATS_TIMEOUT) && (ack == NULL));
    nats_clearLastError();
    s = NATS_OK;

    natsMutex_Lock(args.m);
    args.done = true;
    natsCondition_Broadcast(args.c);
    while ((s != NATS_TIMEOUT) && !args.msgReceived)
        s = natsCondition_TimedWait(args.c, args.m, 1000);
    args.done = false;
    args.msgReceived = false;
    natsMutex_Unlock(args.m);

    test("Check auto-ack sent: ");
    s = natsSubscription_NextMsg(&ack, ackSub, 1000);
    testCond(s == NATS_OK);
    natsMsg_Destroy(ack);
    ack = NULL;

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create queue sub: ");
    natsMutex_Lock(args.m);
    args.control = 0;
    args.sum = 0;
    natsMutex_Unlock(args.m);
    jsSubOptions_Init(&so);
    so.Stream = "TEST";
    so.Queue = "queue";
    so.Config.Durable = "qdurable";
    s = js_Subscribe(&sub, js, "foo", _jsMsgHandler, (void*)&args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Check msgs received: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum != 3))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Check acks sent: ");
    for (i=0; (s == NATS_OK) && (i < 3); i++)
    {
        s = natsSubscription_NextMsg(&ack, ackSub, 1000);
        natsMsg_Destroy(ack);
        ack = NULL;
    }
    testCond(s == NATS_OK);

    test("Add second member with binding: ");
    jsSubOptions_Init(&so);
    so.Stream = "TEST";
    so.Queue = "queue";
    so.Consumer = "qdurable";
    s = js_Subscribe(&sub2, js, "foo", _jsMsgHandler, (void*)&args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub2 != NULL) && (jerr == 0));

    // Wait a bit and make sure no new message is delivered
    test("No new message: ");
    natsMutex_Lock(args.m);
    natsCondition_TimedWait(args.c, args.m, 250);
    s = (args.sum == 3 ? NATS_OK : NATS_ERR);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Attached consumer not destroyed on unsub: ");
    s = natsSubscription_Unsubscribe(sub2);
    IFOK(s, js_GetConsumerInfo(&ci, js, "TEST", "qdurable", NULL, &jerr));
    testCond((s == NATS_OK) && (ci != NULL) && (jerr == 0));
    jsConsumerInfo_Destroy(ci);
    ci = NULL;

    natsSubscription_Destroy(sub);
    sub = NULL;
    natsSubscription_Destroy(sub2);
    sub2 = NULL;

    test("No auto-ack for AckNone: ");
    natsMutex_Lock(args.m);
    args.control = 0;
    args.sum = 0;
    args.done = false;
    natsMutex_Unlock(args.m);
    jsSubOptions_Init(&so);
    so.Config.DeliverPolicy = js_DeliverAll;
    so.Config.AckPolicy = js_AckNone;
    s = js_Subscribe(&sub, js, "foo", _jsMsgHandler, (void*) &args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Check msgs received: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum != 3))
        s = natsCondition_TimedWait(args.c, args.m, 1000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Check no ack was sent: ");
    natsMutex_Lock(sub->mu);
    // We should not have set an internal autoAck CB, so usrCb should be NULL.
    s = (sub->jsi->usrCb == NULL ? NATS_OK : NATS_ERR);
    natsMutex_Unlock(sub->mu);
    IFOK(s, natsSubscription_NextMsg(&ack, ackSub, 300));
    testCond((s == NATS_TIMEOUT) && (ack == NULL));
    nats_clearLastError();

    test("Check user setting onComplete is ok: ");
    s = natsSubscription_SetOnCompleteCB(sub, _jsSubComplete, (void*)&args);
    testCond(s == NATS_OK);

    test("Wait for complete: ");
    natsSubscription_Destroy(sub);
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.done)
        s = natsCondition_TimedWait(args.c, args.m, 1000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

#ifndef _WIN32
    test("Create stream with long subject: ");
    for (i=0; i<((int)sizeof(longsn))-1; i++)
        longsn[i] = 'a';
    longsn[i]='\0';
    jsStreamConfig_Init(&sc);
    sc.Name = longsn;
    sc.Subjects = (const char*[1]){"baz"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, NULL);
    testCond(s == NATS_OK);

    test("Create sub to check lib sends ACKs: ");
    natsSubscription_Destroy(ackSub);
    ackSub = NULL;
    {
        char tmp[512];

        snprintf(tmp, sizeof(tmp), "$JS.ACK.%s.>", longsn);
        s = natsConnection_SubscribeSync(&ackSub, nc, tmp);
    }
    testCond(s == NATS_OK);

    test("Create sub with auto-ack: ");
    sub = NULL;
    natsMutex_Lock(args.m);
    args.sum = 0;
    args.control = 0;
    natsMutex_Unlock(args.m);
    s = js_Subscribe(&sub, js, "baz", _jsMsgHandler, &args, NULL, NULL, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Send 1 msg: ");
    s = js_Publish(NULL, js, "baz", "hello", 5, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Check msg received: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum != 1))
        s = natsCondition_TimedWait(args.c, args.m, 1000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Check ack sent: ");
    s = natsSubscription_NextMsg(&ack, ackSub, 1000);
    natsMsg_Destroy(ack);
    ack = NULL;
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
#endif
    sub = NULL;

    test("Create stream with several subjects: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "MULTIPLE_SUBJS";
    sc.Subjects = (const char*[2]){"sub.1", "sub.2"};
    sc.SubjectsLen = 2;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Create consumer with filter: ");
    jsConsumerConfig_Init(&cc);
    cc.Durable = "dur";
    cc.DeliverSubject = "push.dur.sub.2";
    cc.FilterSubject = "sub.2";
    s = js_AddConsumer(NULL, js, "MULTIPLE_SUBJS", &cc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Subscribe subj != filter: ");
    jsSubOptions_Init(&so);
    so.Stream = "MULTIPLE_SUBJS";
    so.Consumer = "dur";
    s = js_Subscribe(&sub, js, "foo", _jsMsgHandler, &args, NULL, &so, &jerr);
    testCond((s == NATS_ERR) && (sub == NULL)
                && (strstr(nats_GetLastError(NULL), "filter subject") != NULL));
    nats_clearLastError();

    if (serverVersionAtLeast(2, 10, 0))
    {
        test("Create consumer with multiple filters: ");
        jsConsumerConfig_Init(&cc);
        cc.Durable = "dur-multi-filter";
        cc.DeliverSubject = "push.dur.sub.2";
        cc.FilterSubjectsLen = 2;
        cc.FilterSubjects = (const char *[2]){"sub.1", "sub.2"};
        s = js_AddConsumer(NULL, js, "MULTIPLE_SUBJS", &cc, NULL, &jerr);
        testCond((s == NATS_OK) && (jerr == 0));

        test("Subscribe subj != filters: ");
        so.Consumer = "dur-multi-filter";
        s = js_Subscribe(&sub, js, "foo", _jsMsgHandler, &args, NULL, &so, &jerr);
        testCond((s == NATS_ERR) && (sub == NULL)
            && (strstr(nats_GetLastError(NULL), "filter subject") != NULL));
        nats_clearLastError();
        cc.FilterSubject = "sub.2";
        cc.FilterSubjects = NULL;
        cc.FilterSubjectsLen = 0;
        so.Consumer = "dur";
    }

    test("Subject not required when binding to stream/consumer: ");
    s = js_Subscribe(&sub, js, NULL, _jsMsgHandler, &args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create consumer for pull: ");
    jsConsumerConfig_Init(&cc);
    cc.Durable = "dur2";
    s = js_AddConsumer(NULL, js, "MULTIPLE_SUBJS", &cc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Subscribe subj not a pull: ");
    jsSubOptions_Init(&so);
    so.Stream = "MULTIPLE_SUBJS";
    so.Consumer = "dur2";
    s = js_Subscribe(&sub, js, "foo", _jsMsgHandler, &args, NULL, &so, &jerr);
    testCond((s == NATS_ERR) && (sub == NULL)
                && (strstr(nats_GetLastError(NULL), jsErrPullSubscribeRequired) != NULL));
    nats_clearLastError();

    test("Create stream for concurrent test: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST_CONCURRENT";
    sc.Subjects = (const char*[1]){"concurrent"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Start concurrent creation of subs: ");
    natsMutex_Lock(args.m);
    args.attached = 0;
    args.detached = 0;
    args.control = 0;
    args.js = js;
    natsMutex_Unlock(args.m);
    s = NATS_OK;
    for (i=0; ((s == NATS_OK) && (i<10)); i++)
        s = natsThread_Create(&threads[i], _jsCreateSubThread, (void*) &args);
    testCond(s == NATS_OK);

    test("Wait for threads to return: ");
    for (i=0; i<10; i++)
    {
        natsThread_Join(threads[i]);
        natsThread_Destroy(threads[i]);
    }
    testCond(true);

    test("Only 1 should be started, 9 should have failed: ");
    natsMutex_Lock(args.m);
    s = ((args.attached == 1) && (args.detached == 9)) ? NATS_OK : NATS_ERR;
    sub = args.sub;
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    sub = NULL;
    natsSubscription_Destroy(ackSub);
    ackSub = NULL;

    test("Create consumer: ");
    jsSubOptions_Init(&so);
    so.Config.Durable = "delcons1";
    s = js_Subscribe(&sub, js, "foo", _jsMsgHandler, (void*) &args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Unsub deletes consumer: ");
    s = natsSubscription_Unsubscribe(sub);
    IFOK(s, js_GetConsumerInfo(&ci, js, "TEST", "delcons1", NULL, &jerr));
    testCond((s == NATS_NOT_FOUND) && (ci == NULL) && (jerr == JSConsumerNotFoundErr)
                && (nats_GetLastError(NULL) == NULL));
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create consumer: ");
    jsSubOptions_Init(&so);
    so.Config.Durable = "delcons2";
    s = js_Subscribe(&sub, js, "foo", _jsMsgHandler, (void*) &args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Drain deletes consumer: ");
    s = natsSubscription_Drain(sub);
    IFOK(s, natsSubscription_WaitForDrainCompletion(sub, 1000));
    IFOK(s, js_GetConsumerInfo(&ci, js, "TEST", "delcons2", NULL, &jerr));
    testCond((s == NATS_NOT_FOUND) && (ci == NULL) && (jerr == JSConsumerNotFoundErr)
                && (nats_GetLastError(NULL) == NULL));
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create consumer: ");
    jsSubOptions_Init(&so);
    so.Config.Durable = "delcons2sync";
    s = js_SubscribeSync(&sub, js, "foo", NULL, &so, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Drain deletes consumer: ");
    s = natsSubscription_Drain(sub);
    for (i=0; i<3; i++)
    {
        natsMsg *msg = NULL;
        IFOK(s, natsSubscription_NextMsg(&msg, sub, 1000));
        IFOK(s, natsMsg_Ack(msg, NULL));
        natsMsg_Destroy(msg);
        msg = NULL;
    }
    IFOK(s, natsSubscription_WaitForDrainCompletion(sub, 1000));
    IFOK(s, js_GetConsumerInfo(&ci, js, "TEST", "delcons2sync", NULL, &jerr));
    testCond((s == NATS_NOT_FOUND) && (ci == NULL) && (jerr == JSConsumerNotFoundErr)
                && (nats_GetLastError(NULL) == NULL));
    natsSubscription_Destroy(sub);
    sub = NULL;

    if (serverVersionAtLeast(2, 10, 14))
    {
        test("Create consumer (multiple subjects): ");
        jsSubOptions_Init(&so);
        so.Config.Durable = "delcons3sync";
        s = js_SubscribeSyncMulti(&sub, js, subjects, numSubjects, NULL, &so, &jerr);
        testCond((s == NATS_OK) && (jerr == 0));

        test("Drain deletes consumer: ");
        s = natsSubscription_Drain(sub);
        for (i = 0; i < 5; i++)
        {
            natsMsg *msg = NULL;
            IFOK(s, natsSubscription_NextMsg(&msg, sub, 1000));
            IFOK(s, natsMsg_Ack(msg, NULL));
            natsMsg_Destroy(msg);
            msg = NULL;
        }
        IFOK(s, natsSubscription_WaitForDrainCompletion(sub, 1000));
        IFOK(s, js_GetConsumerInfo(&ci, js, "TEST", "delcons3sync", NULL, &jerr));
        testCond((s == NATS_NOT_FOUND) && (ci == NULL) && (jerr == JSConsumerNotFoundErr) && (nats_GetLastError(NULL) == NULL));
        natsSubscription_Destroy(sub);
        sub = NULL;
    }

    test("Create consumer: ");
    jsSubOptions_Init(&so);
    so.Config.Durable = "delcons3";
    s = js_Subscribe(&sub, js, "foo", _jsMsgHandler, (void*) &args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Delete consumer: ");
    s = js_DeleteConsumer(js, "TEST", "delcons3", NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Unsub report error: ");
    s = natsSubscription_Unsubscribe(sub);
    testCond((s != NATS_OK)
                && (strstr(nats_GetLastError(NULL), "not found") != NULL));
    nats_clearLastError();
    natsSubscription_Destroy(sub);
    sub = NULL;

    natsMutex_Lock(args.m);
    args.msgReceived = false;
    natsMutex_Unlock(args.m);

    test("Create consumer: ");
    jsSubOptions_Init(&so);
    so.Config.Durable = "delcons4";
    s = js_Subscribe(&sub, js, "foo", _jsMsgHandler, (void*) &args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Delete consumer: ");
    s = js_DeleteConsumer(js, "TEST", "delcons4", NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Drain report error: ");
    s = natsSubscription_Drain(sub);
    if (s == NATS_OK)
    {
        natsMutex_Lock(args.m);
        while ((s != NATS_TIMEOUT) && !args.msgReceived)
            s = natsCondition_TimedWait(args.c, args.m, 1000);
        natsMutex_Unlock(args.m);
    }
    testCond(s == NATS_OK);
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Destroy sub does not delete consumer: ");
    jsSubOptions_Init(&so);
    so.Config.Durable = "delcons5";
    s = js_Subscribe(&sub, js, "foo", _jsMsgHandler, (void*) &args, NULL, &so, &jerr);
    if (s == NATS_OK)
    {
        natsSubscription_Destroy(sub);
        sub = NULL;
        s = js_GetConsumerInfo(&ci, js, "TEST", "delcons5", NULL, &jerr);
    }
    testCond((s == NATS_OK) && (ci != NULL) && (jerr == 0));
    jsConsumerInfo_Destroy(ci);
    ci = NULL;

    test("Queue and HB is invalid: ");
    jsSubOptions_Init(&so);
    so.Stream = "TEST";
    so.Queue = "queue";
    so.Config.Heartbeat = NATS_SECONDS_TO_NANOS(2);
    s = js_SubscribeSync(&sub, js, "foo", NULL, &so, &jerr);
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrNoHeartbeatForQueueSub) != NULL));
    nats_clearLastError();

    test("Queue and FlowControl is invalid: ");
    jsSubOptions_Init(&so);
    so.Stream = "TEST";
    so.Queue = "queue";
    so.Config.FlowControl = true;
    s = js_SubscribeSync(&sub, js, "foo", NULL, &so, &jerr);
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrNoFlowControlForQueueSub) != NULL));
    nats_clearLastError();

    test("Queue name can't contain dots: ");
    jsSubOptions_Init(&so);
    so.Stream = "TEST";
    so.Queue = "queue.name";
    s = js_SubscribeSync(&sub, js, "foo", NULL, &so, &jerr);
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), "cannot contain '.'") != NULL));
    nats_clearLastError();

    test("Queue group serves as durable: ");
    jsSubOptions_Init(&so);
    so.Stream = "TEST";
    so.Queue = "qgroup";
    s = js_SubscribeSync(&sub, js, "foo", NULL, &so, &jerr);
    IFOK(s, js_GetConsumerInfo(&ci, js, "TEST", "qgroup", NULL, &jerr));
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0)
                && (ci != NULL) && (ci->Config != NULL)
                && (ci->Config->Durable != NULL)
                && (strcmp(ci->Config->Durable, "qgroup") == 0));
    jsConsumerInfo_Destroy(ci);
    ci = NULL;
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Durable name invalid (push): ");
    jsSubOptions_Init(&so);
    so.Config.DeliverSubject = "bar";
    so.Config.Durable = "dur.invalid";
    s = js_SubscribeSync(&sub, js, "foo", NULL, &so, &jerr);
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), "cannot contain '.'") != NULL));
    nats_clearLastError();

    test("Durable name invalid (pull): ");
    s = js_PullSubscribe(&sub, js, "foo", "dur.invalid", NULL, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), "cannot contain '.'") != NULL));
    nats_clearLastError();

    JS_TEARDOWN;
    natsOptions_Destroy(ncOpts);
    _destroyDefaultThreadArgs(&args);
}

void test_JetStreamSubscribeSync(void)
{
    natsStatus          s;
    natsSubscription    *sub= NULL;
    jsErrCode           jerr= 0;
    natsSubscription    *ackSub = NULL;
    natsMsg             *ack = NULL;
    jsStreamConfig      sc;
    int                 i;
    natsMsg             *msgs[4];
    natsMsg             *msg = NULL;
    const char          *consName;
    jsConsumerInfo      *ci = NULL;
    jsConsumerInfo      *ci2 = NULL;
    jsMsgMetaData       *meta = NULL;
    jsSubOptions        so;

    JS_SETUP(2, 7, 0);

    test("Create sync sub (invalid args): ");
    s = js_SubscribeSync(NULL, js, "foo", NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_SubscribeSync(&sub, NULL, "foo", NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_SubscribeSync(&sub, js, NULL, NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_SubscribeSync(&sub, js, "", NULL, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0));
    nats_clearLastError();

    test("Create, no stream exists: ");
    s = js_SubscribeSync(&sub, js, "foo", NULL, NULL, &jerr);
    testCond((s != NATS_OK) && (sub == NULL)
                && (strstr(nats_GetLastError(NULL), jsErrNoStreamMatchesSubject) != NULL));
    nats_clearLastError();

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Subjects = (const char*[1]){"foo"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Populate: ");
    s = js_Publish(NULL, js, "foo", "msg1", 4, NULL, &jerr);
    IFOK(s, js_Publish(NULL, js, "foo", "msg2", 4, NULL, &jerr));
    IFOK(s, js_Publish(NULL, js, "foo", "msg3", 4, NULL, &jerr));
    IFOK(s, js_Publish(NULL, js, "foo", "msg4", 4, NULL, &jerr));
    testCond(s == NATS_OK);

    test("Create sub to check ACKs: ");
    s = natsConnection_SubscribeSync(&ackSub, nc, "$JS.ACK.TEST.>");
    testCond(s == NATS_OK);

    test("Subscribe, no options: ");
    s = js_SubscribeSync(&sub, js, "foo", NULL, NULL, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Check msgs received: ");
    for (i=0; (s == NATS_OK) && (i<4); i++)
        s = natsSubscription_NextMsg(&msgs[i], sub, 1000);
    testCond(s == NATS_OK);

    test("Check get meta data (bad args): ");
    s = natsMsg_GetMetaData(NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = natsMsg_GetMetaData(NULL, msgs[0]);
    if (s == NATS_INVALID_ARG)
        s = natsMsg_GetMetaData(&meta, NULL);
    testCond((s == NATS_INVALID_ARG) && (meta == NULL));
    nats_clearLastError();

    test("Check get meta: ");
    s = natsMsg_GetMetaData(&meta, msgs[0]);
    testCond((s == NATS_OK) && (meta != NULL)
                && (strcmp(meta->Stream, "TEST") == 0)
                && (strlen(meta->Consumer) > 0)
                && (meta->NumPending == 3)
                && (meta->NumDelivered == 1)
                && (meta->Sequence.Consumer == 1)
                && (meta->Sequence.Stream == 1)
                && (meta->Timestamp > 0));
    jsMsgMetaData_Destroy(meta);
    meta = NULL;
    // Check this is ok
    jsMsgMetaData_Destroy(NULL);

    test("Ack: ");
    s = natsMsg_Ack(msgs[0], NULL);
    IFOK(s, natsSubscription_NextMsg(&ack, ackSub, 1000));
    if (s == NATS_OK)
    {
        s = (strncmp(
                natsMsg_GetData(ack),
                jsAckAck,
                natsMsg_GetDataLength(ack)) == 0 ? NATS_OK : NATS_ERR);

        natsMsg_Destroy(ack);
        ack = NULL;
    }
    testCond(s == NATS_OK);

    test("Second ack does nothing: ");
    s = natsMsg_Ack(msgs[0], NULL);
    IFOK(s, natsSubscription_NextMsg(&ack, ackSub, 300));
    testCond(s == NATS_TIMEOUT);
    natsMsg_Destroy(msgs[0]);

    test("AckSync: ");
    s = natsMsg_AckSync(msgs[1], NULL, &jerr);
    IFOK(s, natsSubscription_NextMsg(&ack, ackSub, 1000));
    if (s == NATS_OK)
    {
        s = (strncmp(
                natsMsg_GetData(ack),
                jsAckAck,
                natsMsg_GetDataLength(ack)) == 0 ? NATS_OK : NATS_ERR);

        natsMsg_Destroy(ack);
        ack = NULL;
    }
    natsMsg_Destroy(msgs[1]);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Nack: ");
    s = natsMsg_Nak(msgs[2], NULL);
    IFOK(s, natsSubscription_NextMsg(&ack, ackSub, 1000));
    if (s == NATS_OK)
    {
        s = (strncmp(
                natsMsg_GetData(ack),
                jsAckNak,
                natsMsg_GetDataLength(ack)) == 0 ? NATS_OK : NATS_ERR);

        natsMsg_Destroy(ack);
        ack = NULL;
    }
    natsMsg_Destroy(msgs[2]);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Nack msg resent: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK) && (msg != NULL));

    // Should be able to send InProgress more than once.
    for (i=0; i<2; i++)
    {
        test("InProgress: ");
        s = natsMsg_InProgress(msg, NULL);
        IFOK(s, natsSubscription_NextMsg(&ack, ackSub, 1000));
        if (s == NATS_OK)
        {
            s = (strncmp(
                    natsMsg_GetData(ack),
                    jsAckInProgress,
                    natsMsg_GetDataLength(ack)) == 0 ? NATS_OK : NATS_ERR);

            natsMsg_Destroy(ack);
            ack = NULL;
        }
        testCond(s == NATS_OK);
    }
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Term: ");
    s = natsMsg_Term(msgs[3], NULL);
    IFOK(s, natsSubscription_NextMsg(&ack, ackSub, 1000));
    if (s == NATS_OK)
    {
        s = (strncmp(
                natsMsg_GetData(ack),
                jsAckTerm,
                natsMsg_GetDataLength(ack)) == 0 ? NATS_OK : NATS_ERR);

        natsMsg_Destroy(ack);
        ack = NULL;
    }
    natsMsg_Destroy(msgs[3]);
    testCond(s == NATS_OK);

    test("Ack failure (bad args): ");
    s = natsMsg_Ack(NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = natsMsg_AckSync(NULL, NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = natsMsg_Nak(NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = natsMsg_InProgress(NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = natsMsg_Term(NULL, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Ack failure (not bound): ");
    natsMsg_Create(&msg, "foo", NULL, "test", 4);
    s = natsMsg_Ack(msg, NULL);
    if (s == NATS_ILLEGAL_STATE)
        s = natsMsg_AckSync(msg, NULL, NULL);
    if (s == NATS_ILLEGAL_STATE)
        s = natsMsg_Nak(msg, NULL);
    if (s == NATS_ILLEGAL_STATE)
        s = natsMsg_InProgress(msg, NULL);
    if (s == NATS_ILLEGAL_STATE)
        s = natsMsg_Term(msg, NULL);
    testCond((s == NATS_ILLEGAL_STATE)
                && (strstr(nats_GetLastError(NULL), jsErrMsgNotBound) != NULL));
    nats_clearLastError();

    test("Get Meta failure (not bound): ")
    s = natsMsg_GetMetaData(&meta, msg);
    testCond((s == NATS_ILLEGAL_STATE) && (meta == NULL)
                 && (strstr(nats_GetLastError(NULL), jsErrMsgNotBound) != NULL));
    nats_clearLastError();

    test("Ack failure (no reply): ");
    // artificially bind to existing sub
    msg->sub = sub;
    s = natsMsg_Ack(msg, NULL);
    if (s == NATS_ILLEGAL_STATE)
        s = natsMsg_AckSync(msg, NULL, NULL);
    if (s == NATS_ILLEGAL_STATE)
        s = natsMsg_Nak(msg, NULL);
    if (s == NATS_ILLEGAL_STATE)
        s = natsMsg_InProgress(msg, NULL);
    if (s == NATS_ILLEGAL_STATE)
        s = natsMsg_Term(msg, NULL);
    testCond((s == NATS_ILLEGAL_STATE)
                && (strstr(nats_GetLastError(NULL), jsErrMsgNotJS) != NULL));
    nats_clearLastError();

    test("Get Meta failure (no reply): ");
    s = natsMsg_GetMetaData(&meta, msg);
    testCond((s == NATS_ILLEGAL_STATE) && (meta == NULL)
                && (strstr(nats_GetLastError(NULL), jsErrMsgNotJS) != NULL));
    nats_clearLastError();
    msg->sub = NULL;
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Get Meta failure (not JS Ack): ");
    s = natsMsg_Create(&msg, "foo", "not.a.js.ack", NULL, 0);
    if (s == NATS_OK)
        msg->sub = sub;
    IFOK(s, natsMsg_GetMetaData(&meta, msg));
    testCond((s == NATS_ERR) && (meta == NULL)
                && (strstr(nats_GetLastError(NULL), "invalid meta") != NULL));
    nats_clearLastError();
    msg->sub = NULL;
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Get Meta failure (terminal dot): ");
    s = natsMsg_Create(&msg, "foo", "$JS.ACK.TEST.CONSUMER.1.1.1.1629415486698860000.3.", NULL, 0);
    if (s == NATS_OK)
        msg->sub = sub;
    IFOK(s, natsMsg_GetMetaData(&meta, msg));
    testCond((s == NATS_ERR) && (meta == NULL)
                && (strstr(nats_GetLastError(NULL), "invalid meta") != NULL));
    nats_clearLastError();
    msg->sub = NULL;
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Get Meta failure (too small): ");
    s = natsMsg_Create(&msg, "foo", "$JS.ACK.TEST.CONSUMER.1.1.1.1629415486698860000", NULL, 0);
    if (s == NATS_OK)
        msg->sub = sub;
    IFOK(s, natsMsg_GetMetaData(&meta, msg));
    testCond((s == NATS_ERR) && (meta == NULL)
                && (strstr(nats_GetLastError(NULL), "invalid meta") != NULL));
    nats_clearLastError();
    msg->sub = NULL;
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Get Meta failure (too small v2): ");
    s = natsMsg_Create(&msg, "foo", "$JS.ACK.HUB.accHash.TEST.CONSUMER.1.1.1.1629415486698860000", NULL, 0);
    if (s == NATS_OK)
        msg->sub = sub;
    IFOK(s, natsMsg_GetMetaData(&meta, msg));
    testCond((s == NATS_ERR) && (meta == NULL)
                && (strstr(nats_GetLastError(NULL), "invalid meta") != NULL));
    nats_clearLastError();
    msg->sub = NULL;
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Get Meta failure (invalid content): ");
    s = natsMsg_Create(&msg, "foo", "$JS.ACK.TEST.CONSUMER.and.some.bad.other.things", NULL, 0);
    if (s == NATS_OK)
        msg->sub = sub;
    IFOK(s, natsMsg_GetMetaData(&meta, msg));
    testCond((s == NATS_ERR) && (meta == NULL)
                && (strstr(nats_GetLastError(NULL), "invalid meta") != NULL));
    nats_clearLastError();
    msg->sub = NULL;
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Get Meta failure (invalid content v2): ");
    s = natsMsg_Create(&msg, "foo", "$JS.ACK.HUB.accHash.TEST.CONSUMER.and.some.bad.other.things", NULL, 0);
    if (s == NATS_OK)
        msg->sub = sub;
    IFOK(s, natsMsg_GetMetaData(&meta, msg));
    testCond((s == NATS_ERR) && (meta == NULL)
                && (strstr(nats_GetLastError(NULL), "invalid meta") != NULL));
    nats_clearLastError();
    msg->sub = NULL;
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Get Meta v2: ");
    s = natsMsg_Create(&msg, "foo", "$JS.ACK.HUB.accHash.TEST.CONSUMER.1.2.3.1629415486698860000.4.random", NULL, 0);
    if (s == NATS_OK)
        msg->sub = sub;
    IFOK(s, natsMsg_GetMetaData(&meta, msg));
    testCond((s == NATS_OK) && (meta != NULL)
                && (strcmp(meta->Domain, "HUB") == 0)
                && (strcmp(meta->Stream, "TEST") == 0)
                && (strcmp(meta->Consumer, "CONSUMER") == 0)
                && (meta->NumDelivered == 1)
                && (meta->Sequence.Stream == 2)
                && (meta->Sequence.Consumer == 3)
                && (meta->Timestamp == 1629415486698860000)
                && (meta->NumPending == 4));
    jsMsgMetaData_Destroy(meta);
    meta = NULL;
    msg->sub = NULL;
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Get Meta v2 (empty domain): ");
    s = natsMsg_Create(&msg, "foo", "$JS.ACK._.accHash.TEST.CONSUMER.1.2.3.1629415486698860000.4.random", NULL, 0);
    if (s == NATS_OK)
        msg->sub = sub;
    IFOK(s, natsMsg_GetMetaData(&meta, msg));
    testCond((s == NATS_OK) && (meta != NULL)
                && (meta->Domain == NULL)
                && (strcmp(meta->Stream, "TEST") == 0)
                && (strcmp(meta->Consumer, "CONSUMER") == 0)
                && (meta->NumDelivered == 1)
                && (meta->Sequence.Stream == 2)
                && (meta->Sequence.Consumer == 3)
                && (meta->Timestamp == 1629415486698860000)
                && (meta->NumPending == 4));
    jsMsgMetaData_Destroy(meta);
    meta = NULL;
    msg->sub = NULL;
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Get Meta v2 (no failure with appended tokens): ");
    s = natsMsg_Create(&msg, "foo", "$JS.ACK._.accHash.TEST.CONSUMER.1.2.3.1629415486698860000.4.random.new_one", NULL, 0);
    if (s == NATS_OK)
        msg->sub = sub;
    IFOK(s, natsMsg_GetMetaData(&meta, msg));
    testCond((s == NATS_OK) && (meta != NULL)
                && (meta->Domain == NULL)
                && (strcmp(meta->Stream, "TEST") == 0)
                && (strcmp(meta->Consumer, "CONSUMER") == 0)
                && (meta->NumDelivered == 1)
                && (meta->Sequence.Stream == 2)
                && (meta->Sequence.Consumer == 3)
                && (meta->Timestamp == 1629415486698860000)
                && (meta->NumPending == 4));
    jsMsgMetaData_Destroy(meta);
    meta = NULL;
    msg->sub = NULL;
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Get Meta v2 (no failure with appended tokens, malformed ok): ");
    s = natsMsg_Create(&msg, "foo", "$JS.ACK._.accHash.TEST.CONSUMER.1.2.3.1629415486698860000.4.", NULL, 0);
    if (s == NATS_OK)
        msg->sub = sub;
    IFOK(s, natsMsg_GetMetaData(&meta, msg));
    testCond((s == NATS_OK) && (meta != NULL)
                && (meta->Domain == NULL)
                && (strcmp(meta->Stream, "TEST") == 0)
                && (strcmp(meta->Consumer, "CONSUMER") == 0)
                && (meta->NumDelivered == 1)
                && (meta->Sequence.Stream == 2)
                && (meta->Sequence.Consumer == 3)
                && (meta->Timestamp == 1629415486698860000)
                && (meta->NumPending == 4));
    jsMsgMetaData_Destroy(meta);
    meta = NULL;
    msg->sub = NULL;
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Send msg: ");
    s = js_Publish(NULL, js, "foo", "block", 5, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Drain does not delete consumer: ");
    consName = sub->jsi->consumer;
    s = natsSubscription_Drain(sub);
    nats_Sleep(500);
    IFOK(s, js_GetConsumerInfo(&ci, js, "TEST", consName, NULL, &jerr));
    testCond((s == NATS_OK) && (ci != NULL) && (jerr == 0));
    jsConsumerInfo_Destroy(ci);
    ci = NULL;

    test("Wait for drain to complete: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    natsMsg_Destroy(msg);
    IFOK(s, natsSubscription_WaitForDrainCompletion(sub, 1000));
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create sub with ack-none: ");
    jsSubOptions_Init(&so);
    so.Config.AckPolicy = js_AckNone;
    so.Config.DeliverPolicy = js_DeliverLast;
    s = js_SubscribeSync(&sub, js, "foo", NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Check msg received: ");
    msg = NULL;
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond(s == NATS_OK);

    test("Check ack: ");
    s = natsMsg_InProgress(msg, NULL);
    IFOK(s, natsMsg_Nak(msg, NULL));
    IFOK(s, natsMsg_Ack(msg, NULL));
    IFOK(s, natsMsg_Term(msg, NULL));
    testCond(s == NATS_OK);

    test("Check no ack sent: ");
    natsMsg_Destroy(msg);
    msg = NULL;
    s = natsSubscription_NextMsg(&msg, ackSub, 100);
    testCond((s == NATS_TIMEOUT) && (msg == NULL));

    natsSubscription_Destroy(sub);
    sub = NULL;
    natsSubscription_Destroy(ackSub);
    ackSub = NULL;

    test("Test InactiveThreshold (bad value): ");
    jsSubOptions_Init(&so);
    so.Config.InactiveThreshold = NATS_MILLIS_TO_NANOS(-100);
    s = js_SubscribeSync(&sub, js, "foo", NULL, &so, &jerr);
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), "invalid InactiveThreshold") != NULL));
    nats_clearLastError();

    test("Create normal sub: ");
    s = natsConnection_SubscribeSync(&sub, nc, "test");
    testCond((s == NATS_OK) && (sub != NULL));

    test("sub get info (invalid args): ");
    s = natsSubscription_GetConsumerInfo(NULL, sub, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = natsSubscription_GetConsumerInfo(&ci, NULL, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (jerr == 0));
    nats_clearLastError();

    test("sub get info (invalid sub): ");
    s = natsSubscription_GetConsumerInfo(&ci, sub, NULL, &jerr);
    testCond((s == NATS_INVALID_SUBSCRIPTION) && (ci == NULL) && (jerr == 0));
    nats_clearLastError();
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create sub: ");
    jsSubOptions_Init(&so);
    so.Config.InactiveThreshold = NATS_MILLIS_TO_NANOS(50);
    s = js_SubscribeSync(&sub, js, "foo", NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Get consumer info: ");
    s = natsSubscription_GetConsumerInfo(&ci, sub, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (ci != NULL) && (ci->Config != NULL)
                && (ci->Config->InactiveThreshold == NATS_MILLIS_TO_NANOS(50)));

    test("Close conn: ");
    jsCtx_Destroy(js);
    js = NULL;
    natsConnection_Destroy(nc);
    nc = NULL;
    testCond(true);

    nats_Sleep(150);

    test("Check consumer gone: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_JetStream(&js, nc, NULL));
    IFOK(s, js_GetConsumerInfo(&ci2, js, "TEST", ci->Name, NULL, &jerr));
    testCond((s == NATS_NOT_FOUND) && (jerr == JSConsumerNotFoundErr) && (ci2 == NULL));
    jsConsumerInfo_Destroy(ci);
    natsSubscription_Destroy(sub);
    JS_TEARDOWN;
}

void test_JetStreamSubscribeConfigCheck(void)
{
    natsStatus          s;
    natsSubscription    *sub= NULL;
    const char          *name = NULL;
    jsErrCode           jerr= 0;
    char                durName[64];
    char                testName[64];
    jsStreamConfig      sc;
    jsConsumerConfig    cc;
    int                 i;
    int64_t             backOffListOf3[3] = {1, 2, 3};
    int64_t             backOffListOf2[2] = {1, 2};

    JS_SETUP(2, 9, 0);

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Subjects = (const char*[1]){"foo"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    for (i=0; i<17; i++)
    {
        jsSubOptions    so1;
        jsSubOptions    so2;

        natsNUID_Next(durName, sizeof(durName));

        jsSubOptions_Init(&so1);
        jsSubOptions_Init(&so2);
        switch (i)
        {
            case 0:
            {
                name = "description";
                so1.Config.Description = "a";
                so2.Config.Description = "b";
                break;
            }
            case 1:
            {
                name = "deliver policy";
                so1.Config.DeliverPolicy = js_DeliverAll;
                so2.Config.DeliverPolicy = js_DeliverLast;
                break;
            }
            case 2:
            {
                name = "optional start sequence";
                so1.Config.OptStartSeq = 1;
                so2.Config.OptStartSeq = 10;
                break;
            }
            case 3:
            {
                name = "optional start time";
                so1.Config.OptStartTime = 1000000000;
                so2.Config.OptStartTime = 2000000000;
                break;
            }
            case 4:
            {
                name = "ack wait";
                so1.Config.AckWait = NATS_SECONDS_TO_NANOS(10);
                so2.Config.AckWait = NATS_SECONDS_TO_NANOS(15);
                break;
            }
            case 5:
            {
                name = "max deliver";
                so1.Config.MaxDeliver = 3;
                so2.Config.MaxDeliver = 5;
                break;
            }
            case 6:
            {
                name = "replay policy";
                so1.Config.ReplayPolicy = js_ReplayOriginal;
                so2.Config.ReplayPolicy = js_ReplayInstant;
                break;
            }
            case 7:
            {
                name = "max waiting";
                so1.Config.MaxWaiting = 10;
                so2.Config.MaxWaiting = 20;
                break;
            }
            case 8:
            {
                name = "max ack pending";
                so1.Config.MaxAckPending = 10;
                so2.Config.MaxAckPending = 20;
                break;
            }
            case 9:
            {
                name = "sample frequency";
                so1.Config.SampleFrequency = "100%";
                so2.Config.SampleFrequency = "50%";
                break;
            }
            case 10:
            {
                name = "backoff";
                so1.Config.BackOff = backOffListOf3;
                so1.Config.BackOffLen = 3;
                so1.Config.MaxDeliver = 5;
                so2.Config.BackOff = backOffListOf2;
                so2.Config.BackOffLen = 2;
                so2.Config.MaxDeliver = 5;
                break;
            }
            case 11:
            {
                name = "headers only";
                so1.Config.HeadersOnly = true;
                // Not setting it for the 2nd subscribe call should fail.
                break;
            }
            case 12:
            {
                name = "max request batch";
                so1.Config.MaxRequestBatch = 100;
                so2.Config.MaxRequestBatch = 200;
                break;
            }
            case 13:
            {
                name = "max request expires";
                so1.Config.MaxRequestExpires = NATS_SECONDS_TO_NANOS(1);
                so2.Config.MaxRequestExpires = NATS_SECONDS_TO_NANOS(2);
                break;
            }
            case 14:
            {
                name = "inactive threshold";
                so1.Config.InactiveThreshold = NATS_SECONDS_TO_NANOS(1);
                so2.Config.InactiveThreshold = NATS_SECONDS_TO_NANOS(2);
                break;
            }
            case 15:
            {
                name = "replicas";
                so1.Config.Replicas = 1;
                so2.Config.Replicas = 3;
                break;
            }
            case 16:
            {
                name = "memory storage";
                so1.Config.MemoryStorage = true;
                // Not setting it for the 2nd subscribe call should fail.
                break;
            }
        }
        snprintf(testName, sizeof(testName), "Check %s: ", name);
        test(testName);
        s = js_PullSubscribe(&sub, js, "foo", durName, NULL, &so1, &jerr);
        if ((s == NATS_OK) && (jerr == 0))
        {
            natsSubscription *nsub = NULL;

            s = js_PullSubscribe(&nsub, js, "foo", durName, NULL, &so2, &jerr);
            if ((s != NATS_OK) && (nsub == NULL) && (strstr(nats_GetLastError(NULL), name) != NULL))
            {
                s = NATS_OK;
                nats_clearLastError();
            }
            else
                s = NATS_ERR;
        }
        testCond(s == NATS_OK);
        natsSubscription_Unsubscribe(sub);
        natsSubscription_Destroy(sub);
        sub = NULL;
    }

    for (i=0; i<4; i++)
    {
        jsConsumerConfig    cc;
        natsInbox           *inbox = NULL;

        natsInbox_Create(&inbox);
        natsNUID_Next(durName, sizeof(durName));
        jsConsumerConfig_Init(&cc);
        switch (i)
        {
            case 0: cc.AckPolicy = js_AckAll; break;
            case 1: cc.RateLimit = 10; break;
            case 2: cc.FlowControl = false; break;
            case 3: cc.Heartbeat = NATS_SECONDS_TO_NANOS(10); break;
        }
        cc.Durable = durName;
        cc.DeliverSubject = inbox;
        s = js_AddConsumer(NULL, js, "TEST", &cc, NULL, &jerr);
        if ((s == NATS_OK) && (jerr == 0))
        {
            jsSubOptions so;

            jsSubOptions_Init(&so);
            so.Config.Durable = durName;
            switch (i)
            {
                case 0: name="ack policy"; so.Config.AckPolicy = js_AckNone; break;
                case 1: name="rate limit"; so.Config.RateLimit = 100; break;
                case 2: name="flow control"; so.Config.FlowControl = true; break;
                case 3: name="heartbeat"; so.Config.Heartbeat = NATS_SECONDS_TO_NANOS(20); break;
            }
            snprintf(testName, sizeof(testName), "Check %s: ", name);
            test(testName);
            s = js_SubscribeSync(&sub, js, "foo", NULL, &so, &jerr);
            if ((s != NATS_OK) && (sub == NULL) && (strstr(nats_GetLastError(NULL), name) != NULL))
            {
                s = NATS_OK;
                nats_clearLastError();
            }
            else
                s = NATS_ERR;
            testCond(s == NATS_OK);
        }
        natsInbox_Destroy(inbox);
        inbox = NULL;
    }

    // Verify that we don't fail if user did not set it.
    for (i=0; i<14; i++)
    {
        natsSubscription *nsub = NULL;
        jsSubOptions so;

        jsSubOptions_Init(&so);
        switch (i)
        {
            case 0: name = "description"; so.Config.Description = "a"; break;
            case 1: name = "deliver policy"; so.Config.DeliverPolicy = js_DeliverLast; break;
            case 2: name = "optional start sequence"; so.Config.OptStartSeq = 10; break;
            case 3: name = "optional start time"; so.Config.OptStartTime = 1000000000; break;
            case 4: name = "ack wait"; so.Config.AckWait = NATS_SECONDS_TO_NANOS(10); break;
            case 5: name = "max deliver"; so.Config.MaxDeliver = 3; break;
            case 6: name = "replay policy"; so.Config.ReplayPolicy = js_ReplayInstant; break;
            case 7: name = "max waiting"; so.Config.MaxWaiting = 10; break;
            case 8: name = "max ack pending"; so.Config.MaxAckPending = 10; break;
            case 9:
            {
                name = "backoff";
                so.Config.BackOff = backOffListOf3;
                so.Config.BackOffLen = 3;
                so.Config.MaxDeliver = 4;
                break;
            }
            case 10: name = "max request batch"; so.Config.MaxRequestBatch = 100; break;
            case 11: name = "max request expires"; so.Config.MaxRequestExpires = NATS_SECONDS_TO_NANOS(2); break;
            case 12: name = "inactive threshold"; so.Config.InactiveThreshold = NATS_SECONDS_TO_NANOS(2); break;
            case 13: name = "replicas"; so.Config.Replicas = 1; break;
        }

        natsNUID_Next(durName, sizeof(durName));

        snprintf(testName, sizeof(testName), "Check %s: ", name);
        test(testName);
        s = js_PullSubscribe(&sub, js, "foo", durName, NULL, &so, &jerr);
        if (s == NATS_OK)
        {
            // If not explicitly asked by the user, we are ok
            s = js_PullSubscribe(&nsub, js, "foo", durName, NULL, NULL, &jerr);

            natsSubscription_Unsubscribe(nsub);
            natsSubscription_Destroy(nsub);
            nsub = NULL;
        }
        testCond(s == NATS_OK);
        natsSubscription_Unsubscribe(sub);
        natsSubscription_Destroy(sub);
        sub = NULL;
    }

    // If the option is the same as the server default, it is not an error either.
    for (i=0; i<5; i++)
    {
        natsSubscription *nsub = NULL;
        jsSubOptions so;

        jsSubOptions_Init(&so);
        switch (i)
        {
            case 1: name = "default deliver policy"; so.Config.DeliverPolicy = js_DeliverAll; break;
            case 4: name = "default ack wait"; so.Config.AckWait = NATS_SECONDS_TO_NANOS(30); break;
            case 6: name = "default replay policy"; so.Config.ReplayPolicy = js_ReplayInstant; break;
            case 7: name = "default max waiting"; so.Config.MaxWaiting = 512; break;
            case 8: name = "default max ack pending"; so.Config.MaxAckPending = 65536; break;
        }

        natsNUID_Next(durName, sizeof(durName));

        snprintf(testName, sizeof(testName), "Check %s: ", name);
        test(testName);
        s = js_PullSubscribe(&sub, js, "foo", durName, NULL, NULL, &jerr);
        if (s == NATS_OK)
        {
            s = js_PullSubscribe(&nsub, js, "foo", durName, NULL, &so, &jerr);
            natsSubscription_Unsubscribe(nsub);
            natsSubscription_Destroy(nsub);
            nsub = NULL;
        }
        testCond(s == NATS_OK);
        natsSubscription_Unsubscribe(sub);
        natsSubscription_Destroy(sub);
        sub = NULL;
    }

    for (i=0; i<5; i++)
    {
        jsSubOptions so;

        jsSubOptions_Init(&so);
        switch (i)
        {
            case 0: name = "deliver policy"; so.Config.DeliverPolicy = js_DeliverNew; break;
            case 1: name = "ack wait"; so.Config.AckWait = NATS_SECONDS_TO_NANOS(31); break;
            case 2: name = "replay policy"; so.Config.ReplayPolicy = js_ReplayOriginal; break;
            case 3: name = "max waiting"; so.Config.MaxWaiting = 513; break;
            case 4: name = "max ack pending"; so.Config.MaxAckPending = 2; break;
        }

        natsNUID_Next(durName, sizeof(durName));

        snprintf(testName, sizeof(testName), "Check %s: ", name);
        test(testName);
        s = js_PullSubscribe(&sub, js, "foo", durName, NULL, NULL, &jerr);
        if (s == NATS_OK)
        {
            natsSubscription *nsub = NULL;

            // First time it was created with defaults and the
            // second time a change is attempted, so it is an error.
            s = js_PullSubscribe(&nsub, js, "foo", durName, NULL, &so, &jerr);
            if ((s != NATS_OK) && (nsub == NULL) && (strstr(nats_GetLastError(NULL), name) != NULL))
            {
                s = NATS_OK;
                nats_clearLastError();
            }
            else
                s = NATS_ERR;
        }
        testCond(s == NATS_OK);
        natsSubscription_Unsubscribe(sub);
        natsSubscription_Destroy(sub);
        sub = NULL;
    }

    // Check that binding to a durable (without specifying durable option) works
    test("Create durable: ");
    jsConsumerConfig_Init(&cc);
    cc.Durable = "BindDurable";
    cc.DeliverSubject = "bar";
    s = js_AddConsumer(NULL, js, "TEST", &cc, NULL, &jerr);
    if (s == NATS_OK)
    {
        jsSubOptions so;

        jsSubOptions_Init(&so);
        so.Stream = "TEST";
        so.Consumer = "BindDurable";
        s = js_SubscribeSync(&sub, js, "foo", NULL, &so, &jerr);
        natsSubscription_Destroy(sub);
    }
    testCond(s == NATS_OK);

    JS_TEARDOWN;
}

static void
_jsSeqMismatch(natsConnection *nc, natsSubscription *sub, natsStatus err, void *closure)
{
    struct threadArg *args = (struct threadArg*) closure;

    natsMutex_Lock(args->m);
    if (err == NATS_MISMATCH)
        args->done = true;
    else if (err == NATS_MISSED_HEARTBEAT)
        args->results[args->control]++;
    natsCondition_Broadcast(args->c);
    natsMutex_Unlock(args->m);
}

static void
_setMsgReply(natsConnection *nc, natsMsg **msg, void* closure)
{
    natsMsg *m = NULL;

    natsMsg_Create(&m, (*msg)->subject, (const char*) closure, (*msg)->data, (*msg)->dataLen);
    natsMsg_Destroy(*msg);
    *msg = m;
    natsConn_setFilter(nc, NULL);
}

void test_JetStreamSubscribeIdleHearbeat(void)
{
    natsStatus          s;
    natsConnection      *nc = NULL;
    natsSubscription    *sub= NULL;
    jsCtx               *js = NULL;
    natsPid             pid = NATS_INVALID_PID;
    jsErrCode           jerr= 0;
    char                datastore[256] = {'\0'};
    char                cmdLine[1024] = {'\0'};
    jsStreamConfig      sc;
    jsSubOptions        so;
    natsMsg             *msg;
    struct threadArg    args;
    natsOptions         *opts = NULL;
    natsSubscription    *nsub = NULL;
    char                *inbox = NULL;
    int                 i;
    jsConsumerSequenceMismatch csm;
    jsConsumerConfig cc;

    ENSURE_JS_VERSION(2, 3, 5);

    s = natsOptions_Create(&opts);
    IFOK(s, _createDefaultThreadArgsForCbTests(&args));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    _makeUniqueDir(datastore, sizeof(datastore), "datastore_");

    test("Start JS server: ");
    snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s", datastore);
    pid = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(pid);
    testCond(true);

    test("Connect: ");
    s = natsOptions_SetErrorHandler(opts, _jsSeqMismatch, (void*) &args);
    IFOK(s, natsOptions_SetReconnectWait(opts, 50));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);

    test("Get context: ");
    s = natsConnection_JetStream(&js, nc, NULL);
    testCond(s == NATS_OK);

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Subjects = (const char*[1]){"foo"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Populate: ");
    s = js_Publish(NULL, js, "foo", "msg1", 4, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Subscribe async: ");
    jsSubOptions_Init(&so);
    so.Config.Durable = "dur1";
    so.Config.Heartbeat = 150*1000000;
    s = js_Subscribe(&sub, js, "foo", _jsMsgHandler, (void*) &args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Check msg received: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum != 1))
        s = natsCondition_TimedWait(args.c, args.m, 1000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Check HB received: ");
    nats_Sleep(300);
    nats_lockSubAndDispatcher(sub);
    s = (sub->jsi->mismatch.dseq == 1 ? NATS_OK : NATS_ERR);
    nats_unlockSubAndDispatcher(sub);
    testCond(s == NATS_OK);

    test("Check HB is not given to app: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum == 1))
        s = natsCondition_TimedWait(args.c, args.m, 300);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_TIMEOUT);

    test("Check get mismatch (bad args): ");
    // Create regular sub to check invalid args.
    s = natsConnection_SubscribeSync(&nsub, nc, "normal_sub");
    IFOK(s, natsSubscription_GetSequenceMismatch(NULL, NULL));
    if (s == NATS_INVALID_ARG)
        s = natsSubscription_GetSequenceMismatch(NULL, sub);
    if (s == NATS_INVALID_ARG)
        s = natsSubscription_GetSequenceMismatch(&csm, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Not a JS sub: ")
    s = natsSubscription_GetSequenceMismatch(&csm, nsub);
    testCond((s == NATS_INVALID_SUBSCRIPTION)
                && (strstr(nats_GetLastError(NULL), jsErrNotAJetStreamSubscription) != NULL));
    nats_clearLastError();

    test("Check get mismatch returns not found: ");
    s = natsSubscription_GetSequenceMismatch(&csm, sub);
    testCond((s == NATS_NOT_FOUND) && (nats_GetLastError(NULL) == NULL));

    test("Send new message: ");
    s = js_Publish(NULL, js, "foo", "msg2", 4, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    // Cheat by pretending that the server sends message seq 3, while client
    // received only seq 1. Disable auto-ack for this message, or we mess up the
    // server state.
#define PUBLISH_FAKE_JS_MSG_WITH_SEQ(_reply, _msg)                         \
    {                                                                      \
        nats_lockSubAndDispatcher(sub);                                    \
        inbox = sub->subject;                                              \
        sub->jsi->ackNone = true;                                          \
        nats_unlockSubAndDispatcher(sub);                                  \
                                                                           \
        natsConn_setFilterWithClosure(nc, _setMsgReply, (void *)(_reply)); \
        s = natsConnection_PublishString(nc, inbox, (_msg));               \
    }

#define PUBLISH_FAKE_RESET()       \
    {                              \
        natsSub_Lock(sub);         \
        sub->jsi->ackNone = false; \
        natsSub_Unlock(sub);       \
    }

    test("Check seq mismatch: ");
    PUBLISH_FAKE_JS_MSG_WITH_SEQ("$JS.ACK.TEST.dur1.1.3.3.1624472520000000000.0", "msg3 fake");
    // Wait for past the next HB and we should get an async error
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.done)
        s = natsCondition_TimedWait(args.c, args.m, 300);
    natsMutex_Unlock(args.m);
    PUBLISH_FAKE_RESET();
    testCond(s == NATS_OK);

    test("Check that notification is sent only once: ");
    natsMutex_Lock(args.m);
    args.done = false;
    natsMutex_Unlock(args.m);
    // Wait for more than 1 HB, the callback should not
    // be invoked because we have notified once and
    // the mismatch still exist.
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.done)
        s = natsCondition_TimedWait(args.c, args.m, 300);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_TIMEOUT);

    test("Check get mismatch: ");
    // Server will say that the consumer seq is at 2
    s = natsSubscription_GetSequenceMismatch(&csm, sub);
    testCond((s == NATS_OK)
                && (csm.Stream == 3)
                && (csm.ConsumerClient == 3)
                && (csm.ConsumerServer == 2));

    test("Check mismatch suppression cleared: ");
    // Send real message so that all clears up
    s = js_Publish(NULL, js, "foo", "msg3", 4, NULL, &jerr);
    nats_Sleep(300);
    nats_lockSubAndDispatcher(sub);
    s = (sub->jsi->ssmn == false ? NATS_OK : NATS_ERR);
    nats_unlockSubAndDispatcher(sub);
    testCond(s == NATS_OK);

    test("Skip again: ");
    PUBLISH_FAKE_JS_MSG_WITH_SEQ("$JS.ACK.TEST.dur1.1.4.4.1624482520000000000.0", "msg4 fake");
    testCond(s == NATS_OK);

    test("Check async cb invoked: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.done)
        s = natsCondition_TimedWait(args.c, args.m, 1000);
    natsMutex_Unlock(args.m);
    PUBLISH_FAKE_RESET();
    testCond(s == NATS_OK);

    test("Check HB timer reports missed HB: ");
    _stopServer(pid);
    // The HB interval is 150ms, but we check *2, so be a bit more
    // than 300ms to avoid flapping.
    nats_Sleep(500);
    pid = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(pid);
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.results[0] == 0))
        s = natsCondition_TimedWait(args.c, args.m, 1000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    sub = NULL;

    natsMutex_Lock(args.m);
    args.sum = 0;
    args.done = false;
    args.control = 1;
    natsMutex_Unlock(args.m);

    test("Subscribe sync: ");
    jsSubOptions_Init(&so);
    so.Config.Durable = "dur2";
    so.Config.Heartbeat = 150*1000000;
    s = js_SubscribeSync(&sub, js, "foo", NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Check msgs received: ");
    for (i=0; (s == NATS_OK) && (i<3); i++)
    {
        s = natsSubscription_NextMsg(&msg, sub, 1000);
        IFOK(s, natsMsg_Ack(msg, NULL));
        natsMsg_Destroy(msg);
        msg = NULL;
    }
    testCond(s == NATS_OK);

    test("Check HB received: ");
    nats_Sleep(300);
    natsMutex_Lock(sub->mu);
    s = (sub->jsi->mismatch.dseq == 3 ? NATS_OK : NATS_ERR);
    natsMutex_Unlock(sub->mu);
    testCond(s == NATS_OK);

    test("Check HB is not given to app: ");
    s = natsSubscription_NextMsg(&msg, sub, 250);
    testCond((s == NATS_TIMEOUT) && (msg == NULL));
    nats_clearLastError();

    test("Skip: ");
    PUBLISH_FAKE_JS_MSG_WITH_SEQ("$JS.ACK.TEST.dur2.1.4.4.1624482520000000000.0", "msg4 fake");
    testCond(s == NATS_OK);

    // For sync subs, we should not get async error
    test("No async error: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.done)
        s = natsCondition_TimedWait(args.c, args.m, 300);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_TIMEOUT);
    nats_clearLastError();
    PUBLISH_FAKE_RESET();

    test("NextMsg reports error: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_MISMATCH) && (msg == NULL));
    nats_clearLastError();

    test("NextMsg again is ok: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK) && (msg != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Check get mismatch: ");
    s = natsSubscription_GetSequenceMismatch(&csm, sub);
    testCond((s == NATS_OK)
                && (csm.Stream == 4)
                && (csm.ConsumerClient == 4)
                && (csm.ConsumerServer == 3));

    test("Check mismatch suppression cleared: ");
    // Send real message so that all clears up
    s = js_Publish(NULL, js, "foo", "msg4", 4, NULL, &jerr);
    nats_Sleep(300);
    nats_lockSubAndDispatcher(sub);
    s = (sub->jsi->ssmn == false && sub->jsi->sm == false ? NATS_OK : NATS_ERR);
    nats_unlockSubAndDispatcher(sub);
    testCond(s == NATS_OK);

    test("Skip again: ");
    PUBLISH_FAKE_JS_MSG_WITH_SEQ("$JS.ACK.TEST.dur1.1.5.5.1624492520000000000.0", "msg5 fake");
    testCond(s == NATS_OK);

    test("NextMsg reports error: ");
    nats_Sleep(300);
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_MISMATCH) && (msg == NULL));
    nats_clearLastError();
    PUBLISH_FAKE_RESET();

    test("Check HB timer reports missed HB: ");
    s = NATS_OK;
    _stopServer(pid);
    nats_Sleep(500);
    pid = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(pid);
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.results[1] == 0))
        s = natsCondition_TimedWait(args.c, args.m, 1000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create consumer with HB: ");
    jsConsumerConfig_Init(&cc);
    cc.Durable = "qdur";
    cc.DeliverSubject = "bar";
    cc.DeliverGroup = "queue";
    cc.Heartbeat = 150*1000000;
    s = js_AddConsumer(NULL, js, "TEST", &cc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Queue sub against this consumer: ");
    jsSubOptions_Init(&so);
    so.Queue = "queue";
    so.Stream = "TEST";
    so.Consumer = "qdur";
    so.Config.DeliverSubject = "bar";
    s = js_Subscribe(&sub, js, "bar", _jsMsgHandler, (void*)&args, NULL, &so, &jerr);
    if (s == NATS_ERR)
        s = js_SubscribeSync(&sub, js, "bar", NULL, &so, &jerr);
    testCond((s == NATS_ERR) && (sub == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrNoHeartbeatForQueueSub) != NULL));
    nats_clearLastError();

    natsSubscription_Destroy(nsub);
    JS_TEARDOWN;
    _destroyDefaultThreadArgs(&args);
    natsOptions_Destroy(opts);
}

void test_JetStreamSubscribeFlowControl(void)
{
    natsStatus          s;
    natsSubscription    *sub= NULL;
    jsErrCode           jerr= 0;
    jsStreamConfig      sc;
    jsSubOptions        so;
    natsMsg             *msg  = NULL;
    struct threadArg    args;
    natsSubscription    *nsub = NULL;
    int                 i;
    int                 total = 20000;
    char                *data = NULL;
    char                *subj = NULL;
    natsBuffer          *buf  = NULL;

    natsConnection *nc = NULL;
    jsCtx *js = NULL;
    natsPid pid = NATS_INVALID_PID;
    char confFile[256] = {'\0'};
    char datastore[256] = {'\0'};
    char cmdLine[1024] = {'\0'};

    ENSURE_JS_VERSION(2, 3, 3);

    test("Start server: ");
    _makeUniqueDir(datastore, sizeof(datastore), "datastore_");

    if (serverVersionAtLeast(2, 11, 0))
    {
        _createConfFile(confFile, sizeof(confFile),
                        "jetstream: {\n"
                        "   enabled: true\n"
                        "   max_buffered_size: 1Gb\n"
                        "   max_buffered_msgs: 20000\n"
                        "}\n");
        snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s -c %s", datastore, confFile);
    }
    else
    {
        snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s", datastore);
    }
    pid = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(pid);
    testCond(true);

    test("Connect: ");
    s = natsConnection_Connect(&nc, NULL);
    testCond(s == NATS_OK);

    test("Get context: ");
    s = natsConnection_JetStream(&js, nc, NULL);
    testCond(s == NATS_OK);

    data = malloc(100 * 1024);
    if (data == NULL)
        FAIL("Unable to allocate data");

    if (valgrind)
        total = 2000;

    if (data == NULL)
        FAIL("Unable to allocate data");
    for (i=0; i<1024; i++)
        data[i] = 'A';

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Unable to setup test");
    args.control = 2;
    args.results[0] = total;

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Subjects = (const char*[2]){"foo", "bar"};
    sc.SubjectsLen = 2;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Populate: ");
    for (i=0; (s == NATS_OK) && (i<total); i++)
        s = js_PublishAsync(js, "foo", data, 1024, NULL);
    testCond(s == NATS_OK);

    test("Wait complete: ");
    s = js_PublishAsyncComplete(js, NULL);
    testCond(s == NATS_OK);

    test("Create sub to check for FC: ");
    s = natsConnection_SubscribeSync(&nsub, nc, "$JS.FC.TEST.>");
    testCond((s == NATS_OK) && (nsub != NULL));

    test("FC requires HB: ");
    jsSubOptions_Init(&so);
    so.Config.FlowControl = true;
    s = js_Subscribe(&sub, js, "foo", _jsMsgHandler, (void*) &args, NULL, &so, &jerr);
    testCond((s == NATS_ERR) && (sub == NULL) && (jerr == JSConsumerWithFlowControlNeedsHeartbeatsErr));
    nats_clearLastError();

    test("Subscribe async: ");
    jsSubOptions_Init(&so);
    so.Config.FlowControl = true;
    so.Config.Heartbeat = NATS_SECONDS_TO_NANOS(1);
    s = js_Subscribe(&sub, js, "foo", _jsMsgHandler, (void*) &args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Check msg received: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum != total))
        s = natsCondition_TimedWait(args.c, args.m, 10000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Check FC responses were sent: ");
    s = natsSubscription_NextMsg(&msg, nsub, 2000);
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    natsSubscription_Destroy(sub);
    sub = NULL;
    natsSubscription_Destroy(nsub);
    nsub = NULL;

    test("Create sub to check for FC: ");
    s = natsConnection_SubscribeSync(&nsub, nc, "$JS.FC.TEST.>");
    testCond((s == NATS_OK) && (nsub != NULL));

    test("Subscribe sync: ");
    jsSubOptions_Init(&so);
    so.Config.FlowControl = true;
    so.Config.Heartbeat = NATS_SECONDS_TO_NANOS(1);
    s = js_SubscribeSync(&sub, js, "foo", NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Check msg received: ");
    for (i=0; (s == NATS_OK) && (i<total); i++)
    {
        s = natsSubscription_NextMsg(&msg, sub, 1000);
        IFOK(s, natsMsg_Ack(msg, NULL));
        natsMsg_Destroy(msg);
        msg = NULL;
    }
    testCond(s == NATS_OK);

    test("Check FC responses were sent: ");
    s = natsSubscription_NextMsg(&msg, nsub, 2000);
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    free(data);
    natsSubscription_Destroy(sub);
    sub = NULL;
    natsSubscription_Destroy(nsub);
    nsub = NULL;

    test("Create sub to check for FC: ");
    s = natsConnection_SubscribeSync(&nsub, nc, "$JS.FC.TEST.>");
    testCond((s == NATS_OK) && (nsub != NULL));

    test("Subscribe: ");
    jsSubOptions_Init(&so);
    so.Config.FlowControl = true;
    so.Config.Heartbeat = NATS_SECONDS_TO_NANOS(1);
    s = js_Subscribe(&sub, js, "bar", _jsMsgHandler, (void*) &args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Send hb header: ");
    natsSub_Lock(sub);
    subj = sub->subject;
    natsSub_Unlock(sub);
    s = natsBuf_Create(&buf, 256);
    IFOK(s, natsBuf_Append(buf, "HPUB ", -1));
    IFOK(s, natsBuf_Append(buf, subj, -1));
    IFOK(s, natsBuf_Append(buf, " 76 76\r\n", -1));
    IFOK(s, natsBuf_Append(buf, "NATS/1.0 100 Idle Heartbeat\r\n", -1));
    IFOK(s, natsBuf_Append(buf, "Nats-Consumer-Stalled: $JS.FC.TEST.fc.reply\r\n\r\n\r\n", -1));
    natsConn_Lock(nc);
    IFOK(s, natsSock_WriteFully(&nc->sockCtx, natsBuf_Data(buf), natsBuf_Len(buf)));
    natsConn_Unlock(nc);
    testCond(s == NATS_OK);

    test("Check FC reply due to HB header: ");
    IFOK(s, natsSubscription_NextMsg(&msg, nsub, 1000));
    testCond(s == NATS_OK);

    natsBuf_Destroy(buf);
    natsMsg_Destroy(msg);
    natsSubscription_Destroy(sub);
    natsSubscription_Destroy(nsub);
    _destroyDefaultThreadArgs(&args);
    JS_TEARDOWN;
    remove(confFile);
}

static void
_jsPubThread(void *closure)
{
    jsCtx   *js = (jsCtx*) closure;
    int     i;

    nats_Sleep(300);
    for (i=0; i<5; i++)
        js_Publish(NULL, js, "foo", "hello", 5, NULL, NULL);
}

static void
_sendToPullSub(void *closure)
{
    struct threadArg    *args = (struct threadArg*) closure;
    natsMsg             *msg  = NULL;
    char                *subj = NULL;
    natsSubscription    *sub  = NULL;
    uint64_t            id    = 0;
    natsStatus          s;

    nats_Sleep(250);
    natsMutex_Lock(args->m);
    sub = args->sub;
    natsSub_Lock(sub);
    id = sub->jsi->fetchID;
    if (args->sum != 0)
        id = (uint64_t) args->sum;
    if (nats_asprintf(&subj, "%.*s%" PRIu64, (int) strlen(sub->subject)-1, sub->subject, id) < 0)
        s = NATS_NO_MEMORY;
    else
        s = natsMsg_create(&msg, subj, (int) strlen(subj),
                           NULL, 0, args->string, (int) strlen(args->string), (int) strlen(args->string));
    natsSub_Unlock(sub);
    IFOK(s, natsConnection_PublishMsg(args->nc, msg));
    natsMutex_Unlock(args->m);
    natsMsg_Destroy(msg);
    free(subj);
}

static void
_fetchRequest(void *closure)
{
    struct threadArg    *args = (struct threadArg*) closure;
    natsSubscription    *sub = NULL;
    jsFetchRequest      fr;
    natsStatus          s;
    natsMsgList         list;
    int64_t             start;

    natsMutex_Lock(args->m);
    sub = args->sub;
    natsMutex_Unlock(args->m);

    jsFetchRequest_Init(&fr);
    // With current messages, for a MaxBytes of 150, we should get 2 messages,
    // for a total size of 142.
    fr.Batch = 10;
    fr.MaxBytes = 150;
    fr.Expires = NATS_SECONDS_TO_NANOS(2);
    start = nats_Now();
    s = natsSubscription_FetchRequest(&list, sub, &fr);
    if (s == NATS_OK)
    {
        int i;
        int total = 0;

        for (i=0; i<list.Count; i++)
        {
            total += list.Msgs[i]->wsz;
            natsMsg_AckSync(list.Msgs[i], NULL, NULL);
        }

        if ((total > 150) || (list.Count != 2) || ((nats_Now() - start) >= 1900))
            s = NATS_ERR;

        natsMsgList_Destroy(&list);
    }

    natsMutex_Lock(args->m);
    args->status = s;
    natsMutex_Unlock(args->m);
}

static void
_dropIdleHBs(natsConnection *nc, natsMsg **msg, void* closure)
{
    int ct = 0;

    if (natsMsg_GetDataLength(*msg) > 0)
        return;

    if (!natsMsg_isJSCtrl(*msg, &ct))
        return;

    if (ct != jsCtrlHeartbeat)
        return;

    natsMsg_Destroy(*msg);
    *msg = NULL;
}

static void
_dropTimeoutProto(natsConnection *nc, natsMsg **msg, void* closure)
{
    const char *val = NULL;

    if (natsMsgHeader_Get(*msg, STATUS_HDR, &val) != NATS_OK)
        return;

    natsMsg_Destroy(*msg);
    *msg = NULL;
}

void test_JetStreamSubscribePull(void)
{
    natsStatus          s;
    natsSubscription    *sub= NULL;
    natsMsg             *msg = NULL;
    jsErrCode           jerr= 0;
    natsMsgList         list;
    jsStreamConfig      sc;
    jsSubOptions        so;
    struct threadArg    args;
    int64_t             start, dur;
    int                 i;
    natsThread          *t = NULL;
    jsConsumerConfig    cc;
    jsFetchRequest      fr;
    natsSubscription    *sub2 = NULL;
    natsSubscription    *sub3 = NULL;

    JS_SETUP(2, 9, 2);

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    test("Create pull sub (invalid args): ");
    s = js_PullSubscribe(NULL, js, "foo", "dur", NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_PullSubscribe(&sub, NULL, "foo", "dur", NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_PullSubscribe(&sub, js, NULL, "dur", NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_PullSubscribe(&sub, js, "", "dur", NULL, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0));
    nats_clearLastError();

    test("Create reg sync sub: ");
    s = natsConnection_SubscribeSync(&sub, nc, "foo");
    testCond((s == NATS_OK) && (sub != NULL));

    test("Fetch bad args: ");
    s = natsSubscription_Fetch(NULL, sub, 1, 1000, NULL);
    if (s == NATS_INVALID_ARG)
        s = natsSubscription_Fetch(&list, NULL, 1, 1000, NULL);
    if (s == NATS_INVALID_ARG)
        s = natsSubscription_Fetch(&list, sub, 0, 1000, NULL);
    if (s == NATS_INVALID_ARG)
        s = natsSubscription_Fetch(&list, sub, -1, 1000, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Fetch bad timeout: ");
    s = natsSubscription_Fetch(&list, sub, 1, 0, NULL);
    if (s == NATS_INVALID_TIMEOUT)
        s = natsSubscription_Fetch(&list, sub, 1, -1, NULL);
    testCond(s == NATS_INVALID_TIMEOUT);
    nats_clearLastError();

    test("Fetch bad sub: ");
    s = natsSubscription_Fetch(&list, sub, 1, 1000, NULL);
    testCond((s == NATS_INVALID_SUBSCRIPTION)
                && (strstr(nats_GetLastError(NULL), jsErrNotAPullSubscription) != NULL));
    nats_clearLastError();

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create reg async sub: ");
    s = natsConnection_Subscribe(&sub, nc, "foo", _dummyMsgHandler, NULL);
    testCond((s == NATS_OK) && (sub != NULL));

    test("Fetch bad sub: ");
    s = natsSubscription_Fetch(&list, sub, 1, 1000, NULL);
    testCond((s == NATS_INVALID_SUBSCRIPTION)
                && (strstr(nats_GetLastError(NULL), jsErrNotAPullSubscription) != NULL));
    nats_clearLastError();

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Subjects = (const char*[1]){"foo"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("AckNone ok: ");
    jsSubOptions_Init(&so);
    so.Config.AckPolicy = js_AckNone;
    s = js_PullSubscribe(&sub, js, "foo", "ackNone", NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));
    natsSubscription_Unsubscribe(sub);
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("AckAll ok: ");
    jsSubOptions_Init(&so);
    so.Config.AckPolicy = js_AckAll;
    s = js_PullSubscribe(&sub, js, "foo", "ackAll", NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));
    natsSubscription_Unsubscribe(sub);
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create push consumer: ");
    jsConsumerConfig_Init(&cc);
    cc.Durable = "push_dur";
    cc.DeliverSubject = "push.deliver";
    s = js_AddConsumer(NULL, js, "TEST", &cc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Try create pull sub from push consumer: ");
    s = js_PullSubscribe(&sub, js, "foo", "push_dur", NULL, NULL, &jerr);
    testCond((s == NATS_ERR) && (sub == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrPullSubscribeToPushConsumer) != NULL));
    nats_clearLastError();

    test("Create pull bound failure: ");
    jsSubOptions_Init(&so);
    so.Stream = "TEST";
    so.Consumer = "bar";
    s = js_PullSubscribe(&sub, js, "foo", "bar", NULL, &so, &jerr);
    testCond((s == NATS_NOT_FOUND) && (sub == NULL) && (jerr == JSConsumerNotFoundErr));
    nats_clearLastError();

    test("Create pull sub: ");
    jsSubOptions_Init(&so);
    so.Config.MaxAckPending = 10;
    so.Config.AckWait = NATS_MILLIS_TO_NANOS(300);
    s = js_PullSubscribe(&sub, js, "foo", "dur", NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Can't call NextMsg: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_INVALID_SUBSCRIPTION) && (msg == NULL)
                && (strstr(nats_GetLastError(NULL), jsErrNotApplicableToPullSub) != NULL));
    nats_clearLastError();

    test("Fetch, no msg avail, timeout: ");
    start = nats_Now();
    s = natsSubscription_Fetch(&list, sub, 1, 500, &jerr);
    dur = nats_Now() - start;
    testCond((s == NATS_TIMEOUT) && (list.Msgs == NULL) && (list.Count == 0) && (jerr == 0)
                && (dur >= 450) && (dur <= 600));
    nats_clearLastError();

    test("Send a message: ");
    s = js_Publish(NULL, js, "foo", "hello", 5, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Fetch batch 1: ");
    start = nats_Now();
    s = natsSubscription_Fetch(&list, sub, 1, 1000, &jerr);
    dur = nats_Now() - start;
    testCond((s == NATS_OK) && (list.Msgs != NULL) && (list.Count == 1) && (jerr == 0)
                && (dur <= 500));

    test("Ack: ");
    for (i=0; (s == NATS_OK) && (i<list.Count); i++)
        s = natsMsg_AckSync(list.Msgs[i], NULL, &jerr);
    natsMsgList_Destroy(&list);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Send a message: ");
    s = js_Publish(NULL, js, "foo", "hello", 5, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Fetch batch more than avail: ");
    start = nats_Now();
    s = natsSubscription_Fetch(&list, sub, 5, 1000, &jerr);
    dur = nats_Now() - start;
    testCond((s == NATS_OK) && (list.Msgs != NULL) && (list.Count == 1) && (jerr == 0)
                && (dur <= 500));

    test("Ack: ");
    for (i=0; (s == NATS_OK) && (i<list.Count); i++)
        s = natsMsg_Ack(list.Msgs[i], NULL);
    natsMsgList_Destroy(&list);
    testCond(s == NATS_OK);

    test("Start thread to send: ");
    s = natsThread_Create(&t, _jsPubThread, (void*) js);
    testCond(s == NATS_OK);

    test("Fetch: ");
    {
        int got = 0;

        while ((s == NATS_OK) && (got != 5))
        {
            s = natsSubscription_Fetch(&list, sub, 5-got, 2000, &jerr);
            if (s == NATS_OK)
                got += list.Count;
            // Destroy msgs without ack'ing them.
            natsMsgList_Destroy(&list);
        }
        testCond(s == NATS_OK);
    }

    // Wait for AckWait and messages should be redelivered.
    nats_Sleep(500);

    test("Fetch get redelivered msgs: ");
    s = natsSubscription_Fetch(&list, sub, 5, 2000, &jerr);
    testCond((s == NATS_OK) && (list.Msgs != NULL) && (list.Count == 5) && (jerr == 0));

    test("Ack: ");
    for (i=0; (s == NATS_OK) && (i<list.Count); i++)
        s = natsMsg_Ack(list.Msgs[i], NULL);
    natsMsgList_Destroy(&list);
    testCond(s == NATS_OK);

    natsThread_Join(t);
    natsThread_Destroy(t);
    t = NULL;

    test("Receive msg with header no data: ");
    s = natsMsg_create(&msg, sub->subject, (int) strlen(sub->subject), NULL, 0,
                        "NATS/1.0\r\nk:v\r\n\r\n", 17, 17);
    IFOK(s, natsConnection_PublishMsg(nc, msg));
    IFOK(s, natsSubscription_Fetch(&list, sub, 1, 1000, &jerr));
    testCond((s == NATS_OK) && (list.Msgs != NULL) && (list.Count == 1) && (jerr == 0));
    natsMsg_Destroy(msg);
    msg = NULL;
    natsMsgList_Destroy(&list);

    // Make sure there is no more pending.
    _waitSubPending(sub, 0);

    test("Msg with 404 status present before fetch: ");
    s = natsMsg_create(&msg, sub->subject, (int) strlen(sub->subject), NULL, 0,
                        "NATS/1.0 404 No Messages\r\n\r\n", 28, 28);
    IFOK(s, natsConnection_PublishMsg(nc, msg));
    if (s == NATS_OK)
        _waitSubPending(sub, 1);
    IFOK(s, natsSubscription_Fetch(&list, sub, 1, 100, &jerr));
    testCond((s == NATS_TIMEOUT) && (list.Msgs == NULL) && (list.Count == 0) && (jerr == 0));
    nats_clearLastError();
    natsMsg_Destroy(msg);
    msg = NULL;
    natsMsgList_Destroy(&list);

    // Since we faked the 404, the server is going to send a 408 when the request
    // expires, so wait for it to be sent.
    nats_Sleep(200);

    test("Fetch returns on 408: ");
    natsMutex_Lock(args.m);
    args.nc = nc;
    args.sub = sub;
    args.string = "NATS/1.0 408 Request Timeout\r\n\r\n";
    natsMutex_Unlock(args.m);
    start = nats_Now();
    s = natsThread_Create(&t, _sendToPullSub, (void*) &args);
    IFOK(s, natsSubscription_Fetch(&list, sub, 1, 1000, &jerr));
    dur = nats_Now() - start;
    // Since we wait 250ms to publish, it will take aound 250ms
    testCond((s == NATS_TIMEOUT) && (list.Msgs == NULL) && (list.Count == 0) && (jerr == 0)
                && (dur < 500));
    nats_clearLastError();
    natsMsgList_Destroy(&list);

    natsThread_Join(t);
    natsThread_Destroy(t);
    t = NULL;

    test("Fetch gets 503: ");
    natsMutex_Lock(args.m);
    args.string = "NATS/1.0 503\r\n\r\n";
    natsMutex_Unlock(args.m);
    s = natsThread_Create(&t, _sendToPullSub, (void*) &args);
    IFOK(s, natsSubscription_Fetch(&list, sub, 1, 10000, &jerr));
    testCond((s == NATS_NO_RESPONDERS) && (list.Msgs == NULL) && (list.Count == 0) && (jerr == 0));
    nats_clearLastError();
    natsMsg_Destroy(msg);
    msg = NULL;
    natsMsgList_Destroy(&list);

    natsThread_Join(t);
    natsThread_Destroy(t);
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST2";
    sc.Subjects = (const char*[4]){"bar", "baz", "bat", "box"};
    sc.SubjectsLen = 4;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Pull with max waiting: ");
    jsSubOptions_Init(&so);
    so.Config.MaxWaiting = 2;
    s = js_PullSubscribe(&sub, js, "bar", "pullmaxwaiting", NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Fill requests: ");
    // Send requests manually to use the max requests
    s = natsConnection_SubscribeSync(&sub2, nc, "my.pull.cons.inbox1");
    IFOK(s, natsConnection_SubscribeSync(&sub3, nc, "my.pull.cons.inbox2"));
    IFOK(s, natsConnection_PublishRequestString(nc, "$JS.API.CONSUMER.MSG.NEXT.TEST2.pullmaxwaiting", "my.pull.cons.inbox1", "{\"batch\":1,\"expires\":1000000000}"));
    IFOK(s, natsConnection_PublishRequestString(nc, "$JS.API.CONSUMER.MSG.NEXT.TEST2.pullmaxwaiting", "my.pull.cons.inbox1", "{\"batch\":1,\"expires\":1000000000}"));
    testCond(s == NATS_OK);

    test("Max waiting error: ");
    s = natsSubscription_Fetch(&list, sub, 1, 1000, &jerr);
    testCond((s == NATS_LIMIT_REACHED) && (strstr(nats_GetLastError(NULL), "Exceeded") != NULL));
    nats_clearLastError();

    natsSubscription_Destroy(sub2);
    natsSubscription_Destroy(sub3);
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Max request batch: ");
    jsSubOptions_Init(&so);
    so.Config.MaxRequestBatch = 2;
    s = js_PullSubscribe(&sub, js, "baz", "max-request-batch", NULL, &so, &jerr);
    IFOK(s, natsSubscription_Fetch(&list, sub, 10, 1000, &jerr));
    testCond((s != NATS_OK) && (list.Count == 0) && (list.Msgs == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), "MaxRequestBatch of 2") != NULL));
    nats_clearLastError();
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Max request expires: ");
    jsSubOptions_Init(&so);
    so.Config.MaxRequestExpires = NATS_MILLIS_TO_NANOS(50);
    s = js_PullSubscribe(&sub, js, "baz", "max-request-expires", NULL, &so, &jerr);
    IFOK(s, natsSubscription_Fetch(&list, sub, 10, 1000, &jerr));
    testCond((s != NATS_OK) && (list.Count == 0) && (list.Msgs == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), "MaxRequestExpires of 50ms") != NULL));
    nats_clearLastError();

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Ephemeral pull allowed (NULL): ");
    s = js_PullSubscribe(&sub, js, "bar", NULL, NULL, NULL, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Send a message: ");
    s = js_Publish(NULL, js, "bar", "hello", 5, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Msgs received: ");
    s = natsSubscription_Fetch(&list, sub, 1, 1000, &jerr);
    testCond((s == NATS_OK) && (list.Msgs != NULL) && (list.Count == 1) && (jerr == 0));

    natsMsgList_Destroy(&list);
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Ephemeral pull allowed (empty): ");
    s = js_PullSubscribe(&sub, js, "bar", "", NULL, NULL, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Msgs received: ");
    s = natsSubscription_Fetch(&list, sub, 1, 1000, &jerr);
    testCond((s == NATS_OK) && (list.Msgs != NULL) && (list.Count == 1) && (jerr == 0));

    natsMsgList_Destroy(&list);

    test("Fetch returns before 408: ");
    natsConn_setFilter(nc, _dropTimeoutProto);
    start = nats_Now();
    s = natsSubscription_Fetch(&list, sub, 1, 1000, NULL);
    dur = nats_Now() - start;
    testCond((s == NATS_TIMEOUT) && (list.Count == 0) && (dur >= 600));
    nats_clearLastError();

    test("Next Fetch waits for proper timeout: ");
    nats_Sleep(100);
    natsConn_setFilter(nc, NULL);
    natsMutex_Lock(args.m);
    args.nc = nc;
    args.sub = sub;
    args.string = "NATS/1.0 408 Request Timeout\r\n\r\n";
    // Will make the 408 sent to a subject ID with 1 while we are likely at 2 or above.
    // So this will be considered a "late" 408 timeout and should be ignored.
    args.sum = 1;
    natsMutex_Unlock(args.m);
    s = natsThread_Create(&t, _sendToPullSub, (void*) &args);
    start = nats_Now();
    IFOK(s, natsSubscription_Fetch(&list, sub, 1, 1000, NULL));
    dur = nats_Now() - start;
    testCond((s == NATS_TIMEOUT) && (list.Count == 0) && (dur >= 600));
    nats_clearLastError();

    natsThread_Join(t);
    natsThread_Destroy(t);
    t = NULL;

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("jsFetchRequest init bad args: ");
    s = jsFetchRequest_Init(NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Create pull consumer with MaxRequestBytes: ");
    jsSubOptions_Init(&so);
    so.Config.MaxRequestMaxBytes = 1024;
    s = js_PullSubscribe(&sub, js, "bat", "max-request-bytes", NULL, &so, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    jsFetchRequest_Init(&fr);

    test("FetchRequest bad args: ");
    s = natsSubscription_FetchRequest(NULL, sub, &fr);
    if (s == NATS_INVALID_ARG)
        s = natsSubscription_FetchRequest(&list, NULL, &fr);
    if (s == NATS_INVALID_ARG)
        s = natsSubscription_FetchRequest(&list, sub, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("FetchRequest no expiration err: ");
    jsFetchRequest_Init(&fr);
    // If NoWait is false, then Expires must be set.
    fr.Batch = 1;
    s = natsSubscription_FetchRequest(&list, sub, &fr);
    testCond((s == NATS_INVALID_TIMEOUT && (list.Count == 0) && (list.Msgs == NULL)));
    nats_clearLastError();

    test("Batch must be set: ");
    jsFetchRequest_Init(&fr);
    fr.MaxBytes = 100;
    fr.Expires = NATS_SECONDS_TO_NANOS(1);
    s = natsSubscription_FetchRequest(&list, sub, &fr);
    testCond((s == NATS_INVALID_ARG) && (list.Count == 0) && (list.Msgs == NULL));
    nats_clearLastError();

    test("MaxBytes must be > 0: ");
    jsFetchRequest_Init(&fr);
    fr.Batch = 1;
    fr.MaxBytes = -100;
    fr.Expires = NATS_SECONDS_TO_NANOS(1);
    s = natsSubscription_FetchRequest(&list, sub, &fr);
    testCond((s == NATS_INVALID_ARG) && (list.Count == 0) && (list.Msgs == NULL));
    nats_clearLastError();

    test("Requesting more than allowed max bytes: ");
    jsFetchRequest_Init(&fr);
    fr.Batch = 1;
    fr.MaxBytes = 2048;
    fr.Expires = NATS_SECONDS_TO_NANOS(1);
    s = natsSubscription_FetchRequest(&list, sub, &fr);
    testCond((s == NATS_LIMIT_REACHED) && (list.Count == 0) && (list.Msgs == NULL)
                && (strstr(nats_GetLastError(NULL), "Exceeded MaxRequestMaxBytes") != NULL));
    nats_clearLastError();

    test("No concurrent call: ");
    natsMutex_Lock(args.m);
    args.sub = sub;
    args.status = NATS_OK;
    natsMutex_Unlock(args.m);
    s = natsThread_Create(&t, _fetchRequest, (void*) &args);
    if (s == NATS_OK)
    {
        nats_Sleep(250);
        jsFetchRequest_Init(&fr);
        fr.Batch = 1;
        fr.Expires = NATS_SECONDS_TO_NANOS(1);
        s = natsSubscription_FetchRequest(&list, sub, &fr);
    }
    testCond((s == NATS_ERR) && (strstr(nats_GetLastError(NULL), jsErrConcurrentFetchNotAllowed) != NULL));
    nats_clearLastError();
    s = NATS_OK;

    test("Populate: ");
    for (i=0; (s == NATS_OK) && (i<10); i++)
        s = js_PublishAsync(js, "bat", (const void*) "abcdefghij", 10, NULL);
    testCond(s == NATS_OK);

    test("Received ok: ");
    natsThread_Join(t);
    natsMutex_Lock(args.m);
    s = args.status;
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    natsThread_Destroy(t);
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create pull consumer: ");
    s = js_PullSubscribe(&sub, js, "box", "feth-request", NULL, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Populate: ");
    s = js_PublishAsync(js, "box", (const void*) "abcdefghij", 10, NULL);
    testCond(s == NATS_OK);

    test("Check expiration: ");
    // Unlike with the simple fetch, asking for more than is avail will
    // wait until expiration to return.
    jsFetchRequest_Init(&fr);
    fr.Batch = 10;
    fr.Expires = NATS_MILLIS_TO_NANOS(500);
    fr.Heartbeat = NATS_MILLIS_TO_NANOS(50);
    start = nats_Now();
    s = natsSubscription_FetchRequest(&list, sub, &fr);
    dur = nats_Now() - start;
    testCond((s == NATS_OK) && (list.Count == 1) && (list.Msgs != NULL)
                && (dur > 400) && (dur < 600));
    natsMsgList_Destroy(&list);

#if _WIN32
    nats_Sleep(1000);
#endif

    test("Check invalid hb: ");
    jsFetchRequest_Init(&fr);
    fr.Batch = 10;
    fr.Expires = NATS_SECONDS_TO_NANOS(1);
    fr.Heartbeat = NATS_SECONDS_TO_NANOS(10);
    s = natsSubscription_FetchRequest(&list, sub, &fr);
    testCond((s == NATS_ERR) && (strstr(nats_GetLastError(NULL), "too large") != NULL));
    nats_clearLastError();

    test("Check idle hearbeat: ");
    jsFetchRequest_Init(&fr);
    fr.Batch = 10;
    // Let's make it wait for 2 seconds
    fr.Expires = NATS_SECONDS_TO_NANOS(2);
    // And have HBs every 50ms
    fr.Heartbeat = NATS_MILLIS_TO_NANOS(50);
    // Set a message filter that will drop HB messages
    natsConn_setFilter(nc, _dropIdleHBs);
    start = nats_Now();
    // We should be kicked out of the fetch request with an error indicating
    // that we missed hearbeats.
    s = natsSubscription_FetchRequest(&list, sub, &fr);
    dur = nats_Now() - start;
    testCond((s == NATS_MISSED_HEARTBEAT) && (dur < 500));

    natsSubscription_Destroy(sub);
    JS_TEARDOWN;
    _destroyDefaultThreadArgs(&args);
}

static void
_jsPubReconnectThread(void *closure)
{
    jsCtx   *js = (jsCtx*) closure;

    nats_Sleep(5);
    js_Publish(NULL, js, "foo", "hello", 5, NULL, NULL);
    natsConnection_Flush(js->nc);

    natsConnection_Reconnect(js->nc);
    nats_Sleep(500);

    js_Publish(NULL, js, "foo", "hell1", 5, NULL, NULL);
    natsConnection_Flush(js->nc);
}

void test_JetStreamSubscribePull_Reconnect(void)
{
    natsStatus s;
    JS_SETUP(2, 9, 2);

    test("Create stream: ");
    jsErrCode jerr = 0;
    jsStreamConfig sc;
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Subjects = (const char *[1]){"foo"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Subscribe: ");
    natsSubscription *sub = NULL;
    s = js_PullSubscribe(&sub, js, "foo", "dur", NULL, NULL, &jerr);
    testCond(s == NATS_OK);

    test("Start thread to send: ");
    natsThread *t = NULL;
    s = natsThread_Create(&t, _jsPubReconnectThread, (void*) js);
    testCond(s == NATS_OK);

    test("Fetch request succeeds over a reconnect: ");
    jsFetchRequest fr = {
        .Batch = 2,
        .Expires = NATS_SECONDS_TO_NANOS(10),
    };
    natsMsgList list;
    s = natsSubscription_FetchRequest(&list, sub, &fr);
    testCond((s == NATS_OK) && (list.Count == 2));
    natsMsgList_Destroy(&list);

    natsThread_Join(t);
    natsThread_Destroy(t);
    natsSubscription_Destroy(sub);
    JS_TEARDOWN;
}

static void
_recvPullAsync(natsConnection *nc, natsSubscription *sub, natsMsg *msg,
                void *closure)
{
    struct threadArg *arg = (struct threadArg *)closure;

    natsMutex_Lock(arg->m);
    arg->sum++;

    switch (arg->control)
    {
    case 1:
        arg->msgReceived = true;
        natsMsg_Ack(msg, NULL);
        natsCondition_Signal(arg->c);
        break;

    case 2:
        arg->msgReceived = true;
        natsCondition_Signal(arg->c);
        break;

    case 3:
        arg->msgReceived = true;
        natsCondition_Signal(arg->c);
        natsCondition_Wait(arg->c, arg->m);
        break;
    }

    natsMsg_Destroy(msg);
    natsMutex_Unlock(arg->m);
}

static void
_completePullAsync(natsConnection *nc, natsSubscription *sub, natsStatus exitStatus,
                void *closure)
{
    struct threadArg *arg = (struct threadArg *)closure;
    natsMutex_Lock(arg->m);
    arg->closed = true;
    arg->status = exitStatus;
    const char *le = nats_GetLastError(NULL);
    if (le != NULL)
        strncpy(arg->lastErrorBuf, le, sizeof(arg->lastErrorBuf)-1);
    natsCondition_Broadcast(arg->c);
    natsMutex_Unlock(arg->m);
}

static bool
_testBatchCompleted(struct threadArg *args, natsSubscription *sub, natsStatus expectedStatus, int expectedMsgs, bool orFewer)
{
    natsStatus s = NATS_OK;
    natsMutex_Lock(args->m);
    while ((s != NATS_TIMEOUT) && !args->closed)
        s = natsCondition_TimedWait(args->c, args->m, 1000); // 1 second should always be enough

    bool result = ((s == NATS_OK) &&
                   args->closed &&
                   (args->status == expectedStatus));

    if (orFewer)
        result = result && (args->sum <= expectedMsgs);
    else
        result = result && (args->sum == expectedMsgs);
    natsMutex_Unlock(args->m);

    // We may get called before the delivery thread terminates the sub, this
    // yields and avoids the race for the purpose of the test.
    nats_Sleep(1);

    result = result && !natsSubscription_IsValid(sub);
    if (!result)
    {
        if (s != NATS_OK)
            printf("FAILED: timed out: %d\n", s);
        if (!args->closed)
            printf("FAILED: onComplete has not been called\n");
        if (args->status != expectedStatus)
            printf("FAILED: status: %d, expected: %d\n", args->status, expectedStatus);
        if (orFewer)
        {
            if (args->sum > expectedMsgs)
                printf("FAILED: msgs: %d, expected at most: %d\n", args->sum, expectedMsgs);
        }
        else
        {
            if (args->sum != expectedMsgs)
                printf("FAILED: msgs: %d, expected: %d\n", args->sum, expectedMsgs);
        }
        if (natsSubscription_IsValid(sub))
            printf("FAILED: subscription is still valid\n");
    }
    return result;
}

void test_JetStreamSubscribePullAsync(void)
{
    natsStatus s;
    natsSubscription *sub = NULL;
    natsMsg *msg = NULL;
    jsErrCode jerr = 0;
    natsMsgList list;
    jsStreamConfig sc;
    jsOptions jsOpts;
    jsSubOptions so;
    struct threadArg args;
    jsConsumerConfig cc;

    JS_SETUP(2, 9, 2);

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Subjects = (const char *[1]){"foo"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    // TEST various error conditions.

    test("Create pull sub async (invalid args): ");
    s = js_PullSubscribeAsync(NULL, js, "foo", "dur", _recvPullAsync, &args, NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_PullSubscribeAsync(&sub, NULL, "foo", "dur", _recvPullAsync, &args, NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_PullSubscribeAsync(&sub, js, NULL, "dur", _recvPullAsync, &args, NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_PullSubscribeAsync(NULL, js, "", "dur", _recvPullAsync, &args, NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_PullSubscribeAsync(&sub, js, "foo", "dur", NULL, &args, NULL, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0));
    nats_clearLastError();

    test("AckNone ok: ");
    jsSubOptions_Init(&so);
    so.Config.AckPolicy = js_AckNone;
    s = js_PullSubscribeAsync(&sub, js, "foo", "ackNone", _recvPullAsync, &args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));
    natsSubscription_Unsubscribe(sub);
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("AckAll ok: ");
    jsSubOptions_Init(&so);
    so.Config.AckPolicy = js_AckAll;
    s = js_PullSubscribeAsync(&sub, js, "foo", "ackAll", _recvPullAsync, &args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));
    natsSubscription_Unsubscribe(sub);
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Create push consumer: ");
    jsConsumerConfig_Init(&cc);
    cc.Durable = "push_dur";
    cc.DeliverSubject = "push.deliver";
    s = js_AddConsumer(NULL, js, "TEST", &cc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Try create pull sub from push consumer: ");
    s = js_PullSubscribeAsync(&sub, js, "foo", "push_dur", _recvPullAsync, &args, NULL, &so, &jerr);
    testCond((s == NATS_ERR) && (sub == NULL) && (jerr == 0) && (strstr(nats_GetLastError(NULL), jsErrPullSubscribeToPushConsumer) != NULL));
    nats_clearLastError();

    test("Create pull bound failure: ");
    jsSubOptions_Init(&so);
    so.Stream = "TEST";
    so.Consumer = "bar";
    s = js_PullSubscribeAsync(&sub, js, "foo", "bar", _recvPullAsync, &args, NULL, &so, &jerr);
    testCond((s == NATS_NOT_FOUND) && (sub == NULL) && (jerr == JSConsumerNotFoundErr));
    nats_clearLastError();

    // TEST real subscription, auto-ack.

    test("js_PullAsyncSubscribe for real, manual ack: ");
    // Signal as soon as we get the first message.
    args.control = 2; // don't ack

    jsSubOptions_Init(&so);
    so.Config.MaxAckPending = 10;
    so.Config.AckWait = NATS_MILLIS_TO_NANOS(300);
    so.ManualAck = true;
    s = js_PullSubscribeAsync(&sub, js, "foo", "dur", _recvPullAsync, &args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Can't call NextMsg: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_ILLEGAL_STATE) && (msg == NULL));
    nats_clearLastError();

    test("Can't call Fetch: ");
    s = natsSubscription_Fetch(&list, sub, 1, 500, &jerr);
    testCond((s == NATS_ERR) && (msg == NULL) && (strstr(nats_GetLastError(NULL), jsErrConcurrentFetchNotAllowed) != NULL));
    nats_clearLastError();

    int noMessageTimeout = 80;
    int messageArrivesImmediatelyTimeout = 40;
    int ackTimeout = (int)(so.Config.AckWait / 1E6) + 100;
    testf("No messages yet, timeout in %dms: ", noMessageTimeout);
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.msgReceived)
        s = natsCondition_TimedWait(args.c, args.m, noMessageTimeout);
    testCond(s == NATS_TIMEOUT);
    natsMutex_Unlock(args.m);

    test("Send a message: ");
    s = js_Publish(NULL, js, "foo", "hello", 5, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    testf("Arrives in under %dms: ", messageArrivesImmediatelyTimeout);
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.msgReceived)
        s = natsCondition_TimedWait(args.c, args.m, messageArrivesImmediatelyTimeout);
    testCond((s == NATS_OK) && args.msgReceived);
    args.msgReceived = false;
    natsMutex_Unlock(args.m);

    testf("No more messages yet, timeout in %dms: ", noMessageTimeout);
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.msgReceived)
        s = natsCondition_TimedWait(args.c, args.m, noMessageTimeout);
    testCond(s == NATS_TIMEOUT);
    s = NATS_OK;
    args.msgReceived = false;
    args.control = 1; // ack next
    natsMutex_Unlock(args.m);

    testf("Wait another so.Config.AckWait+100 = %dms and see that the message is re-delivered, ACK it: ", ackTimeout);
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.msgReceived)
        s = natsCondition_TimedWait(args.c, args.m, ackTimeout);
    testCond(s == NATS_OK);
    args.msgReceived = false;
    natsMutex_Unlock(args.m);

    testf("Wait another so.Config.AckWait+100 = %dms and see that the message is not re-delivered: ", ackTimeout);
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.msgReceived)
        s = natsCondition_TimedWait(args.c, args.m, ackTimeout);
    testCond((s == NATS_TIMEOUT) && !args.msgReceived);
    s = NATS_OK;
    natsMutex_Unlock(args.m);

    test("Receive msg with header and no data comes through: ");
    s = natsMsg_create(&msg, sub->subject, (int)strlen(sub->subject), NULL, 0,
                       "NATS/1.0\r\nk:v\r\n\r\n", 17, 17);
    IFOK(s, natsConnection_PublishMsg(nc, msg));
    natsMsg_Destroy(msg);

    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.msgReceived)
        s = natsCondition_TimedWait(args.c, args.m, messageArrivesImmediatelyTimeout);
    testCond(s == NATS_OK);
    args.msgReceived = false;
    natsMutex_Unlock(args.m);

    natsSubscription_Destroy(sub);
    sub = NULL;

    // TEST exit criteria.
    typedef struct
    {
        const char *name;
        bool noWait;
        int want;
        int expires; // ms
        int maxBytes;
        int before;
        int during;
        int fetchSize;
        natsStatus expectedStatus;
        int expectedN;
        bool orFewer;
    } nowaitTC_t;
    nowaitTC_t nowaitTCs[] = {
        {
            .name = "single fetch NOWAIT, partially fulfilled NATS_TIMEOUT",
            .noWait = true,
            .want = 1000,
            .before = 2,
            .fetchSize = 13,
            .expectedStatus = NATS_TIMEOUT,
            .expectedN = 2,
        },
        {
            .name = "multi-fetch NOWAIT, partially fulfilled NATS_TIMEOUT",
            .noWait = true,
            .want = 1000,
            .before = 117,
            .fetchSize = 20, // 117 is not divisible by 20
            .expectedStatus = NATS_TIMEOUT,
            .expectedN = 117,
        },
        {
            .name = "fetch NOWAIT can get a NATS_NOT_FOUND immediately",
            .noWait = true,
            .want = 1000,
            .before = 0,
            .fetchSize = 5,
            .expectedStatus = NATS_NOT_FOUND, // on the second request, since there are no messages waiting.
            .expectedN = 0,
        },
        {
            .name = "fetch NOWAIT can get a NATS_NOT_FOUND on Nth request",
            .noWait = true,
            .want = 1000,
            .before = 10,
            .fetchSize = 5,
            .expectedStatus = NATS_NOT_FOUND, // on the second request, since there are no messages waiting.
            .expectedN = 10,
        },
        {
            .name = "single fetch NOWAIT, fulfilled msgs NATS_MAX_DELIVERED_MSGS",
            .noWait = true,
            .want = 2,
            .before = 20,
            .fetchSize = 13,
            .expectedStatus = NATS_MAX_DELIVERED_MSGS,
            .expectedN = 2,
        },
        {
            .name = "multi-fetch NOWAIT, fulfilled msgs NATS_MAX_DELIVERED_MSGS",
            .noWait = true,
            .want = 100,
            .before = 117,
            .fetchSize = 13,
            .expectedStatus = NATS_MAX_DELIVERED_MSGS,
            .expectedN = 100,
        },
        {
            .name = "single fetch, fulfilled msgs NATS_MAX_DELIVERED_MSGS",
            .want = 2,
            .before = 20,
            .fetchSize = 13,
            .expectedStatus = NATS_MAX_DELIVERED_MSGS,
            .expectedN = 2,
        },
        {
            .name = "multi-fetch WAIT, fulfilled msgs NATS_MAX_DELIVERED_MSGS",
            .want = 100,
            .before = 117,
            .fetchSize = 13,
            .expectedStatus = NATS_MAX_DELIVERED_MSGS,
            .expectedN = 100,
        },
        {
            .name = "MaxBytes",
            .want = 1000,
            .maxBytes = 100,
            .before = 20,
            .expectedStatus = NATS_LIMIT_REACHED,
            .expectedN = 1,
        },
        {
            .name = "Fetch with expiration is fulfilled NATS_MAX_DELIVERED_MSGS",
            .want = 30,
            .expires = 100,
            .before = 20,
            .during = 10,
            .expectedStatus = NATS_MAX_DELIVERED_MSGS,
            .expectedN = 30,
        },
        {
            .name = "Fetch with a short expiration is partially fulfilled NATS_TIMEOUT",
            .fetchSize = 11, // just to slow things down
            .want = 200,
            .expires = 5,
            .during = 200,
            .expectedStatus = NATS_TIMEOUT,
            .expectedN = 200,
            .orFewer = true,
        },
        {
            .name = NULL,
        },
    };
    for (nowaitTC_t *tc = nowaitTCs; tc->name != NULL; tc++)
    {
        for (int i = 0; (s == NATS_OK) && (i < tc->before); i++)
            s = js_Publish(NULL, js, "foo", "hello", 5, NULL, &jerr);
        if (s != NATS_OK)
            FAIL("Failed to publish, unusual");

        natsMutex_Lock(args.m);
        args.control = 0;      // don't ack, will be auto-ack
        args.status = NATS_OK; // batch exit status will be here
        args.msgReceived = false;
        args.closed = false;
        args.sum = 0;
        natsMutex_Unlock(args.m);

        jsSubOptions_Init(&so);
        so.Config.MaxAckPending = 10;
        so.Config.AckWait = NATS_MILLIS_TO_NANOS(300);
        so.Config.MaxRequestMaxBytes = 777;
        so.ManualAck = false;

        jsOptions_Init(&jsOpts);
        jsOpts.PullSubscribeAsync.CompleteHandler = _completePullAsync;
        jsOpts.PullSubscribeAsync.CompleteHandlerClosure = &args;
        jsOpts.PullSubscribeAsync.FetchSize = tc->fetchSize;
        jsOpts.PullSubscribeAsync.MaxMessages = tc->want;
        jsOpts.PullSubscribeAsync.MaxBytes = tc->maxBytes;
        jsOpts.PullSubscribeAsync.NoWait = tc->noWait;
        jsOpts.PullSubscribeAsync.Timeout = tc->expires;
        s = js_PullSubscribeAsync(&sub, js, "foo", "dur", _recvPullAsync, &args, &jsOpts, &so, &jerr);
        if ((s != NATS_OK) && (sub == NULL) && (jerr != 0))
            FAIL("Failed to create pull subscription, unusual");

        for (int i = 0; (s == NATS_OK) && (i < tc->during); i++)
            s = js_Publish(NULL, js, "foo", "hello", 5, NULL, &jerr);
        if (s != NATS_OK)
            FAIL("Failed to publish, unusual");

        testf("%s: ", tc->name);
        testCond(_testBatchCompleted(
            &args, sub, tc->expectedStatus, tc->expectedN, tc->orFewer));

        natsSubscription_Destroy(sub);
        sub = NULL;
    }

    JS_TEARDOWN;
    _destroyDefaultThreadArgs(&args);
}

void test_JetStreamSubscribePullAsync_MissedHB(void)
{
    natsStatus s;
    natsSubscription *sub = NULL;
    jsErrCode jerr = 0;
    jsStreamConfig sc;
    jsOptions jsOpts;
    struct threadArg args;

    JS_SETUP(2, 9, 2);

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Subjects = (const char *[1]){"foo"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Check invalid heartbeat : ");
    natsMutex_Lock(args.m);
    args.control = 0;      // don't ack, will be auto-ack
    args.status = NATS_OK; // batch exit status will be here
    args.msgReceived = false;
    args.closed = false;
    args.sum = 0;
    natsMutex_Unlock(args.m);

    // Heartbeat too large for the timeout
    jsOptions_Init(&jsOpts);
    jsOpts.PullSubscribeAsync.CompleteHandler = _completePullAsync;
    jsOpts.PullSubscribeAsync.CompleteHandlerClosure = &args;
    jsOpts.PullSubscribeAsync.MaxMessages = 100;
    jsOpts.PullSubscribeAsync.Timeout = 100;
    jsOpts.PullSubscribeAsync.Heartbeat = 200;

    s = js_PullSubscribeAsync(&sub, js, "foo", "dur", _recvPullAsync, &args, &jsOpts, NULL, &jerr);
    testCond((s == NATS_OK) && _testBatchCompleted(&args, sub, NATS_ERR, 0, false));

    test("Check the error to be 'heartbeat value too large': ");
    natsMutex_Lock(args.m);
    testCond(strstr(args.lastErrorBuf, "heartbeat value too large") != NULL);
    natsMutex_Unlock(args.m);
    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Subscribe with 50ms idle hearbeat: ");
    natsMutex_Lock(args.m);
    args.control = 0;      // don't ack, will be auto-ack
    args.status = NATS_OK; // batch exit status will be here
    args.msgReceived = false;
    args.closed = false;
    args.sum = 0;
    natsMutex_Unlock(args.m);

    // Let's make it wait for 20 seconds, and have HBs every 50ms
    jsOptions_Init(&jsOpts);
    jsOpts.PullSubscribeAsync.CompleteHandler = _completePullAsync;
    jsOpts.PullSubscribeAsync.CompleteHandlerClosure = &args;
    jsOpts.PullSubscribeAsync.MaxMessages = 100;
    jsOpts.PullSubscribeAsync.Timeout = 20 * 1000;
    jsOpts.PullSubscribeAsync.Heartbeat = 50;

    s = js_PullSubscribeAsync(&sub, js, "foo", "dur", _recvPullAsync, &args, &jsOpts, NULL, &jerr);
    testCond(s == NATS_OK);

    test("Subscription is active after 2 heartbeats: ");
    nats_Sleep(100);
    natsMutex_Lock(args.m);
    testCond(!args.closed)
    natsMutex_Unlock(args.m);

    // Set a message filter that will drop subsequent server's heartbeat
    // messages.
    test("Drop heartbeats and see the sub terminate: ");
    natsConn_setFilter(nc, _dropIdleHBs);
    testCond((s == NATS_OK) &&
             _testBatchCompleted(&args, sub,NATS_MISSED_HEARTBEAT, 0, false));

    natsSubscription_Destroy(sub);
    JS_TEARDOWN;
    _destroyDefaultThreadArgs(&args);
}

void test_JetStreamSubscribePullAsync_Unsubscribe(void)
{
    natsStatus s;
    natsSubscription *sub = NULL;
    jsErrCode jerr = 0;
    jsStreamConfig sc;
    jsOptions jsOpts;
    struct threadArg args;

    JS_SETUP(2, 9, 2);

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Subjects = (const char *[1]){"foo"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Publish a few messages: ");
    s = js_Publish(NULL, js, "foo", "hell0", 5, NULL, &jerr);
    if (s == NATS_OK)
        s = js_Publish(NULL, js, "foo", "hell1", 5, NULL, &jerr);
    if (s == NATS_OK)
        s = js_Publish(NULL, js, "foo", "hell2", 5, NULL, &jerr);
    testCond(s == NATS_OK);

    test("Subscribe: ");
    natsMutex_Lock(args.m);
    args.control = 3;      // wait for signal back
    args.status = NATS_OK; // batch exit status will be here
    args.msgReceived = false;
    args.closed = false;
    args.sum = 0;
    natsMutex_Unlock(args.m);

    jsOptions_Init(&jsOpts);
    jsOpts.PullSubscribeAsync.CompleteHandler = _completePullAsync;
    jsOpts.PullSubscribeAsync.CompleteHandlerClosure = &args;
    jsOpts.PullSubscribeAsync.MaxMessages = 100;
    jsOpts.PullSubscribeAsync.Timeout = 1000;

    s = js_PullSubscribeAsync(&sub, js, "foo", "dur", _recvPullAsync, &args, &jsOpts, NULL, &jerr);
    testCond(s == NATS_OK);

    test("Receive 1 message: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.msgReceived)
        s = natsCondition_TimedWait(args.c, args.m, 100);
    testCond((s == NATS_OK) && args.msgReceived && (args.closed == false));
    args.msgReceived = false;

    test("Unsubscribe: ");
    s = natsSubscription_Unsubscribe(sub);
    testCond(s == NATS_OK);
    natsCondition_Signal(args.c); // release the callback
    natsMutex_Unlock(args.m);

    testCond((s == NATS_OK) &&
             _testBatchCompleted(&args, sub, NATS_OK, 1, false));

    natsSubscription_Destroy(sub);
    sub = NULL;
    JS_TEARDOWN;
    _destroyDefaultThreadArgs(&args);
}

void test_JetStreamSubscribePullAsync_Reconnect(void)
{
    natsStatus s;
    JS_SETUP(2, 9, 2);

    test("Create stream: ");
    jsErrCode jerr = 0;
    jsStreamConfig sc;
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Subjects = (const char *[1]){"foo"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Subscribe, expect 3 messsages, fetch size 2: ");
    natsSubscription *sub = NULL;
    jsOptions jsOpts;
    struct threadArg args;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    natsMutex_Lock(args.m);
    args.control = 2;      // no ack
    args.status = NATS_OK; // batch exit status will be here
    natsMutex_Unlock(args.m);

    jsOptions_Init(&jsOpts);
    jsOpts.PullSubscribeAsync.CompleteHandler = _completePullAsync;
    jsOpts.PullSubscribeAsync.CompleteHandlerClosure = &args;
    jsOpts.PullSubscribeAsync.FetchSize = 2;
    jsOpts.PullSubscribeAsync.MaxMessages = 3;
    jsOpts.PullSubscribeAsync.Timeout = 10 * 1000;

    s = js_PullSubscribeAsync(&sub, js, "foo", "dur", _recvPullAsync, &args, &jsOpts, NULL, &jerr);
    testCond(s == NATS_OK);

    test("Send 1 message: ");
    s = js_Publish(NULL, js, "foo", "hell0", 5, NULL, &jerr);
    testCond(s == NATS_OK);

    test("Receive 1 message: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.msgReceived)
        s = natsCondition_TimedWait(args.c, args.m, 20);
    testCond((s == NATS_OK) && args.msgReceived && (args.closed == false));
    args.msgReceived = false;
    natsMutex_Unlock(args.m);

    test("Reconnect: ");
    s = natsConnection_Flush(nc);
    if (s == NATS_OK)
        s = natsConnection_Reconnect(nc);
    nats_Sleep(100);
    testCond(s == NATS_OK);

    test("Send 2 more messages: ");
    s = js_Publish(NULL, js, "foo", "hell1", 5, NULL, &jerr);
    if (s == NATS_OK)
        s = js_Publish(NULL, js, "foo", "hell2", 5, NULL, &jerr);
    testCond(s == NATS_OK);

    test("Receive all expected messages: ");
    testCond(_testBatchCompleted(&args, sub, NATS_MAX_DELIVERED_MSGS, 3, false));

    natsSubscription_Destroy(sub);
    JS_TEARDOWN;
    _destroyDefaultThreadArgs(&args);
}

void test_JetStreamSubscribePullAsync_Disconnect(void)
{
    natsStatus s;
    natsOptions *opts = NULL;
    s = natsOptions_Create(&opts);
    if (s != NATS_OK)
        FAIL("Unable to create options");
    natsOptions_SetAllowReconnect(opts, false);
    JS_SETUP_WITH_OPTS(2, 9, 2, opts);

    test("Create stream: ");
    jsErrCode jerr = 0;
    jsStreamConfig sc;
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Subjects = (const char *[1]){"foo"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Subscribe, expect 2 messsages, fetch size 2: ");
    natsSubscription *sub = NULL;
    jsOptions jsOpts;
    struct threadArg args;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    natsMutex_Lock(args.m);
    args.control = 2;      // no ack
    args.status = NATS_OK; // batch exit status will be here
    natsMutex_Unlock(args.m);

    jsOptions_Init(&jsOpts);
    jsOpts.PullSubscribeAsync.CompleteHandler = _completePullAsync;
    jsOpts.PullSubscribeAsync.CompleteHandlerClosure = &args;
    jsOpts.PullSubscribeAsync.FetchSize = 2;
    jsOpts.PullSubscribeAsync.MaxMessages = 2;
    jsOpts.PullSubscribeAsync.Timeout = 10 * 1000;

    s = js_PullSubscribeAsync(&sub, js, "foo", "dur", _recvPullAsync, &args, &jsOpts, NULL, &jerr);
    testCond(s == NATS_OK);

    test("Send 1 message: ");
    s = js_Publish(NULL, js, "foo", "hell0", 5, NULL, &jerr);
    testCond(s == NATS_OK);

    test("Receive 1 message: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.msgReceived)
        s = natsCondition_TimedWait(args.c, args.m, 20);
    testCond((s == NATS_OK) && args.msgReceived && (args.closed == false));
    args.msgReceived = false;
    natsMutex_Unlock(args.m);

    test("Disconnect: ");
    natsConn_Lock(nc);
    natsSock_Shutdown(nc->sockCtx.fd);
    natsConn_Unlock(nc);
    testCond(s == NATS_OK);

    test("Check fetch completion, expect NATS_OK: ");
    testCond(_testBatchCompleted(&args, sub, NATS_CONNECTION_CLOSED, 1, false));

    natsSubscription_Destroy(sub);
    JS_TEARDOWN;
    _destroyDefaultThreadArgs(&args);
    natsOptions_Destroy(opts);
}

void test_JetStreamSubscribeHeadersOnly(void)
{
    natsStatus          s;
    natsSubscription    *sub= NULL;
    natsMsg             *msg = NULL;
    jsErrCode           jerr= 0;
    jsStreamConfig      sc;
    jsSubOptions        so;
    int                 i;

    JS_SETUP(2, 6, 2);

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "S";
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Populate: ");
    for (i=0; (s == NATS_OK) && (i<10); i++)
    {
        s = natsMsg_Create(&msg, "S", NULL, "hello", 5);
        IFOK(s, natsMsgHeader_Set(msg, "User-Header", "MyValue"));
        IFOK(s, js_PublishMsg(NULL, js, msg, NULL, NULL));
        natsMsg_Destroy(msg);
        msg = NULL;
    }
    testCond(s == NATS_OK);

    test("Create consumer with headers only: ");
    jsSubOptions_Init(&so);
    so.Config.HeadersOnly = true;
    s = js_SubscribeSync(&sub, js, "S", NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Verify only headers: ");
    for (i=0; (s == NATS_OK) && (i<10); i++)
    {
        const char *val = NULL;
        s = natsSubscription_NextMsg(&msg, sub, 1000);
        IFOK(s, (natsMsg_GetDataLength(msg) == 0 ? NATS_OK : NATS_ERR));
        IFOK(s, natsMsgHeader_Get(msg, "User-Header", &val));
        IFOK(s, (strcmp(val, "MyValue") == 0 ? NATS_OK : NATS_ERR));
        IFOK(s, natsMsgHeader_Get(msg, JSMsgSize, &val));
        IFOK(s, (strcmp(val, "5") == 0 ? NATS_OK : NATS_ERR));
        natsMsg_Destroy(msg);
        msg = NULL;
    }
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    jsCtx_Destroy(js);
    natsConnection_Destroy(nc);
    _stopServer(pid);
    rmtree(datastore);
}

static void
_orderedConsCB(natsConnection *nc, natsSubscription *sub, natsMsg *msg, void *closure)
{
    struct threadArg *args = (struct threadArg*) closure;

    natsMutex_Lock(args->m);
    if (natsMsg_GetDataLength(msg) == 0)
    {
        args->done = true;
        natsCondition_Signal(args->c);
    }
    else
    {
        natsBuf_Append(args->buf, natsMsg_GetData(msg), natsMsg_GetDataLength(msg));
        args->sum++;
    }
    natsMutex_Unlock(args->m);
    natsMsg_Destroy(msg);
}

static natsStatus
_testOrderedCons(jsCtx *js, jsStreamInfo *si, char *asset, int assetLen, struct threadArg *args, bool async)
{
    natsStatus          s    = NATS_OK;
    natsSubscription    *sub = NULL;
    int                 received = 0;
    jsSubOptions        so;

    jsSubOptions_Init(&so);
    so.Ordered = true;
    so.Config.Heartbeat = NATS_MILLIS_TO_NANOS(250);

    s = natsBuf_Create(&args->buf, assetLen);
    if ((s == NATS_OK) && async)
    {
        s = js_Subscribe(&sub, js, "a", _orderedConsCB, (void*) args, NULL, &so, NULL);
        if (s == NATS_OK)
        {
            natsMutex_Lock(args->m);
            while ((s != NATS_TIMEOUT) && !args->done)
                s = natsCondition_TimedWait(args->c, args->m, 5000);
            received = args->sum;
            natsMutex_Unlock(args->m);
        }
    }
    else if (s == NATS_OK)
    {
        s = js_SubscribeSync(&sub, js, "a", NULL, &so, NULL);
        if (s == NATS_OK)
        {
            bool    done  = false;
            int64_t start = 0;
            natsMsg *msg  = NULL;

            start = nats_Now();
            while ((s == NATS_OK) && !done)
            {
                s = natsSubscription_NextMsg(&msg, sub, 5000);
                if (s == NATS_OK)
                {
                    done = (natsMsg_GetDataLength(msg) == 0 ? true : false);
                    if (!done)
                    {
                        s = natsBuf_Append(args->buf, natsMsg_GetData(msg), natsMsg_GetDataLength(msg));
                        received++;
                    }
                    natsMsg_Destroy(msg);
                    msg = NULL;
                }
                if ((s == NATS_OK) && (nats_Now() - start > 5500))
                    s = NATS_TIMEOUT;
            }
        }
    }
    if ((s == NATS_OK) && (natsBuf_Len(args->buf) != assetLen))
    {
        fprintf(stderr, "\nAsset length (%d) does not match received data length (%d)\n",
                assetLen, natsBuf_Len(args->buf));
        s = NATS_MISMATCH;
    }
    else if (s == NATS_OK)
    {
        int     i;
        char    *data = natsBuf_Data(args->buf);

        for (i=0; i<assetLen; i++)
        {
            if (data[i] != asset[i])
            {
                fprintf(stderr, "\nAsset does not match received data:\nAsset=\n%s\nData=\n%s\n",
                        asset, data);
                s = NATS_MISMATCH;
                break;
            }
        }
    }
    else if (s == NATS_TIMEOUT)
        fprintf(stderr, "\nReceived %d chunks out of %d\n", received, (int) (si->State.Msgs-1));

    fflush(stderr);
    natsSubscription_Destroy(sub);
    natsMutex_Lock(args->m);
    natsBuf_Destroy(args->buf);
    args->buf = NULL;
    args->sum = 0;
    args->done = false;
    natsMutex_Unlock(args->m);

    return s;
}

static void
_singleLoss(natsConnection *nc, natsMsg **msg, void* closure)
{
    const char  *val = NULL;
    int         res  = rand() % 100;

    if ((res <= 10) && (natsMsgHeader_Get(*msg, "data", &val) == NATS_OK))
    {
        natsMsg_Destroy(*msg);
        *msg = NULL;
        natsConn_setFilter(nc, NULL);
    }
}

static void
_multiLoss(natsConnection *nc, natsMsg **msg, void* closure)
{
    const char  *val = NULL;
    int         res  = rand() % 100;

    if ((res <= 10) && (natsMsgHeader_Get(*msg, "data", &val) == NATS_OK))
    {
        natsMsg_Destroy(*msg);
        *msg = NULL;
    }
}

static void
_firstOnlyLoss(natsConnection *nc, natsMsg **msg, void* closure)
{
    jsMsgMetaData *meta = NULL;

    if (natsMsg_GetMetaData(&meta, *msg) == NATS_OK)
    {
        if (meta->Sequence.Consumer == 1)
        {
            natsMsg_Destroy(*msg);
            *msg = NULL;
            natsConn_setFilter(nc, NULL);
        }
        jsMsgMetaData_Destroy(meta);
    }
}

static void
_lastOnlyLoss(natsConnection *nc, natsMsg **msg, void* closure)
{
    jsMsgMetaData   *meta = NULL;
    jsStreamInfo    *si   = (jsStreamInfo*) closure;

    if (natsMsg_GetMetaData(&meta, *msg) == NATS_OK)
    {
        if (meta->Sequence.Stream == si->State.LastSeq-1)
        {
            natsMsg_Destroy(*msg);
            *msg = NULL;
            natsConn_setFilter(nc, NULL);
        }
        jsMsgMetaData_Destroy(meta);
    }
}

void test_JetStreamOrderedConsumer(void)
{
    natsStatus          s;
    natsSubscription    *sub= NULL;
    jsErrCode           jerr= 0;
    jsStreamConfig      sc;
    jsSubOptions        so;
    struct threadArg    args;
    int                 i;
    char                *asset = NULL;
    int                 assetLen = 1024*1024;
    const int           chunkSize = 1024;
    jsStreamInfo        *si = NULL;
    jsConsumerInfo      *ci1 = NULL;
    jsConsumerInfo      *ci2 = NULL;

    JS_SETUP(2, 3, 3);

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "OBJECT";
    sc.Subjects = (const char*[1]){"a"};
    sc.SubjectsLen = 1;
    sc.Storage = js_MemoryStorage;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    // Create a sample asset.
    asset = malloc(assetLen);
    for (i=0; i<assetLen; i++)
        asset[i] = (rand() % 26) + 'A';

    test("Send chunks: ");
    for (i=0; ((s == NATS_OK) && (i<assetLen)); i+=chunkSize)
    {
        char    *chunk = asset+i;
        int     csize  = (assetLen-i <= chunkSize ? assetLen-i : chunkSize);
        natsMsg *msg   = NULL;

        s = natsMsg_Create(&msg, "a", NULL, (const void*) chunk, csize);
        IFOK(s, natsMsgHeader_Set(msg, "data", "true"));
        IFOK(s, js_PublishMsgAsync(js, &msg, NULL));
        natsMsg_Destroy(msg);
        msg = NULL;
    }
    IFOK(s, js_PublishAsync(js, "a", NULL, 0, NULL));
    testCond(s == NATS_OK);

    test("Wait for complete: ");
    s = js_PublishAsyncComplete(js, NULL);
    testCond(s == NATS_OK);

    // Do some tests on simple misconfigurations first.
    // For ordered delivery a couple of things need to be set properly.
    // Can't be durable or have ack policy that is not ack none or max deliver set.
    test("Ordered consumer, no durable: ");
    jsSubOptions_Init(&so);
    so.Ordered = true;
    so.Config.Durable = "dlc";
    s = js_SubscribeSync(&sub, js, "a", NULL, &so, &jerr);
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrOrderedConsNoDurable) != NULL));
    nats_clearLastError();

    test("Ordered consumer, ack explicit: ");
    jsSubOptions_Init(&so);
    so.Ordered = true;
    so.Config.AckPolicy = js_AckExplicit;
    s = js_SubscribeSync(&sub, js, "a", NULL, &so, &jerr);
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrOrderedConsNoAckPolicy) != NULL));
    nats_clearLastError();

    test("Ordered consumer, max deliver: ");
    jsSubOptions_Init(&so);
    so.Ordered = true;
    so.Config.MaxDeliver = 10;
    s = js_SubscribeSync(&sub, js, "a", NULL, &so, &jerr);
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrOrderedConsNoMaxDeliver) != NULL));
    nats_clearLastError();

    test("Ordered consumer, no deliver subject: ");
    jsSubOptions_Init(&so);
    so.Ordered = true;
    so.Config.DeliverSubject = "not.allowed";
    s = js_SubscribeSync(&sub, js, "a", NULL, &so, &jerr);
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrOrderedConsNoDeliverSubject) != NULL));
    nats_clearLastError();

    test("Ordered consumer, no queue: ");
    jsSubOptions_Init(&so);
    so.Ordered = true;
    so.Queue = "queue";
    s = js_SubscribeSync(&sub, js, "a", NULL, &so, &jerr);
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrOrderedConsNoQueue) != NULL));
    nats_clearLastError();

    test("Ordered consumer, no consumer: ");
    jsSubOptions_Init(&so);
    so.Ordered = true;
    so.Consumer = "consumer";
    s = js_SubscribeSync(&sub, js, "a", NULL, &so, &jerr);
    if (s == NATS_INVALID_ARG)
    {
        so.Stream = "OBJECT";
        s = js_SubscribeSync(&sub, js, "a", NULL, &so, &jerr);
    }
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrOrderedConsNoBind) != NULL));
    nats_clearLastError();

    test("Ordered consumer, no pull mode: ");
    jsSubOptions_Init(&so);
    so.Ordered = true;
    s = js_PullSubscribe(&sub, js, "a", "dur", NULL, &so, &jerr);
    testCond((s == NATS_INVALID_ARG) && (sub == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrOrderedConsNoPullMode) != NULL));
    nats_clearLastError();

    test("Get stream info: ");
    s = js_GetStreamInfo(&si, js, "OBJECT", NULL, &jerr);
    testCond((s == NATS_OK) && (si != NULL) && (jerr == 0));

    test("Test async consumer: ");
    s = _testOrderedCons(js, si, asset, assetLen, &args, true);
    testCond(s == NATS_OK);

    test("Test sync consumer: ");
    s = _testOrderedCons(js, si, asset, assetLen, &args, false);
    testCond(s == NATS_OK);

    test("Test with single loss (async): ");
    natsConn_setFilter(nc, _singleLoss);
    s = _testOrderedCons(js, si, asset, assetLen, &args, true);
    testCond(s == NATS_OK);

    test("Test with single loss (sync): ");
    natsConn_setFilter(nc, _singleLoss);
    s = _testOrderedCons(js, si, asset, assetLen, &args, false);
    testCond(s == NATS_OK);

    test("Test with multi loss (async): ");
    natsConn_setFilter(nc, _multiLoss);
    s = _testOrderedCons(js, si, asset, assetLen, &args, true);
    testCond(s == NATS_OK);

    test("Test with multi loss (sync): ");
    natsConn_setFilter(nc, _multiLoss);
    s = _testOrderedCons(js, si, asset, assetLen, &args, false);
    testCond(s == NATS_OK);

    test("Test with first msg loss (async): ");
    natsConn_setFilter(nc, _firstOnlyLoss);
    s = _testOrderedCons(js, si, asset, assetLen, &args, true);
    testCond(s == NATS_OK);

    test("Test with first msg loss (sync): ");
    natsConn_setFilter(nc, _firstOnlyLoss);
    s = _testOrderedCons(js, si, asset, assetLen, &args, false);
    testCond(s == NATS_OK);

    test("Test with last msg loss (async): ");
    natsConn_setFilterWithClosure(nc, _lastOnlyLoss, (void*) si);
    s = _testOrderedCons(js, si, asset, assetLen, &args, true);
    testCond(s == NATS_OK);

    test("Test with last msg loss (sync): ");
    natsConn_setFilterWithClosure(nc, _lastOnlyLoss, (void*) si);
    s = _testOrderedCons(js, si, asset, assetLen, &args, false);
    testCond(s == NATS_OK);

    // A bug was causing the ordered consumer to be recreated at
    // each hearbeat. Use a low hearbeat and check that name
    // does not change after some HB intervals.
    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TESTNAME";
    sc.Subjects = (const char*[1]){"b"};
    sc.SubjectsLen = 1;
    sc.Storage = js_MemoryStorage;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Send 1 message: ");
    s = js_Publish(NULL, js, "b", "hello", 5, NULL, NULL);
    testCond(s == NATS_OK);

    test("Create ordered cons with small HB: ");
    jsSubOptions_Init(&so);
    so.Ordered = true;
    so.Config.Heartbeat = NATS_MILLIS_TO_NANOS(100);
    s = js_SubscribeSync(&sub, js, "b", NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Get name: ");
    s = natsSubscription_GetConsumerInfo(&ci1, sub, NULL, NULL);
    testCond((s == NATS_OK) && (ci1 != NULL));

    nats_Sleep(300);

    test("Send 1 message: ");
    s = js_Publish(NULL, js, "b", "hello", 5, NULL, NULL);
    testCond(s == NATS_OK);

    nats_Sleep(300);

    test("Check name does not change: ");
    s = natsSubscription_GetConsumerInfo(&ci2, sub, NULL, NULL);
    testCond((s == NATS_OK) && (ci2 != NULL)
                && (strcmp(ci1->Name, ci2->Name) == 0));
    jsConsumerInfo_Destroy(ci1);
    jsConsumerInfo_Destroy(ci2);

    free(asset);
    jsStreamInfo_Destroy(si);
    natsSubscription_Destroy(sub);
    _destroyDefaultThreadArgs(&args);
    JS_TEARDOWN;
}

static void
_jsOrderedErrHandler(natsConnection *nc, natsSubscription *subscription, natsStatus err, void *closure)
{
    struct threadArg    *args = (struct threadArg*) closure;

    if (err != NATS_MISSED_HEARTBEAT)
        return;

    natsMutex_Lock(args->m);
    args->status = err;
    natsCondition_Signal(args->c);
    natsMutex_Unlock(args->m);
}

void test_JetStreamOrderedConsumerWithErrors(void)
{
    natsStatus          s;
    natsConnection      *nc = NULL;
    natsOptions         *opts = NULL;
    natsSubscription    *sub= NULL;
    jsCtx               *js = NULL;
    natsPid             pid = NATS_INVALID_PID;
    jsErrCode           jerr= 0;
    char                datastore[256] = {'\0'};
    char                cmdLine[1024] = {'\0'};
    jsStreamConfig      sc;
    jsSubOptions        so;
    int                 i, iter;
    char                *asset = NULL;
    int                 assetLen = 128*1024;
    const int           chunkSize = 256;
    struct threadArg    args;

    ENSURE_JS_VERSION(2, 3, 3);

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    _makeUniqueDir(datastore, sizeof(datastore), "datastore_");

    test("Start JS server: ");
    snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s", datastore);
    pid = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(pid);
    testCond(true);

    test("Connect: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetErrorHandler(opts, _jsOrderedErrHandler, (void*) &args));
    IFOK(s, natsConnection_Connect(&nc, opts));
    testCond(s == NATS_OK);

    test("Get context: ");
    s = natsConnection_JetStream(&js, nc, NULL);
    testCond(s == NATS_OK);

    // Create a sample asset.
    asset = malloc(assetLen);
    for (i=0; i<assetLen; i++)
        asset[i] = (rand() % 26) + 'A';

    for (iter=0; iter<2; iter++)
    {
        test("Create stream: ");
        jsStreamConfig_Init(&sc);
        sc.Name = "OBJECT";
        sc.Subjects = (const char*[1]){"a"};
        sc.SubjectsLen = 1;
        sc.Storage = js_MemoryStorage;
        s = js_AddStream(NULL, js, &sc, NULL, &jerr);
        testCond((s == NATS_OK) && (jerr == 0));

        test("Send chunks: ");
        for (i=0; ((s == NATS_OK) && (i<assetLen)); i+=chunkSize)
        {
            char    *chunk = asset+i;
            int     csize  = (assetLen-i <= chunkSize ? assetLen-i : chunkSize);

            s = js_PublishAsync(js, "a", chunk, csize, NULL);
        }
        testCond(s == NATS_OK);

        test("Wait for complete: ");
        s = js_PublishAsyncComplete(js, NULL);
        testCond(s == NATS_OK);

        test("Create ordered sub: ");
        jsSubOptions_Init(&so);
        so.Ordered = true;
        so.Config.Heartbeat = NATS_MILLIS_TO_NANOS(200);
        s = js_SubscribeSync(&sub, js, "a", NULL, &so, &jerr);
        testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

        // Since we are sync we will be paused here due to flow control.
        nats_Sleep(100);

        test("Delete asset: ");
        if (iter == 0)
        {
            s = js_DeleteStream(js, "OBJECT", NULL, &jerr);
        }
        else
        {
            char *cons = NULL;

            natsSub_Lock(sub);
            cons = sub->jsi->consumer;
            natsSub_Unlock(sub);
            s = js_DeleteConsumer(js, "OBJECT", cons, NULL, &jerr);
        }
        testCond((s == NATS_OK) && (jerr == 0));

        test("Check we get error: ");
        natsMutex_Lock(args.m);
        while ((s != NATS_TIMEOUT) && (args.status != NATS_MISSED_HEARTBEAT))
            s = natsCondition_TimedWait(args.c, args.m, 1000);
        args.status = NATS_OK;
        natsMutex_Unlock(args.m);
        testCond(s == NATS_OK);

        natsSubscription_Destroy(sub);
        sub = NULL;
    }

    free(asset);
    JS_TEARDOWN;
    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&args);
}

static void
_dropMsgFive(natsConnection *nc, natsMsg **msg, void* closure)
{
    const char          *val  = NULL;
    struct threadArg    *args = (struct threadArg*) closure;

    if (natsMsgHeader_Get(*msg, "data", &val) == NATS_OK)
    {
        if (++(args->results[0]) == 5)
        {
            natsMsg_Destroy(*msg);
            *msg = NULL;
            natsConn_setFilter(nc, NULL);
        }
    }
}

void test_JetStreamOrderedConsumerWithAutoUnsub(void)
{
    natsStatus          s;
    natsConnection      *nc2= NULL;
    natsSubscription    *sub= NULL;
    jsCtx               *js2= NULL;
    jsErrCode           jerr= 0;
    jsStreamConfig      sc;
    jsSubOptions        so;
    struct threadArg    args;
    int                 i;
    natsStatistics      stats;
    uint64_t            in1 = 0;
    uint64_t            in2 = 0;

    JS_SETUP(2, 3, 3);

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "OBJECT";
    sc.Subjects = (const char*[1]){"a"};
    sc.SubjectsLen = 1;
    sc.Storage = js_MemoryStorage;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Subscribe: ");
    jsSubOptions_Init(&so);
    so.Ordered = true;
    so.Config.Heartbeat = NATS_MILLIS_TO_NANOS(250);
    s = js_Subscribe(&sub, js, "a", _jsMsgHandler, (void*)&args, NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Auto-unsub: ");
    s = natsSubscription_AutoUnsubscribe(sub, 10);
    testCond(s == NATS_OK);

    // Set a message filter that will drop 1 message
    natsConn_setFilterWithClosure(nc, _dropMsgFive, (void*)&args);

    // Now produce 20 messages
    test("Produce: ");
    for (i=0; (s == NATS_OK) && (i < 20); i++)
    {
        natsMsg *msg = NULL;

        s = natsMsg_Create(&msg, "a", NULL, "hello", 5);
        IFOK(s, natsMsgHeader_Set(msg, "data", "true"));
        IFOK(s, js_PublishMsgAsync(js, &msg, NULL));
        natsMsg_Destroy(msg);
        msg = NULL;
    }
    testCond(s == NATS_OK);

    test("Wait for all pubs done: ");
    s = js_PublishAsyncComplete(js, NULL);
    testCond(s == NATS_OK);

    test("Wait for subscription to be invalid: ");
    s = NATS_ERR;
    for (i=0; i<10; i++)
    {
        if (!natsSubscription_IsValid(sub))
        {
            s = NATS_OK;
            break;
        }
        nats_Sleep(100);
    }
    testCond(s == NATS_OK);

    // Wait a bit to make sure we are not receiving more than expected,
    // and give a chance for the server to process the auto-unsub
    // protocol.
    nats_Sleep(500);

    test("Check count: ");
    natsMutex_Lock(args.m);
    s = (args.sum == 10 ? NATS_OK : NATS_ERR);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    // Now capture the in msgs count for the connection
    test("Get stats: ");
    s = natsConnection_GetStats(nc, &stats);
    IFOK(s, natsStatistics_GetCounts(&stats, &in1, NULL, NULL, NULL, NULL));
    testCond(s == NATS_OK);

    // Send one more message and this count should not increase if the
    // server had properly processed the auto-unsub after the
    // reset of the ordered consumer. Use a different connection
    // to send.
    test("Send one msg: ");
    s = natsConnection_ConnectTo(&nc2, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_JetStream(&js2, nc2, NULL));
    IFOK(s, js_Publish(NULL, js2, "a", "bad", 3, NULL, NULL));
    testCond(s == NATS_OK);

    test("Get stats 2: ");
    s = natsConnection_GetStats(nc, &stats);
    IFOK(s, natsStatistics_GetCounts(&stats, &in2, NULL, NULL, NULL, NULL));
    testCond(s == NATS_OK);

    test("Msg not received: ");
    s = (in1 == in2 ? NATS_OK : NATS_ERR);
    testCond(s == NATS_OK);

    natsSubscription_Destroy(sub);
    jsCtx_Destroy(js2);
    natsConnection_Destroy(nc2);
    _destroyDefaultThreadArgs(&args);
    JS_TEARDOWN;
}

void test_JetStreamOrderedConsSrvRestart(void)
{
    natsStatus          s;
    natsSubscription    *sub    = NULL;
    natsMsg             *msg    = NULL;
    natsOptions         *opts   = NULL;
    jsConsumerInfo      *ci     = NULL;
    jsErrCode           jerr= 0;
    jsStreamConfig      sc;
    jsSubOptions        so;
    struct threadArg    args;
    int                 i;

    JS_SETUP(2, 9, 2);

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    // JS_SETUP creates a basic connection, we want to know when
    // we reconnected, so create a new one.
    test("Connect: ");
    natsConnection_Destroy(nc);
    nc = NULL;
    jsCtx_Destroy(js);
    js = NULL;
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetReconnectedCB(opts, _reconnectedCb, (void*) &args));
    IFOK(s, natsOptions_SetReconnectWait(opts, 100));
    IFOK(s, natsConnection_Connect(&nc, opts));
    IFOK(s, natsConnection_JetStream(&js, nc, NULL));
    testCond(s == NATS_OK);

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "OCRESTART";
    sc.Subjects = (const char*[1]){"foo"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Subscribe: ");
    jsSubOptions_Init(&so);
    so.Ordered = true;
    so.Config.Heartbeat = NATS_MILLIS_TO_NANOS(250);
    so.Config.HeadersOnly = true;
    s = js_SubscribeSync(&sub, js, "foo", NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Send 1 message: ");
    s = js_Publish(NULL, js, "foo", "hello", 5, NULL, NULL);
    testCond(s == NATS_OK)

    test("Consume: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Stopping server: ");
    _stopServer(pid);
    testCond(true);
    // Wait more than the HB failure detection so that we check
    // that resetting the ordered consumer works even while the
    // server is still down.
    test("Waiting before restarting: ");
    nats_Sleep(1500);
    testCond(true);
    // Restart
    test("Restarting server: ");
    pid = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(pid);
    testCond(true);

    test("Wait for reconnect: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.reconnected)
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    test("Send 1 message: ");
    s = js_Publish(NULL, js, "foo", "hello", 5, NULL, NULL);
    testCond(s == NATS_OK)

    test("Consume: ");
    s = natsSubscription_NextMsg(&msg, sub, 5000);
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Check configuration is similar: ");
    for (i=0; (s==NATS_OK) && (i<10); i++)
    {
        s = natsSubscription_GetConsumerInfo(&ci, sub, NULL, NULL);
        if (s == NATS_OK)
            break;
        else if (s == NATS_NOT_FOUND)
        {
            s = NATS_OK;
            nats_Sleep(100);
        }
    }
    testCond((s == NATS_OK)
                && (ci != NULL) && (ci->Config != NULL)
                && (ci->Config->MemoryStorage)
                && (ci->Config->Replicas == 1)
                && (ci->Config->HeadersOnly));
    jsConsumerInfo_Destroy(ci);

    natsSubscription_Destroy(sub);
    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&args);
    JS_TEARDOWN;
}

void test_JetStreamSubscribeWithFWC(void)
{
    natsStatus          s;
    natsSubscription    *sub= NULL;
    jsErrCode           jerr= 0;
    jsStreamConfig      sc;
    jsConsumerConfig    cc;
    jsSubOptions        so;

    JS_SETUP(2, 3, 5);

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST_WC";
    sc.Subjects = (const char*[1]){"fwc.>"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Create consumer: ");
    jsConsumerConfig_Init(&cc);
    cc.Durable = "dur";
    cc.DeliverSubject = "bar";
    cc.FilterSubject = "fwc.>";
    s = js_AddConsumer(NULL, js, "TEST_WC", &cc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Subscribe: ");
    jsSubOptions_Init(&so);
    so.Config.Durable = "dur";
    s = js_SubscribeSync(&sub, js, "fwc.>", NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL));
    natsSubscription_Destroy(sub);

    JS_TEARDOWN;
}

void test_JetStreamStreamsSealAndRollup(void)
{
    natsStatus          s;
    jsStreamInfo        *si = NULL;
    jsStreamConfig      cfg;
    jsErrCode           jerr = 0;
    natsMsg             *msg  = NULL;
    natsSubscription    *sub  = NULL;
    int                 i;

    JS_SETUP(2, 6, 2);

    test("Create sealed stream fails: ");
    jsStreamConfig_Init(&cfg);
    cfg.Name = "SEAL_FAIL";
    cfg.Sealed = true;
    s = js_AddStream(&si, js, &cfg, NULL, &jerr);
    testCond((s == NATS_ERR) && (si == NULL) && (jerr == JSStreamInvalidConfig));
    nats_clearLastError();

    test("Create stream: ");
    jsStreamConfig_Init(&cfg);
    cfg.Name = "SEAL";
    s = js_AddStream(NULL, js, &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Seal stream: ");
    jsStreamConfig_Init(&cfg);
    cfg.Name = "SEAL";
    cfg.Sealed = true;
    s = js_UpdateStream(&si, js, &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (si != NULL)
                && (si->Config != NULL) && si->Config->Sealed);
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("Can't send: ");
    s = js_Publish(NULL, js, "SEAL", "a", 1, NULL, &jerr);
    testCond((s == NATS_ERR) && (jerr == JSStreamSealedErr));
    nats_clearLastError();

    test("Create stream with deny purge/delete: ");
    jsStreamConfig_Init(&cfg);
    cfg.Name = "AUDIT";
    cfg.Storage = js_MemoryStorage;
    cfg.DenyPurge = true;
    cfg.DenyDelete = true;
    s = js_AddStream(&si, js, &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (si != NULL)
                && (si->Config != NULL) && si->Config->DenyDelete
                && si->Config->DenyPurge);
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("Publish: ");
    for (i=0; (s == NATS_OK) && (i < 10); i++)
        s = js_Publish(NULL, js, "AUDIT", "ok", 2, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Can't delete: ");
    s = js_DeleteMsg(js, "AUDIT", 1, NULL, &jerr);
    testCond((s == NATS_ERR) && (jerr == JSStreamMsgDeleteFailed)
                && (strstr(nats_GetLastError(NULL), "message delete not permitted") != NULL));
    nats_clearLastError();

    test("Can't purge: ");
    s = js_PurgeStream(js, "AUDIT", NULL, &jerr);
    testCond((s == NATS_ERR) && (jerr == JSStreamPurgeFailedErr)
                && (strstr(nats_GetLastError(NULL), "stream purge not permitted") != NULL));
    nats_clearLastError();

    test("Try to remove deny clauses: ");
    cfg.DenyPurge = false;
    cfg.DenyDelete = false;
    s = js_UpdateStream(NULL, js, &cfg, NULL, &jerr);
    testCond((s == NATS_ERR) && (jerr == JSStreamInvalidConfig));
    nats_clearLastError();

    test("Create stream for rollup: ");
    jsStreamConfig_Init(&cfg);
    cfg.Name = "ROLLUP";
    cfg.Subjects = (const char*[1]){"rollup.*"};
    cfg.SubjectsLen = 1;
    cfg.AllowRollup = true;
    s = js_AddStream(&si, js, &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (si != NULL)
                && (si->Config != NULL) && si->Config->AllowRollup);
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("Populate: ");
    for (i=0; (s == NATS_OK) && (i<10); i++)
        s = js_Publish(NULL, js, "rollup.a", "a", 1, NULL, &jerr);
    for (i=0; (s == NATS_OK) && (i<10); i++)
        s = js_Publish(NULL, js, "rollup.b", "b", 1, NULL, &jerr);
    for (i=0; (s == NATS_OK) && (i<10); i++)
        s = js_Publish(NULL, js, "rollup.c", "c", 1, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Check stream: ");
    s = js_GetStreamInfo(&si, js, "ROLLUP", NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (si != NULL)
                && (si->State.Msgs == 30));
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("Rollup per subject: ");
    s = natsMsg_Create(&msg, "rollup.b", NULL, "Rollup", 6);
    IFOK(s, natsMsgHeader_Set(msg, JSMsgRollup, JSMsgRollupSubject));
    IFOK(s, js_PublishMsg(NULL, js, msg, NULL, &jerr));
    testCond((s == NATS_OK) && (jerr == 0));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Create consumer: ");
    s = js_SubscribeSync(&sub, js, "rollup.b", NULL, NULL, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Check content: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    IFOK(s, (strcmp(natsMsg_GetData(msg), "Rollup") == 0 ? NATS_OK : NATS_ERR));
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Make sure single msg: ");
    s = natsSubscription_NextMsg(&msg, sub, 250);
    testCond((s == NATS_TIMEOUT) && (msg == NULL));
    nats_clearLastError();

    natsSubscription_Destroy(sub);
    sub = NULL;

    test("Rollup for all: ");
    s = natsMsg_Create(&msg, "rollup.c", NULL, "RollupAll", 9);
    IFOK(s, natsMsgHeader_Set(msg, JSMsgRollup, JSMsgRollupAll));
    IFOK(s, js_PublishMsg(NULL, js, msg, NULL, &jerr));
    testCond((s == NATS_OK) && (jerr == 0));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Create consumer: ");
    s = js_SubscribeSync(&sub, js, "rollup.c", NULL, NULL, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Check content: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    IFOK(s, (strcmp(natsMsg_GetData(msg), "RollupAll") == 0 ? NATS_OK : NATS_ERR));
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Make sure single msg: ");
    s = natsSubscription_NextMsg(&msg, sub, 250);
    testCond((s == NATS_TIMEOUT) && (msg == NULL));
    nats_clearLastError();

    test("Check stream: ");
    s = js_GetStreamInfo(&si, js, "ROLLUP", NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (si != NULL)
                && (si->State.Msgs == 1));
    jsStreamInfo_Destroy(si);
    si = NULL;

    natsSubscription_Destroy(sub);
    JS_TEARDOWN;
}

void test_JetStreamGetMsgAndLastMsg(void)
{
    natsStatus          s;
    natsMsg             *msg = NULL;
    jsStreamConfig      cfg;
    jsErrCode           jerr = 0;
    const char          *val = NULL;

    JS_SETUP(2, 3, 1);

    test("Create stream: ");
    jsStreamConfig_Init(&cfg);
    cfg.Name = "GET_MSG";
    cfg.Subjects = (const char*[1]){"foo.*"};
    cfg.SubjectsLen = 1;
    cfg.Storage = js_MemoryStorage;
    s = js_AddStream(NULL, js, &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Populate: ");
    s = js_Publish(NULL, js, "foo.bar", "msg1", 4, NULL, NULL);
    IFOK(s, js_Publish(NULL, js, "foo.bar", "msg2", 4, NULL, NULL));
    IFOK(s, js_Publish(NULL, js, "foo.baz", "msg3", 4, NULL, NULL));
    IFOK(s, natsMsg_Create(&msg, "foo.baz", NULL, "msg4", 4));
    IFOK(s, natsMsgHeader_Set(msg, "Some-Header", "Some-Value"));
    IFOK(s, js_PublishMsg(NULL, js, msg, NULL, NULL));
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    test("GetMsg bad args: ");
    s = js_GetMsg(NULL, js, "GET_MSG", 1, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_GetMsg(&msg, NULL, "GET_MSG", 1, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_GetMsg(&msg, js, "GET_MSG", 0, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (msg == NULL) && (jerr == 0));
    nats_clearLastError();

    test("GetMsg stream name required: ");
    s = js_GetMsg(&msg, js, NULL, 1, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_GetMsg(&msg, js, "", 1, NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (msg == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrStreamNameRequired) != NULL));
    nats_clearLastError();

    test("GetMsg stream not found: ");
    s = js_GetMsg(&msg, js, "NOT_FOUND", 1, NULL, &jerr);
    testCond((s == NATS_ERR) && (msg == NULL) && (jerr == JSStreamNotFoundErr)
                && (strstr(nats_GetLastError(NULL), "stream not found") != NULL));
    nats_clearLastError();

    test("GetMsg message not found: ");
    s = js_GetMsg(&msg, js, "GET_MSG", 100, NULL, &jerr);
    testCond((s == NATS_NOT_FOUND) && (msg == NULL) && (jerr == JSNoMessageFoundErr));
    nats_clearLastError();

    test("GetMsg message ok: ");
    s = js_GetMsg(&msg, js, "GET_MSG", 1, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (msg != NULL)
                && (strcmp(natsMsg_GetSubject(msg), "foo.bar") == 0)
                && (strcmp(natsMsg_GetData(msg), "msg1") == 0)
                && (natsMsg_GetSequence(msg) == 1)
                && (natsMsg_GetTime(msg) != 0));

    natsMsg_Destroy(msg);
    msg = NULL;

    // GetLasMsg tests now....

    test("GetLastMsg bad args: ");
    s = js_GetLastMsg(NULL, js, "GET_MSG", "foo.bar", NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_GetLastMsg(&msg, NULL, "GET_MSG", "foo.bar", NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_GetLastMsg(&msg, js, "GET_MSG", NULL, NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_GetLastMsg(&msg, js, "GET_MSG", "", NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (msg == NULL) && (jerr == 0));
    nats_clearLastError();

    test("GetLastMsg stream name required: ");
    s = js_GetLastMsg(&msg, js, NULL, "foo.bar", NULL, &jerr);
    if (s == NATS_INVALID_ARG)
        s = js_GetLastMsg(&msg, js, "", "foo.bar", NULL, &jerr);
    testCond((s == NATS_INVALID_ARG) && (msg == NULL) && (jerr == 0)
                && (strstr(nats_GetLastError(NULL), jsErrStreamNameRequired) != NULL));
    nats_clearLastError();

    test("GetLastMsg stream not found: ");
    s = js_GetLastMsg(&msg, js, "NOT_FOUND", "foo.bar", NULL, &jerr);
    testCond((s == NATS_ERR) && (msg == NULL) && (jerr == JSStreamNotFoundErr)
                && (strstr(nats_GetLastError(NULL), "stream not found") != NULL));
    nats_clearLastError();

    test("GetLastMsg message not found: ");
    s = js_GetLastMsg(&msg, js, "GET_MSG", "not.found", NULL, &jerr);
    testCond((s == NATS_NOT_FOUND) && (msg == NULL) && (jerr == JSNoMessageFoundErr));
    nats_clearLastError();

    test("GetLastMsg message ok: ");
    s = js_GetLastMsg(&msg, js, "GET_MSG", "foo.baz", NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (msg != NULL) && (msg != NULL)
                && (strcmp(natsMsg_GetSubject(msg), "foo.baz") == 0)
                && (strcmp(natsMsg_GetData(msg), "msg4") == 0)
                && (natsMsgHeader_Get(msg, "Some-Header", &val) == NATS_OK)
                && (strcmp(val, "Some-Value") == 0)
                && (natsMsg_GetSequence(msg) == 4)
                && (natsMsg_GetTime(msg) != 0));
    natsMsg_Destroy(msg);
    JS_TEARDOWN;
}

void test_JetStreamConvertDirectMsg(void)
{
    natsStatus  s;
    natsMsg     *msg = NULL;
    const char  *val = NULL;

    test("Bad request: ");
    s = natsMsg_Create(&msg, "inbox", NULL, NULL, 0);
    IFOK(s, natsMsgHeader_Set(msg, STATUS_HDR, HDR_STATUS_TIMEOUT_408));
    IFOK(s, natsMsgHeader_Set(msg, DESCRIPTION_HDR, "Bad Request"));
    IFOK(s, js_directGetMsgToJSMsg("test", msg));
    testCond((s == NATS_ERR) && (strstr(nats_GetLastError(NULL), "Bad Request") != NULL));
    nats_clearLastError();

    test("Not found: ");
    s = natsMsgHeader_Set(msg, STATUS_HDR, HDR_STATUS_NOT_FOUND_404);
    IFOK(s, natsMsgHeader_Set(msg, DESCRIPTION_HDR, "Message Not Found"));
    IFOK(s, js_directGetMsgToJSMsg("test", msg));
    testCond((s == NATS_NOT_FOUND) && (strstr(nats_GetLastError(NULL), natsStatus_GetText(NATS_NOT_FOUND)) != NULL));
    nats_clearLastError();
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Msg has no header: ");
    s = natsMsg_Create(&msg, "inbox", NULL, "1", 1);
    IFOK(s, js_directGetMsgToJSMsg("test", msg));
    testCond((s == NATS_ERR) && (strstr(nats_GetLastError(NULL), "should have headers") != NULL));
    nats_clearLastError();

    test("Missing stream: ");
    s = natsMsgHeader_Set(msg, "some", "header");
    IFOK(s, js_directGetMsgToJSMsg("test", msg));
    testCond((s == NATS_ERR) && (strstr(nats_GetLastError(NULL), "missing stream") != NULL));
    nats_clearLastError();

    test("Missing sequence: ");
    s = natsMsgHeader_Set(msg, JSStream, "test");
    IFOK(s, js_directGetMsgToJSMsg("test", msg));
    testCond((s == NATS_ERR) && (strstr(nats_GetLastError(NULL), "invalid sequence") != NULL));
    nats_clearLastError();

    test("Invalid sequence: ");
    s = natsMsgHeader_Set(msg, JSSequence, "abc");
    IFOK(s, js_directGetMsgToJSMsg("test", msg));
    testCond((s == NATS_ERR) && (strstr(nats_GetLastError(NULL), "invalid sequence 'abc'") != NULL));
    nats_clearLastError();

    test("Missing timestamp: ");
    s = natsMsgHeader_Set(msg, JSSequence, "1");
    IFOK(s, js_directGetMsgToJSMsg("test", msg));
    testCond((s == NATS_ERR) && (strstr(nats_GetLastError(NULL), "missing or invalid timestamp") != NULL));
    nats_clearLastError();

    test("Invalid timestamp: ");
    s = natsMsgHeader_Set(msg, JSTimeStamp, "aaaaaaaaa bbbbbbbbbbbb cccccccccc ddddddddddd eeeeeeeeee ffffff");
    IFOK(s, js_directGetMsgToJSMsg("test", msg));
    testCond((s == NATS_ERR) && (strstr(nats_GetLastError(NULL), "missing or invalid timestamp 'aaaaaaaaa bbbbbbbbbbbb cccccccccc ddddddddddd eeeeeeeeee ffffff'") != NULL));
    nats_clearLastError();

    test("Missing subject: ");
    s = natsMsgHeader_Set(msg, JSTimeStamp, "2006-01-02T15:04:05Z");
    IFOK(s, js_directGetMsgToJSMsg("test", msg));
    testCond((s == NATS_ERR) && (strstr(nats_GetLastError(NULL), "missing or invalid subject") != NULL));
    nats_clearLastError();

    test("Invalid subject: ");
    s = natsMsgHeader_Set(msg, JSSubject, "");
    IFOK(s, js_directGetMsgToJSMsg("test", msg));
    testCond((s == NATS_ERR) && (strstr(nats_GetLastError(NULL), "missing or invalid subject ''") != NULL));
    nats_clearLastError();

    test("Valid msg: ");
    s = natsMsgHeader_Set(msg, JSSubject, "foo");
    IFOK(s, js_directGetMsgToJSMsg("test", msg));
    testCond((s == NATS_OK)
                && (strcmp(natsMsg_GetSubject(msg), "foo") == 0)
                && (natsMsg_GetSequence(msg) == 1)
                && (natsMsg_GetTime(msg) == 1136214245000000000L)
                && (natsMsgHeader_Get(msg, "some", &val) == NATS_OK)
                && (strcmp(val, "header") == 0));

    natsMsg_Destroy(msg);
}

static natsStatus
_checkDirectGet(jsCtx *js, uint64_t seq, const char *nextBySubj, const char *lastBySubj,
                const char *expectedSubj, uint64_t expectedSeq)
{
    natsStatus              s    = NATS_OK;
    natsMsg                 *msg = NULL;
    jsDirectGetMsgOptions   o;

    jsDirectGetMsgOptions_Init(&o);
    o.Sequence = seq;
    o.NextBySubject = nextBySubj;
    o.LastBySubject = lastBySubj;

    s = js_DirectGetMsg(&msg, js, "DGM", NULL, &o);
    if ((s != NATS_OK)
        || (msg == NULL)
        || (strcmp(natsMsg_GetSubject(msg), expectedSubj) != 0)
        || (natsMsg_GetSequence(msg) != expectedSeq)
        || (natsMsg_GetTime(msg) == 0))
    {
        s = NATS_ERR;
    }

    natsMsg_Destroy(msg);
    return s;
}

void test_JetStreamDirectGetMsg(void)
{
    natsStatus              s;
    natsMsg                 *msg = NULL;
    jsStreamConfig          cfg;
    jsErrCode               jerr = 0;
    const char              *val = NULL;
    jsDirectGetMsgOptions   dgo;

    JS_SETUP(2, 9, 0);

    test("Create stream: ");
    jsStreamConfig_Init(&cfg);
    cfg.Name = "DGM";
    cfg.Subjects = (const char*[2]){"foo", "bar"};
    cfg.SubjectsLen = 2;
    cfg.Storage = js_MemoryStorage;
    cfg.AllowDirect = true;
    s = js_AddStream(NULL, js, &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Populate: ");
    s = js_Publish(NULL, js, "foo", "a", 1, NULL, NULL);
    IFOK(s, js_Publish(NULL, js, "foo", "b", 1, NULL, NULL));
    IFOK(s, js_Publish(NULL, js, "foo", "c", 1, NULL, NULL));
    IFOK(s, js_Publish(NULL, js, "bar", "d", 1, NULL, NULL));
    IFOK(s, js_Publish(NULL, js, "foo", "e", 1, NULL, NULL));
    testCond(s == NATS_OK);

    test("DirecGetMsg bad args: ");
    s = js_DirectGetMsg(NULL, js, "DGM", NULL, &dgo);
    if (s == NATS_INVALID_ARG)
        s = js_DirectGetMsg(&msg, NULL, "DGM", NULL, &dgo);
    if (s == NATS_INVALID_ARG)
        s = js_DirectGetMsg(&msg, js, "DGM", NULL, NULL);
    testCond((s == NATS_INVALID_ARG) && (msg == NULL));
    nats_clearLastError();

    test("GetMsg stream name required: ");
    s = js_DirectGetMsg(&msg, js, NULL, NULL, &dgo);
    if (s == NATS_INVALID_ARG)
        s = js_DirectGetMsg(&msg, js, "", NULL, &dgo);
    testCond((s == NATS_INVALID_ARG) && (msg == NULL)
                && (strstr(nats_GetLastError(NULL), jsErrStreamNameRequired) != NULL));
    nats_clearLastError();

    test("DirectGetMsg options init bad args: ");
    s = jsDirectGetMsgOptions_Init(NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Seq==0 next_by_subj(bar): ");
    testCond(_checkDirectGet(js, 0, "bar", NULL, "bar", 4) == NATS_OK);

    test("Seq==0 last_by_subj: ");
    testCond(_checkDirectGet(js, 0, NULL, "foo", "foo", 5) == NATS_OK);

    test("Seq==0 next_by_subj(foo): ");
    testCond(_checkDirectGet(js, 0, "foo", NULL, "foo", 1) == NATS_OK);

    test("Seq==4 next_by_subj: ");
    testCond(_checkDirectGet(js, 4, "foo", NULL, "foo", 5) == NATS_OK);

    test("Seq==2 next_by_subj: ");
    testCond(_checkDirectGet(js, 2, "foo", NULL, "foo", 2) == NATS_OK);

    test("Publish msg with header: ");
    s = natsMsg_Create(&msg, "foo", NULL, "f", 1);
    IFOK(s, natsMsgHeader_Add(msg, "MyHeader", "MyValue"));
    IFOK(s, js_PublishMsg(NULL, js, msg, NULL, NULL));
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Check headers preserved: ");
    jsDirectGetMsgOptions_Init(&dgo);
    dgo.Sequence = 6;
    s = js_DirectGetMsg(&msg, js, "DGM", NULL, &dgo);
    testCond((s == NATS_OK)
                && (natsMsgHeader_Get(msg, "MyHeader", &val) == NATS_OK)
                && (val != NULL)
                && (strcmp(val, "MyValue") == 0));

    test("Change subject header: ");
    s = natsMsgHeader_Set(msg, JSSubject, "xxx");
    testCond(s == NATS_OK);

    test("Msg subject not affected: ");
    testCond(strcmp(natsMsg_GetSubject(msg), "foo") == 0);

    natsMsg_Destroy(msg);

    JS_TEARDOWN;
}

void test_JetStreamNakWithDelay(void)
{
    natsStatus          s;
    natsSubscription    *sub= NULL;
    jsErrCode           jerr= 0;
    jsStreamConfig      sc;
    natsMsg             *msg = NULL;
    int64_t             start=0;
    int64_t             dur  = 0;
    jsOptions           o;

    JS_SETUP(2, 7, 2);

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Subjects = (const char*[1]){"foo"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Publish: ");
    s = js_Publish(NULL, js, "foo", "ok", 2, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Create sub: ");
    s = js_SubscribeSync(&sub, js, "foo", NULL, NULL, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Get message: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK) && (msg != NULL));

    test("Nak with 500ms: ");
    s = natsMsg_NakWithDelay(msg, 500, NULL);
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Should not be redelivered yet: ");
    s = natsSubscription_NextMsg(&msg, sub, 250);
    testCond((s == NATS_TIMEOUT) && (msg == NULL));
    nats_clearLastError();

    test("Wait for redelivery: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK) && (msg != NULL));

    test("NakWithDelay with 0: ");
    s = natsMsg_NakWithDelay(msg, 0, NULL);
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Wait for redelivery: ");
    s = natsSubscription_NextMsg(&msg, sub, 250);
    testCond((s == NATS_OK) && (msg != NULL));

    natsSub_retain(sub);
    natsSubscription_Destroy(sub);

    // Stop the server...
    _stopServer(pid);
    pid = NATS_INVALID_PID;

    test("Check ack sync with options: ");
    jsOptions_Init(&o);
    o.Wait = 500;
    start = nats_Now();
    s = natsMsg_Ack(msg, &o);
    dur = nats_Now()-start;
    testCond((s == NATS_TIMEOUT) && (dur > 250) && (dur < 750));
    natsMsg_Destroy(msg);
    natsSub_release(sub);
    JS_TEARDOWN;
}

void test_JetStreamBackOffRedeliveries(void)
{
    natsStatus          s;
    natsSubscription    *sub= NULL;
    jsErrCode           jerr= 0;
    jsStreamConfig      sc;
    natsMsg             *msg = NULL;
    int64_t             start =0;
    int64_t             dur = 0;
    natsInbox           *inbox = NULL;
    jsConsumerInfo      *ci = NULL;
    jsSubOptions        so;
    int                 i;

    JS_SETUP(2, 7, 2);

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Subjects = (const char*[1]){"foo"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Create inbox: ");
    s = natsInbox_Create(&inbox);
    testCond(s == NATS_OK);

    test("Wrong MaxDeliver: ");
    jsSubOptions_Init(&so);
    so.Stream = "TEST";
    so.Config.Durable = "backoff";
    so.Config.AckPolicy = js_AckExplicit;
    so.Config.DeliverPolicy = js_DeliverAll;
    so.Config.DeliverSubject = inbox;
    so.Config.MaxDeliver = 1;
    so.Config.BackOff = (int64_t[]){NATS_MILLIS_TO_NANOS(50), NATS_MILLIS_TO_NANOS(250)};
    so.Config.BackOffLen = 2;
    s = js_SubscribeSync(&sub, js, "foo", NULL, &so, &jerr);
    testCond((s != NATS_OK) && (sub == NULL) && (jerr == JSConsumerMaxDeliverBackoffErr)
                && (strstr(nats_GetLastError(NULL), "max deliver is required") != NULL));
    nats_clearLastError();

    test("Create ok: ");
    so.Config.MaxDeliver = 4;
    s = js_SubscribeSync(&sub, js, "foo", NULL, &so, &jerr);
    testCond((s == NATS_OK) && (sub != NULL) && (jerr == 0));

    test("Send: ");
    s = js_Publish(NULL, js, "foo", "ok", 2, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Consume msg: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond(s == NATS_OK);
    natsMsg_Destroy(msg);
    msg = NULL;

    // We should get a redelivery at around 50ms
    test("Redelivered at 50ms: ");
    start = nats_Now();
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    dur = (nats_Now()-start);
    testCond((s == NATS_OK) && (dur > 25) && (dur < 100));
    natsMsg_Destroy(msg);
    msg = NULL;

    // Now it should be every 250ms or so
    for (i=0; i<2; i++)
    {
        test("Redelivered at 250ms: ");
        start = nats_Now();
        s = natsSubscription_NextMsg(&msg, sub, 1000);
        dur = (nats_Now()-start);
        testCond((s == NATS_OK) && (dur > 200) && (dur < 300));
        natsMsg_Destroy(msg);
        msg = NULL;
    }

    // At this point, we should have go reach MaxDeliver
    test("No more: ");
    s = natsSubscription_NextMsg(&msg, sub, 300);
    testCond(s == NATS_TIMEOUT);

    test("Check consumer info: ");
    s = js_GetConsumerInfo(&ci, js, "TEST", "backoff", NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (ci != NULL) && (ci->Config != NULL)
                && (ci->Config->BackOffLen == 2)
                && (ci->Config->BackOff[0] == NATS_MILLIS_TO_NANOS(50))
                && (ci->Config->BackOff[1] == NATS_MILLIS_TO_NANOS(250)));
    jsConsumerInfo_Destroy(ci);
    natsInbox_Destroy(inbox);
    natsSubscription_Destroy(sub);
    JS_TEARDOWN;
}

static void
_subjectsInfoReq(natsConnection *nc, natsMsg **msg, void *closure)
{
    natsMsg     *newMsg     = NULL;
    int         *count      = (int*) closure;
    const char  *payload    = NULL;

    if (strstr((*msg)->data, "stream_info_response") == NULL)
        return;

    (*count)++;
    if (*count == 1)
    {
        // Pretend that we have 5 subjects and a limit of 2
        payload = "{\"type\":\"io.nats.jetstream.api.v1.stream_info_response\",\"total\":5,\"offset\":0,\"limit\":2,"\
        "\"config\":{\"name\":\"TEST\",\"subjects\":[\"foo.>\"]},"\
        "\"state\":{\"num_subjects\":5,\"subjects\":{\"foo.bar\":1,\"foo.baz\":2}}}";
    }
    else if (*count == 2)
    {
        // Continue with the pagination
        payload = "{\"type\":\"io.nats.jetstream.api.v1.stream_info_response\",\"total\":5,\"offset\":2,\"limit\":2,"\
        "\"config\":{\"name\":\"TEST\",\"subjects\":[\"foo.>\"]},"\
        "\"state\":{\"num_subjects\":5,\"subjects\":{\"foo.bat\":3,\"foo.box\":4}}}";
    }
    else if (*count == 3)
    {
        // Pretend that the number went down in between page requests and the
        // last request got nothing in return.
        payload = "{\"type\":\"io.nats.jetstream.api.v1.stream_info_response\",\"total\":3,\"offset\":3,\"limit\":2,"\
        "\"config\":{\"name\":\"TEST\",\"subjects\":[\"foo.>\"]},"\
        "\"state\":{\"num_subjects\":3}}";
    }
    else
    {
        // Keep the original message
        return;
    }
    if (natsMsg_create(&newMsg, (*msg)->subject, (int) strlen((*msg)->subject),
                        NULL, 0, payload, (int) strlen(payload), 0) == NATS_OK)
    {
        natsMsg_Destroy(*msg);
        *msg = newMsg;
    }
}

void test_JetStreamInfoWithSubjects(void)
{
    natsStatus          s;
    jsStreamInfo        *si = NULL;
    jsStreamConfig      cfg;
    jsErrCode           jerr = 0;
    jsOptions           o;
    int                 count = 0;
    natsSubscription    *sub  = NULL;
    natsMsg             *msg  = NULL;

    JS_SETUP(2, 9, 0);

    test("Create stream: ");
    jsStreamConfig_Init(&cfg);
    cfg.Name = "TEST";
    cfg.Subjects = (const char*[1]){"foo.>"};
    cfg.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &cfg, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Send to different subjects: ");
    s = js_Publish(NULL, js, "foo.bar", "m1", 2, NULL, &jerr);
    IFOK(s, js_Publish(NULL, js, "foo.baz", "m1", 2, NULL, &jerr));
    IFOK(s, js_Publish(NULL, js, "foo.baz", "m2", 2, NULL, &jerr));
    IFOK(s, js_Publish(NULL, js, "foo.baz.bat", "m1", 2, NULL, &jerr));
    IFOK(s, js_Publish(NULL, js, "foo.baz.bat", "m2", 2, NULL, &jerr));
    IFOK(s, js_Publish(NULL, js, "foo.baz.bat", "m3", 2, NULL, &jerr));
    testCond(s == NATS_OK);

    test("Check number subjects: ");
    s = js_GetStreamInfo(&si, js, "TEST", NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (si != NULL) && (si->State.NumSubjects == 3));
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("Get subjects list (no match): ");
    jsOptions_Init(&o);
    o.Stream.Info.SubjectsFilter = "none";
    s = js_GetStreamInfo(&si, js, "TEST", &o, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (si != NULL) && (si->State.NumSubjects == 3)
                && (si->State.Subjects == NULL));
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("Get subjects list (1 match): ");
    jsOptions_Init(&o);
    o.Stream.Info.SubjectsFilter = "foo.bar";
    s = js_GetStreamInfo(&si, js, "TEST", &o, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (si != NULL) && (si->State.NumSubjects == 3)
                && (si->State.Subjects != NULL)
                && (si->State.Subjects->Count == 1)
                && (strcmp(si->State.Subjects->List[0].Subject, "foo.bar") == 0)
                && (si->State.Subjects->List[0].Msgs == 1));
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("Get subjects list (all matches): ");
    jsOptions_Init(&o);
    o.Stream.Info.SubjectsFilter = "foo.>";
    s = js_GetStreamInfo(&si, js, "TEST", &o, &jerr);
    if ((s == NATS_OK) && ((si == NULL) || (si->State.Subjects == NULL) || (si->State.Subjects->Count != 3)))
        s = NATS_ERR;
    else
    {
        int i;
        bool got1, got2, got3;

        for (i=0; i<si->State.Subjects->Count; i++)
        {
            jsStreamStateSubject *subj = &(si->State.Subjects->List[i]);

            if ((strcmp(subj->Subject, "foo.bar") == 0) && (subj->Msgs == 1))
                got1 = true;
            else if ((strcmp(subj->Subject, "foo.baz") == 0) && (subj->Msgs == 2))
                got2 = true;
            else if ((strcmp(subj->Subject, "foo.baz.bat") == 0) && (subj->Msgs == 3))
                got3 = true;
        }
        s = (got1 && got2 && got3 ? NATS_OK : NATS_ERR);
    }
    testCond((s == NATS_OK) && (jerr == 0) && (si->State.NumSubjects == 3));
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("Create sub for pagination check: ");
    s = natsConnection_SubscribeSync(&sub, nc, "$JS.API.STREAM.INFO.TEST");
    testCond(s == NATS_OK);

    natsConn_setFilterWithClosure(nc, _subjectsInfoReq, (void*) &count);

    test("Get all subjects with pagination: ");
    jsOptions_Init(&o);
    o.Stream.Info.SubjectsFilter = ">";
    s = js_GetStreamInfo(&si, js, "TEST", &o, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (si != NULL) && (si->State.NumSubjects == 3)
                && (si->State.Subjects != NULL)
                && (si->State.Subjects->List != NULL)
                && (si->State.Subjects->Count == 4));
    jsStreamInfo_Destroy(si);
    si = NULL;

    natsConn_setFilter(nc, NULL);

    test("Check 1st request: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK)
                && (strstr(natsMsg_GetData(msg), "offset") == NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Check 2nd request: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK)
                && (strstr(natsMsg_GetData(msg), "offset\":2") != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    test("Check 3rd request: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond((s == NATS_OK)
                && (strstr(natsMsg_GetData(msg), "offset\":4") != NULL));
    natsMsg_Destroy(msg);
    msg = NULL;

    natsSubscription_Destroy(sub);

    JS_TEARDOWN;
}

void test_JetStreamInfoAlternates(void)
{
    char                datastore1[256] = {'\0'};
    char                datastore2[256] = {'\0'};
    char                datastore3[256] = {'\0'};
    char                cmdLine[1024]   = {'\0'};
    natsPid             pid1            = NATS_INVALID_PID;
    natsPid             pid2            = NATS_INVALID_PID;
    natsPid             pid3            = NATS_INVALID_PID;
    natsConnection      *nc             = NULL;
    jsCtx               *js             = NULL;
    jsStreamInfo        *si             = NULL;
    jsStreamConfig      sc;
    jsStreamSource      ss;
    natsStatus          s;
    int                 i;

    ENSURE_JS_VERSION(2, 9, 0);

    test("Start cluster: ");
    _makeUniqueDir(datastore1, sizeof(datastore1), "datastore_");
    snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s -cluster_name abc -server_name A -cluster nats://127.0.0.1:6222 -routes nats://127.0.0.1:6222,nats://127.0.0.1:6223,nats://127.0.0.1:6224 -p 4222", datastore1);
    pid1 = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(pid1);

    _makeUniqueDir(datastore2, sizeof(datastore2), "datastore_");
    snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s -cluster_name abc -server_name B -cluster nats://127.0.0.1:6223 -routes nats://127.0.0.1:6222,nats://127.0.0.1:6223,nats://127.0.0.1:6224 -p 4223", datastore2);
    pid2 = _startServer("nats://127.0.0.1:4223", cmdLine, true);
    CHECK_SERVER_STARTED(pid1);

    _makeUniqueDir(datastore3, sizeof(datastore3), "datastore_");
    snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s -cluster_name abc -server_name C -cluster nats://127.0.0.1:6224 -routes nats://127.0.0.1:6222,nats://127.0.0.1:6223,nats://127.0.0.1:6224 -p 4224", datastore3);
    pid3 = _startServer("nats://127.0.0.1:4224", cmdLine, true);
    CHECK_SERVER_STARTED(pid1);
    testCond(true);

    test("Connect: ");
    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    testCond(s == NATS_OK);

    test("Get context: ");
    s = natsConnection_JetStream(&js, nc, NULL);
    testCond(s == NATS_OK);

    test("Create stream: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "TEST";
    sc.Subjects = (const char*[1]){"foo"};
    sc.SubjectsLen = 1;
    // We will try up to 10 times to create the stream. It may fail if the cluster
    // is not ready to accept the request.
    for (i=0; (s == NATS_OK) && (i < 10); i++)
    {
        s = js_AddStream(NULL, js, &sc, NULL, NULL);
        if ((s != NATS_OK) && (i < 9))
        {
            nats_clearLastError();
            s = NATS_OK;
            nats_Sleep(100);
        }
    }
    testCond(s == NATS_OK);

    test("Create mirror: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "MIRROR";
    jsStreamSource_Init(&ss);
    ss.Name = "TEST";
    sc.Mirror = &ss;
    s = js_AddStream(NULL, js, &sc, NULL, NULL);
    testCond(s == NATS_OK);

    test("Check for alternate: ");
    s = js_GetStreamInfo(&si, js, "TEST", NULL, NULL);
    testCond((s == NATS_OK) && (si != NULL) && (si->AlternatesLen == 2));

    test("Check alternate content: ");
    if ((strcmp(si->Alternates[0]->Cluster, "abc") != 0)
        || (strcmp(si->Alternates[1]->Cluster, "abc") != 0))
    {
        s = NATS_ERR;
    }
    else if (((strcmp(si->Alternates[0]->Name, "TEST") == 0) && (strcmp(si->Alternates[1]->Name, "MIRROR") != 0))
                || ((strcmp(si->Alternates[0]->Name, "MIRROR") == 0) && (strcmp(si->Alternates[1]->Name, "TEST") != 0)))
    {
        s = NATS_ERR;
    }
    testCond(s == NATS_OK);
    jsStreamInfo_Destroy(si);

    jsCtx_Destroy(js);
    natsConnection_Destroy(nc);

    _stopServer(pid3);
    _stopServer(pid2);
    _stopServer(pid1);
    rmtree(datastore1);
    rmtree(datastore2);
    rmtree(datastore3);
}

void test_KeyValueManager(void)
{
    natsStatus          s;
    kvStore             *kv = NULL;
    kvConfig            kvc;
    jsStreamConfig      sc;
    jsErrCode           jerr = 0;
    jsStreamInfo        *si = NULL;

    JS_SETUP(2, 7, 2);

    test("kvConfig Init (bad args): ");
    s = kvConfig_Init(NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Create KV - bad args: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "TEST";
    s = js_CreateKeyValue(NULL, js, &kvc);
    if (s == NATS_INVALID_ARG)
        s = js_CreateKeyValue(&kv, NULL, &kvc);
    if (s == NATS_INVALID_ARG)
        s = js_CreateKeyValue(&kv, js, NULL);
    testCond((s == NATS_INVALID_ARG) && (kv == NULL));
    nats_clearLastError();

    test("Create KV - bad bucket name: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "This.is.not.a.valid.name!";
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond((s == NATS_INVALID_ARG) && (kv == NULL)
                && (strstr(nats_GetLastError(NULL), kvErrInvalidBucketName) != NULL));
    nats_clearLastError();

    test("Create KV - history too big: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "TEST";
    kvc.History = kvMaxHistory + 10;
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond((s == NATS_INVALID_ARG) && (kv == NULL)
                && (strstr(nats_GetLastError(NULL), kvErrHistoryTooLarge) != NULL));
    nats_clearLastError();

    test("Create KV: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "TEST";
    kvc.History = 3;
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond((s == NATS_OK) && (kv != NULL));

    test("Check discard policy: ");
    s = js_GetStreamInfo(&si, js, "KV_TEST", NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0) && (si != NULL) && (si->Config != NULL)
                && (si->Config->Discard == js_DiscardNew));
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("Destroy kv store: ")
    kvStore_Destroy(kv);
    kv = NULL;
    // Check that this is ok
    kvStore_Destroy(NULL);
    testCond(true);

    test("Bind (bad args): ");
    s = js_KeyValue(NULL, js, "TEST");
    if (s == NATS_INVALID_ARG)
        s = js_KeyValue(&kv, NULL, "TEST");
    if (s == NATS_INVALID_ARG)
        s = js_KeyValue(&kv, js, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_KeyValue(&kv, NULL, "");
    if (s == NATS_INVALID_ARG)
        s = js_KeyValue(&kv, NULL, "bad.bucket.name");
    testCond((s == NATS_INVALID_ARG) && (kv == NULL));
    nats_clearLastError();

    test("Bind (not found): ");
    s = js_KeyValue(&kv, js, "NOT_FOUND");
    testCond((s == NATS_NOT_FOUND) && (kv == NULL)
                && (nats_GetLastError(NULL) == NULL));

    test("Bind to existing: ");
    s = js_KeyValue(&kv, js, "TEST");
    testCond((s == NATS_OK) && (kv != NULL));

    test("Destroy kv store: ")
    kvStore_Destroy(kv);
    kv = NULL;
    testCond(true);

    test("Create non-kv stream: ");
    jsStreamConfig_Init(&sc);
    // Stream name has to start with "KV_" since this is how we
    // form the stream name: KV_ + bucket name.
    sc.Name = "KV_NON_KV_STREAM";
    sc.Subjects = (const char*[1]){"foo"};
    sc.SubjectsLen = 1;
    s = js_AddStream(NULL, js, &sc, NULL, &jerr);
    testCond((s == NATS_OK) && (jerr == 0));

    test("Bind to non-kv stream: ");
    s = js_KeyValue(&kv, js, "NON_KV_STREAM");
    testCond((s == NATS_INVALID_ARG) && (kv == NULL)
                && (strstr(nats_GetLastError(NULL), kvErrBadBucket) != NULL));
    nats_clearLastError();

    test("Delete kv store (bad args): ");
    s = js_DeleteKeyValue(NULL, "TEST");
    if (s == NATS_INVALID_ARG)
        s = js_DeleteKeyValue(js, NULL);
    if (s == NATS_INVALID_ARG)
        s = js_DeleteKeyValue(js, "");
    if (s == NATS_INVALID_ARG)
        s = js_DeleteKeyValue(js, "bad.bucket.name");
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Delete kv store: ");
    s = js_DeleteKeyValue(js, "TEST");
    testCond(s == NATS_OK);

    test("Check it is gone (bind should fail): ");
    s = js_KeyValue(&kv, js, "TEST");
    testCond((s == NATS_NOT_FOUND) && (kv == NULL));

    JS_TEARDOWN;
}

void test_KeyValueBasics(void)
{
    natsStatus          s;
    kvStore             *kv = NULL;
    kvEntry             *e  = NULL;
    kvStatus            *sts= NULL;
    uint64_t            rev = 0;
    kvConfig            kvc;
    int                 iterMax = 1;
    int                 i;
    char                bucketName[10];

    JS_SETUP(2, 6, 2);

    if (serverVersionAtLeast(2, 9, 0)) {
        iterMax = 2;
    }

    for (i=0; i<iterMax; i++)
    {
        snprintf(bucketName, sizeof(bucketName), "TEST%d", i);

        test("Create KV: ");
        kvConfig_Init(&kvc);
        kvc.Bucket = bucketName;
        kvc.History = 5;
        kvc.TTL = NATS_SECONDS_TO_NANOS(3600);
        kvc.Replicas = 1;
        s = js_CreateKeyValue(&kv, js, &kvc);
        testCond((s == NATS_OK) && (kv != NULL));

        if (i == 1)
        {
            // This means that we are running against 2.9.0+ server and
            // so we want to try without "direct get", so we will
            // artificially set the kv store to not use direct get API.
            natsMutex_Lock(kv->mu);
            kv->useDirect = false;
            natsMutex_Unlock(kv->mu);
        }

        test("Check bucket: ");
        s = (strcmp(kvStore_Bucket(kv), bucketName) == 0 ? NATS_OK : NATS_ERR);
        testCond(s == NATS_OK);

        test("Check bucket (returns NULL): ");
        s = (kvStore_Bucket(NULL) == NULL ? NATS_OK : NATS_ERR);
        testCond(s == NATS_OK);

        test("Check bytes is 0: ");
        s = kvStore_Status(&sts, kv);
        IFOK(s, (kvStatus_Bytes(sts) == 0 ? NATS_OK : NATS_ERR));
        testCond(s == NATS_OK);
        kvStatus_Destroy(sts);
        sts = NULL;

        test("Simple put (bad args): ");
        rev = 1234;
        s = kvStore_Put(&rev, NULL, "key", (const void*) "value", 5);
        testCond((s == NATS_INVALID_ARG) && (rev == 0));
        nats_clearLastError();

        test("Simple put (bad key): ");
        rev = 1234;
        s = kvStore_Put(&rev, kv, NULL, (const void*) "value", 5);
        if (s == NATS_INVALID_ARG)
            s = kvStore_Put(&rev, kv, "", (const void*) "value", 5);
        if (s == NATS_INVALID_ARG)
            s = kvStore_Put(&rev, kv, ".bad.key", (const void*) "value", 5);
        if (s == NATS_INVALID_ARG)
            s = kvStore_Put(&rev, kv, "bad.key.", (const void*) "value", 5);
        if (s == NATS_INVALID_ARG)
            s = kvStore_Put(&rev, kv, "this is a bad key", (const void*) "value", 5);
        testCond((s == NATS_INVALID_ARG) && (rev == 0)
                    && (strstr(nats_GetLastError(NULL), kvErrInvalidKey) != NULL));
        nats_clearLastError();

        test("Simple put: ");
        s = kvStore_Put(&rev, kv, "key", (const void*) "value", 5);
        testCond((s == NATS_OK) && (rev == 1));

        test("Get (bad args): ");
        s = kvStore_Get(NULL, kv, "key");
        if (s == NATS_INVALID_ARG)
            s = kvStore_Get(&e, NULL, "key");
        testCond((s == NATS_INVALID_ARG) && (e == NULL));
        nats_clearLastError();

        test("Get (bad key): ");
        s = kvStore_Get(&e, kv, NULL);
        if (s == NATS_INVALID_ARG)
            s = kvStore_Get(&e, kv, "");
        if (s == NATS_INVALID_ARG)
            s = kvStore_Get(&e, kv, ".bad.key");
        if (s == NATS_INVALID_ARG)
            s = kvStore_Get(&e, kv, "bad.key.");
        if (s == NATS_INVALID_ARG)
            s = kvStore_Get(&e, kv, "this is a bad key");
        testCond((s == NATS_INVALID_ARG) && (e == NULL)
                    && (strstr(nats_GetLastError(NULL), kvErrInvalidKey) != NULL));
        nats_clearLastError();

        test("Simple get: ");
        s = kvStore_Get(&e, kv, "key");
        testCond((s == NATS_OK) && (e != NULL)
                    && (kvEntry_ValueLen(e) == 5)
                    && (memcmp(kvEntry_Value(e), "value", 5) == 0)
                    && (kvEntry_Revision(e) == 1));

        test("Destroy entry: ");
        kvEntry_Destroy(e);
        e = NULL;
        // Check that this is ok
        kvEntry_Destroy(NULL);
        testCond(true);

        test("Get not found: ");
        s = kvStore_Get(&e, kv, "not.found");
        testCond((s == NATS_NOT_FOUND) && (e == NULL)
                    && (nats_GetLastError(NULL) == NULL));

        test("Simple put string: ");
        s = kvStore_PutString(&rev, kv, "key", "value2");
        testCond((s == NATS_OK) && (rev == 2));

        test("Simple get string: ");
        s = kvStore_Get(&e, kv, "key");
        testCond((s == NATS_OK) && (e != NULL)
                    && (strcmp(kvEntry_ValueString(e), "value2") == 0)
                    && (kvEntry_Revision(e) == 2));

        test("Destroy entry: ");
        kvEntry_Destroy(e);
        e = NULL;
        testCond(true);

        test("Get revision (bad args): ");
        s = kvStore_GetRevision(&e, kv, "key", 0);
        testCond((s == NATS_INVALID_ARG) && (e == NULL)
                    && (strstr(nats_GetLastError(NULL), kvErrInvalidRevision) != NULL));
        nats_clearLastError();

        test("Get revision: ");
        s = kvStore_GetRevision(&e, kv, "key", 1);
        testCond((s == NATS_OK) && (e != NULL)
                    && (strcmp(kvEntry_ValueString(e), "value") == 0)
                    && (kvEntry_Revision(e) == 1));

        test("Destroy entry: ");
        kvEntry_Destroy(e);
        e = NULL;
        testCond(true);

        test("Delete key (bad args): ");
        s = kvStore_Delete(NULL, "key");
        testCond(s == NATS_INVALID_ARG);
        nats_clearLastError();

        test("Delete key (bad key): ");
        s = kvStore_Delete(kv, NULL);
        if (s == NATS_INVALID_ARG)
            s = kvStore_Delete(kv, "");
        if (s == NATS_INVALID_ARG)
            s = kvStore_Delete(kv, ".bad.key");
        if (s == NATS_INVALID_ARG)
            s = kvStore_Delete(kv, "bad.key.");
        if (s == NATS_INVALID_ARG)
            s = kvStore_Delete(kv, "this is a bad key");
        testCond((s == NATS_INVALID_ARG)
                    && (strstr(nats_GetLastError(NULL), kvErrInvalidKey) != NULL));
        nats_clearLastError();

        test("Delete key: ");
        s = kvStore_Delete(kv, "key");
        testCond(s == NATS_OK);

        test("Check key gone: ");
        s = kvStore_Get(&e, kv, "key");
        testCond((s == NATS_NOT_FOUND) && (e == NULL)
                    && (nats_GetLastError(NULL) == NULL));

        test("Create (bad args): ");
        s = kvStore_Create(&rev, NULL, "key", (const void*) "value3", 6);
        testCond(s == NATS_INVALID_ARG);
        nats_clearLastError();

        test("Create (bad key): ");
        s = kvStore_Create(&rev, kv, NULL, (const void*) "value3", 6);
        if (s == NATS_INVALID_ARG)
            s = kvStore_Create(&rev, kv, "", (const void*) "value3", 6);
        if (s == NATS_INVALID_ARG)
            s = kvStore_Create(&rev, kv, ".bad.key", (const void*) "value3", 6);
        if (s == NATS_INVALID_ARG)
            s = kvStore_Create(&rev, kv, "bad.key.", (const void*) "value3", 6);
        if (s == NATS_INVALID_ARG)
            s = kvStore_Create(&rev, kv, "this..is a bad key", (const void*) "value3", 6);
        testCond((s == NATS_INVALID_ARG)
                    && (strstr(nats_GetLastError(NULL), kvErrInvalidKey) != NULL));
        nats_clearLastError();

        test("Create: ");
        s = kvStore_Create(&rev, kv, "key", (const void*) "value3", 6);
        testCond((s == NATS_OK) && (rev == 4));

        test("Create fail, since already exists: ");
        s = kvStore_Create(&rev, kv, "key", (const void*) "value4", 6);
        testCond((s == NATS_ERR) && (rev == 0));
        nats_clearLastError();

        test("Update (bad args): ");
        s = kvStore_Update(&rev, NULL, "key", (const void*) "value4", 6, 4);
        testCond((s == NATS_INVALID_ARG) && (rev == 0));
        nats_clearLastError();

        test("Update (bad key): ");
        s = kvStore_Update(&rev, kv, NULL, (const void*) "value4", 6, 4);
        if (s == NATS_INVALID_ARG)
            s = kvStore_Update(&rev, kv, "", (const void*) "value4", 6, 4);
        if (s == NATS_INVALID_ARG)
            s = kvStore_Update(&rev, kv, ".bad.key", (const void*) "value4", 6, 4);
        if (s == NATS_INVALID_ARG)
            s = kvStore_Update(&rev, kv, "bad.key.", (const void*) "value4", 6, 4);
        if (s == NATS_INVALID_ARG)
            s = kvStore_Update(&rev, kv, "bad&key", (const void*) "value4", 6, 4);
        testCond((s == NATS_INVALID_ARG) && (rev == 0)
                    && (strstr(nats_GetLastError(NULL), kvErrInvalidKey) != NULL));
        nats_clearLastError();

        test("Update: ");
        s = kvStore_Update(&rev, kv, "key", (const void*) "value4", 6, 4);
        testCond((s == NATS_OK) && (rev == 5));

        test("Update fail because wrong rev: ");
        s = kvStore_Update(NULL, kv, "key", (const void*) "value5", 6, 4);
        testCond(s == NATS_ERR);
        nats_clearLastError();

        test("Update ok: ");
        s = kvStore_Update(&rev, kv, "key", (const void*) "value5", 6, rev);
        testCond((s == NATS_OK) && (rev == 6));
        nats_clearLastError();

        test("Create (string): ");
        s = kvStore_CreateString(&rev, kv, "key2", "value1");
        testCond((s == NATS_OK) && (rev == 7));

        test("Update ok (string): ");
        s = kvStore_UpdateString(&rev, kv, "key2", "value2", rev);
        testCond((s == NATS_OK) && (rev == 8));

        test("Status (bad args): ");
        s = kvStore_Status(NULL, kv);
        if (s == NATS_INVALID_ARG)
            s = kvStore_Status(&sts, NULL);
        testCond((s == NATS_INVALID_ARG) && (sts == NULL));
        nats_clearLastError();

        test("Status: ");
        s = kvStore_Status(&sts, kv);
        testCond((s == NATS_OK) && (sts != NULL));

        test("Check history: ");
        s = (kvStatus_History(sts) == 5 ? NATS_OK : NATS_ERR);
        testCond(s == NATS_OK);

        test("Check bucket: ");
        s = (strcmp(kvStatus_Bucket(sts), bucketName) == 0 ? NATS_OK : NATS_ERR);
        testCond(s == NATS_OK);

        test("Check TTL: ");
        s = (kvStatus_TTL(sts) == NATS_SECONDS_TO_NANOS(3600) ? NATS_OK : NATS_ERR);
        testCond(s == NATS_OK);

        test("Check values: ");
        s = (kvStatus_Values(sts) == 7 ? NATS_OK : NATS_ERR);
        testCond(s == NATS_OK);

        test("Check replicas: ");
        s = (kvStatus_Replicas(sts) == 1 ? NATS_OK : NATS_ERR);
        testCond(s == NATS_OK);

        test("Check bytes: ");
        {
            jsStreamInfo *si = NULL;

            if (i == 0)
                s = js_GetStreamInfo(&si, js, "KV_TEST0", NULL, NULL);
            else
                s = js_GetStreamInfo(&si, js, "KV_TEST1", NULL, NULL);
            IFOK(s, (kvStatus_Bytes(sts) == si->State.Bytes ? NATS_OK : NATS_ERR));

            jsStreamInfo_Destroy(si);
        }
        testCond(s == NATS_OK);

        test("Check status with NULL: ");
        if ((kvStatus_History(NULL) != 0) || (kvStatus_Bucket(NULL) != NULL)
            || (kvStatus_TTL(NULL) != 0) || (kvStatus_Values(NULL) != 0)
            || (kvStatus_Bytes(NULL) != 0))
        {
            s = NATS_ERR;
        }
        testCond(s == NATS_OK);

        test("Destroy status: ");
        kvStatus_Destroy(sts);
        sts = NULL;
        // Check that this is ok
        kvStatus_Destroy(NULL);
        testCond(true);

        test("Put for revision check: ");
        s = kvStore_PutString(&rev, kv, "test.rev.one", "val1");
        IFOK(s, kvStore_PutString(NULL, kv, "test.rev.two", "val2"));
        testCond(s == NATS_OK);

        test("Get revision (bad args): ");
        s = kvStore_GetRevision(&e, kv, "test.rev.one", 0);
        testCond((s == NATS_INVALID_ARG) && (e == NULL)
                    && (strstr(nats_GetLastError(NULL), kvErrInvalidRevision) != NULL));
        nats_clearLastError();

        test("Get revision: ");
        s = kvStore_GetRevision(&e, kv, "test.rev.one", rev);
        testCond((s == NATS_OK) && (e != NULL)
                    && (strcmp(kvEntry_ValueString(e), "val1") == 0)
                    && (kvEntry_Revision(e) == rev));
        kvEntry_Destroy(e);
        e = NULL;

        test("Get wrong revision for the key: ");
        s = kvStore_GetRevision(&e, kv, "test.rev.two", rev);
        testCond((s == NATS_NOT_FOUND) && (e == NULL));

        test("Destroy kv store: ");
        kvStore_Destroy(kv);
        testCond(true);
        kv = NULL;
    }

    JS_TEARDOWN;
}

static bool
_expectInitDone(kvWatcher *w)
{
    natsStatus  s;
    kvEntry     *e = NULL;

    test("Check init done: ");
    s = kvWatcher_Next(&e, w, 1000);
    return ((s == NATS_OK) && (e == NULL));
}

static bool
_expectUpdate(kvWatcher *w, const char *key, const char *val, uint64_t rev)
{
    natsStatus  s;
    kvEntry     *e = NULL;

    test("Check update: ");
    s = kvWatcher_Next(&e, w, 1000);
    if ((s != NATS_OK) || (e == NULL))
        return false;

    if ((strcmp(kvEntry_Bucket(e), "WATCH") != 0)
        || (strcmp(kvEntry_Key(e), key) != 0)
        || (strcmp(kvEntry_ValueString(e), val) != 0)
        || (kvEntry_Revision(e) != rev)
        || (kvEntry_Created(e) == 0))
    {
        return false;
    }
    kvEntry_Destroy(e);
    return true;
}

static bool
_expectDelete(kvWatcher *w, const char *key, uint64_t rev)
{
    natsStatus  s;
    kvEntry     *e = NULL;

    test("Check update: ");
    s = kvWatcher_Next(&e, w, 1000);
    if ((s != NATS_OK) || (e == NULL))
        return false;

    if ((kvEntry_Operation(e) != kvOp_Delete)
        || (kvEntry_Revision(e) != rev))
    {
        return false;
    }
    kvEntry_Destroy(e);
    return true;
}

static void
_stopWatcher(void *closure)
{
    kvWatcher *w = (kvWatcher*) closure;

    nats_Sleep(100);
    kvWatcher_Stop(w);
}

void test_KeyValueWatch(void)
{
    natsStatus          s;
    kvStore             *kv = NULL;
    kvWatcher           *w  = NULL;
    kvEntry             *e  = NULL;
    natsThread          *t  = NULL;
    int                 plc = 0;
    int                 plb = 0;
    kvConfig            kvc;
    int64_t             start;

    JS_SETUP(2, 6, 2);

    test("Create KV: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "WATCH";
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond(s == NATS_OK);

    test("Create watcher (bad args): ");
    s = kvStore_Watch(NULL, kv, "foo", NULL);
    if (s == NATS_INVALID_ARG)
        s = kvStore_Watch(&w, NULL, "foo", NULL);
    if (s == NATS_INVALID_ARG)
        s = kvStore_Watch(&w, kv, NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = kvStore_Watch(&w, kv, "", NULL);
    testCond((s == NATS_INVALID_ARG) && (w == NULL));
    nats_clearLastError();

    test("Create watcher: ");
    s = kvStore_WatchAll(&w, kv, NULL);
    testCond((s == NATS_OK) && (w != NULL));

    testCond(_expectInitDone(w));

    test("Create: ");
    s = kvStore_CreateString(NULL, kv, "name", "derek");
    testCond(s == NATS_OK);
    testCond(_expectUpdate(w, "name", "derek", 1));

    test("Put: ");
    s = kvStore_PutString(NULL, kv, "name", "rip");
    testCond(s == NATS_OK);
    testCond(_expectUpdate(w, "name", "rip", 2));

    test("Put: ");
    s = kvStore_PutString(NULL, kv, "name", "ik");
    testCond(s == NATS_OK);
    testCond(_expectUpdate(w, "name", "ik", 3));

    test("Put: ");
    s = kvStore_PutString(NULL, kv, "age", "22");
    testCond(s == NATS_OK);
    testCond(_expectUpdate(w, "age", "22", 4));

    test("Put: ");
    s = kvStore_PutString(NULL, kv, "age", "33");
    testCond(s == NATS_OK);
    testCond(_expectUpdate(w, "age", "33", 5));

    test("Delete: ");
    s = kvStore_Delete(kv, "age");
    testCond(s == NATS_OK);
    testCond(_expectDelete(w, "age", 6));

    test("Next (bad args): ");
    s = kvWatcher_Next(NULL, w, 1000);
    if (s == NATS_INVALID_ARG)
        s = kvWatcher_Next(&e, NULL, 1000);
    if (s == NATS_INVALID_ARG)
        s = kvWatcher_Next(&e, w, 0);
    if (s == NATS_INVALID_ARG)
        s = kvWatcher_Next(&e, w, -1000);
    testCond((s == NATS_INVALID_ARG) && (e == NULL));
    nats_clearLastError();

    test("Next (timeout): ");
    s = kvWatcher_Next(&e, w, 1);
    testCond((s == NATS_TIMEOUT) && (e == NULL));
    nats_clearLastError();

    test("Stop (bad args): ");
    s = kvWatcher_Stop(NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Stop: ");
    s = kvWatcher_Stop(w);
    testCond(s == NATS_OK);

    test("Stop again: ");
    s = kvWatcher_Stop(w);
    testCond(s == NATS_OK);

    test("Next fails: ");
    s = kvWatcher_Next(&e, w, 1000);
    testCond((s == NATS_ILLEGAL_STATE) && (e == NULL))
    nats_clearLastError();

    test("Destroy watcher: ");
    kvWatcher_Destroy(w);
    w = NULL;
    // Check that this is ok
    kvWatcher_Destroy(NULL);
    testCond(true);

    // Now try wildcard matching and make sure we only get last value when starting.
    test("Put values in different keys: ");
    s = kvStore_PutString(NULL, kv, "t.name", "derek");
    IFOK(s, kvStore_PutString(NULL, kv, "t.name", "ik"));
    IFOK(s, kvStore_PutString(NULL, kv, "t.age", "22"));
    IFOK(s, kvStore_PutString(NULL, kv, "t.age", "49"));
    testCond(s == NATS_OK);

    test("Create watcher: ");
    s = kvStore_Watch(&w, kv, "t.*", NULL);
    testCond(s == NATS_OK);

    test("Check pending limits: ");
    natsMutex_Lock(w->mu);
    s = natsSubscription_GetPendingLimits(w->sub, &plc, &plb);
    natsMutex_Unlock(w->mu);
    testCond((s == NATS_OK) && (plc == -1) && (plb == -1));

    testCond(_expectUpdate(w, "t.name", "ik", 8));
    testCond(_expectUpdate(w, "t.age", "49", 10));
    testCond(_expectInitDone(w));

    test("Block: ");
    start = nats_Now();
    s = natsThread_Create(&t, _stopWatcher, (void*) w);
    IFOK(s, kvWatcher_Next(&e, w, 10000));
    testCond((s == NATS_ILLEGAL_STATE) && (e == NULL)
                && ((nats_Now() - start) <= 1000));
    nats_clearLastError();

    natsThread_Join(t);
    natsThread_Destroy(t);
    kvWatcher_Destroy(w);
    w = NULL;

    // Now try with UpdatesOnly and make sure we don't get the initial values.
    test("Create watcher with UpdatesOnly: ");
    kvWatchOptions o = {.UpdatesOnly = true};
    s = kvStore_Watch(&w, kv, "t.*", &o);
    testCond(s == NATS_OK);

    IFOK(s, kvStore_PutString(NULL, kv, "t.age", "57"));
    testCond(_expectUpdate(w, "t.age", "57", 11));
    kvWatcher_Destroy(w);
    kvStore_Destroy(kv);

    JS_TEARDOWN;
}

void test_KeyValueWatchMulti(void)
{
    natsStatus s;
    kvStore *kv = NULL;
    kvWatcher *w = NULL;
    kvConfig kvc;

    JS_SETUP(2, 10, 14);

    test("Create KV: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "WATCH";
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond(s == NATS_OK);

    // Now try wildcard matching and make sure we only get last value when starting.
    test("Put values in different keys: ");
    s = kvStore_PutString(NULL, kv, "a.name", "A");
    IFOK(s, kvStore_PutString(NULL, kv, "b.name", "B"));
    IFOK(s, kvStore_PutString(NULL, kv, "a.name", "AA"));
    IFOK(s, kvStore_PutString(NULL, kv, "b.name", "BB"));
    IFOK(s, kvStore_PutString(NULL, kv, "a.age", "22"));
    IFOK(s, kvStore_PutString(NULL, kv, "a.age", "99"));
    testCond(s == NATS_OK);

    test("Create watcher: ");
    const char **subjects = (const char *[]){"a.*", "b.*", "c.*"};
    s = kvStore_WatchMulti(&w, kv, subjects, 3, NULL);
    testCond(s == NATS_OK);

    testCond(_expectUpdate(w, "a.name", "AA", 3));
    testCond(_expectUpdate(w, "b.name", "BB", 4));
    testCond(_expectUpdate(w, "a.age", "99", 6));
    testCond(_expectInitDone(w));

    IFOK(s, kvStore_PutString(NULL, kv, "c.occupation", "gardener"));
    IFOK(s, kvStore_PutString(NULL, kv, "XXX.occupation", "no, plumber"));
    IFOK(s, kvStore_PutString(NULL, kv, "c.occupation", "a digital plumber for a digital garden"));
    testCond(_expectUpdate(w, "c.occupation", "gardener", 7));
    testCond(_expectUpdate(w, "c.occupation", "a digital plumber for a digital garden", 9));

    kvWatcher_Destroy(w);
    kvStore_Destroy(kv);

    JS_TEARDOWN;
}

void test_KeyValueHistory(void)
{
    natsStatus          s;
    kvStore             *kv = NULL;
    kvEntry             *e  = NULL;
    kvEntryList         l;
    kvWatchOptions      o;
    kvConfig            kvc;
    int                 i;

    JS_SETUP(2, 6, 2);

    test("Create KV: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "WATCH";
    kvc.History = 10;
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond(s == NATS_OK);

    test("Populate: ");
    for (i=0; (s == NATS_OK) && (i<50); i++)
    {
        char tmp[16];

        snprintf(tmp, sizeof(tmp), "%d", i+22);
        s = kvStore_PutString(NULL, kv, "age", tmp);
    }
    testCond(s == NATS_OK);

    test("Get history (bad args): ");
    s = kvStore_History(NULL, kv, "age", NULL);
    if (s == NATS_INVALID_ARG)
        s = kvStore_History(&l, NULL, "age", NULL);
    if (s == NATS_INVALID_ARG)
        s = kvStore_History(&l, kv, NULL, NULL);
    if (s == NATS_INVALID_ARG)
        s = kvStore_History(&l, kv, "", NULL);
    testCond((s == NATS_INVALID_ARG) && (l.Entries == NULL) && (l.Count == 0));
    nats_clearLastError();

    test("Get history (timeout): ");
    kvWatchOptions_Init(&o);
    o.Timeout = 1;
    s = kvStore_History(&l, kv, "age", &o);
    testCond(((s == NATS_OK) && (l.Entries != NULL) && (l.Count == 10))
            || ((s == NATS_TIMEOUT) && (l.Entries == NULL) && (l.Count == 0)));
    nats_clearLastError();
    kvEntryList_Destroy(&l);

    test("Get History: ");
    s = kvStore_History(&l, kv, "age", NULL);
    testCond((s == NATS_OK) && (l.Entries != NULL) && (l.Count == 10));

    test("Check values: ");
    for (i=0; (s == NATS_OK) && (i < 10); i++)
    {
        e = l.Entries[i];
        if (e == NULL)
            s = NATS_ERR;
        if (strcmp(kvEntry_Key(e), "age") != 0)
            s = NATS_ERR;
        else if (kvEntry_Revision(e) != (uint64_t)(i+41))
            s = NATS_ERR;
        else
        {
            int val = (int) nats_ParseInt64(kvEntry_Value(e), kvEntry_ValueLen(e));
            if (val != i+62)
                s = NATS_ERR;
        }
    }
    testCond(s == NATS_OK);

    test("Destroy list: ")
    kvEntryList_Destroy(&l);
    testCond((l.Entries == NULL) && (l.Count == 0));

    // Check that this is ok
    kvEntryList_Destroy(NULL);

    kvStore_Destroy(kv);

    JS_TEARDOWN;
}

void test_KeyValueKeys(void)
{
    natsStatus          s;
    kvStore             *kv = NULL;
    kvKeysList          l;
    bool                nameOK = false;
    bool                ageOK = false;
    bool                countryOK = false;
    char                *k = NULL;
    kvConfig            kvc;
    kvWatchOptions      o;
    int                 i;

    JS_SETUP(2, 6, 2);

    test("Create KV: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "KVS";
    kvc.History = 2;
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond(s == NATS_OK);

    test("Populate: ");
    s = kvStore_PutString(NULL, kv, "name", "derek");
    IFOK(s, kvStore_PutString(NULL, kv, "age", "22"));
    IFOK(s, kvStore_PutString(NULL, kv, "country", "US"));
    IFOK(s, kvStore_PutString(NULL, kv, "name", "ivan"));
    IFOK(s, kvStore_PutString(NULL, kv, "age", "33"));
    IFOK(s, kvStore_PutString(NULL, kv, "country", "US"));
    IFOK(s, kvStore_PutString(NULL, kv, "name", "rip"));
    IFOK(s, kvStore_PutString(NULL, kv, "age", "44"));
    IFOK(s, kvStore_PutString(NULL, kv, "country", "MT"));
    testCond(s == NATS_OK);

    test("Get keys (bad args): ");
    s = kvStore_Keys(NULL, kv, NULL);
    if (s == NATS_INVALID_ARG)
        s = kvStore_Keys(&l, NULL, NULL);
    testCond((s == NATS_INVALID_ARG) && (l.Keys == NULL) && (l.Count == 0));
    nats_clearLastError();

    test("Get keys (timeout): ");
    kvWatchOptions_Init(&o);
    o.Timeout = 1;
    s = kvStore_Keys(&l, kv, &o);
    testCond((((s == NATS_OK) && (l.Keys != NULL) && (l.Count == 3)))
                || ((s == NATS_TIMEOUT) && (l.Keys == NULL) && (l.Count == 0)));
    nats_clearLastError();
    kvKeysList_Destroy(&l);

    test("Get keys: ");
    s = kvStore_Keys(&l, kv, NULL);
    testCond((s == NATS_OK) && (l.Keys != NULL) && (l.Count == 3));

    test("Check keys: ");
    for (i=0; (s == NATS_OK) && (i<3); i++)
    {
        char *k = l.Keys[i];
        if (k == NULL)
            s = NATS_ERR;
        else
        {
            if (strcmp(k, "name") == 0)
                nameOK = true;
            else if (strcmp(k, "age") == 0)
                ageOK = true;
            else if (strcmp(k, "country") == 0)
                countryOK = true;
        }
    }
    testCond((s == NATS_OK) && nameOK && ageOK && countryOK);

    test("Destroy list: ");
    kvKeysList_Destroy(&l);
    testCond((l.Keys == NULL) && (l.Count == 0));
    // Check this is ok
    kvKeysList_Destroy(NULL);

    // Make sure delete and purge do the right thing and not return the keys.
    test("Delete name: ");
    s = kvStore_Delete(kv, "name");
    testCond(s == NATS_OK);

    test("Purge country: ");
    s = kvStore_Purge(kv, "country", NULL);
    testCond(s == NATS_OK);

    test("Get keys: ");
    s = kvStore_Keys(&l, kv, NULL);
    testCond((s == NATS_OK) && (l.Keys != NULL) && (l.Count == 1));

    test("Check key: ");
    k = l.Keys[0];
    s = ((k != NULL) && (strcmp(k, "age") == 0) ? NATS_OK : NATS_ERR);
    testCond(s == NATS_OK);
    kvKeysList_Destroy(&l);

    kvStore_Destroy(kv);

    JS_TEARDOWN;
}

void test_KeyValueKeysWithFilters(void)
{
    natsStatus          s;
    kvStore             *kv = NULL;
    kvKeysList          l;
    kvConfig            kvc;
    const char **defaultSubject = (const char *[]){">"};
    l.Count = 0;
    l.Keys = NULL;

    JS_SETUP(2, 10, 14);

    test("Create KV: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "KVSF";
    kvc.History = 2;
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond(s == NATS_OK);

    test("Populate: ");
    s = kvStore_PutString(NULL, kv, "a.b", "a.b");
    IFOK(s, kvStore_PutString(NULL, kv, "a.d", "a.d"));
    IFOK(s, kvStore_PutString(NULL, kv, "c.d", "c.d"));
    IFOK(s, kvStore_PutString(NULL, kv, "e.f", "e.f"));
    IFOK(s, kvStore_PutString(NULL, kv, "e.a.f", "e.a.f"));
    testCond(s == NATS_OK);

    test("Get keys with filters (bad args): List is NULL");
    s = kvStore_KeysWithFilters(NULL, kv, defaultSubject, 1, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Get keys with filters (bad args): filters is NULL");
    s = kvStore_KeysWithFilters(&l, kv, NULL, 0, NULL);
    testCond((s == NATS_INVALID_ARG) && (l.Keys == NULL) && (l.Count == 0));
    nats_clearLastError();

    test("Get keys with filters (bad args): numFilters is 0");
    s = kvStore_KeysWithFilters(&l, kv, defaultSubject, 0, NULL);
    testCond((s == NATS_INVALID_ARG) && (l.Keys == NULL) && (l.Count == 0));
    nats_clearLastError();

    test("Get keys with filters (bad args): numFilters is <0");
    s = kvStore_KeysWithFilters(&l, kv, defaultSubject, -10, NULL);
    testCond((s == NATS_INVALID_ARG) && (l.Keys == NULL) && (l.Count == 0));
    nats_clearLastError();

    test("Get keys with filters (bad args): empty string");
    const char **filter0 = (const char *[]){"a.*", "", "b.*"};
    s = kvStore_KeysWithFilters(&l, kv, filter0, 3, NULL);
    testCond((s == NATS_INVALID_ARG) && (l.Keys == NULL) && (l.Count == 0));
    nats_clearLastError();

    test("Get keys with filters (bad args): kv is NULL");
    s = kvStore_KeysWithFilters(&l, NULL, defaultSubject, 1, NULL);
    testCond((s == NATS_INVALID_ARG) && (l.Keys == NULL) && (l.Count == 0));
    nats_clearLastError();
    kvKeysList_Destroy(&l);

    test("filter: a.*");
    const char **filter1 = (const char *[]){"a.*"};
    s = kvStore_KeysWithFilters(&l, kv, filter1, 1, NULL);
    testCond((s == NATS_OK) && (l.Keys != NULL) && (l.Count == 2));
    nats_clearLastError();
    kvKeysList_Destroy(&l);

    test("filter: *.a.*");
    const char **filter2 = (const char *[]){"*.a.*"};
    s = kvStore_KeysWithFilters(&l, kv, filter2, 1, NULL);
    testCond((s == NATS_OK) && (l.Keys != NULL) && (l.Count == 1));
    nats_clearLastError();
    kvKeysList_Destroy(&l);

    test("filter: *.a");
    const char **filter3 = (const char *[]){"*.a"};
    s = kvStore_KeysWithFilters(&l, kv, filter3, 1, NULL);
    testCond((s == NATS_OK) && (l.Keys == NULL) && (l.Count == 0));
    nats_clearLastError();
    kvKeysList_Destroy(&l);

    test("filter: e.a.f");
    const char **filter4 = (const char *[]){"e.a.f"};
    s = kvStore_KeysWithFilters(&l, kv, filter4, 1, NULL);
    testCond((s == NATS_OK) && (l.Keys != NULL) && (l.Count == 1));
    nats_clearLastError();
    kvKeysList_Destroy(&l);

    test("filter: >");
    const char **filter5 = (const char *[]){">"};
    s = kvStore_KeysWithFilters(&l, kv, filter5, 1, NULL);
    testCond((s == NATS_OK) && (l.Keys != NULL) && (l.Count == 5));
    nats_clearLastError();
    kvKeysList_Destroy(&l);

    test("filter: multiple overlapping filters");
    const char **filter6 = (const char *[]){"*.a","a.*","*.a.*"};
    s = kvStore_KeysWithFilters(&l, kv, filter6, 3, NULL);
    // consumer subject filters cannot overlap
    testCond((s == NATS_ERR) && (l.Keys == NULL) && (l.Count == 0));
    nats_clearLastError();
    kvKeysList_Destroy(&l);

    test("filter: multiple non overlapping filters");
    const char **filter7 = (const char *[]){"a.*","e.*"};
    s = kvStore_KeysWithFilters(&l, kv, filter7, 2, NULL);
    testCond((s == NATS_OK) && (l.Keys != NULL) && (l.Count == 3));
    nats_clearLastError();
    kvKeysList_Destroy(&l);

    // Delete the key and check if returned after filtering
    test("Delete a.b:");
    s = kvStore_Delete(kv, "a.b");
    testCond(s == NATS_OK);

    test("a.b should not be returned post deletion")
    const char **filter8 = (const char *[]){"a.b"};
    s = kvStore_KeysWithFilters(&l, kv, filter8, 1, NULL);
    testCond((s == NATS_OK) && (l.Keys == NULL) && (l.Count == 0));
    nats_clearLastError();
    kvKeysList_Destroy(&l);

    // Purge the key and check if returned after filtering
    test("Purge a.b:");
    s = kvStore_Purge(kv, "a.d", NULL);
    testCond(s == NATS_OK);

    test("a.d should not be returned post purge")
    const char **filter9 = (const char *[]){"a.d"};
    s = kvStore_KeysWithFilters(&l, kv, filter9, 1, NULL);
    testCond((s == NATS_OK) && (l.Keys == NULL) && (l.Count == 0));
    nats_clearLastError();
    kvKeysList_Destroy(&l);

    kvStore_Destroy(kv);

    JS_TEARDOWN;
}

void test_KeyValueDeleteVsPurge(void)
{
    natsStatus          s;
    kvStore             *kv = NULL;
    kvEntry             *e  = NULL;
    kvEntryList         l;
    kvConfig            kvc;
    int                 i;

    JS_SETUP(2, 6, 2);

    test("Create KV: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "KVS";
    kvc.History = 10;
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond(s == NATS_OK);

    test("Populate: ");
    s = kvStore_PutString(NULL, kv, "name", "derek");
    IFOK(s, kvStore_PutString(NULL, kv, "age", "22"));
    IFOK(s, kvStore_PutString(NULL, kv, "name", "ivan"));
    IFOK(s, kvStore_PutString(NULL, kv, "age", "33"));
    IFOK(s, kvStore_PutString(NULL, kv, "name", "rip"));
    IFOK(s, kvStore_PutString(NULL, kv, "age", "44"));
    testCond(s == NATS_OK);

    test("Delete age: ");
    s = kvStore_Delete(kv, "age");
    testCond(s == NATS_OK);

    test("Get age history: ");
    s = kvStore_History(&l, kv, "age", NULL);
    testCond((s == NATS_OK) && (l.Entries != NULL) && (l.Count == 4));

    test("Check: ");
    for (i=0;(s == NATS_OK) && (i<4); i++)
    {
        e = l.Entries[i];
        if (e == NULL)
            s = NATS_ERR;
        else if ((int) kvEntry_Delta(e) != (3-i))
            s = NATS_ERR;
    }
    testCond(s == NATS_OK);
    kvEntryList_Destroy(&l);

    test("Purge name: ");
    s = kvStore_Purge(kv, "name", NULL);
    testCond(s == NATS_OK);

    test("Check marker: ");
    s = kvStore_Get(&e, kv, "age");
    testCond((s == NATS_NOT_FOUND) && (e == NULL));

    test("Get history: ");
    s = kvStore_History(&l, kv, "name", NULL);
    testCond((s == NATS_OK) && (l.Entries != NULL) && (l.Count == 1));

    test("Check: ");
    e = l.Entries[0];
    s = ((e != NULL) && (kvEntry_Operation(e) == kvOp_Purge) ? NATS_OK : NATS_ERR);
    testCond(s == NATS_OK);
    kvEntryList_Destroy(&l);

    kvStore_Destroy(kv);

    JS_TEARDOWN;
}

void test_KeyValueDeleteTombstones(void)
{
    natsStatus          s;
    kvStore             *kv = NULL;
    char                *v  = NULL;
    jsStreamInfo        *si = NULL;
    kvConfig            kvc;
    kvPurgeOptions      po;
    int                 i;

    JS_SETUP(2, 6, 2);

    test("Create KV: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "KVS";
    kvc.History = 10;
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond(s == NATS_OK);

    test("Create asset: ");
    v = (char*) malloc(100);
    if (v != NULL)
    {
        for (i=0; i<100; i++)
            v[i] = 'A' + (char)(i % 26);
        v[99] = '\0';
    }
    testCond(v != NULL);

    test("Populate: ");
    for (i=0; (s == NATS_OK) && (i<100); i++)
    {
        char tmp[64];
        snprintf(tmp, sizeof(tmp), "key-%d", i);
        s = kvStore_PutString(NULL, kv, tmp, v);
    }
    testCond(s == NATS_OK);
    free(v);

    test("Delete: ");
    for (i=0; (s == NATS_OK) && (i<100); i++)
    {
        char tmp[64];
        snprintf(tmp, sizeof(tmp), "key-%d", i);
        s = kvStore_Delete(kv, tmp);
    }
    testCond(s == NATS_OK);

    test("Purge deletes (bad args): ");
    s = kvStore_PurgeDeletes(NULL, NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Purge deletes: ");
    kvPurgeOptions_Init(&po);
    po.DeleteMarkersOlderThan = -1;
    s = kvStore_PurgeDeletes(kv, &po);
    testCond(s == NATS_OK);

    test("Check stream: ");
    s = js_GetStreamInfo(&si, js, "KV_KVS", NULL, NULL);
    testCond((s == NATS_OK) && (si != NULL) && (si->State.Msgs == 0));
    jsStreamInfo_Destroy(si);

    kvStore_Destroy(kv);

    JS_TEARDOWN;
}

void test_KeyValuePurgeDeletesMarkerThreshold(void)
{
    natsStatus          s;
    kvStore             *kv = NULL;
    kvConfig            kvc;
    kvPurgeOptions      po;
    kvEntryList         list;

    JS_SETUP(2, 7, 2);

    test("Create KV: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "KVS";
    kvc.History = 10;
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond(s == NATS_OK);

    test("Put keys: ")
    s = kvStore_PutString(NULL, kv, "foo", "foo1");
    IFOK(s, kvStore_PutString(NULL, kv, "bar", "bar1"));
    IFOK(s, kvStore_PutString(NULL, kv, "foo", "foo2"));
    testCond(s == NATS_OK);

    test("Delete foo: ");
    s = kvStore_Delete(kv, "foo");
    testCond(s == NATS_OK);

    nats_Sleep(500);

    test("Delete bar: ");
    s = kvStore_Delete(kv, "bar");
    testCond(s == NATS_OK);

    test("PurgeOptions init bad args: ");
    s = kvPurgeOptions_Init(NULL);
    testCond(s == NATS_INVALID_ARG);
    nats_clearLastError();

    test("Purge deletes: ");
    kvPurgeOptions_Init(&po);
    po.DeleteMarkersOlderThan = NATS_MILLIS_TO_NANOS(100);
    s = kvStore_PurgeDeletes(kv, &po);
    testCond(s == NATS_OK);

	// The key foo should have been completely cleared of the data
	// and the delete marker.
    test("Check foo history: ")
    s = kvStore_History(&list, kv, "foo", NULL);
    testCond((s == NATS_NOT_FOUND) && (list.Entries == NULL) && (list.Count == 0));

    test("Check bar history: ");
    s = kvStore_History(&list, kv, "bar", NULL);
    testCond((s == NATS_OK) && (list.Count == 1) && (list.Entries != NULL)
                && (kvEntry_Operation(list.Entries[0]) == kvOp_Delete));
    kvEntryList_Destroy(&list);
    kvStore_Destroy(kv);

    JS_TEARDOWN;
}

void test_KeyValueCrossAccount(void)
{
    natsStatus          s;
    natsOptions         *opts= NULL;
    natsConnection      *nc1 = NULL;
    natsConnection      *nc2 = NULL;
    jsCtx               *js1 = NULL;
    jsCtx               *js2 = NULL;
    natsPid             pid  = NATS_INVALID_PID;
    kvStore             *kv1 = NULL;
    kvWatcher           *w1  = NULL;
    kvStore             *kv2 = NULL;
    kvWatcher           *w2  = NULL;
    kvEntry             *e   = NULL;
    jsStreamInfo        *si  = NULL;
    uint64_t            rev  = 0;
    kvConfig            kvc;
    jsOptions           o;
    char                datastore[256] = {'\0'};
    char                cmdLine[1024] = {'\0'};
    char                confFile[256] = {'\0'};
    kvPurgeOptions      po;

    ENSURE_JS_VERSION(2, 6, 2);

    _makeUniqueDir(datastore, sizeof(datastore), "datastore_");
    _createConfFile(confFile, sizeof(confFile),
        "jetstream: enabled\n"\
        "accounts: {\n"\
        "   A: {\n"\
        "       users: [ {user: a, password: a} ]\n"\
        "       jetstream: enabled\n"\
        "       exports: [\n"\
        "           {service: '$JS.API.>' }\n"\
        "           {service: '$KV.>'}\n"\
        "           {stream: 'accI.>'}\n"\
        "       ]\n"\
        "   },\n"\
        "   I: {\n"\
        "       users: [ {user: i, password: i} ]\n"\
        "       imports: [\n"\
        "           {service: {account: A, subject: '$JS.API.>'}, to: 'fromA.>' }\n"\
        "           {service: {account: A, subject: '$KV.>'}, to: 'fromA.$KV.>' }\n"\
        "           {stream: {subject: 'accI.>', account: A}}\n"\
        "       ]\n"\
        "   }\n"\
        "}");

    test("Start JS server: ");
    snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s -c %s", datastore, confFile);
    pid = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(pid);
    testCond(true);

    test("Create conn1: ");
    s = natsConnection_ConnectTo(&nc1, "nats://a:a@127.0.0.1:4222");
    testCond(s == NATS_OK);

    test("Get context1: ");
    s = natsConnection_JetStream(&js1, nc1, NULL);
    testCond(s == NATS_OK);

    test("Create KV1: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "Map";
    s = js_CreateKeyValue(&kv1, js1, &kvc);
    testCond(s == NATS_OK);

    test("Create Watcher1: ");
    s = kvStore_Watch(&w1, kv1, "map", NULL);
    IFOK(s, kvWatcher_Next(&e, w1, 1000));
    testCond((s == NATS_OK) && (e == NULL));

    test("Create conn2: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetURL(opts, "nats://i:i@127.0.0.1:4222"));
    IFOK(s, natsOptions_SetCustomInboxPrefix(opts, "accI"));
    IFOK(s, natsConnection_Connect(&nc2, opts));
    testCond(s == NATS_OK);

    test("Get context2: ");
    jsOptions_Init(&o);
    o.Prefix = "fromA";
    s = natsConnection_JetStream(&js2, nc2, &o);
    testCond(s == NATS_OK);

    test("Create KV2: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "Map";
    s = js_CreateKeyValue(&kv2, js2, &kvc);
    testCond(s == NATS_OK);

    test("Create Watcher2: ");
    s = kvStore_Watch(&w2, kv2, "map", NULL);
    IFOK(s, kvWatcher_Next(&e, w2, 1000));
    testCond((s == NATS_OK) && (e == NULL));

    test("Put: ");
    s = kvStore_PutString(&rev, kv2, "map", "value");
    testCond(s == NATS_OK);

    test("Get from kv1: ");
    s = kvStore_Get(&e, kv1, "map");
    testCond((s == NATS_OK) && (e != NULL)
            && (strcmp(kvEntry_Key(e), "map") == 0)
            && (strcmp(kvEntry_ValueString(e), "value") == 0));
    kvEntry_Destroy(e);
    e = NULL;

    test("Get from kv2: ");
    s = kvStore_Get(&e, kv2, "map");
    testCond((s == NATS_OK) && (e != NULL)
            && (strcmp(kvEntry_Key(e), "map") == 0)
            && (strcmp(kvEntry_ValueString(e), "value") == 0));
    kvEntry_Destroy(e);
    e = NULL;

    test("Watcher1 Next: ");
    s = kvWatcher_Next(&e, w1, 1000);
    testCond((s == NATS_OK) && (e != NULL)
            && (strcmp(kvEntry_Key(e), "map") == 0)
            && (strcmp(kvEntry_ValueString(e), "value") == 0));
    kvEntry_Destroy(e);
    e = NULL;

    test("Watcher2 Next: ");
    s = kvWatcher_Next(&e, w2, 1000);
    testCond((s == NATS_OK) && (e != NULL)
            && (strcmp(kvEntry_Key(e), "map") == 0)
            && (strcmp(kvEntry_ValueString(e), "value") == 0));
    kvEntry_Destroy(e);
    e = NULL;

    test("Update from kv2: ");
    s = kvStore_UpdateString(NULL, kv2, "map", "updated", rev);
    testCond(s == NATS_OK);

    test("Get from kv1: ");
    s = kvStore_Get(&e, kv1, "map");
    testCond((s == NATS_OK) && (e != NULL)
            && (strcmp(kvEntry_Key(e), "map") == 0)
            && (strcmp(kvEntry_ValueString(e), "updated") == 0));
    kvEntry_Destroy(e);
    e = NULL;

    test("Get from kv2: ");
    s = kvStore_Get(&e, kv2, "map");
    testCond((s == NATS_OK) && (e != NULL)
            && (strcmp(kvEntry_Key(e), "map") == 0)
            && (strcmp(kvEntry_ValueString(e), "updated") == 0));
    kvEntry_Destroy(e);
    e = NULL;

    test("Watcher1 Next: ");
    s = kvWatcher_Next(&e, w1, 1000);
    testCond((s == NATS_OK) && (e != NULL)
            && (strcmp(kvEntry_Key(e), "map") == 0)
            && (strcmp(kvEntry_ValueString(e), "updated") == 0));
    kvEntry_Destroy(e);
    e = NULL;

    test("Watcher2 Next: ");
    s = kvWatcher_Next(&e, w2, 1000);
    testCond((s == NATS_OK) && (e != NULL)
            && (strcmp(kvEntry_Key(e), "map") == 0)
            && (strcmp(kvEntry_ValueString(e), "updated") == 0));
    kvEntry_Destroy(e);
    e = NULL;

    test("Purge key from kv2: ");
    s = kvStore_Purge(kv2, "map", NULL);
    testCond(s == NATS_OK);

    test("Check purge ok from w1: ");
    s = kvWatcher_Next(&e, w1, 1000);
    testCond((s == NATS_OK) && (e != NULL) && (kvEntry_Operation(e) == kvOp_Purge));
    kvEntry_Destroy(e);
    e = NULL;

    test("Check purge ok from w2: ");
    s = kvWatcher_Next(&e, w2, 1000);
    testCond((s == NATS_OK) && (e != NULL) && (kvEntry_Operation(e) == kvOp_Purge));
    kvEntry_Destroy(e);
    e = NULL;

    test("Delete purge records: ");
    kvPurgeOptions_Init(&po);
    po.DeleteMarkersOlderThan = -1;
    s = kvStore_PurgeDeletes(kv2, &po);
    testCond(s == NATS_OK);

    test("All gone: ");
    s = js_GetStreamInfo(&si, js1, "KV_Map", NULL, NULL);
    testCond((s == NATS_OK) && (si != NULL) && (si->State.Msgs == 0));
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("Delete key from kv2: ");
    s = kvStore_Delete(kv2, "map");
    testCond(s == NATS_OK);

    test("Check key gone: ");
    s = kvStore_Get(&e, kv1, "map");
    testCond((s == NATS_NOT_FOUND) && (e == NULL));

    kvWatcher_Destroy(w2);
    w2 = NULL;
    kvStore_Destroy(kv2);
    kv2 = NULL;
    jsCtx_Destroy(js2);
    js2 = NULL;

    test("Get context2 (with trailing dot for prefix): ");
    jsOptions_Init(&o);
    o.Prefix = "fromA";
    s = natsConnection_JetStream(&js2, nc2, &o);
    testCond(s == NATS_OK);

    test("Create KV2: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "Map";
    s = js_CreateKeyValue(&kv2, js2, &kvc);
    testCond(s == NATS_OK);

    test("Put: ");
    s = kvStore_PutString(NULL, kv2, "map", "value2");
    testCond(s == NATS_OK);

    test("Get from kv1: ");
    s = kvStore_Get(&e, kv1, "map");
    testCond((s == NATS_OK) && (e != NULL)
            && (strcmp(kvEntry_Key(e), "map") == 0)
            && (strcmp(kvEntry_ValueString(e), "value2") == 0));
    kvEntry_Destroy(e);
    e = NULL;

    kvWatcher_Destroy(w1);
    kvStore_Destroy(kv1);
    jsCtx_Destroy(js1);
    kvWatcher_Destroy(w2);
    kvStore_Destroy(kv2);
    jsCtx_Destroy(js2);
    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc1);
    natsConnection_Destroy(nc2);
    _stopServer(pid);
    rmtree(datastore);
    remove(confFile);
}

static natsStatus
_checkDiscard(jsCtx *js, jsDiscardPolicy expected, kvStore **newKV)
{
    kvStore             *kv = NULL;
    jsStreamInfo        *si = NULL;
    kvConfig            kvc;
    natsStatus          s;

    kvConfig_Init(&kvc);
    kvc.Bucket = "TEST";
    s = js_CreateKeyValue(&kv, js, &kvc);
    IFOK(s, js_GetStreamInfo(&si, js, "KV_TEST", NULL, NULL));
    IFOK(s, (si->Config->Discard == expected ? NATS_OK : NATS_ERR));

    jsStreamInfo_Destroy(si);

    *newKV = kv;

    return s;
}

void test_KeyValueDiscardOldToNew(void)
{
    kvStore             *kv = NULL;
    kvConfig            kvc;
    natsStatus          s;

    JS_SETUP(2, 7, 2);

    // Change the server version in the connection to
    // create as-if we were connecting to a v2.7.1 server.
    natsConn_Lock(nc);
    nc->srvVersion.ma = 2;
    nc->srvVersion.mi = 7;
    nc->srvVersion.up = 1;
    natsConn_Unlock(nc);

    test("Check discard (old): ");
    s = _checkDiscard(js, js_DiscardOld, &kv);
    testCond(s == NATS_OK);
    kvStore_Destroy(kv);
    kv = NULL;

    // Now change version to 2.7.2
    natsConn_Lock(nc);
    nc->srvVersion.ma = 2;
    nc->srvVersion.mi = 7;
    nc->srvVersion.up = 2;
    natsConn_Unlock(nc);

    test("Check discard (old, no auto-update): ");
    s = _checkDiscard(js, js_DiscardOld, &kv);
    testCond((s == NATS_ERR) && (kv == NULL)
                && (strstr(nats_GetLastError(NULL), "different configuration") != NULL));
    nats_clearLastError();

    // Now delete the kv store and create against 2.7.2+
    test("Delete KV: ");
    s = js_DeleteStream(js, "KV_TEST", NULL, NULL);
    testCond(s == NATS_OK);

    test("Check discard (new): ");
    s = _checkDiscard(js, js_DiscardNew, &kv);
    testCond(s == NATS_OK);
    kvStore_Destroy(kv);
    kv = NULL;

    test("Check that other changes are rejected: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "TEST";
    kvc.MaxBytes = 1024*1024;
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond((s == NATS_ERR)
                && (strstr(nats_GetLastError(NULL), "different configuration") != NULL));
    kvStore_Destroy(kv);

    JS_TEARDOWN;
}

void test_KeyValueRePublish(void)
{
    kvStore             *kv     = NULL;
    jsStreamInfo        *si     = NULL;
    natsSubscription    *sub    = NULL;
    natsMsg             *msg    = NULL;
    kvConfig            kvc;
    jsRePublish         rp;
    natsStatus          s;

    JS_SETUP(2, 9, 0);

    test("Create KV: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "TEST_UPDATE";
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond(s == NATS_OK);

    kvStore_Destroy(kv);
    kv = NULL;

    test("Set RePublish should fail: ");
    jsRePublish_Init(&rp);
    rp.Source = ">";
    rp.Destination = "bar.>";
    kvc.RePublish =&rp;
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond((s == NATS_ERR) && (strstr(nats_GetLastError(NULL), "different configuration") != NULL));
    nats_clearLastError();

    test("Create with repub: ");
    kvc.Bucket = "TEST";
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond(s == NATS_OK);

    test("Check set: ");
    s = js_GetStreamInfo(&si, js, "KV_TEST", NULL, NULL);
    testCond((s == NATS_OK) && (si->Config != NULL) && (si->Config->RePublish != NULL));
    jsStreamInfo_Destroy(si);

    test("Sub: ");
    s = natsConnection_SubscribeSync(&sub, nc, "bar.>");
    testCond(s == NATS_OK);

    test("Put: ");
    s = kvStore_PutString(NULL, kv, "foo", "value");
    testCond(s == NATS_OK);

    test("Get msg: ");
    s = natsSubscription_NextMsg(&msg, sub, 1000);
    testCond(s == NATS_OK);

    test("Check msg: ");
    s = (strcmp(natsMsg_GetData(msg), "value") == 0 ? NATS_OK : NATS_ERR);
    if (s == NATS_OK)
    {
        const char *subj = NULL;

        s = natsMsgHeader_Get(msg, JSSubject, &subj);
        if (s == NATS_OK)
            s = (strcmp(subj, "$KV.TEST.foo") == 0 ? NATS_OK : NATS_ERR);
    }
    testCond(s == NATS_OK);

    natsMsg_Destroy(msg);
    natsSubscription_Destroy(sub);
    kvStore_Destroy(kv);

    JS_TEARDOWN;
}

void test_KeyValueMirrorDirectGet(void)
{
    kvStore             *kv     = NULL;
    kvConfig            kvc;
    jsStreamConfig      sc;
    jsStreamSource      ss;
    natsStatus          s;
    int                 i;

    JS_SETUP(2, 9, 0);

    test("Create KV: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "DIRECT_GET";
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond(s == NATS_OK);

    test("Add mirror: ");
    jsStreamConfig_Init(&sc);
    sc.Name = "MIRROR";
    jsStreamSource_Init(&ss);
    ss.Name = "KV_DIRECT_GET";
    sc.Mirror = &ss;
    sc.MirrorDirect = true;
    s = js_AddStream(NULL, js, &sc, NULL, NULL);
    testCond(s == NATS_OK);

    test("Populate: ");
    for (i=0; (s==NATS_OK) && (i<100); i++)
    {
        char key[64];

        snprintf(key, sizeof(key), "KEY.%d", i);
        s = kvStore_PutString(NULL, kv, key, key);
    }
    testCond(s == NATS_OK);

    test("Check get: ");
    for (i=0; (s==NATS_OK) && (i<100); i++)
    {
        kvEntry *e = NULL;
        s = kvStore_Get(&e, kv, "KEY.22");
        if (s == NATS_OK)
        {
            s = (strcmp(kvEntry_ValueString(e), "KEY.22") == 0 ? NATS_OK : NATS_ERR);
            kvEntry_Destroy(e);
        }
    }
    testCond(s == NATS_OK);

    kvStore_Destroy(kv);

    JS_TEARDOWN;
}

static natsStatus
_connectToHubAndCheckLeaf(natsConnection **hub, natsConnection *lnc)
{
    natsStatus          s       = NATS_OK;
    natsConnection      *nc     = NULL;
    natsSubscription    *sub    = NULL;
    int                 i;

    s = natsConnection_ConnectTo(&nc, NATS_DEFAULT_URL);
    IFOK(s, natsConnection_SubscribeSync(&sub, nc, "check"));
    IFOK(s, natsConnection_Flush(nc));
    if (s == NATS_OK)
    {
        for (i=0; i<10; i++)
        {
            s = natsConnection_PublishString(lnc, "check", "hello");
            if (s == NATS_OK)
            {
                natsMsg *msg = NULL;
                s = natsSubscription_NextMsg(&msg, sub, 500);
                natsMsg_Destroy(msg);
                if (s == NATS_OK)
                    break;
            }
        }
    }
    natsSubscription_Destroy(sub);
    if (s == NATS_OK)
        *hub = nc;
    else
        natsConnection_Destroy(nc);
    return s;
}

void test_KeyValueMirrorCrossDomains(void)
{
    natsStatus          s;
    natsConnection      *nc = NULL;
    natsConnection      *lnc= NULL;
    jsCtx               *js = NULL;
    jsCtx               *ljs= NULL;
    jsCtx               *rjs= NULL;
    natsPid             pid = NATS_INVALID_PID;
    natsPid             pid2= NATS_INVALID_PID;
    jsOptions           o;
    jsErrCode           jerr = 0;
    char                datastore[256]  = {'\0'};
    char                datastore2[256] = {'\0'};
    char                cmdLine[1024]   = {'\0'};
    char                confFile[256]   = {'\0'};
    char                lconfFile[256]  = {'\0'};
    kvStore             *kv     = NULL;
    kvStore             *lkv    = NULL;
    kvStore             *mkv    = NULL;
    kvStore             *rkv    = NULL;
    kvEntry             *e      = NULL;
    jsStreamInfo        *si     = NULL;
    kvWatcher           *w      = NULL;
    int                 ok      = 0;
    kvPurgeOptions      po;
    kvConfig            kvc;
    jsStreamSource      src;
    int                 i;

    ENSURE_JS_VERSION(2, 9, 0);

    _makeUniqueDir(datastore, sizeof(datastore), "datastore_");
    _createConfFile(confFile, sizeof(confFile),
        "server_name: HUB\n"\
        "listen: 127.0.0.1:4222\n"\
        "jetstream: { domain: HUB }\n"\
        "leafnodes { listen: 127.0.0.1:7422 }\n");

    test("Start hub: ");
    snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s -c %s", datastore, confFile);
    pid = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(pid);
    testCond(true);

    _makeUniqueDir(datastore2, sizeof(datastore2), "datastore_");
    _createConfFile(lconfFile, sizeof(lconfFile),
        "server_name: LEAF\n"\
        "listen: 127.0.0.1:4223\n"\
        "jetstream: { domain: LEAF }\n"\
        "leafnodes {\n"\
        "   remotes = [ { url: leaf://127.0.0.1:7422 } ]\n"\
        "}\n");

    test("Start leaf: ");
    snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s -c %s", datastore, lconfFile);
    pid2 = _startServer("nats://127.0.0.1:4223", cmdLine, true);
    CHECK_SERVER_STARTED(pid2);
    testCond(true);

    test("Connect to leaf: ");
    s = natsConnection_ConnectTo(&lnc, "nats://127.0.0.1:4223");
    testCond(s == NATS_OK);

    test("Get context: ");
    s = natsConnection_JetStream(&ljs, lnc, NULL);
    testCond(s == NATS_OK);

    test("Connect to hub and check connectivity through leaf: ");
    s = _connectToHubAndCheckLeaf(&nc, lnc);
    testCond(s == NATS_OK);

    test("Get context: ");
    s = natsConnection_JetStream(&js, nc, NULL);
    testCond(s == NATS_OK);

    test("Create KV value: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "TEST";
    s = js_CreateKeyValue(&kv, js, &kvc);
    testCond(s == NATS_OK);

    test("Put keys: ");
    s = kvStore_PutString(NULL, kv, "name", "derek");
    IFOK(s, kvStore_PutString(NULL, kv, "age", "22"));
    testCond(s == NATS_OK);

    test("Create KV: ");
    kvConfig_Init(&kvc);
    kvc.Bucket = "MIRROR";
    jsStreamSource_Init(&src);
    src.Name = "TEST";
    src.Domain = "HUB";
    kvc.Mirror = &src;
    s = js_CreateKeyValue(&lkv, ljs, &kvc);
    testCond(s == NATS_OK);

    test("Check config not changed: ");
    testCond((strcmp(kvc.Bucket, "MIRROR") == 0)
                && (kvc.Mirror != NULL)
                && (strcmp(kvc.Mirror->Name, "TEST") == 0)
                && (strcmp(kvc.Mirror->Domain, "HUB") == 0)
                && (kvc.Mirror->External == NULL));

    test("Get stream info: ");
    s = js_GetStreamInfo(&si, ljs, "KV_MIRROR", NULL, &jerr);
    testCond((s == NATS_OK) && (si != NULL) && (jerr == 0));

    test("Check mirror direct: ");
    testCond(si->Config->MirrorDirect);
    jsStreamInfo_Destroy(si);
    si = NULL;

    test("Check mirror syncs: ");
    for (i=0; i<10; i++)
    {
        s = js_GetStreamInfo(&si, ljs, "KV_MIRROR", NULL, NULL);
        if (s != NATS_OK)
            break;

        if (si->State.Msgs != 2)
            s = NATS_ERR;

        jsStreamInfo_Destroy(si);
        si = NULL;
        if (s == NATS_OK)
            break;
        nats_Sleep(250);
    }
    testCond(s == NATS_OK);

    // Bind locally from leafnode and make sure both get and put work.
    test("Leaf KV: ");
    s = js_KeyValue(&mkv, ljs, "MIRROR");
    testCond(s == NATS_OK);

    test("Put key: ");
    s = kvStore_PutString(NULL, mkv, "name", "rip");
    testCond(s == NATS_OK);

    test("Get key: ");
    s = kvStore_Get(&e, mkv, "name");
    // levb: we seem to get the old value at times here, so try one more time if
    // there's a mismatch
    for (i = 0; i < 3; i++)
        if ((s == NATS_OK) && (e != NULL) && (strcmp(kvEntry_ValueString(e), "rip") != 0))
        {
            kvEntry_Destroy(e);
            e = NULL;
            s = kvStore_Get(&e, mkv, "name");
        }
    if ((s == NATS_OK) && (e != NULL))
        s = (strcmp(kvEntry_ValueString(e), "rip") == 0 ? NATS_OK : NATS_ERR);
    testCond(s == NATS_OK);
    kvEntry_Destroy(e);
    e = NULL;

    test("Get context for HUB: ");
    jsOptions_Init(&o);
    o.Domain = "HUB";
    s = natsConnection_JetStream(&rjs, lnc, &o);
    testCond(s == NATS_OK);

    test("Get KV: ");
    s = js_KeyValue(&rkv, rjs, "TEST");
    testCond(s == NATS_OK);

    test("Put key: ");
    s = kvStore_PutString(NULL, rkv, "name", "ivan");
    testCond(s == NATS_OK);

    test("Get key: ");
    s = kvStore_Get(&e, rkv, "name");
    if ((s == NATS_OK) && (e != NULL))
        s = (strcmp(kvEntry_ValueString(e), "ivan") == 0 ? NATS_OK : NATS_ERR);
    testCond(s == NATS_OK);
    kvEntry_Destroy(e);
    e = NULL;

    test("Shutdown hub: ");
    jsCtx_Destroy(js);
    kvStore_Destroy(kv);
    natsConnection_Destroy(nc);
    nc = NULL;
    _stopServer(pid);
    pid = NATS_INVALID_PID;
    testCond(true);
    nats_Sleep(500);

    test("Get key: ");
    // Use mkv here, not rkv.
    s = kvStore_Get(&e, mkv, "name");
    if ((s == NATS_OK) && (e != NULL))
        s = (strcmp(kvEntry_ValueString(e), "ivan") == 0 ? NATS_OK : NATS_ERR);
    testCond(s == NATS_OK);
    kvEntry_Destroy(e);
    e = NULL;

    test("Create watcher (name): ");
    s = kvStore_Watch(&w, mkv, "name", NULL);
    testCond(s == NATS_OK);

    test("Check watcher: ");
    s = kvWatcher_Next(&e, w, 1000);
    if (s == NATS_OK)
    {
        if ((strcmp(kvEntry_Key(e), "name") != 0) || (strcmp(kvEntry_ValueString(e), "ivan") != 0))
            s = NATS_ERR;
        kvEntry_Destroy(e);
        e = NULL;
    }
    IFOK(s, kvWatcher_Next(&e, w, 1000));
    if (s == NATS_OK)
    {
        if ((kvEntry_Key(e) != NULL) || (kvEntry_ValueString(e) != NULL))
            s = NATS_ERR;
        kvEntry_Destroy(e);
        e = NULL;
    }
    testCond(s == NATS_OK);

    test("No more: ");
    s = kvWatcher_Next(&e, w, 250);
    testCond((s == NATS_TIMEOUT) && (e == NULL));
    nats_clearLastError();

    kvWatcher_Destroy(w);
    w = NULL;

    test("Create watcher (all): ")
    s = kvStore_WatchAll(&w, mkv, NULL);
    testCond((s == NATS_OK) && (w != NULL));

    test("Check watcher: ");
    s = kvWatcher_Next(&e, w, 1000);
    if (s == NATS_OK)
    {
        if ((strcmp(kvEntry_Key(e), "age") != 0) || (strcmp(kvEntry_ValueString(e), "22") != 0))
            s = NATS_ERR;
        kvEntry_Destroy(e);
        e = NULL;
    }
    IFOK(s, kvWatcher_Next(&e, w, 1000));
    if (s == NATS_OK)
    {
        if ((strcmp(kvEntry_Key(e), "name") != 0) || (strcmp(kvEntry_ValueString(e), "ivan") != 0))
            s = NATS_ERR;
        kvEntry_Destroy(e);
        e = NULL;
    }
    IFOK(s, kvWatcher_Next(&e, w, 1000));
    if (s == NATS_OK)
    {
        if ((kvEntry_Key(e) != NULL) || (kvEntry_ValueString(e) != NULL))
            s = NATS_ERR;
        kvEntry_Destroy(e);
        e = NULL;
    }
    testCond(s == NATS_OK);

    test("No more: ");
    s = kvWatcher_Next(&e, w, 250);
    testCond((s == NATS_TIMEOUT) && (e == NULL));
    nats_clearLastError();

    test("Restart hub: ");
    snprintf(cmdLine, sizeof(cmdLine), "-js -sd %s -c %s", datastore, confFile);
    pid = _startServer("nats://127.0.0.1:4222", cmdLine, true);
    CHECK_SERVER_STARTED(pid);
    testCond(true);

    test("Connect to hub and check connectivity through leaf: ");
    s = _connectToHubAndCheckLeaf(&nc, lnc);
    testCond(s == NATS_OK);

    test("Delete keys: ");
    s = kvStore_Delete(mkv, "age");
    IFOK(s, kvStore_Delete(mkv, "name"));
    testCond(s == NATS_OK);

    test("Check mirror syncs: ");
    for (i=0; (ok != 2) && (i < 10); i++)
    {
        if (kvWatcher_Next(&e, w, 10000) == NATS_OK)
        {
            if (((strcmp(kvEntry_Key(e), "age") == 0) || (strcmp(kvEntry_Key(e), "name") == 0))
                && (kvEntry_Operation(e) == kvOp_Delete))
            {
                ok++;
            }
            kvEntry_Destroy(e);
            e = NULL;
        }
    }
    testCond((s == NATS_OK) && (ok == 2));

    test("Purge deletes: ");
    kvPurgeOptions_Init(&po);
    po.DeleteMarkersOlderThan = -1;
    s = kvStore_PurgeDeletes(mkv, &po);
    testCond(s == NATS_OK);

    nats_clearLastError();
    test("Check stream: ");
    s = js_GetStreamInfo(&si, ljs, "KV_MIRROR", NULL, NULL);
    testCond((s == NATS_OK) && (si != NULL) && (si->State.Msgs == 0));
    jsStreamInfo_Destroy(si);

    kvWatcher_Destroy(w);
    natsConnection_Destroy(nc);
    kvStore_Destroy(rkv);
    kvStore_Destroy(mkv);
    kvStore_Destroy(lkv);
    jsCtx_Destroy(rjs);
    jsCtx_Destroy(ljs);
    natsConnection_Destroy(lnc);
    _stopServer(pid2);
    rmtree(datastore2);
    _stopServer(pid);
    rmtree(datastore);
    remove(confFile);
    remove(lconfFile);
}

void test_MicroMatchEndpointSubject(void)
{
    // endpoint, actual, match
    const char *test_cases[] = {
        "foo", "foo", "true",
        "foo.bar.meh", "foo.bar.meh", "true",
        "foo.bar.meh", "foo.bar", NULL,
        "foo.bar", "foo.bar.meh", NULL,
        "foo.*.meh", "foo.bar.meh", "true",
        "*.bar.meh", "foo.bar.meh", "true",
        "foo.bar.*", "foo.bar.meh", "true",
        "foo.bar.>", "foo.bar.meh", "true",
        "foo.>", "foo.bar.meh", "true",
        "foo.b*ar.meh", "foo.bar.meh", NULL,
    };
    char buf[1024];
    int i;
    const char *ep_subject;
    const char *actual_subject;
    bool expected_match;

    for (i = 0; i < (int)(sizeof(test_cases) / sizeof(const char *)); i = i + 3)
    {
        ep_subject = test_cases[i];
        actual_subject = test_cases[i + 1];
        expected_match = (test_cases[i + 2] != NULL);

        snprintf(buf, sizeof(buf), "endpoint subject '%s', actual subject: '%s': ", ep_subject, actual_subject);
        test(buf);
        testCond(micro_match_endpoint_subject(ep_subject, actual_subject) == expected_match);
    }
}

static microError *
_microHandleRequest42(microRequest *req)
{
    return microRequest_Respond(req, "42", 2);
}

static microError *
_microHandleRequestNoisy42(microRequest *req)
{
    if ((rand() % 10) == 0)
        return micro_Errorf("Unexpected error!");

    // Happy Path.
    // Random delay between 5-10ms
    nats_Sleep(5 + (rand() % 5));

    return microRequest_Respond(req, "42", 2);
}

static void
_microServiceDoneHandler(microService *m)
{
    struct threadArg *arg = (struct threadArg*) microService_GetState(m);

    natsMutex_Lock(arg->m);
    arg->microRunningServiceCount--;
    if (arg->microRunningServiceCount == 0)
    {
        arg->microAllDone = true;
        natsCondition_Broadcast(arg->c);
    }
    natsMutex_Unlock(arg->m);
}

static microError *
_startMicroservice(microService** new_m, natsConnection *nc, microServiceConfig *cfg, microEndpointConfig **eps, int num_eps, struct threadArg *arg)
{
    microError *err = NULL;
    bool prev_done;
    int i;

    cfg->DoneHandler = _microServiceDoneHandler;
    cfg->State = arg;

    natsMutex_Lock(arg->m);

    arg->microRunningServiceCount++;
    prev_done = arg->microAllDone;
    arg->microAllDone = false;

    err = micro_AddService(new_m, nc, cfg);
    if (err != NULL)
    {
        arg->microRunningServiceCount--;
        arg->microAllDone = prev_done;
    }

    natsMutex_Unlock(arg->m);

    if (err != NULL)
        return err;

    for (i=0; i < num_eps; i++)
    {
        err = microService_AddEndpoint(*new_m, eps[i]);
        if (err != NULL)
        {
            microService_Destroy(*new_m);
            *new_m = NULL;
            return err;
        }
    }

    return NULL;
}

static void
_startMicroserviceOK(microService** new_m, natsConnection *nc, microServiceConfig *cfg, microEndpointConfig **eps, int num_eps, struct threadArg *arg)
{
    char buf[256];

    snprintf(buf, sizeof(buf), "Start microservice %s: ", cfg->Name);
    test(buf);

    testCond (NULL == _startMicroservice(new_m, nc, cfg, eps, num_eps, arg));
}

static void
_startManyMicroservices(microService** svcs, int n, natsConnection *nc, microServiceConfig *cfg, microEndpointConfig **eps, int num_eps, struct threadArg *arg)
{
    int i;

    for (i = 0; i < n; i++)
    {
        char buf[64];
        cfg->Version = "1.0.0";
        cfg->Description = "returns 42";

        if (nats_IsStringEmpty(cfg->Name))
        {
            snprintf(buf, sizeof(buf), "CoolService-%d", i);
            cfg->Name = buf;
        }
        _startMicroserviceOK(&(svcs[i]), nc, cfg, eps, num_eps, arg);
    }

    testCond(true);
}

#define _waitForMicroservicesAllDone(_arg)                                              \
    {                                                                                   \
        natsMutex_Lock((_arg)->m);                                                      \
        testf("Wait for %d microservices to stop: ", (_arg)->microRunningServiceCount); \
        natsStatus waitStatus = NATS_OK;                                                \
        bool allDone = false;                                                           \
        while ((waitStatus != NATS_TIMEOUT) && !(_arg)->microAllDone)                   \
            waitStatus = natsCondition_TimedWait((_arg)->c, (_arg)->m, 1000);           \
        allDone = (_arg)->microAllDone;                                                 \
        natsMutex_Unlock((_arg)->m);                                                    \
        testCond((NATS_OK == waitStatus) && allDone);                                   \
    }

#define _destroyMicroservice(_s)                                                                  \
    testf("Destroy microservice %s: ", (_s)->cfg->Name);                                          \
    microError *_err = microService_Destroy(_s);                                                  \
    if (_err != NULL)                                                                             \
    {                                                                                             \
        char _buf[256];                                                                           \
        FAILf("Unable to destroy microservice: %s", microError_String(_err, _buf, sizeof(_buf))); \
        microError_Destroy(_err);                                                                 \
    }                                                                                             \
    testCond(true);

typedef struct
{
    const char *name;
    microServiceConfig *cfg;
    microEndpointConfig **endpoints;
    int num_endpoints;
    bool null_nc;
    bool null_receiver;
    const char *expected_err;
    int expected_num_subjects;
} add_service_test_case_t;

void test_MicroAddService(void)
{
    natsStatus s = NATS_OK;
    microError *err = NULL;
    struct threadArg arg;
    natsOptions *opts = NULL;
    natsConnection *nc = NULL;
    natsPid serverPid = NATS_INVALID_PID;
    microService *m = NULL;
    microServiceInfo *info = NULL;
    natsMsg *reply = NULL;
    int i, j, n;
    char buf[1024];
    char *subject = NULL;
    nats_JSON *js = NULL;
    nats_JSON **array = NULL;
    int array_len;
    const char *str;

    microEndpointConfig default_ep_cfg = {
        .Name = "default",
        .Handler = _microHandleRequestNoisy42,
    };
    microEndpointConfig ep1_cfg = {
        .Name = "ep1",
        .Handler = _microHandleRequestNoisy42,
    };
    microEndpointConfig ep2_cfg = {
        .Name = "ep2",
        .Subject = "different-from-name",
        .Handler = _microHandleRequestNoisy42,
    };
    microEndpointConfig ep3_cfg = {
        .Name = "ep3",
        .Handler = _microHandleRequestNoisy42,
    };
    microEndpointConfig *all_ep_cfgs[] = {&ep1_cfg, &ep2_cfg, &ep3_cfg};

    microServiceConfig minimal_cfg = {
        .Version = "1.0.0",
        .Name = "minimal",

    };
    microServiceConfig full_cfg = {
        .Version = "1.0.0",
        .Name = "full",
        .Endpoint = &default_ep_cfg,
        .Description = "fully declared microservice",
    };
    microServiceConfig err_no_name_cfg = {
        .Version = "1.0.0",
    };
    microServiceConfig err_no_version_cfg = {
        .Name = "no-version",
    };
    microServiceConfig err_invalid_version_cfg = {
        .Version = "BLAH42",
        .Name = "invalid-version",
    };

    add_service_test_case_t tcs[] = {
        {
            .name = "Minimal",
            .cfg = &minimal_cfg,
        },
        {
            .name = "Full",
            .cfg = &full_cfg,
            .expected_num_subjects = 1,
        },
        {
            .name = "Full-with-endpoints",
            .cfg = &full_cfg,
            .endpoints = all_ep_cfgs,
            .num_endpoints = sizeof(all_ep_cfgs) / sizeof(all_ep_cfgs[0]),
            .expected_num_subjects = 4,
        },
        {
            .name = "Err-null-connection",
            .cfg = &minimal_cfg,
            .null_nc = true,
            .expected_err = "status 16: invalid function argument",
        },
        {
            .name = "Err-null-receiver",
            .cfg = &minimal_cfg,
            .null_receiver = true,
            .expected_err = "status 16: invalid function argument",
        },
        {
            .name = "Err-no-name",
            .cfg = &err_no_name_cfg,
            .expected_err = "status 16: invalid function argument",
        },
        {
            .name = "Err-no-version",
            .cfg = &err_no_version_cfg,
            .expected_err = "status 16: invalid function argument",
        },
        {
            .name = "Err-invalid-version",
            .cfg = &err_invalid_version_cfg,
            // TODO: validate the version format.
        },
    };
    add_service_test_case_t tc;

    srand((unsigned int)nats_NowInNanoSeconds());

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
        opts = _createReconnectOptions();
    if ((opts == NULL)
        || (natsOptions_SetClosedCB(opts, _closedCb, &arg) != NATS_OK)
        || (natsOptions_SetURL(opts, NATS_DEFAULT_URL) != NATS_OK))
    {
        FAIL("Unable to setup test for MicroConnectionEvents!");
    }

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect to server: ");
    testCond(NATS_OK == natsConnection_Connect(&nc, opts));

    for (n = 0; n < (int)(sizeof(tcs) / sizeof(tcs[0])); n++)
    {
        tc = tcs[n];

        snprintf(buf, sizeof(buf), "%s: AddService: ", tc.name);
        test(buf);
        m = NULL;
        err = _startMicroservice(
            tc.null_receiver ? NULL : &m,
            tc.null_nc ? NULL : nc,
            tc.cfg, NULL, 0, &arg);
        if (nats_IsStringEmpty(tc.expected_err))
        {
            testCond(err == NULL);
        }
        else
        {
            if (strcmp(tc.expected_err, microError_String(err, buf, sizeof(buf))) != 0)
            {
                char buf2[2*1024];
                snprintf(buf2, sizeof(buf2), "Expected error '%s', got '%s'", tc.expected_err, buf);
                FAIL(buf2);
            }
            testCond(true);
            microError_Destroy(err);
            continue;
        }

        for (i = 0; i < tc.num_endpoints; i++)
        {
            snprintf(buf, sizeof(buf), "%s: AddEndpoint '%s': ", tc.name, tc.endpoints[i]->Name);
            test(buf);
            testCond(NULL == microService_AddEndpoint(m, tc.endpoints[i]));
        }

        err = microService_GetInfo(&info, m);
        if (err != NULL)
            FAIL("failed to get service info!")

        // run through PING and INFO subject variations: 0: all, 1: .name, 2: .name.id
        for (j = 0; j < 3; j++)
        {
            err = micro_new_control_subject(&subject, MICRO_PING_VERB,
                                            (j > 0 ? tc.cfg->Name : NULL),
                                            (j > 1 ? info->Id : NULL));
            if (err != NULL)
                FAIL("failed to generate PING subject!");

            snprintf(buf, sizeof(buf), "%s: Verify PING subject %s: ", tc.name, subject);
            test(buf);
            s = natsConnection_Request(&reply, nc, subject, NULL, 0, 1000);
            IFOK(s, nats_JSONParse(&js, natsMsg_GetData(reply), natsMsg_GetDataLength(reply)));
            IFOK(s, nats_JSONGetStrPtr(js, "id", &str));
            IFOK(s, (strcmp(str, info->Id) == 0) ? NATS_OK : NATS_ERR);
            IFOK(s, nats_JSONGetStrPtr(js, "name", &str));
            IFOK(s, (strcmp(str, tc.cfg->Name) == 0) ? NATS_OK : NATS_ERR);
            IFOK(s, nats_JSONGetStrPtr(js, "version", &str));
            IFOK(s, (strcmp(str, tc.cfg->Version) == 0) ? NATS_OK : NATS_ERR);
            IFOK(s, nats_JSONGetStrPtr(js, "type", &str));
            IFOK(s, (strcmp(str, MICRO_PING_RESPONSE_TYPE) == 0) ? NATS_OK : NATS_ERR);
            testCond(s == NATS_OK);
            nats_JSONDestroy(js);
            natsMsg_Destroy(reply);
            NATS_FREE(subject);

            err = micro_new_control_subject(&subject, MICRO_INFO_VERB,
                                            (j > 0 ? tc.cfg->Name : NULL),
                                            (j > 1 ? info->Id : NULL));
            if (err != NULL)
                FAIL("failed to generate INFO subject!");

            snprintf(buf, sizeof(buf), "%s: Verify INFO subject %s: ", tc.name, subject);
            test(buf);
            s = natsConnection_Request(&reply, nc, subject, NULL, 0, 1000);
            array = NULL;
            array_len = 0;
            testCond((NATS_OK == s)
                && (NATS_OK == nats_JSONParse(&js, natsMsg_GetData(reply), natsMsg_GetDataLength(reply)))
                && (NATS_OK == nats_JSONGetStrPtr(js, "id", &str)) && (strcmp(str, info->Id) == 0)
                && (NATS_OK == nats_JSONGetStrPtr(js, "name", &str)) && (strcmp(str, tc.cfg->Name) == 0)
                && (NATS_OK == nats_JSONGetStrPtr(js, "type", &str)) && (strcmp(str, MICRO_INFO_RESPONSE_TYPE) == 0)
                && (NATS_OK == nats_JSONGetArrayObject(js, "endpoints", &array, &array_len)) && (array_len == tc.expected_num_subjects)
            );

            NATS_FREE(array);
            nats_JSONDestroy(js);
            natsMsg_Destroy(reply);
            NATS_FREE(subject);
        }

        microServiceInfo_Destroy(info);
        _destroyMicroservice(m);
        _waitForMicroservicesAllDone(&arg);
    }

    test("Destroy the test connection: ");
    natsConnection_Destroy(nc);
    testCond(NATS_OK == _waitForConnClosed(&arg));

    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);
    _stopServer(serverPid);
}

void test_MicroGroups(void)
{
    natsStatus s = NATS_OK;
    microError *err = NULL;
    struct threadArg arg;
    natsOptions *opts = NULL;
    natsConnection *nc = NULL;
    natsPid serverPid = NATS_INVALID_PID;
    microService *m = NULL;
    microGroup *g1 = NULL;
    microGroup *g2 = NULL;
    microServiceInfo *info = NULL;
    int i;

    microEndpointConfig ep2_cfg = {
        .Name = "ep2",
        .Handler = _microHandleRequest42,
    };
    microServiceConfig cfg = {
        .Version = "1.0.0",
        .Name = "with-groups",
    };

    const char* expected_subjects[] = {
        "g1.ep2",
        "g1.g2.ep2",
        "g1.g2.ep1",
        "g1.ep1",
        "ep1",
    };
    int expected_num_endpoints = sizeof(expected_subjects) / sizeof(expected_subjects[0]);

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
        opts = _createReconnectOptions();
    if ((opts == NULL)
        || (natsOptions_SetClosedCB(opts, _closedCb, &arg) != NATS_OK)
        || (natsOptions_SetURL(opts, NATS_DEFAULT_URL) != NATS_OK))
    {
        FAIL("Unable to setup test for MicroConnectionEvents!");
    }

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect to server: ");
    testCond(NATS_OK == natsConnection_Connect(&nc, opts));

    _startMicroservice(&m, nc, &cfg, NULL, 0, &arg);

    test("AddEndpoint 1 to service: ");
    microEndpointConfig ep1_cfg = { .Name = "ep1", .Handler = _microHandleRequest42 };
    testCond(NULL == microService_AddEndpoint(m, &ep1_cfg));

    test("AddGroup g1: ");
    microGroupConfig g1_cfg = { .Prefix = "g1" };
    testCond(NULL == microService_AddGroup(&g1, m, &g1_cfg));

    test("AddEndpoint 1 to g1: ");
    testCond(NULL == microGroup_AddEndpoint(g1, &ep1_cfg));

    test("Add sub-Group g2: ");
    microGroupConfig g2_cfg = { .Prefix = "g2" };
    testCond(NULL == microGroup_AddGroup(&g2, g1, &g2_cfg));

    test("AddEndpoint 1 to g2: ");
    testCond(NULL == microGroup_AddEndpoint(g2, &ep1_cfg));

    test("AddEndpoint 2 to g2: ");
    testCond(NULL == microGroup_AddEndpoint(g2, &ep2_cfg));

    test("AddEndpoint 2 to g1: ");
    testCond(NULL == microGroup_AddEndpoint(g1, &ep2_cfg));

    err = microService_GetInfo(&info, m);
    if (err != NULL)
        FAIL("failed to get service info!")

    testf("Verify number of endpoints %d is %d: ", info->EndpointsLen, expected_num_endpoints);
    testCond(info->EndpointsLen == expected_num_endpoints);

    test("Verify endpoint subjects: ");
    for (i = 0; i < info->EndpointsLen; i++)
    {
        if (strcmp(info->Endpoints[i].Subject, expected_subjects[i]) != 0) {
            char buf[1024];
            snprintf(buf, sizeof(buf), "expected %s, got %s", expected_subjects[i], info->Endpoints[i].Subject);
            FAIL(buf);
        }
    }
    testCond(true);

    microServiceInfo_Destroy(info);

    _destroyMicroservice(m);
    _waitForMicroservicesAllDone(&arg);

    test("Destroy the test connection: ");
    natsConnection_Destroy(nc);
    testCond(NATS_OK == _waitForConnClosed(&arg));

    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);
    _stopServer(serverPid);
}

void test_MicroQueueGroupForEndpoint(void)
{
    microError *err = NULL;
    natsPid serverPid = NATS_INVALID_PID;
    natsOptions *opts = NULL;
    natsConnection *nc = NULL;

    microServiceConfig serviceConfig = { .Name = "testService", .Version = "1.0.0" };
    microGroupConfig group1Config = { .Prefix = "testGroup1" };
    microGroupConfig group2Config = { .Prefix = "testGroup2" };
    microEndpointConfig epConfig = { .Name = "testEP", .Handler = _microHandleRequest42 };

    typedef struct {
        const char *name;

        bool serviceNoQueueGroup;
        const char *serviceQueueGroup;
        bool group1NoQueueGroup;
        const char *group1QueueGroup;
        bool group2NoQueueGroup;
        const char *group2QueueGroup;
        bool epNoQueueGroup;
        const char *epQueueGroup;

        const char *expectedServiceLevel;
        const char *expectedGroup1Level;
        const char *expectedGroup2Level;
    } TC;
    TC tcs[] = {
        {.name="default",
            .expectedServiceLevel=MICRO_DEFAULT_QUEUE_GROUP, .expectedGroup1Level=MICRO_DEFAULT_QUEUE_GROUP, .expectedGroup2Level=MICRO_DEFAULT_QUEUE_GROUP},
        {.name="service value override", .serviceQueueGroup="test",
            .expectedServiceLevel="test", .expectedGroup1Level="test", .expectedGroup2Level="test"},
        {.name="G1 value override", .group1QueueGroup="G1",
            .expectedServiceLevel=MICRO_DEFAULT_QUEUE_GROUP, .expectedGroup1Level="G1", .expectedGroup2Level="G1"},
        {.name="service and G1 value overrides", .serviceQueueGroup="S", .group1QueueGroup="G1",
            .expectedServiceLevel="S", .expectedGroup1Level="G1", .expectedGroup2Level="G1"},
        {.name="service and G2 value overrides", .serviceQueueGroup="S", .group2QueueGroup="G2",
            .expectedServiceLevel="S", .expectedGroup1Level="S", .expectedGroup2Level="G2"},
        {.name="disabled", .serviceNoQueueGroup=true,
            .expectedServiceLevel=NULL, .expectedGroup1Level=NULL, .expectedGroup2Level=NULL},
        {.name="disabled for S, set for G1", .serviceNoQueueGroup=true, .group1QueueGroup="G1",
            .expectedServiceLevel=NULL, .expectedGroup1Level="G1", .expectedGroup2Level="G1"},
        {.name="disabled for G1", .group1NoQueueGroup=true,
            .expectedServiceLevel=MICRO_DEFAULT_QUEUE_GROUP, .expectedGroup1Level=NULL, .expectedGroup2Level=NULL},
        {.name="disabled for G1, set for G2", .group1NoQueueGroup=true, .group2QueueGroup="G2",
            .expectedServiceLevel=MICRO_DEFAULT_QUEUE_GROUP, .expectedGroup1Level=NULL, .expectedGroup2Level="G2"},
    };

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect to server: ");
    testCond(NATS_OK == natsConnection_Connect(&nc, opts));

    for (int i = 0; i < (int)(sizeof(tcs) / sizeof(tcs[0])); i++)
    {
        TC tc = tcs[i];
        struct threadArg arg;
        microService *service = NULL;
        microGroup *group1 = NULL, *group2 = NULL;
        microServiceInfo *info = NULL;
        microServiceStats *stats = NULL;

        if(_createDefaultThreadArgsForCbTests(&arg) != NATS_OK)
            FAIL("Unable to setup test args");

        testf("%s: ", tc.name);

        serviceConfig.NoQueueGroup = tc.serviceNoQueueGroup;
        serviceConfig.QueueGroup = tc.serviceQueueGroup;
        epConfig.NoQueueGroup = tc.epNoQueueGroup;
        epConfig.QueueGroup = tc.epQueueGroup;
        group1Config.NoQueueGroup = tc.group1NoQueueGroup;
        group1Config.QueueGroup = tc.group1QueueGroup;
        group2Config.NoQueueGroup = tc.group2NoQueueGroup;
        group2Config.QueueGroup = tc.group2QueueGroup;

        err = _startMicroservice(&service, nc, &serviceConfig, NULL, 0, &arg);
        MICRO_CALL(err, microService_AddEndpoint(service, &epConfig));
        MICRO_CALL(err, microService_AddGroup(&group1, service, &group1Config));
        MICRO_CALL(err, microGroup_AddEndpoint(group1, &epConfig));
        MICRO_CALL(err, microGroup_AddGroup(&group2, group1, &group2Config));
        MICRO_CALL(err, microGroup_AddEndpoint(group2, &epConfig));
        MICRO_CALL(err, microService_GetInfo(&info, service));
        MICRO_CALL(err, microService_GetStats(&stats, service));

#define _testQueueGroup(_expected, _actual) \
        (_expected) == NULL ? (_actual) == NULL : strcmp((_expected), (_actual)) == 0

        testCond((err == NULL) &&
            (info != NULL) && (info->EndpointsLen == 3) &&
            (stats != NULL) && (stats->EndpointsLen == 3) &&
            (_testQueueGroup(tc.expectedServiceLevel, info->Endpoints[2].QueueGroup)) &&
            (_testQueueGroup(tc.expectedServiceLevel, stats->Endpoints[2].QueueGroup)) &&
            (_testQueueGroup(tc.expectedGroup1Level, stats->Endpoints[1].QueueGroup)) &&
            (_testQueueGroup(tc.expectedGroup1Level, info->Endpoints[1].QueueGroup)) &&
            (_testQueueGroup(tc.expectedGroup2Level, info->Endpoints[0].QueueGroup)) &&
            (_testQueueGroup(tc.expectedGroup2Level, stats->Endpoints[0].QueueGroup)));

        _destroyMicroservice(service);
        _waitForMicroservicesAllDone(&arg);
        microServiceInfo_Destroy(info);
        microServiceStats_Destroy(stats);
        _destroyDefaultThreadArgs(&arg);
    }

    natsConnection_Destroy(nc);
    natsOptions_Destroy(opts);
    _stopServer(serverPid);
}

#define NUM_MICRO_SERVICES 5

void test_MicroBasics(void)
{
    natsStatus s = NATS_OK;
    microError *err = NULL;
    struct threadArg arg;
    natsOptions *opts = NULL;
    natsConnection *nc = NULL;
    natsPid serverPid = NATS_INVALID_PID;
    microService *svcs[NUM_MICRO_SERVICES];
    microEndpointConfig ep1_cfg = {
        .Name = "do",
        .Subject = "svc.do",
        .Handler = _microHandleRequestNoisy42,
        .State = NULL,
    };
    microEndpointConfig ep2_cfg = {
        .Name = "unused",
        .Subject = "svc.unused",
        .Handler = _microHandleRequestNoisy42,
        .Metadata = (natsMetadata){
            .List = (const char *[]){"key1", "value1", "key2", "value2", "key3", "value3"},
            .Count = 3,
        },
        .State = NULL,
    };
    microEndpointConfig *eps[] = {
        &ep1_cfg,
        &ep2_cfg,
    };
    microServiceConfig cfg = {
        .Metadata = (natsMetadata){
            .List = (const char *[]){"skey1", "svalue1", "skey2", "svalue2"},
            .Count = 2,
        },
        .Name = "ManyServicesSameName",
        .Endpoint = NULL,
        .State = NULL,
    };
    natsMsg *reply = NULL;
    microServiceInfo *info = NULL;
    int i;
    char buf[1024];
    char *subject = NULL;
    natsInbox *inbox = NULL;
    natsSubscription *sub = NULL;
    nats_JSON *js = NULL;
    nats_JSON *md = NULL;
    int num_requests = 0;
    int num_errors = 0;
    int n;
    nats_JSON **array;
    int array_len;
    const char *str;

    srand((unsigned int)nats_NowInNanoSeconds());

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
        opts = _createReconnectOptions();
    if ((opts == NULL)
        || (natsOptions_SetClosedCB(opts, _closedCb, &arg) != NATS_OK)
        || (natsOptions_SetURL(opts, NATS_DEFAULT_URL) != NATS_OK))
    {
        FAIL("Unable to setup test for MicroConnectionEvents!");
    }

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect to server: ");
    testCond(NATS_OK == natsConnection_Connect(&nc, opts));

    _startManyMicroservices(svcs, NUM_MICRO_SERVICES, nc, &cfg, eps, sizeof(eps)/sizeof(eps[0]), &arg);

    // Now send 50 requests.
    test("Send 50 requests (no matter response): ");
    for (i = 0; i < 50; i++)
    {
        s = natsConnection_Request(&reply, nc, "svc.do", NULL, 0, 1000);
        if (NATS_OK != s)
            FAIL("Unable to send request");
        natsMsg_Destroy(reply);
    }
    testCond(NATS_OK == s);

    // Make sure we can request valid info with local API.
    for (i = 0; i < NUM_MICRO_SERVICES; i++)
    {
        snprintf(buf, sizeof(buf), "Check local info #%d: ", i);
        test(buf);
        err = microService_GetInfo(&info, svcs[i]);
        testCond((err == NULL) &&
                 (strcmp(info->Name, "ManyServicesSameName") == 0) &&
                 (strlen(info->Id) > 0) &&
                 (strcmp(info->Description, "returns 42") == 0) &&
                 (strcmp(info->Version, "1.0.0") == 0) &&
                 (info->Metadata.Count == 2));
        microServiceInfo_Destroy(info);
    }

    // Make sure we can request valid info with $SRV.INFO request.
    test("Create INFO inbox: ");
    testCond(NATS_OK == natsInbox_Create(&inbox));
    micro_new_control_subject(&subject, MICRO_INFO_VERB, "ManyServicesSameName", NULL);
    test("Subscribe to INFO inbox: ");
    testCond(NATS_OK == natsConnection_SubscribeSync(&sub, nc, inbox));
    test("Publish INFO request: ");
    testCond(NATS_OK == natsConnection_PublishRequest(nc, subject, inbox, NULL, 0));
    for (i = 0;; i++)
    {
        snprintf(buf, sizeof(buf), "Receive INFO response #%d: ", i);
        test(buf);
        reply = NULL;
        s = natsSubscription_NextMsg(&reply, sub, 250);
        if (s == NATS_TIMEOUT)
        {
            testCond(i == NUM_MICRO_SERVICES);
            break;
        }
        testCond(NATS_OK == s);

        snprintf(buf, sizeof(buf), "Parse INFO response#%d: ", i);
        test(buf);
        js = NULL;
        testCond(NATS_OK == nats_JSONParse(&js, reply->data, reply->dataLen)) ;

        snprintf(buf, sizeof(buf), "Validate INFO response strings#%d: ", i);
        test(buf);
        testCond(
            (NATS_OK == nats_JSONGetStrPtr(js, "name", &str)) && (strcmp(str, "ManyServicesSameName") == 0)
            && (NATS_OK == nats_JSONGetStrPtr(js, "description", &str)) && (strcmp(str, "returns 42") == 0)
            && (NATS_OK == nats_JSONGetStrPtr(js, "version", &str)) && (strcmp(str, "1.0.0") == 0)
            && (NATS_OK == nats_JSONGetStrPtr(js, "id", &str)) && (strlen(str) > 0)
        );

        snprintf(buf, sizeof(buf), "Validate INFO service metadata#%d: ", i);
        test(buf);
        md = NULL;
        testCond(
            (NATS_OK == nats_JSONGetObject(js, "metadata", &md))
            && (NATS_OK == nats_JSONGetStrPtr(md, "skey1", &str)) && (strcmp(str, "svalue1") == 0)
            && (NATS_OK == nats_JSONGetStrPtr(md, "skey2", &str)) && (strcmp(str, "svalue2") == 0)
        );
        test("Validate INFO has 2 endpoints: ");
        array = NULL;
        array_len = 0;
        s = nats_JSONGetArrayObject(js, "endpoints", &array, &array_len);
        testCond((NATS_OK == s) && (array != NULL) && (array_len == 2));

        test("Validate INFO unused endpoint with metadata: ");
        md = NULL;
        testCond(
            (NATS_OK == nats_JSONGetStrPtr(array[0], "name", &str)) && (strcmp(str, "unused") == 0)
            && (NATS_OK == nats_JSONGetStrPtr(array[0], "subject", &str)) && (strcmp(str, "svc.unused") == 0)
            && (NATS_OK == nats_JSONGetStrPtr(array[0], "queue_group", &str)) && (strcmp(str, MICRO_DEFAULT_QUEUE_GROUP) == 0)
            && (NATS_OK == nats_JSONGetObject(array[0], "metadata", &md))
            && (NATS_OK == nats_JSONGetStrPtr(md, "key1", &str)) && (strcmp(str, "value1") == 0)
            && (NATS_OK == nats_JSONGetStrPtr(md, "key2", &str)) && (strcmp(str, "value2") == 0)
            && (NATS_OK == nats_JSONGetStrPtr(md, "key3", &str)) && (strcmp(str, "value3") == 0)
        );

        test("Validate INFO svc.do endpoint: ");
        md = NULL;
        testCond(
            (NATS_OK == nats_JSONGetStrPtr(array[1], "name", &str)) && (strcmp(str, "do") == 0)
            && (NATS_OK == nats_JSONGetStrPtr(array[1], "subject", &str)) && (strcmp(str, "svc.do") == 0)
            && (NATS_OK == nats_JSONGetStrPtr(array[1], "queue_group", &str)) && (strcmp(str, MICRO_DEFAULT_QUEUE_GROUP) == 0)
            && (NATS_OK == nats_JSONGetObject(array[1], "metadata", &md)) && (md == NULL)
        );

        nats_JSONDestroy(js);
        natsMsg_Destroy(reply);
        NATS_FREE(array);
    }
    natsSubscription_Destroy(sub);
    natsInbox_Destroy(inbox);
    NATS_FREE(subject);

    // Make sure we can request SRV.PING.
    test("Create PING inbox: ");
    testCond(NATS_OK == natsInbox_Create(&inbox));
    micro_new_control_subject(&subject, MICRO_PING_VERB, "ManyServicesSameName", NULL);
    test("Subscribe to PING inbox: ");
    testCond(NATS_OK == natsConnection_SubscribeSync(&sub, nc, inbox));
    test("Publish PING request: ");
    testCond(NATS_OK == natsConnection_PublishRequest(nc, subject, inbox, NULL, 0));
    for (i = 0;; i++)
    {
        snprintf(buf, sizeof(buf), "Receive PING response #%d: ", i);
        test(buf);
        reply = NULL;
        s = natsSubscription_NextMsg(&reply, sub, 250);
        if (s == NATS_TIMEOUT)
        {
            testCond(i == NUM_MICRO_SERVICES);
            break;
        }
        testCond(NATS_OK == s);
        snprintf(buf, sizeof(buf), "Validate PING response #%d: ", i);
        test(buf);
        js = NULL;
        testCond((NATS_OK == nats_JSONParse(&js, reply->data, reply->dataLen)) &&
                 (NATS_OK == nats_JSONGetStrPtr(js, "name", &str)) &&
                 (strcmp(str, "ManyServicesSameName") == 0));
        nats_JSONDestroy(js);
        natsMsg_Destroy(reply);
    }
    natsSubscription_Destroy(sub);
    natsInbox_Destroy(inbox);
    NATS_FREE(subject);

    // Get and validate $SRV.STATS from all service instances.
    test("Create STATS inbox: ");
    testCond(NATS_OK == natsInbox_Create(&inbox));
    micro_new_control_subject(&subject, MICRO_STATS_VERB, "ManyServicesSameName", NULL);
    test("Subscribe to STATS inbox: ");
    testCond(NATS_OK == natsConnection_SubscribeSync(&sub, nc, inbox));
    test("Publish STATS request: ");
    testCond(NATS_OK == natsConnection_PublishRequest(nc, subject, inbox, NULL, 0));

    for (i = 0;; i++)
    {
        snprintf(buf, sizeof(buf), "Receive STATS response #%d: ", i);
        test(buf);
        reply = NULL;
        s = natsSubscription_NextMsg(&reply, sub, 250);
        if (s == NATS_TIMEOUT)
        {
            testCond(i == NUM_MICRO_SERVICES);
            break;
        }
        testCond(NATS_OK == s);

        test("Parse STATS response: ");
        js = NULL;
        s = nats_JSONParse(&js, reply->data, reply->dataLen);
        testCond((NATS_OK == s) && (js != NULL));

        test("Ensure STATS has 2 endpoints: ");
        array = NULL;
        array_len = 0;
        s = nats_JSONGetArrayObject(js, "endpoints", &array, &array_len);
        testCond((NATS_OK == s) && (array != NULL) && (array_len == 2))

        test("Ensure endpoint 0 has num_requests: ");
        n = 0;
        s = nats_JSONGetInt(array[1], "num_requests", &n);
        testCond(NATS_OK == s);
        num_requests += n;

        test("Ensure endpoint 0 has num_errors: ");
        n = 0;
        s = nats_JSONGetInt(array[1], "num_errors", &n);
        testCond(NATS_OK == s);
        num_errors += n;

        NATS_FREE(array);
        nats_JSONDestroy(js);
        natsMsg_Destroy(reply);
    }
    test("Check that STATS total request counts add up (50): ");
    testCond(num_requests == 50);
    test("Check that STATS total error count is positive, depends on how many instances: ");
    testCond(num_errors > 0);

    natsSubscription_Destroy(sub);
    natsInbox_Destroy(inbox);
    NATS_FREE(subject);

    for (i = 0; i < NUM_MICRO_SERVICES; i++)
    {
        _destroyMicroservice(svcs[i]);
    }
    _waitForMicroservicesAllDone(&arg);

    test("Destroy the test connection: ");
    natsConnection_Destroy(nc);
    testCond(NATS_OK == _waitForConnClosed(&arg));

    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);
    _stopServer(serverPid);
}

void test_MicroStartStop(void)
{
    natsStatus s = NATS_OK;
    struct threadArg arg;
    natsOptions *opts = NULL;
    natsConnection *nc = NULL;
    natsPid serverPid = NATS_INVALID_PID;
    microService *svcs[NUM_MICRO_SERVICES];
    microEndpointConfig ep_cfg = {
        .Name = "do",
        .Subject = "svc.do",
        .Handler = _microHandleRequest42,
    };
    microServiceConfig cfg = {
        .Endpoint = &ep_cfg,
    };
    natsMsg *reply = NULL;
    int i;

    srand((unsigned int)nats_NowInNanoSeconds());

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
        opts = _createReconnectOptions();
    if ((opts == NULL)
        || (natsOptions_SetURL(opts, NATS_DEFAULT_URL) != NATS_OK)
        || (natsOptions_SetClosedCB(opts, _closedCb, &arg) != NATS_OK)
        || (natsOptions_SetAllowReconnect(opts, false) != NATS_OK))
    {
        FAIL("Unable to setup test for MicroConnectionEvents!");
    }

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect to server: ");
    testCond(NATS_OK == natsConnection_Connect(&nc, opts));

    _startManyMicroservices(svcs, NUM_MICRO_SERVICES, nc, &cfg, NULL, 0, &arg);

    // Now send some requests.
    test("Send requests: ");
    for (i = 0; i < 20; i++)
    {
        reply = NULL;
        s = natsConnection_Request(&reply, nc, "svc.do", NULL, 0, 2000);
        if (NATS_OK != s)
            FAIL("Unable to send request");
        if (reply == NULL || reply->dataLen != 2 || memcmp(reply->data, "42", 2) != 0)
            FAIL("Unexpected reply");
        natsMsg_Destroy(reply);
    }
    testCond(NATS_OK == s);

    for (i = 0; i < NUM_MICRO_SERVICES; i++)
    {
        _destroyMicroservice(svcs[i]);
    }
    _waitForMicroservicesAllDone(&arg);

    test("Destroy the test connection: ");
    natsConnection_Destroy(nc);
    testCond(NATS_OK == _waitForConnClosed(&arg));

    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);
    _stopServer(serverPid);
}

void test_MicroServiceStopsOnClosedConn(void)
{
    natsStatus s;
    natsConnection *nc = NULL;
    natsOptions *opts = NULL;
    natsPid serverPid = NATS_INVALID_PID;
    struct threadArg arg;
    microService *m = NULL;
    microServiceConfig cfg = {
        .Name = "test",
        .Version = "1.0.0",
    };
    natsMsg *reply = NULL;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
        opts = _createReconnectOptions();

    if ((opts == NULL) ||
        (natsOptions_SetURL(opts, NATS_DEFAULT_URL) != NATS_OK) ||
        (natsOptions_SetClosedCB(opts, _closedCb, (void*) &arg)) ||
        (natsOptions_SetAllowReconnect(opts, false) != NATS_OK))
    {
        FAIL("Unable to setup test for MicroConnectionEvents!");
    }

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect for microservice: ");
    testCond(NATS_OK == natsConnection_Connect(&nc, opts));

    _startMicroservice(&m, nc, &cfg, NULL, 0, &arg);

    test("Test microservice is running: ");
    testCond(!microService_IsStopped(m))

    test("Test microservice is responding to PING: ");
    testCond(NATS_OK == natsConnection_RequestString(&reply, nc, "$SRV.PING.test", "", 500));
    natsMsg_Destroy(reply);
    reply = NULL;

    test("Close the connection: ");
    testCond(NATS_OK == natsConnection_Drain(nc));
    natsConnection_Close(nc);

    test("Wait for it: ");
    testCond(_waitForConnClosed(&arg) == NATS_OK);

    test("Ensure the connection has closed: ");
    testCond(natsConnection_IsClosed(nc));

    test("Wait for the service to stop: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.microAllDone)
        s = natsCondition_TimedWait(arg.c, arg.m, 1000);
    natsMutex_Unlock(arg.m);
    testCond(arg.microAllDone);

    test("Test microservice is stopped: ");
    testCond(microService_IsStopped(m));

    _destroyMicroservice(m);
    _waitForMicroservicesAllDone(&arg);

    natsOptions_Destroy(opts);
    natsConnection_Destroy(nc);
    _destroyDefaultThreadArgs(&arg);
    _stopServer(serverPid);
}

void test_MicroServiceStopsWhenServerStops(void)
{
    natsStatus s;
    natsConnection *nc = NULL;
    natsOptions *opts = NULL;
    natsPid serverPid = NATS_INVALID_PID;
    struct threadArg arg;
    microService *m = NULL;
    microEndpointConfig ep_cfg = {
        .Handler = _microHandleRequest42,
    };
    microServiceConfig cfg = {
        .Name = "test",
        .Version = "1.0.0",
    };

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
        opts = _createReconnectOptions();

    if ((opts == NULL)
        || (natsOptions_SetURL(opts, NATS_DEFAULT_URL) != NATS_OK)
        || (natsOptions_SetClosedCB(opts, _closedCb, &arg) != NATS_OK)
        || (natsOptions_SetAllowReconnect(opts, false) != NATS_OK))
    {
        FAIL("Unable to setup test for MicroConnectionEvents!");
    }

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect for microservice: ");
    testCond(NATS_OK == natsConnection_Connect(&nc, opts));

    _startMicroservice(&m, nc, &cfg, NULL, 0, &arg);

    const int numEndpoints = 50;
    for (int i=0; i < numEndpoints; i++)
    {
        char buf[32];
        testf("Add endpoint %d: ", i);
        snprintf(buf, sizeof(buf), "do-%d", i);
        ep_cfg.Subject = buf;
        ep_cfg.Name = buf;
        testCond(NULL == microService_AddEndpoint(m, &ep_cfg));
    }

    test("Test microservice is running: ");
    testCond(!microService_IsStopped(m))

    testf("Check that the service has %d endpoints: ", numEndpoints);
    microServiceInfo *info = NULL;
    microError *err = microService_GetInfo(&info, m);
    testCond((err == NULL) && (info->EndpointsLen == numEndpoints));
    microServiceInfo_Destroy(info);

    test("Stop the server: ");
    testCond((_stopServer(serverPid), true));

    _waitForMicroservicesAllDone(&arg);

    test("Test microservice is not running: ");
    testCond(microService_IsStopped(m))

    _destroyMicroservice(m);

    test("Destroy the test connection: ");
    natsConnection_Destroy(nc);
    testCond(NATS_OK == _waitForConnClosed(&arg));

    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);
}

void _microAsyncErrorHandler(microService *m, microEndpoint *ep, natsStatus s)
{
    struct threadArg *arg = (struct threadArg*) microService_GetState(m);

    natsMutex_Lock(arg->m);
    // release the pending test request that caused the error
    arg->closed = true;

    // set the data to verify
    arg->status = s;
    natsCondition_Broadcast(arg->c);
    natsMutex_Unlock(arg->m);
}

microError *
_microAsyncErrorRequestHandler(microRequest *req)
{
    struct threadArg *arg = microRequest_GetServiceState(req);

    natsMutex_Lock(arg->m);

    arg->msgReceived = true;
    natsCondition_Signal(arg->c);

    while (!arg->closed)
        natsCondition_Wait(arg->c, arg->m);

    natsMutex_Unlock(arg->m);

    return NULL;
}

void test_MicroAsyncErrorHandlerMaxPendingMsgs(void)
{
    natsStatus          s;
    struct threadArg    arg;
    natsConnection      *nc       = NULL;
    natsOptions         *opts     = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    microService        *m        = NULL;
    microEndpoint       *ep       = NULL;
    microEndpointConfig ep_cfg = {
        .Name = "do",
        .Subject = "async_test",
        .Handler = _microAsyncErrorRequestHandler,
    };
    microServiceConfig cfg = {
        .Name = "test",
        .Version = "1.0.0",
        .ErrHandler = _microAsyncErrorHandler,
        .State = &arg,
        .DoneHandler = _microServiceDoneHandler,
    };

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetURL(opts, NATS_DEFAULT_URL));
    IFOK(s, natsOptions_SetMaxPendingMsgs(opts, 10));
    if (s != NATS_OK)
        FAIL("Unable to create options for test AsyncErrHandler");

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect to NATS: ");
    testCond(NATS_OK == natsConnection_Connect(&nc, opts));

    _startMicroservice(&m, nc, &cfg, NULL, 0, &arg);

    test("Test microservice is running: ");
    testCond(!microService_IsStopped(m))

    test("Add test endpoint: ");
    testCond(NULL == micro_add_endpoint(&ep, m, NULL, &ep_cfg, true));

    natsMutex_Lock(arg.m);
    arg.status = NATS_OK;
    natsMutex_Unlock(arg.m);

    test("Cause an error by sending too many messages: ");
    for (int i=0;
        (s == NATS_OK) && (i < (opts->maxPendingMsgs + 100)); i++)
    {
        s = natsConnection_PublishString(nc, "async_test", "hello");
    }
    testCond((NATS_OK == s)
        && (NATS_OK == natsConnection_Flush(nc)));

    test("Wait for async err callback: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 1000);
    testCond((s == NATS_OK) && arg.closed && (arg.status == NATS_SLOW_CONSUMER));
    natsMutex_Unlock(arg.m);

    _destroyMicroservice(m);
    _waitForMicroservicesAllDone(&arg);

    test("Destroy the test connection: ");
    natsConnection_Destroy(nc);
    testCond(NATS_OK == _waitForConnClosed(&arg));

    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);
    _stopServer(serverPid);
}

void test_MicroAsyncErrorHandlerMaxPendingBytes(void)
{
    natsStatus          s;
    struct threadArg    arg;
    natsConnection* nc = NULL;
    natsOptions* opts = NULL;
    natsPid             serverPid = NATS_INVALID_PID;
    microService* m = NULL;
    microEndpoint* ep = NULL;
    microEndpointConfig ep_cfg = {
        .Name = "do",
        .Subject = "async_test",
        .Handler = _microAsyncErrorRequestHandler,
    };
    microServiceConfig cfg = {
        .Name = "test",
        .Version = "1.0.0",
        .ErrHandler = _microAsyncErrorHandler,
        .State = &arg,
        .DoneHandler = _microServiceDoneHandler,
    };
    int data_len = 10;
    const char msg[] = { 0,1,2,3,4,5,6,7,8,9 }; //10 bytes long message
    int64_t pendingBytesLimit = 100;
    int i = 0;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test!");

    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetURL(opts, NATS_DEFAULT_URL));
    IFOK(s, natsOptions_SetMaxPendingBytes(opts, pendingBytesLimit));
    if (s != NATS_OK)
        FAIL("Unable to create options for test AsyncErrHandler");

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect to NATS: ");
    testCond(NATS_OK == natsConnection_Connect(&nc, opts));

    _startMicroservice(&m, nc, &cfg, NULL, 0, &arg);

    test("Test microservice is running: ");
    testCond(!microService_IsStopped(m))

    test("Add test endpoint: ");
    testCond(NULL == micro_add_endpoint(&ep, m, NULL, &ep_cfg, true));

    natsMutex_Lock(arg.m);
    arg.status = NATS_OK;
    natsMutex_Unlock(arg.m);

    test("Cause an error by sending too many messages: ");
    for (i=0;
        (s == NATS_OK) && (i < (pendingBytesLimit + 100)); i+=data_len) //increment by 10 (message size) each iteration
    {
        s = natsConnection_Publish(nc, "async_test", msg, data_len);
    }
    testCond((NATS_OK == s)
        && (NATS_OK == natsConnection_Flush(nc)));

    test("Wait for async err callback: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 1000);
    natsMutex_Unlock(arg.m);
    testCond((s == NATS_OK) && arg.closed && (arg.status == NATS_SLOW_CONSUMER));

    _destroyMicroservice(m);
    _waitForMicroservicesAllDone(&arg);

    test("Destroy the test connection: ");
    natsConnection_Destroy(nc);
    testCond(NATS_OK == _waitForConnClosed(&arg));

    natsOptions_Destroy(opts);
    _destroyDefaultThreadArgs(&arg);
    _stopServer(serverPid);
}

#if defined(NATS_HAS_STREAMING)

static int
_roundUp(int val)
{
    return ((val + (MEMALIGN-1))/MEMALIGN)*MEMALIGN;
}

void test_StanPBufAllocator(void)
{
    natsPBufAllocator   *a = NULL;
    natsStatus          s;
    char                *ptr1;
    char                *ptr2;
    char                *ptr3;
    char                *ptr4;
    char                *oldBuf;
    int                 oldCap;
    int                 expectedProtoSize;
    int                 expectedOverhead;
    int                 expectedUsed;
    int                 expectedRemaining;
    int                 expectedCap;
    int                 prevUsed;

    test("Create: ");
    s = natsPBufAllocator_Create(&a, 10, 2);
    expectedProtoSize = MEMALIGN + _roundUp(10);
    expectedOverhead  = (MEMALIGN * 2) + 2 + (2 * (MEMALIGN-1));
    testCond((s == NATS_OK)
            && (a->protoSize == expectedProtoSize)
            && (a->overhead == expectedOverhead)
            && (a->base.alloc != NULL)
            && (a->base.free != NULL)
            && (a->base.allocator_data == a));

    test("Prepare: ");
    natsPBufAllocator_Prepare(a, 20);
    expectedCap = expectedProtoSize + expectedOverhead + 20;
    testCond((a->buf != NULL)
            && (a->cap == expectedCap)
            && (a->remaining == a->cap)
            && (a->used == 0));

    test("Alloc 1: ");
    ptr1 = (char*) a->base.alloc((void*)a, 10);
    expectedUsed = MEMALIGN + _roundUp(10);
    expectedRemaining = expectedCap - expectedUsed;
    testCond((ptr1 != NULL)
            && ((ptr1-MEMALIGN) == a->buf)
            && ((ptr1-MEMALIGN)[0] == '0')
            && (a->used == expectedUsed)
            && (a->remaining == expectedRemaining));

    test("Alloc 2: ");
    ptr2 = (char*) a->base.alloc((void*)a, 5);
    prevUsed           = expectedUsed;
    expectedUsed      += MEMALIGN + _roundUp(5);
    expectedRemaining  = expectedCap - expectedUsed;
    testCond((ptr2 != ptr1)
            && ((ptr2-MEMALIGN) == (a->buf + prevUsed))
            && ((ptr2-MEMALIGN)[0] == '0')
            && (a->used == expectedUsed)
            && (a->remaining == expectedRemaining));

    test("Alloc 3: ");
    ptr3 = (char*) a->base.alloc((void*)a, 3);
    prevUsed           = expectedUsed;
    expectedUsed      += MEMALIGN + _roundUp(3);
    expectedRemaining  = expectedCap - expectedUsed;
    testCond((ptr3 != ptr2)
            && ((ptr3-MEMALIGN) == (a->buf + prevUsed))
            && ((ptr3-MEMALIGN)[0] == '0')
            && (a->used == expectedUsed)
            && (a->remaining == expectedRemaining));

    test("Alloc 4: ");
    ptr4 = (char*) a->base.alloc((void*)a, 50);
    testCond((ptr4 != ptr3)
            && (((ptr4-MEMALIGN) < a->buf) || ((ptr4-MEMALIGN) > (a->buf+a->cap)))
            && ((ptr4-MEMALIGN)[0] == '1')
            && (a->used == expectedUsed)
            && (a->remaining == expectedRemaining));

    // Free out of order, just make sure it does not crash
    // and valgrind will make sure that we freed ptr4.
    test("Free 2: ");
    a->base.free((void*) a, (void*) ptr2);
    testCond(1);

    test("Free 1: ");
    a->base.free((void*) a, (void*) ptr1);
    testCond(1);

    test("Free 4: ");
    a->base.free((void*) a, (void*) ptr4);
    testCond(1);

    test("Free 3: ");
    a->base.free((void*) a, (void*) ptr3);
    testCond(1);

    // Call prepare again with smaller buffer, buf should
    // remain same, but used/remaining should be updated.
    oldBuf = a->buf;
    oldCap = a->cap;
    test("Prepare with smaller buffer: ");
    natsPBufAllocator_Prepare(a, 5);
    testCond((a->buf == oldBuf)
            && (a->cap == oldCap)
            && (a->remaining == a->cap)
            && (a->used == 0));

    test("Prepare requires expand: ");
    natsPBufAllocator_Prepare(a, 100);
    // Realloc may or may not make a->buf be different...
    expectedCap = expectedProtoSize + expectedOverhead + 100;
    testCond((a->buf != NULL)
            && (a->cap == expectedCap)
            && (a->remaining == a->cap)
            && (a->used == 0));

    test("Destroy: ");
    natsPBufAllocator_Destroy(a);
    testCond(1);
}

static void
_stanConnLostCB(stanConnection *sc, const char *errorTxt, void *closure)
{
    struct threadArg *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);
    arg->closed = true;
    arg->status = NATS_OK;
    if ((arg->string != NULL) && (strcmp(errorTxt, arg->string) != 0))
        arg->status = NATS_ERR;
    natsCondition_Signal(arg->c);
    natsMutex_Unlock(arg->m);
}

void test_StanConnOptions(void)
{
    natsStatus      s;
    stanConnOptions *opts = NULL;
    stanConnOptions *clone= NULL;
    natsOptions     *no   = NULL;

    test("Create option: ");
    s = stanConnOptions_Create(&opts);
    testCond(s == NATS_OK);

    test("Has default values: ");
    testCond(
            (opts->connTimeout == STAN_CONN_OPTS_DEFAULT_CONN_TIMEOUT) &&
            (opts->connectionLostCB == stanConn_defaultConnLostHandler) &&
            (opts->connectionLostCBClosure == NULL) &&
            (strcmp(opts->discoveryPrefix, STAN_CONN_OPTS_DEFAULT_DISCOVERY_PREFIX) == 0) &&
            (opts->maxPubAcksInFlightPercentage == STAN_CONN_OPTS_DEFAULT_MAX_PUB_ACKS_INFLIGHT_PERCENTAGE) &&
            (opts->maxPubAcksInflight == STAN_CONN_OPTS_DEFAULT_MAX_PUB_ACKS_INFLIGHT) &&
            (opts->ncOpts == NULL) &&
            (opts->pingInterval == STAN_CONN_OPTS_DEFAULT_PING_INTERVAL) &&
            (opts->pingMaxOut == STAN_CONN_OPTS_DEFAULT_PING_MAX_OUT) &&
            (opts->pubAckTimeout == STAN_CONN_OPTS_DEFAULT_PUB_ACK_TIMEOUT) &&
            (opts->url == NULL));

    test("Check invalid connection wait: ");
    s = stanConnOptions_SetConnectionWait(opts, -10);
    if (s != NATS_OK)
        s = stanConnOptions_SetConnectionWait(opts, 0);
    testCond(s != NATS_OK);
    nats_clearLastError();

    test("Check invalid discovery prefix: ");
    s = stanConnOptions_SetDiscoveryPrefix(opts, NULL);
    if (s != NATS_OK)
        s = stanConnOptions_SetDiscoveryPrefix(opts, "");
    testCond(s != NATS_OK);
    nats_clearLastError();

    test("Check invalid max pub acks: ");
    s = stanConnOptions_SetMaxPubAcksInflight(opts, -1, 1);
    if (s != NATS_OK)
        s = stanConnOptions_SetMaxPubAcksInflight(opts, 0, 1);
    if (s != NATS_OK)
        s = stanConnOptions_SetMaxPubAcksInflight(opts, 10, -1);
    if (s != NATS_OK)
        s = stanConnOptions_SetMaxPubAcksInflight(opts, 10, 0);
    if (s != NATS_OK)
        s = stanConnOptions_SetMaxPubAcksInflight(opts, 10, 2);
    testCond(s != NATS_OK);
    nats_clearLastError();

    test("Check invalid pings: ");
    s = stanConnOptions_SetPings(opts, -1, 10);
    if (s != NATS_OK)
        s = stanConnOptions_SetPings(opts, 0, 10);
    if (s != NATS_OK)
        s = stanConnOptions_SetPings(opts, 1, -1);
    if (s != NATS_OK)
        s = stanConnOptions_SetPings(opts, 1, 0);
    if (s != NATS_OK)
        s = stanConnOptions_SetPings(opts, 1, 1);
    testCond(s != NATS_OK);
    nats_clearLastError();

    test("Check invalid pub ack wait: ");
    s = stanConnOptions_SetPubAckWait(opts, -1);
    if (s != NATS_OK)
        s = stanConnOptions_SetPubAckWait(opts, 0);
    testCond(s != NATS_OK);
    nats_clearLastError();

    test("Set values: ");
    s = stanConnOptions_SetConnectionWait(opts, 10000);
    IFOK(s, stanConnOptions_SetDiscoveryPrefix(opts, "myPrefix"));
    IFOK(s, stanConnOptions_SetMaxPubAcksInflight(opts, 10, (float) 0.8));
    IFOK(s, stanConnOptions_SetPings(opts, 1, 10));
    IFOK(s, stanConnOptions_SetPubAckWait(opts, 2000));
    IFOK(s, stanConnOptions_SetURL(opts, "nats://me:1"));
    IFOK(s, stanConnOptions_SetConnectionLostHandler(opts, _stanConnLostCB, (void*) 1));
    testCond((s == NATS_OK) &&
            (opts->connTimeout == 10000) &&
            (strcmp(opts->discoveryPrefix, "myPrefix") == 0) &&
            (opts->maxPubAcksInFlightPercentage == (float) 0.8) &&
            (opts->maxPubAcksInflight == 10) &&
            (opts->pingInterval == 1) &&
            (opts->pingMaxOut == 10) &&
            (opts->pubAckTimeout == 2000) &&
            (strcmp(opts->url, "nats://me:1") == 0) &&
            (opts->connectionLostCB == _stanConnLostCB) &&
            (opts->connectionLostCBClosure == (void*) 1)
            );

    test("Set NATS options: ");
    s = natsOptions_Create(&no);
    IFOK(s, natsOptions_SetMaxPendingMsgs(no, 1000));
    IFOK(s, stanConnOptions_SetNATSOptions(opts, no));
    // change value from no after setting to stan opts
    // check options were cloned.
    IFOK(s, natsOptions_SetMaxPendingMsgs(no, 2000));
    testCond((s == NATS_OK) &&
            (opts->ncOpts != NULL) && // set
            (opts->ncOpts != no) &&   // not a reference
            (opts->ncOpts->maxPendingMsgs == 1000) // original value
            );

    test("Check clone: ");
    s = stanConnOptions_clone(&clone, opts);
    // Change values from original, check that clone
    // keeps original values.
    IFOK(s, stanConnOptions_SetConnectionWait(opts, 3000));
    IFOK(s, stanConnOptions_SetDiscoveryPrefix(opts, "xxxxx"));
    IFOK(s, stanConnOptions_SetMaxPubAcksInflight(opts, 100, (float) 0.2));
    IFOK(s, stanConnOptions_SetPings(opts, 10, 20));
    IFOK(s, stanConnOptions_SetPubAckWait(opts, 3000));
    IFOK(s, stanConnOptions_SetURL(opts, "nats://metoo:1"));
    IFOK(s, stanConnOptions_SetConnectionLostHandler(opts, NULL, NULL));
    IFOK(s, stanConnOptions_SetNATSOptions(opts, NULL));
    testCond((s == NATS_OK) &&
            (clone != opts) &&
            (clone->connTimeout == 10000) &&
            (strcmp(clone->discoveryPrefix, "myPrefix") == 0) &&
            (clone->maxPubAcksInFlightPercentage == (float) 0.8) &&
            (clone->maxPubAcksInflight == 10) &&
            (clone->pingInterval == 1) &&
            (clone->pingMaxOut == 10) &&
            (clone->pubAckTimeout == 2000) &&
            (strcmp(clone->url, "nats://me:1") == 0) &&
            (clone->connectionLostCB == _stanConnLostCB) &&
            (clone->connectionLostCBClosure == (void*) 1) &&
            (clone->ncOpts != NULL) &&
            (clone->ncOpts != no) &&
            (clone->ncOpts->maxPendingMsgs == 1000)
            );

    test("Check cb and NATS options can be set to NULL: ");
    testCond(
            (opts->ncOpts == NULL) &&
            (opts->connectionLostCB == NULL) &&
            (opts->connectionLostCBClosure == NULL));

    test("Check URL can be set to NULL: ");
    s = stanConnOptions_SetURL(opts, NULL);
    testCond(s == NATS_OK);

    test("Check clone ok after destroy original: ");
    stanConnOptions_Destroy(opts);
    testCond((s == NATS_OK) &&
                (clone->connTimeout == 10000) &&
                (strcmp(clone->discoveryPrefix, "myPrefix") == 0) &&
                (clone->maxPubAcksInFlightPercentage == (float) 0.8) &&
                (clone->maxPubAcksInflight == 10) &&
                (clone->pingInterval == 1) &&
                (clone->pingMaxOut == 10) &&
                (clone->pubAckTimeout == 2000) &&
                (strcmp(clone->url, "nats://me:1") == 0) &&
                (clone->connectionLostCB == _stanConnLostCB) &&
                (clone->connectionLostCBClosure == (void*) 1) &&
                (clone->ncOpts != NULL) &&
                (clone->ncOpts != no) &&
                (clone->ncOpts->maxPendingMsgs == 1000)
                );

    natsOptions_Destroy(no);
    stanConnOptions_Destroy(clone);
}

void test_StanSubOptions(void)
{
    natsStatus      s;
    stanSubOptions  *opts = NULL;
    stanSubOptions  *clone= NULL;
    int64_t         now   = 0;

    test("Create Options: ");
    s = stanSubOptions_Create(&opts);
    testCond(s == NATS_OK);

    test("Default values: ");
    testCond(
            (opts->ackWait == STAN_SUB_OPTS_DEFAULT_ACK_WAIT) &&
            (opts->durableName == NULL) &&
            (opts->manualAcks == false) &&
            (opts->maxInflight == STAN_SUB_OPTS_DEFAULT_MAX_INFLIGHT) &&
            (opts->startAt == PB__START_POSITION__NewOnly) &&
            (opts->startSequence == 0) &&
            (opts->startTime == 0)
            );

    test("Check invalid ackwait: ");
    s = stanSubOptions_SetAckWait(opts, -1);
    if (s != NATS_OK)
        s = stanSubOptions_SetAckWait(opts, 0);
    testCond(s != NATS_OK);
    nats_clearLastError();

    test("Check invalid maxinflight: ");
    s = stanSubOptions_SetMaxInflight(opts, -1);
    if (s != NATS_OK)
        s = stanSubOptions_SetMaxInflight(opts, 0);
    testCond(s != NATS_OK);
    nats_clearLastError();

    test("Check invalid start seq: ");
    s = stanSubOptions_StartAtSequence(opts, 0);
    testCond(s != NATS_OK);
    nats_clearLastError();

    test("Check invalid start time: ");
    s = stanSubOptions_StartAtTime(opts, -1);
    testCond(s != NATS_OK);
    nats_clearLastError();

    test("Check invalid start time: ");
    s = stanSubOptions_StartAtTimeDelta(opts, -1);
    testCond(s != NATS_OK);
    nats_clearLastError();

    test("Check set values: ");
    s = stanSubOptions_SetAckWait(opts, 1000);
    IFOK(s, stanSubOptions_SetDurableName(opts, "myDurable"));
    IFOK(s, stanSubOptions_SetManualAckMode(opts, true));
    IFOK(s, stanSubOptions_SetMaxInflight(opts, 200));
    testCond((s == NATS_OK) &&
            (opts->ackWait == 1000) &&
            (strcmp(opts->durableName, "myDurable") == 0) &&
            (opts->manualAcks == true) &&
            (opts->maxInflight == 200)
            );

    now = nats_Now();
    test("Check start at time delta: ");
    s = stanSubOptions_StartAtTimeDelta(opts, 20000);
    testCond((s == NATS_OK) &&
            (opts->startAt == PB__START_POSITION__TimeDeltaStart) &&
                ((opts->startTime >= now-20200) &&
                 (opts->startTime <= now-19800))
            );

    test("Check start at time: ");
    s = stanSubOptions_StartAtTime(opts, 1234567890);
    testCond((s == NATS_OK) &&
            (opts->startAt == PB__START_POSITION__TimeDeltaStart) &&
            (opts->startTime == 1234567890)
            );

    test("Check start at seq: ");
    s = stanSubOptions_StartAtSequence(opts, 100);
    testCond((s == NATS_OK) &&
            (opts->startAt == PB__START_POSITION__SequenceStart) &&
            (opts->startSequence == 100)
            );

    test("Check deliver all avail: ");
    s = stanSubOptions_DeliverAllAvailable(opts);
    testCond((s == NATS_OK) && (opts->startAt == PB__START_POSITION__First));

    test("Check clone: ");
    s = stanSubOptions_clone(&clone, opts);
    // Change values of opts to show that this does not affect
    // the clone
    IFOK(s, stanSubOptions_SetAckWait(opts, 20000));
    IFOK(s, stanSubOptions_SetDurableName(opts, NULL));
    IFOK(s, stanSubOptions_SetManualAckMode(opts, false));
    IFOK(s, stanSubOptions_SetMaxInflight(opts, 4000));
    IFOK(s, stanSubOptions_StartAtSequence(opts, 100));
    testCond((s == NATS_OK) &&
            (clone != opts) &&
            (clone->ackWait == 1000) &&
            (strcmp(clone->durableName, "myDurable") == 0) &&
            (clone->manualAcks == true) &&
            (clone->maxInflight == 200) &&
            (clone->startAt == PB__START_POSITION__First)
            );

    test("Check clone ok after destroy original: ");
    stanSubOptions_Destroy(opts);
    testCond((s == NATS_OK) &&
            (clone != opts) &&
            (clone->ackWait == 1000) &&
            (strcmp(clone->durableName, "myDurable") == 0) &&
            (clone->manualAcks == true) &&
            (clone->maxInflight == 200) &&
            (clone->startAt == PB__START_POSITION__First)
            );

    stanSubOptions_Destroy(clone);
}

void test_StanMsg(void)
{
    test("GetSequence with NULL msg: ");
    testCond(stanMsg_GetSequence(NULL) == 0);

    test("GetData with NULL msg: ");
    testCond(stanMsg_GetData(NULL) == NULL);

    test("GetDataLength with NULL msg: ");
    testCond(stanMsg_GetDataLength(NULL) == 0);

    test("GetTimestamp with NULL msg: ");
    testCond(stanMsg_GetTimestamp(NULL) == 0);

    test("IsRedelivered with NULL msg: ");
    testCond(stanMsg_IsRedelivered(NULL) == false);

    stanMsg_Destroy(NULL);
}

void test_StanServerNotReachable(void)
{
    natsStatus      s;
    stanConnection  *sc = NULL;
    stanConnOptions *opts = NULL;
    natsPid         serverPid = NATS_INVALID_PID;
    int64_t         now = 0;
    int64_t         elapsed = 0;

    s = stanConnOptions_Create(&opts);
    IFOK(s, stanConnOptions_SetURL(opts, "nats://127.0.0.1:4222"));
    IFOK(s, stanConnOptions_SetConnectionWait(opts, 250));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    serverPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(serverPid);

    test("Connect fails if no streaming server running: ");
    now = nats_Now();
    s = stanConnection_Connect(&sc, clusterName, clientName, opts);
    elapsed = nats_Now()-now;
    if (serverVersionAtLeast(2, 2, 0))
    {
        testCond((s == NATS_NO_RESPONDERS) &&
                 (strstr(nats_GetLastError(NULL), STAN_ERR_CONNECT_REQUEST_NO_RESP) != NULL));
    }
    else
    {
        testCond((s == NATS_TIMEOUT) &&
                 (strstr(nats_GetLastError(NULL), STAN_ERR_CONNECT_REQUEST_TIMEOUT) != NULL) &&
                 (elapsed < 2000));
    }

    stanConnOptions_Destroy(opts);

    _stopServer(serverPid);
}

void test_StanBasicConnect(void)
{
    natsStatus      s;
    stanConnection  *sc = NULL;
    natsPid         pid = NATS_INVALID_PID;
    stanConnOptions *opts   = NULL;
    natsOptions     *nopts  = NULL;

    pid = _startStreamingServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Basic connect: ");
    s = stanConnection_Connect(&sc, clusterName, clientName, NULL);
    testCond(s == NATS_OK);

    test("Connection close: ");
    s = stanConnection_Close(sc);
    testCond(s == NATS_OK);

    test("Connection double close: ");
    s = stanConnection_Close(sc);
    testCond(s == NATS_OK);

    stanConnection_Destroy(sc);
    sc = NULL;

    _stopServer(pid);
    pid = NATS_INVALID_PID;

    pid = _startStreamingServer("nats://127.0.0.1:4223", "-p 4223", true);
    CHECK_SERVER_STARTED(pid);

    test("Connect with non default stan URL: ");
    s = stanConnOptions_Create(&opts);
    IFOK(s, stanConnOptions_SetURL(opts, "nats://127.0.0.1:4223"));
    IFOK(s, stanConnection_Connect(&sc, clusterName, clientName, opts));
    testCond(s == NATS_OK);

    stanConnection_Destroy(sc);
    sc = NULL;

    test("stan URL takes precedence: ");
    s = natsOptions_Create(&nopts);
    IFOK(s, natsOptions_SetURL(nopts, "nats://127.0.0.1:4224")); // wrong URL
    IFOK(s, stanConnOptions_SetNATSOptions(opts, nopts));
    IFOK(s, stanConnection_Connect(&sc, clusterName, clientName, opts));
    // Should connect because it should use the one from stanConnOptions_SetURL.
    testCond(s == NATS_OK);

    stanConnection_Destroy(sc);
    sc = NULL;

    test("If no stan URL set, uses NATS URL: ");
    s = stanConnOptions_SetURL(opts, NULL);
    IFOK(s, natsOptions_SetURL(nopts, "nats://127.0.0.1:4223"));
    IFOK(s, stanConnOptions_SetNATSOptions(opts, nopts));
    IFOK(s, stanConnection_Connect(&sc, clusterName, clientName, opts));
    // Should connect because it should use the one from StanURL.
    testCond(s == NATS_OK);

    stanConnection_Destroy(sc);
    stanConnOptions_Destroy(opts);
    natsOptions_Destroy(nopts);

    _stopServer(pid);
}

void test_StanConnectError(void)
{
    natsStatus          s;
    stanConnection      *sc = NULL;
    stanConnection      *sc2 = NULL;
    natsPid             nPid = NATS_INVALID_PID;
    natsPid             sPid = NATS_INVALID_PID;
    stanConnOptions     *opts = NULL;

    nPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(nPid);

    sPid = _startStreamingServer("nats://127.0.0.1:4222", "-ns nats://127.0.0.1:4222", true);
    CHECK_SERVER_STARTED(sPid);

    test("Check connect response error: ");
    s = stanConnection_Connect(&sc, clusterName, clientName, NULL);
    IFOK(s, stanConnection_Connect(&sc2, clusterName, clientName, NULL));
    testCond((s == NATS_ERR) &&
            (strstr(nats_GetLastError(NULL), "clientID already registered") != NULL));

    test("Check wrong discovery prefix: ");
    s = stanConnOptions_Create(&opts);
    IFOK(s, stanConnOptions_SetDiscoveryPrefix(opts, "wrongprefix"));
    IFOK(s, stanConnOptions_SetConnectionWait(opts, 500));
    IFOK(s, stanConnection_Connect(&sc2, clusterName, "newClient", opts));
    testCond(serverVersionAtLeast(2, 2, 0) ? (s == NATS_NO_RESPONDERS) : (s == NATS_TIMEOUT));

    stanConnection_Destroy(sc);
    stanConnOptions_Destroy(opts);

    _stopServer(sPid);
    _stopServer(nPid);
}


void test_StanBasicPublish(void)
{
    natsStatus      s;
    stanConnection  *sc = NULL;
    natsPid         pid = NATS_INVALID_PID;

    pid = _startStreamingServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Basic publish: ");
    s = stanConnection_Connect(&sc, clusterName, clientName, NULL);
    IFOK(s, stanConnection_Publish(sc, "foo", (const void*) "hello", 5));
    testCond(s == NATS_OK);

    stanConnection_Destroy(sc);

    if (valgrind)
        nats_Sleep(900);

    _stopServer(pid);
}

static void
_stanPubAckHandler(const char *guid, const char *errTxt, void* closure)
{
    struct threadArg *args= (struct threadArg*) closure;

    natsMutex_Lock(args->m);
    args->status = NATS_OK;
    if (errTxt != NULL)
    {
        if ((args->string == NULL) || (strstr(errTxt, args->string) == NULL))
            args->status = NATS_ERR;
    }
    else if (args->string != NULL)
    {
        args->status = NATS_ERR;
    }
    args->msgReceived = true;
    natsCondition_Signal(args->c);
    natsMutex_Unlock(args->m);
}

void test_StanBasicPublishAsync(void)
{
    natsStatus          s;
    stanConnection      *sc = NULL;
    natsPid             pid = NATS_INVALID_PID;
    struct threadArg    args;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    pid = _startStreamingServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Basic publish async: ");
    s = stanConnection_Connect(&sc, clusterName, clientName, NULL);
    IFOK(s, stanConnection_PublishAsync(sc, "foo", (const void*) "hello", 5,
                                        _stanPubAckHandler, (void*) &args));
    testCond(s == NATS_OK);

    test("PubAck callback report no error: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.msgReceived)
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    IFOK(s, args.status);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    stanConnection_Destroy(sc);

    _destroyDefaultThreadArgs(&args);

    if (valgrind)
        nats_Sleep(900);

    _stopServer(pid);
}

void test_StanPublishTimeout(void)
{
    natsStatus          s;
    stanConnection      *sc = NULL;
    struct threadArg    args;
    natsPid             nPid = NATS_INVALID_PID;
    natsPid             sPid = NATS_INVALID_PID;
    stanConnOptions     *opts = NULL;

    s = _createDefaultThreadArgsForCbTests(&args);
    IFOK(s, stanConnOptions_Create(&opts));
    IFOK(s, stanConnOptions_SetPubAckWait(opts, 50));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    nPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(nPid);

    sPid = _startStreamingServer("nats://127.0.0.1:4222", "-ns nats://127.0.0.1:4222", true);
    CHECK_SERVER_STARTED(sPid);

    // First connect, then once that's done, shutdown streaming server
    s = stanConnection_Connect(&sc, clusterName, clientName, opts);

    _stopServer(sPid);

    if (s != NATS_OK)
    {
        _stopServer(nPid);
        FAIL("Not able to create connection for this test");
    }

    args.string = STAN_ERR_PUB_ACK_TIMEOUT;

    test("Check publish async timeout");
    s = stanConnection_PublishAsync(sc, "foo", (const void*) "hello", 5,
                                    _stanPubAckHandler, (void*) &args);
    testCond(s == NATS_OK);

    test("PubAck callback report pub ack timeout error: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.msgReceived)
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    IFOK(s, args.status);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    // Speed up test by closing stan's nc connection to avoid timing out on conn close
    stanConnClose(sc, false);

    stanConnOptions_Destroy(opts);
    stanConnection_Destroy(sc);

    _destroyDefaultThreadArgs(&args);

    _stopServer(nPid);
}

static void
_stanPublishAsyncThread(void *closure)
{
    struct threadArg *args = (struct threadArg*) closure;
    int i;

    for (i = 0; i < 10; i++)
        stanConnection_PublishAsync(args->sc, "foo", (const void*)"hello", 5, NULL, NULL);
}

static void
_stanPublishSyncThread(void *closure)
{
    stanConnection *sc = (stanConnection*) closure;

    stanConnection_Publish(sc, "foo", (const void*)"hello", 5);
}

void test_StanPublishMaxAcksInflight(void)
{
    natsStatus          s;
    stanConnection      *sc1 = NULL;
    stanConnection      *sc2 = NULL;
    struct threadArg    args;
    natsPid             nPid = NATS_INVALID_PID;
    natsPid             sPid = NATS_INVALID_PID;
    stanConnOptions     *opts = NULL;
    natsThread          *t = NULL;
    natsThread          *pts[10];
    int                 i;
    natsConnection      *nc = NULL;

    for (i=0;i<10;i++)
        pts[i] = NULL;

    s = _createDefaultThreadArgsForCbTests(&args);
    IFOK(s, stanConnOptions_Create(&opts));
    IFOK(s, stanConnOptions_SetMaxPubAcksInflight(opts, 5, 1.0));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    nPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(nPid);

    sPid = _startStreamingServer("nats://127.0.0.1:4222", "-ns nats://127.0.0.1:4222", true);
    CHECK_SERVER_STARTED(sPid);

    // First connect, then once that's done, shutdown streaming server
    s = stanConnection_Connect(&sc1, clusterName, clientName, opts);
    IFOK(s, stanConnection_Connect(&sc2, clusterName, "otherClient", opts));
    if (s != NATS_OK)
    {
        stanConnection_Destroy(sc1);
        stanConnection_Destroy(sc2);
        _stopServer(sPid);
        _stopServer(nPid);
        FAIL("Not able to create connection for this test");
    }

    _stopServer(sPid);

    // grap nc first
    natsMutex_Lock(sc1->mu);
    nc = sc1->nc;
    natsMutex_Unlock(sc1->mu);

    test("Check max inflight: ");
    args.sc = sc1;
    // Retain the connection
    stanConn_retain(sc1);
    s = natsThread_Create(&t, _stanPublishAsyncThread, (void*) &args);
    if (s == NATS_OK)
    {
        int i = 0;
        // Check that pubAckMap size is never greater than 5.
        for (i=0; (s == NATS_OK) && (i<10); i++)
        {
            nats_Sleep(100);
            natsMutex_Lock(sc1->pubAckMu);
            s = (natsStrHash_Count(sc1->pubAckMap) <= 5 ? NATS_OK : NATS_ERR);
            natsMutex_Unlock(sc1->pubAckMu);
        }
    }
    testCond(s == NATS_OK);

    test("Close unblock: ");
    natsConn_close(nc);
    nc = NULL;
    stanConnection_Destroy(sc1);
    natsThread_Join(t);
    natsThread_Destroy(t);
    stanConn_release(sc1);
    testCond(s == NATS_OK);

    // Repeat test with sync publishers

    // grap nc first
    natsMutex_Lock(sc2->mu);
    nc = sc2->nc;
    natsMutex_Unlock(sc2->mu);

    test("Check max inflight: ");
    // Retain the connection
    stanConn_retain(sc2);
    for (i=0; (s == NATS_OK) && (i<10); i++)
        s = natsThread_Create(&(pts[i]), _stanPublishSyncThread, (void*) sc2);
    if (s == NATS_OK)
    {
        int i = 0;
        // Check that pubAckMap size is never greater than 5.
        for (i=0; (s == NATS_OK) && (i<10); i++)
        {
            nats_Sleep(100);
            natsMutex_Lock(sc2->pubAckMu);
            s = (natsStrHash_Count(sc2->pubAckMap) <= 5 ? NATS_OK : NATS_ERR);
            natsMutex_Unlock(sc2->pubAckMu);
        }
    }
    testCond(s == NATS_OK);

    test("Close unblock: ");
    natsConn_close(nc);
    nc = NULL;
    stanConnection_Destroy(sc2);
    for (i = 0; i<10; i++)
    {
        if (pts[i] != NULL)
        {
            natsThread_Join(pts[i]);
            natsThread_Destroy(pts[i]);
        }
    }
    stanConn_release(sc2);
    testCond(s == NATS_OK);

    stanConnOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&args);

    _stopServer(nPid);
}

static void
_dummyStanMsgHandler(stanConnection *sc, stanSubscription *sub, const char *channel,
                     stanMsg *msg, void* closure)
{
    stanMsg_Destroy(msg);
}

static void
_stanMsgHandlerBumpSum(stanConnection *sc, stanSubscription *sub, const char *channel,
                      stanMsg *msg, void* closure)
{
    struct threadArg *args = (struct threadArg*) closure;

    natsMutex_Lock(args->m);
    if (!stanMsg_IsRedelivered(msg))
        args->sum++;
    else
        args->redelivered++;
    natsCondition_Broadcast(args->c);
    natsMutex_Unlock(args->m);

    stanMsg_Destroy(msg);
}

void test_StanBasicSubscription(void)
{
    natsStatus          s;
    stanConnection      *sc = NULL;
    stanSubscription    *sub = NULL;
    stanSubscription    *subf = NULL;
    natsPid             pid = NATS_INVALID_PID;

    pid = _startStreamingServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    s = stanConnection_Connect(&sc, clusterName, clientName, NULL);
    if (s != NATS_OK)
    {
        _stopServer(pid);
        FAIL("Unable to create connection for this test");
    }

    test("Basic subscibe: ");
    s = stanConnection_Subscribe(&sub, sc, "foo", _dummyStanMsgHandler, NULL, NULL);
    testCond(s == NATS_OK);

    test("Close connection: ")
    s = stanConnection_Close(sc);
    testCond(s == NATS_OK);

    test("Subscribe should fail after conn closed: ");
    s = stanConnection_Subscribe(&subf, sc, "foo", _dummyStanMsgHandler, NULL, NULL);
    testCond(s == NATS_CONNECTION_CLOSED);

    test("Subscribe should fail after conn closed: ");
    s = stanConnection_QueueSubscribe(&subf, sc, "foo", "bar", _dummyStanMsgHandler, NULL, NULL);
    testCond(s == NATS_CONNECTION_CLOSED);

    stanSubscription_Destroy(sub);
    stanConnection_Destroy(sc);

    if (valgrind)
        nats_Sleep(900);

    _stopServer(pid);
}

void test_StanSubscriptionCloseAndUnsubscribe(void)
{
    natsStatus          s;
    stanConnection      *sc = NULL;
    stanSubscription    *sub = NULL;
    stanSubscription    *sub2 = NULL;
    natsPid             pid = NATS_INVALID_PID;
    natsPid             spid = NATS_INVALID_PID;
    char                *cs = NULL;
    stanConnOptions     *opts = NULL;

    s = stanConnOptions_Create(&opts);
    IFOK(s, stanConnOptions_SetConnectionWait(opts, 250));
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    pid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    spid = _startStreamingServer("nats://127.0.0.1:4222", "-ns nats://127.0.0.1:4222", true);
    CHECK_SERVER_STARTED(spid);

    s = stanConnection_Connect(&sc, clusterName, clientName, opts);
    if (s != NATS_OK)
    {
        _stopServer(spid);
        _stopServer(pid);
        FAIL("Unable to create connection for this test");
    }

    test("Unsubscribe: ");
    s = stanConnection_Subscribe(&sub, sc, "foo", _dummyStanMsgHandler, NULL, NULL);
    IFOK(s, stanSubscription_Unsubscribe(sub));
    testCond(s == NATS_OK);

    stanSubscription_Destroy(sub);
    sub = NULL;

    test("Close: ");
    s = stanConnection_Subscribe(&sub, sc, "foo", _dummyStanMsgHandler, NULL, NULL);
    IFOK(s, stanSubscription_Close(sub));
    testCond(s == NATS_OK);

    stanSubscription_Destroy(sub);
    sub = NULL;

    test("Close not supported: ");
    // Simulate that we connected to an older server
    natsMutex_Lock(sc->mu);
    cs = sc->subCloseRequests;
    sc->subCloseRequests = NULL;
    natsMutex_Unlock(sc->mu);
    s = stanConnection_Subscribe(&sub, sc, "foo", _dummyStanMsgHandler, NULL, NULL);
    IFOK(s, stanSubscription_Close(sub));
    testCond((s == NATS_NO_SERVER_SUPPORT) &&
            (strstr(nats_GetLastError(NULL), STAN_ERR_SUB_CLOSE_NOT_SUPPORTED) != NULL));

    stanSubscription_Destroy(sub);
    sub = NULL;

    natsMutex_Lock(sc->mu);
    sc->subCloseRequests = cs;
    natsMutex_Unlock(sc->mu);

    test("Close/Unsub timeout: ");
    s = stanConnection_Subscribe(&sub, sc, "foo", _dummyStanMsgHandler, NULL, NULL);
    IFOK(s, stanConnection_Subscribe(&sub2, sc, "foo", _dummyStanMsgHandler, NULL, NULL));

    // Stop the serer
    _stopServer(spid);

    if (s == NATS_OK)
    {
        s = stanSubscription_Close(sub);
        if (s != NATS_OK)
            s = stanSubscription_Unsubscribe(sub2);
    }
    if (serverVersionAtLeast(2, 2, 0))
    {
        testCond((s == NATS_NO_RESPONDERS) &&
                 (strstr(nats_GetLastError(NULL), "no streaming server was listening") != NULL));
    }
    else
    {
        testCond((s == NATS_TIMEOUT) &&
                 (strstr(nats_GetLastError(NULL), "request timeout") != NULL));
    }
    stanSubscription_Destroy(sub);
    stanSubscription_Destroy(sub2);

    stanConnClose(sc, false);
    stanConnection_Destroy(sc);
    stanConnOptions_Destroy(opts);

    if (valgrind)
        nats_Sleep(900);

    _stopServer(pid);
}

void test_StanDurableSubscription(void)
{
    natsStatus          s;
    stanConnection      *sc = NULL;
    stanSubscription    *dur = NULL;
    stanSubOptions      *opts = NULL;
    natsPid             pid = NATS_INVALID_PID;
    struct threadArg    args;
    int                 i;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Error setting up test");

    pid = _startStreamingServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    s = stanConnection_Connect(&sc, clusterName, clientName, NULL);
    if (s != NATS_OK)
    {
        _stopServer(pid);
        FAIL("Unable to create connection for this test");
    }

    test("Send some messages: ");
    for (i=0; (s == NATS_OK) && (i<3); i++)
        s = stanConnection_Publish(sc, "foo", (const void*) "hello", 5);
    testCond(s == NATS_OK);

    test("Basic durable subscibe: ");
    s = stanSubOptions_Create(&opts);
    IFOK(s, stanSubOptions_SetDurableName(opts, "dur"));
    IFOK(s, stanSubOptions_DeliverAllAvailable(opts));
    IFOK(s, stanConnection_Subscribe(&dur, sc, "foo", _stanMsgHandlerBumpSum, &args, opts));
    testCond(s == NATS_OK);

    test("Check 3 messages received: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum != 3))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    // Wait a bit to give a chance for the server to process acks.
    nats_Sleep(500);

    test("Close connection: ");
    s = stanConnection_Close(sc);
    testCond(s == NATS_OK);

    stanSubscription_Destroy(dur);
    dur = NULL;
    stanConnection_Destroy(sc);
    sc = NULL;

    test("Connect again: ");
    s = stanConnection_Connect(&sc, clusterName, clientName, NULL);
    testCond(s == NATS_OK);

    test("Send 2 more messages: ");
    for (i=0; (s == NATS_OK) && (i<2); i++)
        s = stanConnection_Publish(sc, "foo", (const void*) "hello", 5);
    testCond(s == NATS_OK);

    test("Recreate durable with start seq 1: ");
    s = stanSubOptions_StartAtSequence(opts, 1);
    IFOK(s, stanConnection_Subscribe(&dur, sc, "foo", _stanMsgHandlerBumpSum, &args, opts));
    testCond(s == NATS_OK);

    test("Check 5 messages total are received: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum != 5))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    testCond(s == NATS_OK);
    test("Check no redelivered: ");
    testCond((s == NATS_OK) && (args.redelivered == 0));
    natsMutex_Unlock(args.m);

    stanSubscription_Destroy(dur);
    stanSubOptions_Destroy(opts);
    stanConnection_Destroy(sc);

    _destroyDefaultThreadArgs(&args);

    _stopServer(pid);
}

void test_StanBasicQueueSubscription(void)
{
    natsStatus          s;
    stanConnection      *sc = NULL;
    stanSubscription    *qsub1 = NULL;
    stanSubscription    *qsub2 = NULL;
    stanSubscription    *qsub3 = NULL;
    stanSubOptions      *opts = NULL;
    natsPid             pid = NATS_INVALID_PID;
    struct threadArg    args;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Error setting up test");

    pid = _startStreamingServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    s = stanConnection_Connect(&sc, clusterName, clientName, NULL);
    if (s != NATS_OK)
    {
        _stopServer(pid);
        FAIL("Unable to create connection for this test");
    }

    test("Basic queue subscibe: ");
    s = stanConnection_QueueSubscribe(&qsub1, sc, "foo", "bar", _stanMsgHandlerBumpSum, &args, NULL);
    IFOK(s, stanConnection_QueueSubscribe(&qsub2, sc, "foo", "bar", _stanMsgHandlerBumpSum, &args, NULL));
    testCond(s == NATS_OK);

    // Test that durable and non durable queue subscribers with
    // same name can coexist and they both receive the same message.
    test("New durable queue sub with same queue name: ");
    s = stanSubOptions_Create(&opts);
    IFOK(s, stanSubOptions_SetDurableName(opts, "durable-queue-sub"));
    IFOK(s, stanConnection_QueueSubscribe(&qsub3, sc, "foo", "bar", _stanMsgHandlerBumpSum, &args, opts));
    testCond(s == NATS_OK);

    test("Check published message ok: ");
    s = stanConnection_Publish(sc, "foo", (const void*)"hello", 5);
    testCond(s == NATS_OK);

    test("Check 1 message published is received once per group: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum != 2))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    stanSubscription_Destroy(qsub1);
    stanSubscription_Destroy(qsub2);
    stanSubscription_Destroy(qsub3);
    stanSubOptions_Destroy(opts);
    stanConnection_Destroy(sc);

    _destroyDefaultThreadArgs(&args);

    if (valgrind)
        nats_Sleep(900);

    _stopServer(pid);
}

void test_StanDurableQueueSubscription(void)
{
    natsStatus          s;
    stanConnection      *sc = NULL;
    stanSubscription    *dur = NULL;
    stanSubOptions      *opts = NULL;
    natsPid             pid = NATS_INVALID_PID;
    struct threadArg    args;
    int                 i;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Error setting up test");

    pid = _startStreamingServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    s = stanConnection_Connect(&sc, clusterName, clientName, NULL);
    if (s != NATS_OK)
    {
        _stopServer(pid);
        FAIL("Unable to create connection for this test");
    }

    test("Send some messages: ");
    for (i=0; (s == NATS_OK) && (i<3); i++)
        s = stanConnection_Publish(sc, "foo", (const void*) "hello", 5);
    testCond(s == NATS_OK);

    test("Basic durable subscibe: ");
    s = stanSubOptions_Create(&opts);
    IFOK(s, stanSubOptions_SetDurableName(opts, "dur"));
    IFOK(s, stanSubOptions_DeliverAllAvailable(opts));
    IFOK(s, stanConnection_QueueSubscribe(&dur, sc, "foo", "bar", _stanMsgHandlerBumpSum, &args, opts));
    testCond(s == NATS_OK);

    test("Check 3 messages received: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum != 3))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    // Give a chance for the server to process those acks
    nats_Sleep(500);

    test("Close connection: ");
    s = stanConnection_Close(sc);
    testCond(s == NATS_OK);

    stanSubscription_Destroy(dur);
    dur = NULL;
    stanConnection_Destroy(sc);
    sc = NULL;

    test("Connect again: ");
    s = stanConnection_Connect(&sc, clusterName, clientName, NULL);
    testCond(s == NATS_OK);

    test("Send 2 more messages: ");
    for (i=0; (s == NATS_OK) && (i<2); i++)
        s = stanConnection_Publish(sc, "foo", (const void*) "hello", 5);
    testCond(s == NATS_OK);

    test("Recreate durable with start seq 1: ");
    s = stanSubOptions_StartAtSequence(opts, 1);
    IFOK(s, stanConnection_QueueSubscribe(&dur, sc, "foo", "bar", _stanMsgHandlerBumpSum, &args, opts));
    testCond(s == NATS_OK);

    test("Check 5 messages total are received: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum != 5))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    testCond(s == NATS_OK);
    test("Check no redelivered: ");
    testCond((s == NATS_OK) && (args.redelivered == 0));
    natsMutex_Unlock(args.m);

    stanSubscription_Destroy(dur);
    stanSubOptions_Destroy(opts);
    stanConnection_Destroy(sc);

    _destroyDefaultThreadArgs(&args);

    _stopServer(pid);
}

static void
_stanCheckRecvStanMsg(stanConnection *sc, stanSubscription *sub, const char *channel,
        stanMsg *msg, void *closure)
{
    struct threadArg *args = (struct threadArg*) closure;

    natsMutex_Lock(args->m);
    if (strcmp(channel, args->channel) != 0)
        args->status = NATS_ERR;
    if ((args->status == NATS_OK) && (strncmp(args->string, (char*) stanMsg_GetData(msg), strlen(args->string)) != 0))
        args->status = NATS_ERR;
    if ((args->status == NATS_OK) && (stanMsg_GetDataLength(msg) != 5))
        args->status = NATS_ERR;
    if ((args->status == NATS_OK) && (stanMsg_GetSequence(msg) == 0))
        args->status = NATS_ERR;
    if ((args->status == NATS_OK) && (stanMsg_GetTimestamp(msg) == 0))
        args->status = NATS_ERR;
    stanMsg_Destroy(msg);
    args->done = true;
    natsCondition_Signal(args->c);
    natsMutex_Unlock(args->m);
}

void test_StanCheckReceivedMsg(void)
{
    natsStatus          s;
    stanConnection      *sc = NULL;
    stanSubscription    *sub = NULL;
    natsPid             pid = NATS_INVALID_PID;
    struct threadArg    args;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Error setting up test");
    args.channel = "foo";
    args.string = "hello";

    pid = _startStreamingServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    s = stanConnection_Connect(&sc, clusterName, clientName, NULL);
    if (s != NATS_OK)
    {
        _stopServer(pid);
        FAIL("Unable to create connection for this test");
    }

    test("Create sub: ");
    s = stanConnection_Subscribe(&sub, sc, "foo", _stanCheckRecvStanMsg, (void*) &args, NULL);
    testCond(s == NATS_OK);

    test("Send a message: ");
    s = stanConnection_Publish(sc, "foo", (const void*) "hello", 5);
    testCond(s == NATS_OK);

    test("Check message received: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && !args.done)
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    s = args.status;
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    stanSubscription_Destroy(sub);
    stanConnection_Destroy(sc);

    _destroyDefaultThreadArgs(&args);

    _stopServer(pid);
}

static void
_stanManualAck(stanConnection *sc, stanSubscription *sub, const char *channel,
               stanMsg *msg, void *closure)
{
    struct threadArg *args = (struct threadArg*) closure;
    natsStatus s;

    natsMutex_Lock(args->m);
    // control 1 means auto-ack, so expect ack to fail
    s = stanSubscription_AckMsg(sub, msg);
    args->status = NATS_OK;
    if ((args->control == 1) &&
            (s != NATS_ERR) &&
            (strstr(nats_GetLastError(NULL), STAN_ERR_MANUAL_ACK) == NULL))
    {
        args->status = NATS_ERR;
    }
    else if ((args->control == 2) && (s != NATS_OK))
    {
        args->status = NATS_ERR;
    }
    args->sum++;
    natsCondition_Signal(args->c);
    stanMsg_Destroy(msg);
    natsMutex_Unlock(args->m);
}

static void
_stanGetMsg(stanConnection *sc, stanSubscription *sub, const char *channel,
            stanMsg *msg, void *closure)
{
    struct threadArg *args = (struct threadArg*) closure;

    natsMutex_Lock(args->m);
    args->sMsg = msg;
    args->msgReceived = true;
    natsCondition_Signal(args->c);
    natsMutex_Unlock(args->m);
}

void test_StanSubscriptionAckMsg(void)
{
    natsStatus          s;
    stanConnection      *sc = NULL;
    stanSubscription    *sub = NULL;
    stanSubscription    *sub2 = NULL;
    natsPid             pid = NATS_INVALID_PID;
    stanSubOptions      *opts = NULL;
    struct threadArg    args;

    s = _createDefaultThreadArgsForCbTests(&args);
    IFOK(s, stanSubOptions_Create(&opts));
    IFOK(s, stanSubOptions_SetManualAckMode(opts, true));
    if (s != NATS_OK)
        FAIL("Error setting up test");

    pid = _startStreamingServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    s = stanConnection_Connect(&sc, clusterName, clientName, NULL);
    if (s != NATS_OK)
    {
        _stopServer(pid);
        FAIL("Unable to create connection for this test");
    }

    test("Create sub with auto-ack: ");
    args.control = 1;
    s = stanConnection_Subscribe(&sub, sc, "foo", _stanManualAck, (void*) &args, NULL);
    testCond(s == NATS_OK);

    test("Publish message: ");
    s = stanConnection_Publish(sc, "foo", (const void*) "hello", 5);
    testCond(s == NATS_OK);

    test("Check manual ack fails: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum != 1))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    if (s == NATS_OK)
        s = args.status;
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    stanSubscription_Destroy(sub);
    sub = NULL;

    natsMutex_Lock(args.m);
    args.control = 2;
    args.sum     = 0;
    natsMutex_Unlock(args.m);

    test("Create sub with manual ack: ");
    s = stanConnection_Subscribe(&sub, sc, "foo", _stanManualAck, (void*) &args, opts);
    testCond(s == NATS_OK);

    test("Publish message: ");
    s = stanConnection_Publish(sc, "foo", (const void*) "hello", 5);
    testCond(s == NATS_OK);

    test("Check manual ack ok: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum != 1))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    if (s == NATS_OK)
        s = args.status;
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    stanSubscription_Destroy(sub);
    sub = NULL;

    test("Create sub and get message: ");
    s = stanConnection_Subscribe(&sub, sc, "foo", _stanGetMsg, (void*) &args, NULL);
    IFOK(s, stanConnection_Publish(sc, "foo", (const void*) "hello", 5));
    if (s == NATS_OK)
    {
        natsMutex_Lock(args.m);
        while ((s != NATS_TIMEOUT) && !args.msgReceived)
            s = natsCondition_TimedWait(args.c, args.m, 2000);
        natsMutex_Unlock(args.m);
    }
    testCond(s == NATS_OK)

    test("Create sub with manual ack: ");
    s = stanConnection_Subscribe(&sub2, sc, "foo", _dummyStanMsgHandler, NULL, opts);
    testCond(s == NATS_OK);

    test("NULL Sub should fail: ");
    s = stanSubscription_AckMsg(NULL, args.sMsg);
    testCond(s == NATS_INVALID_ARG);

    test("NULL Msg should fail: ");
    s = stanSubscription_AckMsg(sub2, NULL);
    testCond(s == NATS_INVALID_ARG);

    test("Sub acking not own message fails: ");
    s = stanSubscription_AckMsg(sub2, args.sMsg);
    testCond((s == NATS_ILLEGAL_STATE)
                && (nats_GetLastError(NULL) != NULL)
                && (strstr(nats_GetLastError(NULL), STAN_ERR_SUB_NOT_OWNER) != NULL));

    natsMutex_Lock(args.m);
    if (args.sMsg != NULL)
        stanMsg_Destroy(args.sMsg);
    natsMutex_Unlock(args.m);

    stanSubscription_Destroy(sub);
    stanSubscription_Destroy(sub2);
    stanConnection_Destroy(sc);
    stanSubOptions_Destroy(opts);

    _destroyDefaultThreadArgs(&args);

    if (valgrind)
        nats_Sleep(900);

    _stopServer(pid);
}

void test_StanPings(void)
{
    natsStatus          s;
    natsPid             pid = NATS_INVALID_PID;
    stanConnection      *sc = NULL;
    stanConnOptions     *opts = NULL;
    natsConnection      *nc = NULL;
    natsSubscription    *psub = NULL;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
    {
        testAllowMillisecInPings = true;
        s = stanConnOptions_Create(&opts);
    }
    IFOK(s, stanConnOptions_SetPings(opts, -50, 5));
    if (s == NATS_OK)
    {
        arg.string = STAN_ERR_MAX_PINGS;
        s = stanConnOptions_SetConnectionLostHandler(opts, _stanConnLostCB, (void*) &arg);
    }
    if (s != NATS_OK)
    {
        _destroyDefaultThreadArgs(&arg);
        stanConnOptions_Destroy(opts);
        testAllowMillisecInPings = false;
        FAIL("Unable to setup test");
    }

    pid = _startStreamingServer("nats://127.0.0.1:4222", NULL, true);
    if (pid == NATS_INVALID_PID)
    {
        testAllowMillisecInPings = false;
        FAIL("Unable to start server");
    }

    // Create NATS Subscription on pings subject and count the
    // outgoing pings.
    test("Pings are sent: ");
    s = natsConnection_ConnectTo(&nc, "nats://127.0.0.1:4222");
    // Use _recvTestString with control == 3 to increment sum up to 10
    if (s == NATS_OK)
    {
        char psubject[256];

        snprintf(psubject, sizeof(psubject), "%s.%s.pings", STAN_CONN_OPTS_DEFAULT_DISCOVERY_PREFIX, clusterName);
        arg.control = 3;
        s = natsConnection_Subscribe(&psub, nc, psubject, _recvTestString, (void*) &arg);
    }

    // Connect to Stan
    IFOK(s, stanConnection_Connect(&sc, clusterName, clientName, opts));

    // We should start receiving PINGs
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && (arg.sum < 10))
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    if ((s == NATS_OK) && (arg.sum < 10))
        s = NATS_ERR;
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    natsSubscription_Destroy(psub);
    natsConnection_Destroy(nc);

    test("Connection lost handler invoked: ");
    _stopServer(pid);
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    IFOK(s, arg.status);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Connection closed: ");
    natsMutex_Lock(sc->mu);
    s = (sc->closed ? NATS_OK : NATS_ERR);
    natsMutex_Unlock(sc->mu);
    testCond(s == NATS_OK);

    stanConnClose(sc, false);
    stanConnection_Destroy(sc);
    stanConnOptions_Destroy(opts);
    testAllowMillisecInPings = false;

    _destroyDefaultThreadArgs(&arg);
}

void test_StanPingsNoResponder(void)
{
    natsStatus          s;
    natsPid             nPid = NATS_INVALID_PID;
    natsPid             sPid = NATS_INVALID_PID;
    stanConnection      *sc = NULL;
    stanConnOptions     *opts = NULL;
    struct threadArg    arg;

    nPid = _startServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(nPid);

    sPid = _startStreamingServer("nats://127.0.0.1:4222", "-ns nats://127.0.0.1:4222", true);
    CHECK_SERVER_STARTED(sPid);

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
    {
        testAllowMillisecInPings = true;
        s = stanConnOptions_Create(&opts);
    }
    IFOK(s, stanConnOptions_SetPings(opts, -50, 5));
    IFOK(s, stanConnOptions_SetConnectionLostHandler(opts, _stanConnLostCB, (void*) &arg));
    if (s != NATS_OK)
    {
        _destroyDefaultThreadArgs(&arg);
        stanConnOptions_Destroy(opts);
        testAllowMillisecInPings = false;
        FAIL("Unable to setup test");
    }

    // Connect to Stan
    test("Connects ok: ")
    s = stanConnection_Connect(&sc, clusterName, clientName, opts);
    testCond(s == NATS_OK);

    // Shutdown the streaming server, but not NATS Server
    _stopServer(sPid);

    // We should get the connection lost callback invoked.
    test("Connection lost: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    stanConnection_Destroy(sc);
    stanConnOptions_Destroy(opts);
    testAllowMillisecInPings = false;

    _stopServer(nPid);
    _destroyDefaultThreadArgs(&arg);
}

void test_StanConnectionLostHandlerNotSet(void)
{
    natsStatus          s;
    natsPid             pid = NATS_INVALID_PID;
    stanConnection      *sc = NULL;
    stanConnOptions     *opts = NULL;
    natsOptions         *nOpts = NULL;
    struct threadArg    arg;

    testAllowMillisecInPings = true;

    s = _createDefaultThreadArgsForCbTests(&arg);
    IFOK(s, natsOptions_Create(&nOpts));
    IFOK(s, natsOptions_SetClosedCB(nOpts, _closedCb, (void*)&arg));
    IFOK(s, stanConnOptions_Create(&opts));
    IFOK(s, stanConnOptions_SetNATSOptions(opts, nOpts));
    IFOK(s, stanConnOptions_SetPings(opts, -50, 5));
    if (s != NATS_OK)
    {
        _destroyDefaultThreadArgs(&arg);
        stanConnOptions_Destroy(opts);
        testAllowMillisecInPings = false;
        FAIL("Unable to setup test");
    }

    pid = _startStreamingServer("nats://127.0.0.1:4222", NULL, true);
    if (pid == NATS_INVALID_PID)
    {
        testAllowMillisecInPings = false;
        FAIL("Unable to start server");
    }

    test("Connect: ");
    s = stanConnection_Connect(&sc, clusterName, clientName, opts);
    testCond(s == NATS_OK);

    // Stop server and wait for closed notification
    _stopServer(pid);

    test("Connection closed: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Connection closed: ");
    natsMutex_Lock(sc->mu);
    s = (sc->closed ? NATS_OK : NATS_ERR);
    natsMutex_Unlock(sc->mu);
    testCond(s == NATS_OK);

    stanConnClose(sc, false);
    stanConnection_Destroy(sc);
    stanConnOptions_Destroy(opts);
    natsOptions_Destroy(nOpts);
    testAllowMillisecInPings = false;

    _destroyDefaultThreadArgs(&arg);
}

void test_StanPingsUnblockPubCalls(void)
{
    natsStatus          s;
    natsPid             pid = NATS_INVALID_PID;
    stanConnection      *sc = NULL;
    stanConnOptions     *opts = NULL;
    natsThread          *t    = NULL;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s == NATS_OK)
    {
        testAllowMillisecInPings = true;
        s = stanConnOptions_Create(&opts);
    }
    IFOK(s, stanConnOptions_SetMaxPubAcksInflight(opts, 1, 1.0));
    IFOK(s, stanConnOptions_SetPings(opts, -100, 5));
    if (s == NATS_OK)
    {
        arg.string = STAN_ERR_MAX_PINGS;
        s = stanConnOptions_SetConnectionLostHandler(opts, _stanConnLostCB, (void*) &arg);
    }
    if (s != NATS_OK)
    {
        _destroyDefaultThreadArgs(&arg);
        stanConnOptions_Destroy(opts);
        testAllowMillisecInPings = false;
        FAIL("Unable to setup test");
    }

    pid = _startStreamingServer("nats://127.0.0.1:4222", NULL, true);
    if (pid == NATS_INVALID_PID)
    {
        testAllowMillisecInPings = false;
        FAIL("Unable to start server");
    }

    test("Connect: ");
    s = stanConnection_Connect(&sc, clusterName, clientName, opts);
    testCond(s == NATS_OK);

    _stopServer(pid);

    natsThread_Create(&t, _stanPublishAsyncThread, (void*) &arg);

    test("Sync publish released: ");
    s = stanConnection_Publish(sc, "foo", (const void*)"hello", 5);
    testCond(s != NATS_OK);
    nats_clearLastError();
    s = NATS_OK;

    test("Async publish released: ");
    if (t != NULL)
    {
        natsThread_Join(t);
        natsThread_Destroy(t);
    }
    testCond(s == NATS_OK);

    test("Connection lost handler invoked: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.closed)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    IFOK(s, arg.status);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Connection closed: ");
    natsMutex_Lock(sc->mu);
    s = (sc->closed ? NATS_OK : NATS_ERR);
    natsMutex_Unlock(sc->mu);
    testCond(s == NATS_OK);

    stanConnClose(sc, false);
    stanConnection_Destroy(sc);

    stanConnOptions_Destroy(opts);
    testAllowMillisecInPings = false;

    _destroyDefaultThreadArgs(&arg);
}

void test_StanGetNATSConnection(void)
{
    natsStatus          s;
    natsPid             pid     = NATS_INVALID_PID;
    stanConnection      *sc     = NULL;
    stanSubscription    *ssub   = NULL;
    natsConnection      *nc     = NULL;
    natsConnection      *nc2    = NULL;
    natsSubscription    *nsub   = NULL;
    struct threadArg    args;

    s = _createDefaultThreadArgsForCbTests(&args);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    pid = _startStreamingServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Connect: ");
    s = stanConnection_Connect(&sc, clusterName, clientName, NULL);
    testCond(s == NATS_OK);

    test("Create sub: ");
    s = stanConnection_Subscribe(&ssub, sc, "foo", _stanMsgHandlerBumpSum, (void*) &args, NULL);
    testCond(s == NATS_OK);

    test("Get NATS Connection: ");
    s = stanConnection_GetNATSConnection(sc, &nc);
    testCond((s == NATS_OK) && (nc != NULL));

    test("Create sub & pub with NATS OK: ");
    s = natsConnection_Subscribe(&nsub, nc, "foo", _dummyMsgHandler, NULL);
    IFOK(s, natsConnection_PublishString(nc, "foo", "hello"));
    testCond(s == NATS_OK);

    test("Invalid to drain: ");
    s = natsConnection_Drain(nc);
    if (s == NATS_ILLEGAL_STATE)
        s = natsConnection_DrainTimeout(nc, 1000);
    if ((s == NATS_ILLEGAL_STATE)
            && (nats_GetLastError(NULL) != NULL)
            && (strstr(nats_GetLastError(NULL), "owned") != NULL))
    {
        s = NATS_OK;
        nats_clearLastError();
    }
    testCond(s == NATS_OK);

    test("Closing NATS Conn has no effect: ");
    natsConnection_Close(nc);
    s = stanConnection_Publish(sc, "foo", (const void*)"hello", 5);
    testCond(s == NATS_OK);

    test("Destroying NATS Conn has no effect: ");
    natsConnection_Destroy(nc);
    s = stanConnection_Publish(sc, "foo", (const void*)"hello", 5);
    testCond(s == NATS_OK);

    test("Calling release more than get does not crash: ");
    stanConnection_ReleaseNATSConnection(sc);
    stanConnection_ReleaseNATSConnection(sc);
    s = stanConnection_Publish(sc, "foo", (const void*)"hello", 5);
    testCond(s == NATS_OK);

    test("Should have received 3 messages: ");
    natsMutex_Lock(args.m);
    while ((s != NATS_TIMEOUT) && (args.sum != 3))
        s = natsCondition_TimedWait(args.c, args.m, 2000);
    natsMutex_Unlock(args.m);
    testCond(s == NATS_OK);

    nc2 = NULL;
    test("Invalid arg (sc == NULL): ");
    s = stanConnection_GetNATSConnection(NULL, &nc2);
    testCond((s == NATS_INVALID_ARG) && (nc2 == NULL));
    nats_clearLastError();

    test("Invalid arg (nc == NULL): ");
    s = stanConnection_GetNATSConnection(sc, NULL);
    testCond((s == NATS_INVALID_ARG) && (nc2 == NULL));
    nats_clearLastError();

    test("GetNATSConnection on closed connection fails: ");
    s = stanConnection_Close(sc);
    IFOK(s, stanConnection_GetNATSConnection(sc, &nc2));
    testCond(s == NATS_CONNECTION_CLOSED);
    nats_clearLastError();

    natsSubscription_Destroy(nsub);
    stanSubscription_Destroy(ssub);
    // Valgrind will tell us if the NATS connection/STAN connection
    // were properly released.
    stanConnection_Destroy(sc);

    _destroyDefaultThreadArgs(&args);

    if (valgrind)
        nats_Sleep(900);

    _stopServer(pid);
}

void test_StanNoRetryOnFailedConnect(void)
{
    natsStatus      s;
    natsOptions     *opts  = NULL;
    stanConnOptions *sopts = NULL;
    stanConnection  *sc    = NULL;

    test("RetryOnFailedConnect not supported: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_SetRetryOnFailedConnect(opts, true, _dummyConnHandler, (void*)1));
    IFOK(s, stanConnOptions_Create(&sopts));
    IFOK(s, stanConnOptions_SetNATSOptions(sopts, opts));
    IFOK(s, stanConnection_Connect(&sc, clusterName, clientName, sopts));
    testCond(s == NATS_NO_SERVER);

    natsOptions_Destroy(opts);
    stanConnOptions_Destroy(sopts);
}

static bool
_subDlvThreadPooled(natsSubscription *sub)
{
    bool pooled;
    natsSub_Lock(sub);
    pooled = (sub->dispatcher->ownedBy == NULL);
    natsSub_Unlock(sub);
    return pooled;
}

void test_StanInternalSubsNotPooled(void)
{
    natsStatus          s;
    natsPid             pid         = NATS_INVALID_PID;
    natsOptions         *opts       = NULL;
    stanConnOptions     *sopts      = NULL;
    stanSubscription    *sub        = NULL;
    stanConnection      *sc         = NULL;
    natsSubscription    *hbSub      = NULL;
    natsSubscription    *ackSub     = NULL;
    natsSubscription    *pingSub    = NULL;
    natsSubscription    *inboxSub   = NULL;

    pid = _startStreamingServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Can connet: ");
    s = natsOptions_Create(&opts);
    IFOK(s, natsOptions_UseGlobalMessageDelivery(opts, true));
    IFOK(s, stanConnOptions_Create(&sopts));
    IFOK(s, stanConnOptions_SetNATSOptions(sopts, opts));
    IFOK(s, stanConnection_Connect(&sc, clusterName, clientName, sopts));
    testCond(s == NATS_OK);

    test("Create sub: ");
    s = stanConnection_Subscribe(&sub, sc, "foo", _dummyStanMsgHandler, NULL, NULL);
    testCond(s == NATS_OK);

    stanConn_Lock(sc);
    hbSub   = sc->hbSubscription;
    ackSub  = sc->ackSubscription;
    pingSub = sc->pingSub;
    stanConn_Unlock(sc);

    stanSub_Lock(sub);
    inboxSub = sub->inboxSub;
    stanSub_Unlock(sub);

    test("HBSub not pooled: ");
    testCond((s == NATS_OK) && !_subDlvThreadPooled(hbSub));

    test("AckSub not pooled: ");
    testCond((s == NATS_OK) && !_subDlvThreadPooled(ackSub));

    test("PingSub not pooled: ");
    testCond((s == NATS_OK) && !_subDlvThreadPooled(pingSub));

    test("InboxSub pooled: ");
    testCond((s == NATS_OK) && _subDlvThreadPooled(inboxSub));

    natsOptions_Destroy(opts);
    stanConnOptions_Destroy(sopts);
    stanSubscription_Destroy(sub);
    stanConnection_Destroy(sc);

    if (valgrind)
        nats_Sleep(900);

    _stopServer(pid);
}

static void
_stanSubOnComplete(void *closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);
    if (arg->control == 1)
    {
        arg->done = true;
        natsCondition_Signal(arg->c);
    }
    natsMutex_Unlock(arg->m);
}

static void
_stanSubOnCompleteMsgCB(stanConnection *sc, stanSubscription *sub, const char *channel,
    stanMsg *msg, void* closure)
{
    struct threadArg    *arg = (struct threadArg*) closure;

    natsMutex_Lock(arg->m);
    arg->msgReceived = true;
    natsCondition_Signal(arg->c);
    natsMutex_Unlock(arg->m);
    // Sleep to simulate callbac doing some processing and let the
    // main thread close the subscription.
    nats_Sleep(500);
    // Update a field that _stanSubOnComplete will ensure is set to
    // prove that the onComplete is invoked after the msg callback
    // has returned.
    natsMutex_Lock(arg->m);
    arg->control = 1;
    natsMutex_Unlock(arg->m);

    stanMsg_Destroy(msg);
}

void test_StanSubOnComplete(void)
{
    natsStatus          s;
    natsPid             pid         = NATS_INVALID_PID;
    stanConnection      *sc         = NULL;
    stanSubscription    *sub        = NULL;
    struct threadArg    arg;

    s = _createDefaultThreadArgsForCbTests(&arg);
    if (s != NATS_OK)
        FAIL("Unable to setup test");

    pid = _startStreamingServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Can connet: ");
    s = stanConnection_Connect(&sc, clusterName, clientName, NULL);
    testCond(s == NATS_OK);

    test("SetOnComplete error: ");
    s = stanSubscription_SetOnCompleteCB(NULL, _stanSubOnComplete, NULL);
    testCond(s == NATS_INVALID_ARG);

    test("Create sub: ");
    s = stanConnection_Subscribe(&sub, sc, "foo", _stanSubOnCompleteMsgCB, &arg, NULL);
    testCond(s == NATS_OK);

    test("SetOnComplete ok: ");
    s = stanSubscription_SetOnCompleteCB(sub, _stanSubOnComplete, &arg);
    testCond(s == NATS_OK);

    test("Remove onComplete: ");
    s = stanSubscription_SetOnCompleteCB(sub, NULL, NULL);
    if (s == NATS_OK)
    {
        stanSub_Lock(sub);
        if ((sub->onCompleteCB != NULL) || (sub->onCompleteCBClosure != NULL))
            s = NATS_ERR;
        stanSub_Unlock(sub);
    }
    testCond(s == NATS_OK);

    test("SetOnComplete ok: ");
    s = stanSubscription_SetOnCompleteCB(sub, _stanSubOnComplete, &arg);
    testCond(s == NATS_OK);

    test("Publish msg: ");
    s = stanConnection_Publish(sc, "foo", (const void*) "hello", 5);
    testCond(s == NATS_OK);

    test("Wait for message to be received: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.msgReceived)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("Close subscription: ")
    s = stanSubscription_Close(sub);
    testCond(s == NATS_OK);

    test("Ensure onComplete invoked after cb returned: ");
    natsMutex_Lock(arg.m);
    while ((s != NATS_TIMEOUT) && !arg.done)
        s = natsCondition_TimedWait(arg.c, arg.m, 2000);
    natsMutex_Unlock(arg.m);
    testCond(s == NATS_OK);

    test("SetOnComplete after close: ");
    s = stanSubscription_SetOnCompleteCB(sub, _stanSubOnComplete, &arg);
    testCond(s == NATS_INVALID_SUBSCRIPTION);

    stanSubscription_Destroy(sub);
    stanConnection_Destroy(sc);
    _stopServer(pid);

    _destroyDefaultThreadArgs(&arg);
}

void test_StanSubTimeout(void)
{
    natsStatus          s;
    natsPid             pid         = NATS_INVALID_PID;
    stanConnection      *sc         = NULL;
    stanSubscription    *sub        = NULL;
    natsConnection      *nc         = NULL;
    natsSubscription    *ncSub      = NULL;
    const char          *closeSubj  = NULL;
    natsMsg             *resp       = NULL;
    int                 i           = 0;
    Pb__SubscriptionRequest *r      = NULL;

    pid = _startStreamingServer("nats://127.0.0.1:4222", NULL, true);
    CHECK_SERVER_STARTED(pid);

    test("Can connect: ");
    s = stanConnection_Connect(&sc, clusterName, clientName, NULL);
    testCond(s == NATS_OK);

    test("Get NATS subscription: ");
    s = stanConnection_GetNATSConnection(sc, &nc);
    testCond(s == NATS_OK);

    test("Setup NATS sub: ");
    natsMutex_Lock(sc->mu);
    closeSubj = (const char*) sc->subCloseRequests;
    natsMutex_Unlock(sc->mu);
    s = natsConnection_SubscribeSync(&ncSub, nc, closeSubj);
    testCond(s == NATS_OK);

    // Artificially lower the timeout to make sure it fails.
    natsMutex_Lock(sc->mu);
    sc->opts->connTimeout=1;
    natsMutex_Unlock(sc->mu);

    test("Subscribe should timeout: ");
    // Try to get the timeout...
    for (i=0;i<50;i++)
    {
        s = stanConnection_Subscribe(&sub, sc, "foo", _dummyStanMsgHandler, NULL, NULL);
        if (s == NATS_TIMEOUT)
            break;
        stanSubscription_Destroy(sub);
        sub = NULL;
    }
    // We don't want to fail the thest if we did not get a timeout.
    testCond((s == NATS_OK) || (s == NATS_TIMEOUT));

    // However, proceed with the rest of the test only if it was a timeout.
    if (s == NATS_TIMEOUT)
    {
        test("Check sub close request sent: ");
        s = natsSubscription_NextMsg(&resp, ncSub, 1000);
        testCond((s == NATS_OK) && (resp != NULL));

        test("Check request: ");
        r = pb__subscription_request__unpack(NULL,
                                            (size_t) natsMsg_GetDataLength(resp),
                                            (const uint8_t*) natsMsg_GetData(resp));
        testCond((r != NULL)
                    && (strcmp(r->clientid, clientName) == 0)
                    && (strcmp(r->subject, "foo") == 0)
                    && (r->inbox != NULL));
        if (r != NULL)
            pb__subscription_request__free_unpacked(r, NULL);

        natsMsg_Destroy(resp);
    }

    natsSubscription_Destroy(ncSub);
    stanSubscription_Destroy(sub);
    stanConnection_ReleaseNATSConnection(sc);
    stanConnection_Destroy(sc);

    if (valgrind)
        nats_Sleep(900);

    _stopServer(pid);
}

#endif

#ifndef _WIN32
static void _sigsegv_handler(int sig) {
  void *array[20];
  int size = backtrace(array, 20);

  // print out all the frames to stderr
  fprintf(stderr, "Error: signal %d:\n", sig);
  backtrace_symbols_fd(array, size, STDERR_FILENO);
  exit(1);
}
#endif // _WIN32

int main(int argc, char **argv)
{
    const char *envStr;
    int i;
    const char *testName = NULL;

    if (argc != 2)
    {
        printf("@@ Usage: %s [testname]\n", argv[0]);
        return 1;
    }
    testName = argv[1];

#ifndef _WIN32
    signal(SIGSEGV, _sigsegv_handler);
#endif // _WIN32

    envStr = getenv("NATS_TEST_TRAVIS");
    if ((envStr != NULL) && (envStr[0] != '\0'))
        runOnTravis = true;

    envStr = getenv("NATS_TEST_VALGRIND");
    if ((envStr != NULL) && (envStr[0] != '\0'))
    {
        valgrind = true;
        printf("Test running in VALGRIND mode.\n");
    }

    envStr = getenv("NATS_TEST_KEEP_SERVER_OUTPUT");
    if ((envStr != NULL) && (envStr[0] != '\0'))
    {
        keepServerOutput = true;
        printf("Test prints server's output.\n");
    }

    envStr = getenv("NATS_TEST_SERVER_EXE");
    if ((envStr != NULL) && (envStr[0] != '\0'))
    {
        natsServerExe = envStr;
        printf("Test using server executable: %s\n", natsServerExe);
    }

    envStr = getenv("NATS_TEST_STREAMING_SERVER_EXE");
    if ((envStr != NULL) && (envStr[0] != '\0'))
    {
        natsStreamingServerExe = envStr;
        printf("Test using server executable: %s\n", natsStreamingServerExe);
    }

    envStr = getenv("NATS_TEST_SERVER_VERSION");
    if ((envStr != NULL) && (envStr[0] != '\0'))
    {
        serverVersion = envStr;
        printf("Test server version: %s\n", serverVersion);
    }

    if (nats_Open(-1) != NATS_OK)
    {
        printf("@@ Unable to run tests: unable to initialize the library!\n");
        return 1;
    }

    if (natsMutex_Create(&slMu) != NATS_OK)
    {
        printf("@@ Unable to create lock for servers map\n");
        return 1;
    }
    if (natsHash_Create(&slMap, 4) != NATS_OK)
    {
        natsMutex_Destroy(slMu);
        printf("@@ Unable to create lock for servers map\n");
        return 1;
    }

    // Execute tests
    testFunc f = NULL;
    for (i = 0; i < (int)(sizeof(allTests) / sizeof(*allTests)); i++)
    {
        if (strcmp(testName, allTests[i].name) != 0)
            continue;

#ifdef _WIN32
        printf("\n== %s ==\n", allTests[i].name);
#else
        printf("\033[0;34m\n== %s ==\n\033[0;0m", allTests[i].name);
#endif
        f = allTests[i].func;
        f();
        break;
    }
    if (f == NULL)
    {
        printf("@@ Test '%s' not found!\n", testName);
        return 1;
    }

#ifdef _WIN32
    if (logHandle != NULL)
    {
        CloseHandle(logHandle);
        DeleteFile(LOGFILE_NAME);
    }
#else
    remove(LOGFILE_NAME);
#endif

    // Shutdown servers that are still running likely due to failed test
    {
        natsHash     *pids = NULL;
        natsHashIter iter;
        int64_t      key;

        if (natsHash_Create(&pids, 16) == NATS_OK)
        {
            natsMutex_Lock(slMu);
            natsHashIter_Init(&iter, slMap);
            while (natsHashIter_Next(&iter, &key, NULL))
            {
                natsHash_Set(pids, key, NULL, NULL);
                natsHashIter_RemoveCurrent(&iter);
            }
            natsHashIter_Done(&iter);
            natsHash_Destroy(slMap);
            slMap = NULL;
            natsMutex_Unlock(slMu);

            natsHashIter_Init(&iter, pids);
            while (natsHashIter_Next(&iter, &key, NULL))
                _stopServer((natsPid) key);

            natsHash_Destroy(pids);
        }
        else
        {
            natsHash_Destroy(slMap);
        }
        natsMutex_Destroy(slMu);
    }

    // Makes valgrind happy
    nats_CloseAndWait((failed ? 1 : 2000));

    if (failed)
    {
        printf("*** TEST FAILED ***\n");
        return 1;
    }

    printf("ALL PASSED\n");
    return 0;
}
