#if HAVE_CONFIG_H
#include <config.h>
#endif
#include "cunit/cyrunit.h"
#include "assert.h"
#include "xmalloc.h"
#include "retry.h"
#include "util.h"
#include "xunlink.h"
#include "imap/global.h"
#include "libcyr_cfg.h"
#include "imap/annotate.h"
#include "imap/append.h"
#include "imap/mboxlist.h"
#include "imap/imap_err.h"

#define DBDIR           "test-dbdir"
#define MBOXNAME1_INT   "user.smurf"
#define MBOXNAME2_INT   "user.smurfette"
#define MBOXNAME3_INT   "user.smurfine"
#define PARTITION       "default"
#define COMMENT         "/comment"
#define EXENTRY         "/vendor/example.com/a-non-default-entry"
#define UCEXENTRY       "/vendor/EXAMPLE.COM/a-non-default-entry"
#define VALUE_SHARED    "value.shared"
#define SIZE_SHARED     "size.shared"
#define VALUE0          "Hello World"
#define LENGTH0         "11"
#define VALUE1          "lorem ipsum"
#define VALUE2          "dolor sit amet"
#define ACL             "anyone\tlrswipkxtecdan\t"

#define MBOX1_MAXUID    3
#define MBOX2_MAXUID    1

static int isadmin;
static const char *userid;
static struct auth_state *auth_state;
static const char *old_annotation_definitions = NULL;

static void set_annotation_definitions(const char *s)
{
    static const char *fname = DBDIR"/conf/annotations.def";
    int fd;

    if (s) {
        fd = open(fname, O_WRONLY|O_CREAT|O_TRUNC, 0666);
        assert(fd >= 0);
        retry_write(fd, s, strlen(s));
        close(fd);
    }
    else {
        xunlink(fname);
    }

    imapopts[IMAPOPT_ANNOTATION_DEFINITIONS].val.s = fname;
}

static int fexists(const char *fname)
{
    struct stat sb;
    int r;

    r = stat(fname, &sb);
    if (r < 0)
        r = -errno;
    return r;
}


static void fetch_cb(const char *mboxname, uint32_t uid,
                     const char *entry, struct attvaluelist *avlist,
                     void *rock)
{
    strarray_t *results = (strarray_t *)rock;
    struct buf buf = BUF_INITIALIZER;

    buf_printf(&buf, "mboxname=\"%s\" uid=%u entry=\"%s\"",
               mboxname, uid, entry);

    for ( ; avlist ; avlist = avlist->next) {
        buf_printf(&buf, " %s=", avlist->attrib);
        if (avlist->value.s)
            buf_printf(&buf, "\"%s\"", buf_cstring(&avlist->value));
        else
            buf_printf(&buf, "NIL");
    }

    strarray_appendm(results, buf_release(&buf));
}

/* test_begin_without_open deleted -- begin has been removed */

/* test_commit_without_begin deleted -- begin has been removed */

/* test_store_without_begin deleted -- begin has been removed */

static void test_getset_server_shared(void)
{
    int r;
    annotate_state_t *astate = NULL;
    strarray_t entries = STRARRAY_INITIALIZER;
    strarray_t attribs = STRARRAY_INITIALIZER;
    strarray_t results = STRARRAY_INITIALIZER;
    struct entryattlist *ealist = NULL;
    struct buf val = BUF_INITIALIZER;
    struct buf val2 = BUF_INITIALIZER;

    annotate_init(NULL, NULL);

    annotatemore_open();

    astate = annotate_state_new();
    r = annotate_state_set_server(astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    strarray_append(&entries, COMMENT);
    strarray_append(&attribs, VALUE_SHARED);

    /* check that there is no value initially */

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" COMMENT "\" " \
           VALUE_SHARED "=NIL"
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(/*mboxname*/"", COMMENT, /*userid*/"", &val);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(val.s);

    /* set a value */

    /* pretend to be admin */
    annotate_state_set_auth(astate, /*isadmin*/1, userid, auth_state);

    buf_appendcstr(&val, VALUE0);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    /* check that we can fetch the value back in the same txn */
    r = annotate_state_fetch(astate, &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" COMMENT "\" " \
           VALUE_SHARED "=\"" VALUE0 "\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(/*mboxname*/"", COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    r = annotate_state_commit(&astate);
    CU_ASSERT_EQUAL(r, 0);

    /* check that we can fetch the value back in a new txn */
    astate = annotate_state_new();
    annotate_state_set_auth(astate, /*isadmin*/0, userid, auth_state);
    r = annotate_state_set_server(astate);
    CU_ASSERT_EQUAL(r, 0);

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" COMMENT "\" " \
           VALUE_SHARED "=\"" VALUE0 "\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(/*mboxname*/"", COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    annotate_state_abort(&astate);
    annotatemore_close();

    /* check that we can fetch the value back after close and re-open */

    annotatemore_open();

    astate = annotate_state_new();
    annotate_state_set_auth(astate, /*isadmin*/0, userid, auth_state);
    r = annotate_state_set_server(astate);
    CU_ASSERT_EQUAL(r, 0);

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" COMMENT "\" " \
           VALUE_SHARED "=\"" VALUE0 "\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(/*mboxname*/"", COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    /* delete the value */

    buf_free(&val);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    /* pretend to be admin */
    annotate_state_set_auth(astate, /*isadmin*/1, userid, auth_state);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    r = annotate_state_commit(&astate);
    CU_ASSERT_EQUAL(r, 0);

    /* check that there is no value any more */
    astate = annotate_state_new();
    annotate_state_set_auth(astate, /*isadmin*/0, userid, auth_state);
    r = annotate_state_set_server(astate);
    CU_ASSERT_EQUAL(r, 0);

    annotate_state_set_auth(astate, isadmin, userid, auth_state);
    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" COMMENT "\" " \
           VALUE_SHARED "=NIL"
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(/*mboxname*/"", COMMENT, /*userid*/"", &val);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(val.s);


    annotate_state_abort(&astate);
    annotatemore_close();

    strarray_fini(&entries);
    strarray_fini(&attribs);
    strarray_fini(&results);
    buf_free(&val);
}

static void test_getset_mailbox_shared(void)
{
    int r;
    annotate_state_t *astate = NULL;
    strarray_t entries = STRARRAY_INITIALIZER;
    strarray_t attribs = STRARRAY_INITIALIZER;
    strarray_t results = STRARRAY_INITIALIZER;
    struct entryattlist *ealist = NULL;
    struct buf val = BUF_INITIALIZER;
    struct buf val2 = BUF_INITIALIZER;
    struct mailbox *mailbox = NULL;

    annotate_init(NULL, NULL);

    annotatemore_open();

    r = mailbox_open_iwl(MBOXNAME1_INT, &mailbox);
    CU_ASSERT_EQUAL_FATAL(r, 0);

    astate = annotate_state_new();
    r = annotate_state_set_mailbox(astate, mailbox);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    strarray_append(&entries, COMMENT);
    strarray_append(&attribs, VALUE_SHARED);

    /* check that there is no value initially */

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"" MBOXNAME1_INT "\" " \
           "uid=0 " \
           "entry=\"" COMMENT "\" " \
           VALUE_SHARED "=NIL"
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(MBOXNAME1_INT, COMMENT, /*userid*/"", &val);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(val.s);

    /* set a value */
    buf_appendcstr(&val, VALUE0);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    /* check that we can fetch the value back in the same txn */
    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"" MBOXNAME1_INT "\" " \
           "uid=0 " \
           "entry=\"" COMMENT "\" " \
           VALUE_SHARED "=\"" VALUE0 "\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(MBOXNAME1_INT, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    r = annotate_state_commit(&astate);
    CU_ASSERT_EQUAL(r, 0);

    /* check that we can fetch the value back in a new txn */
    astate = annotate_state_new();
    r = annotate_state_set_mailbox(astate, mailbox);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"" MBOXNAME1_INT "\" " \
           "uid=0 " \
           "entry=\"" COMMENT "\" " \
           VALUE_SHARED "=\"" VALUE0 "\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(MBOXNAME1_INT, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    annotate_state_abort(&astate);
    annotatemore_close();

    /* check that we can fetch the value back after close and re-open */

    annotatemore_open();

    astate = annotate_state_new();
    r = annotate_state_set_mailbox(astate, mailbox);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"" MBOXNAME1_INT "\" " \
           "uid=0 " \
           "entry=\"" COMMENT "\" " \
           VALUE_SHARED "=\"" VALUE0 "\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(MBOXNAME1_INT, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    /* delete the value */

    buf_free(&val);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    r = annotate_state_commit(&astate);
    CU_ASSERT_EQUAL(r, 0);

    /* check that there is no value any more */
    astate = annotate_state_new();
    r = annotate_state_set_mailbox(astate, mailbox);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"" MBOXNAME1_INT "\" " \
           "uid=0 " \
           "entry=\"" COMMENT "\" " \
           VALUE_SHARED "=NIL"
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    buf_free(&val);
    r = annotatemore_lookup(MBOXNAME1_INT, COMMENT, /*userid*/"", &val);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(val.s);

    annotate_state_abort(&astate);
    annotatemore_close();

    strarray_fini(&entries);
    strarray_fini(&attribs);
    strarray_fini(&results);
    buf_free(&val);
    mailbox_close(&mailbox);
}


static void test_getset_message_shared(void)
{
    int r;
    annotate_state_t *astate = NULL;
    strarray_t entries = STRARRAY_INITIALIZER;
    strarray_t attribs = STRARRAY_INITIALIZER;
    strarray_t results = STRARRAY_INITIALIZER;
    struct entryattlist *ealist = NULL;
    struct buf val = BUF_INITIALIZER;
    struct buf val2 = BUF_INITIALIZER;
    struct mailbox *mailbox = NULL;

    annotate_init(NULL, NULL);

    annotatemore_open();

    r = mailbox_open_iwl(MBOXNAME1_INT, &mailbox);
    CU_ASSERT_EQUAL_FATAL(r, 0);

    r = mailbox_get_annotate_state(mailbox, 1, &astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    strarray_append(&entries, COMMENT);
    strarray_append(&attribs, VALUE_SHARED);

    /* check that there is no value initially */

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"" MBOXNAME1_INT "\" " \
           "uid=1 " \
           "entry=\"" COMMENT "\" " \
           VALUE_SHARED "=NIL"
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_msg_lookup(mailbox, 1, COMMENT, /*userid*/"", &val);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(val.s);

    /* set a value */

    buf_appendcstr(&val, VALUE0);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    /* check that we can fetch the value back in the same txn */
    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"" MBOXNAME1_INT "\" " \
           "uid=1 " \
           "entry=\"" COMMENT "\" " \
           VALUE_SHARED "=\"" VALUE0 "\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_msg_lookup(mailbox, 1, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    r = mailbox_commit(mailbox);
    CU_ASSERT_EQUAL(r, 0);
    mailbox_close(&mailbox);

    /* check that we can fetch the value back in a new txn */
    r = mailbox_open_iwl(MBOXNAME1_INT, &mailbox);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    r = mailbox_get_annotate_state(mailbox, 1, &astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"" MBOXNAME1_INT "\" " \
           "uid=1 " \
           "entry=\"" COMMENT "\" " \
           VALUE_SHARED "=\"" VALUE0 "\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_msg_lookup(mailbox, 1, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    r = mailbox_commit(mailbox);
    CU_ASSERT_EQUAL(r, 0);
    mailbox_close(&mailbox);

    annotatemore_close();

    /* check that we can fetch the value back after close and re-open */

    annotatemore_open();

    r = mailbox_open_iwl(MBOXNAME1_INT, &mailbox);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    r = mailbox_get_annotate_state(mailbox, 1, &astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"" MBOXNAME1_INT "\" " \
           "uid=1 " \
           "entry=\"" COMMENT "\" " \
           VALUE_SHARED "=\"" VALUE0 "\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_msg_lookup(mailbox, 1, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    /* delete the value */

    buf_free(&val);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    r = mailbox_commit(mailbox);
    CU_ASSERT_EQUAL(r, 0);
    mailbox_close(&mailbox);

    /* check that there is no value any more */
    r = mailbox_open_iwl(MBOXNAME1_INT, &mailbox);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    r = mailbox_get_annotate_state(mailbox, 1, &astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"" MBOXNAME1_INT "\" " \
           "uid=1 " \
           "entry=\"" COMMENT "\" " \
           VALUE_SHARED "=NIL"
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    buf_free(&val);
    r = annotatemore_msg_lookup(mailbox, 1, COMMENT, /*userid*/"", &val);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(val.s);

    r = mailbox_commit(mailbox);
    CU_ASSERT_EQUAL(r, 0);
    mailbox_close(&mailbox);

    annotatemore_close();

    strarray_fini(&entries);
    strarray_fini(&attribs);
    strarray_fini(&results);
    buf_free(&val);
    mailbox_close(&mailbox);
}


static void test_delete(void)
{
    int r;
    annotate_state_t *astate = NULL;
    struct mboxlist_entry mbentry;
    struct entryattlist *ealist = NULL;
    struct buf val = BUF_INITIALIZER;
    struct buf val2 = BUF_INITIALIZER;
    struct buf path = BUF_INITIALIZER;
    struct mailbox *mailbox = NULL;

    annotate_init(NULL, NULL);

    annotatemore_open();

    r = mailbox_open_iwl(MBOXNAME1_INT, &mailbox);
    CU_ASSERT_EQUAL_FATAL(r, 0);

    memset(&mbentry, 0, sizeof(mbentry));
    mbentry.name = MBOXNAME1_INT;
    mbentry.uniqueid = (char *)mailbox_uniqueid(mailbox);
    mbentry.mbtype = 0;
    mbentry.partition = PARTITION;
    mbentry.acl = ACL;

    /* set some values */

    buf_appendcstr(&val, VALUE0);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    astate = annotate_state_new();
    annotate_state_set_auth(astate, isadmin, userid, auth_state);
    r = annotate_state_set_mailbox(astate, mailbox);
    CU_ASSERT_EQUAL(r, 0);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    r = annotate_state_commit(&astate);
    CU_ASSERT_EQUAL(r, 0);

    buf_reset(&val);
    buf_appendcstr(&val, VALUE1);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    r = mailbox_get_annotate_state(mailbox, 1, &astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    buf_reset(&val);
    buf_appendcstr(&val, VALUE2);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    r = mailbox_get_annotate_state(mailbox, 3, &astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    r = mailbox_commit(mailbox);
    CU_ASSERT_EQUAL(r, 0);

    /* check that we can fetch the values back */

    r = annotatemore_msg_lookup(mailbox, 0, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    r = annotatemore_msg_lookup(mailbox, 1, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE1);
    buf_free(&val2);

    r = annotatemore_msg_lookup(mailbox, 3, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE2);
    buf_free(&val2);

    buf_printf(&path, "%s/data/uuid/%c/%c/%s/cyrus.annotations", DBDIR,
               mailbox_uniqueid(mailbox)[0], mailbox_uniqueid(mailbox)[1], mailbox_uniqueid(mailbox));
    CU_ASSERT_EQUAL(fexists(buf_cstring(&path)), 0);
    mailbox_close(&mailbox);
    buf_free(&val);
    buf_free(&path);
}

static void test_rename(void)
{
    int r;
    annotate_state_t *astate = NULL;
    strarray_t entries = STRARRAY_INITIALIZER;
    strarray_t attribs = STRARRAY_INITIALIZER;
    struct entryattlist *ealist = NULL;
    struct buf val = BUF_INITIALIZER;
    struct buf val2 = BUF_INITIALIZER;
    struct buf path1 = BUF_INITIALIZER;
    struct buf path3 = BUF_INITIALIZER;
    struct mailbox *mailbox1 = NULL;
    struct mailbox *mailbox3 = NULL;

    annotate_init(NULL, NULL);

    annotatemore_open();

    r = mailbox_open_iwl(MBOXNAME1_INT, &mailbox1);
    CU_ASSERT_EQUAL_FATAL(r, 0);

    r = mailbox_open_iwl(MBOXNAME3_INT, &mailbox3);
    CU_ASSERT_EQUAL_FATAL(r, 0);

    strarray_append(&entries, COMMENT);
    strarray_append(&attribs, VALUE_SHARED);

    /* set some values */

    buf_appendcstr(&val, VALUE0);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    astate = annotate_state_new();
    annotate_state_set_auth(astate, isadmin, userid, auth_state);
    r = annotate_state_set_mailbox(astate, mailbox1);
    CU_ASSERT_EQUAL(r, 0);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    r = annotate_state_commit(&astate);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    buf_reset(&val);
    buf_appendcstr(&val, VALUE1);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    r = mailbox_get_annotate_state(mailbox1, 2, &astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    buf_reset(&val);
    buf_appendcstr(&val, VALUE2);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    r = mailbox_get_annotate_state(mailbox1, 3, &astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    r = mailbox_commit(mailbox1);
    CU_ASSERT_EQUAL(r, 0);
    mailbox_close(&mailbox1);

    r = mailbox_open_iwl(MBOXNAME1_INT, &mailbox1);
    CU_ASSERT_EQUAL_FATAL(r, 0);

    /* check that we can fetch the values back */

    r = annotatemore_msg_lookup(mailbox1, 0, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    r = annotatemore_msg_lookup(mailbox1, 2, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE1);
    buf_free(&val2);

    r = annotatemore_msg_lookup(mailbox1, 3, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE2);
    buf_free(&val2);

    const char *u1 = mailbox_uniqueid(mailbox1);
    buf_printf(&path1, "%s/data/uuid/%c/%c/%s/cyrus.annotations", DBDIR,
               u1[0], u1[1], u1);
    const char *u3 = mailbox_uniqueid(mailbox3);
    buf_printf(&path3, "%s/data/uuid/%c/%c/%s/cyrus.annotations", DBDIR,
               u3[0], u3[1], u3);
    CU_ASSERT_EQUAL(fexists(buf_cstring(&path1)), 0);
    CU_ASSERT_EQUAL(fexists(buf_cstring(&path3)), -ENOENT);

    /* rename MBOXNAME1 -> MBOXNAME3 */

    r = annotate_rename_mailbox(mailbox1, mailbox3);
    CU_ASSERT_EQUAL(r, 0);
    r = mailbox_copy_files(mailbox1, PARTITION, MBOXNAME3_INT, mailbox_uniqueid(mailbox3));
    CU_ASSERT_EQUAL(r, 0);
    r = mailbox_rename_cleanup(&mailbox1);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(mailbox1);

    CU_ASSERT_EQUAL(fexists(buf_cstring(&path1)), -ENOENT);
    CU_ASSERT_EQUAL(fexists(buf_cstring(&path3)), 0);

    /* check that the values are gone under the old name */

    r = annotatemore_lookup(MBOXNAME1_INT, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NULL_FATAL(val2.s);
    buf_free(&val2);

    r = annotatemore_msg_lookup(mailbox1, 2, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NULL_FATAL(val2.s);
    buf_free(&val2);

    r = annotatemore_msg_lookup(mailbox1, 3, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NULL_FATAL(val2.s);
    buf_free(&val2);

    /* check that the values are present under the new name */

    r = annotatemore_msg_lookup(mailbox3, 0, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    r = annotatemore_msg_lookup(mailbox3, 2, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE1);
    buf_free(&val2);

    r = annotatemore_msg_lookup(mailbox3, 3, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE2);
    buf_free(&val2);

    CU_ASSERT_EQUAL(fexists(buf_cstring(&path1)), -ENOENT);
    CU_ASSERT_EQUAL(fexists(buf_cstring(&path3)), 0);
    buf_free(&path1);
    buf_free(&path3);

    annotatemore_close();

    strarray_fini(&entries);
    strarray_fini(&attribs);
    buf_free(&val);
    mailbox_close(&mailbox1);
    mailbox_close(&mailbox3);
}

static void test_abort(void)
{
    int r;
    annotate_state_t *astate = NULL;
    strarray_t entries = STRARRAY_INITIALIZER;
    strarray_t attribs = STRARRAY_INITIALIZER;
    struct entryattlist *ealist = NULL;
    struct buf val = BUF_INITIALIZER;
    struct buf val2 = BUF_INITIALIZER;
    struct mailbox *mailbox = NULL;

    annotate_init(NULL, NULL);

    annotatemore_open();

    r = mailbox_open_iwl(MBOXNAME1_INT, &mailbox);
    CU_ASSERT_EQUAL_FATAL(r, 0);

    strarray_append(&entries, COMMENT);
    strarray_append(&attribs, VALUE_SHARED);

    /* check that the values we'll be setting are not already present */

    buf_free(&val2);
    r = annotatemore_msg_lookup(NULL, 0, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NULL_FATAL(val2.s);

    buf_free(&val2);
    r = annotatemore_msg_lookup(mailbox, 0, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NULL_FATAL(val2.s);

    buf_free(&val2);
    r = annotatemore_msg_lookup(mailbox, 42, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NULL_FATAL(val2.s);

    /* set a per-server value */

    buf_appendcstr(&val, VALUE0);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    astate = annotate_state_new();
    r = annotate_state_set_server(astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, /*isadmin*/1, userid, auth_state);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    /* but abort the txn */
    annotate_state_abort(&astate);

    /* check that the values are still not present */

    buf_free(&val2);
    r = annotatemore_msg_lookup(NULL, 0, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NULL_FATAL(val2.s);

    /* set a per-mailbox value */

    buf_appendcstr(&val, VALUE0);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    astate = annotate_state_new();
    r = annotate_state_set_mailbox(astate, mailbox);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, /*isadmin*/0, userid, auth_state);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    /* but abort the txn */
    annotate_state_abort(&astate);

    /* check that the values are still not present */

    buf_free(&val2);
    r = annotatemore_msg_lookup(NULL, 0, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NULL_FATAL(val2.s);

    buf_free(&val2);
    r = annotatemore_msg_lookup(mailbox, 0, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NULL_FATAL(val2.s);

    /* set a per-message value */

    buf_reset(&val);
    buf_appendcstr(&val, VALUE1);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    r = mailbox_get_annotate_state(mailbox, 1, &astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    /* but abort the txn */
    annotate_state_abort(&mailbox->annot_state);

    /* check that the values are still not present */

    buf_free(&val2);
    r = annotatemore_msg_lookup(NULL, 0, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NULL_FATAL(val2.s);

    buf_free(&val2);
    r = annotatemore_msg_lookup(mailbox, 0, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NULL_FATAL(val2.s);

    buf_free(&val2);
    r = annotatemore_msg_lookup(mailbox, 42, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NULL_FATAL(val2.s);

    annotatemore_close();

    strarray_fini(&entries);
    strarray_fini(&attribs);
    buf_free(&val);
    buf_free(&val2);
    mailbox_close(&mailbox);
}


static void test_msg_copy(void)
{
    int r;
    annotate_state_t *astate = NULL;
    strarray_t entries = STRARRAY_INITIALIZER;
    strarray_t attribs = STRARRAY_INITIALIZER;
    struct entryattlist *ealist = NULL;
    struct buf val = BUF_INITIALIZER;
    struct buf val2 = BUF_INITIALIZER;
    struct buf path1 = BUF_INITIALIZER;
    struct buf path2 = BUF_INITIALIZER;
    struct mailbox *mailbox1 = NULL;
    struct mailbox *mailbox2 = NULL;

    annotate_init(NULL, NULL);

    annotatemore_open();

    r = mailbox_open_iwl(MBOXNAME1_INT, &mailbox1);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    r = mailbox_open_iwl(MBOXNAME2_INT, &mailbox2);
    CU_ASSERT_EQUAL_FATAL(r, 0);

    strarray_append(&entries, COMMENT);
    strarray_append(&attribs, VALUE_SHARED);

    /* set some values */

    buf_appendcstr(&val, VALUE0);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    r = mailbox_get_annotate_state(mailbox1, 1, &astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    buf_reset(&val);
    buf_appendcstr(&val, VALUE1);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    r = mailbox_get_annotate_state(mailbox1, 2, &astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    buf_reset(&val);
    buf_appendcstr(&val, VALUE2);
    setentryatt(&ealist, COMMENT, VALUE_SHARED, &val);
    r = mailbox_get_annotate_state(mailbox1, 3, &astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);
    r = annotate_state_store(astate, ealist);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    r = mailbox_commit(mailbox1);
    CU_ASSERT_EQUAL(r, 0);

    /* check that we can fetch the values back */

    buf_free(&val2);
    r = annotatemore_msg_lookup(mailbox1, 1, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);

    buf_free(&val2);
    r = annotatemore_msg_lookup(mailbox1, 2, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE1);

    buf_free(&val2);
    r = annotatemore_msg_lookup(mailbox1, 3, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE2);

    buf_free(&val2);
    r = annotatemore_msg_lookup(mailbox2, 1, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NULL_FATAL(val2.s);

    const char *u1 = mailbox_uniqueid(mailbox1);
    buf_printf(&path1, "%s/data/uuid/%c/%c/%s/cyrus.annotations", DBDIR,
               u1[0], u1[1], u1);
    const char *u2 = mailbox_uniqueid(mailbox2);
    buf_printf(&path2, "%s/data/uuid/%c/%c/%s/cyrus.annotations", DBDIR,
               u2[0], u2[1], u2);
    CU_ASSERT_EQUAL(fexists(buf_cstring(&path1)), 0);
    CU_ASSERT_EQUAL(fexists(buf_cstring(&path2)), 0);
    mailbox_close(&mailbox1);

    /* copy MBOXNAME1,1 -> MBOXNAME2,1 */
    r = mailbox_open_iwl(MBOXNAME1_INT, &mailbox1);
    CU_ASSERT_EQUAL_FATAL(r, 0);

    /* ensure we have an astate connected to the destination
     * mailbox, so that the annotation txn will be committed
     * when we close the mailbox */
    r = mailbox_get_annotate_state(mailbox2, 1, &astate);
    CU_ASSERT_EQUAL(r, 0);

    r = annotate_msg_copy(mailbox1, 1, mailbox2, 1, "smurf");
    CU_ASSERT_EQUAL(r, 0);

    r = mailbox_commit(mailbox2);
    CU_ASSERT_EQUAL(r, 0);

    CU_ASSERT_EQUAL(fexists(buf_cstring(&path1)), 0);
    CU_ASSERT_EQUAL(fexists(buf_cstring(&path2)), 0);

    /* check that the values copied are present for both mailboxes */

    buf_free(&val2);
    r = annotatemore_msg_lookup(mailbox1, 1, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);

    buf_free(&val2);
    r = annotatemore_msg_lookup(mailbox2, 1, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);

    /* check that the values not copied are only present in the source
     * mailbox */

    buf_free(&val2);
    r = annotatemore_msg_lookup(mailbox1, 2, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE1);

    buf_free(&val2);
    r = annotatemore_msg_lookup(mailbox1, 3, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE2);

    buf_free(&val2);
    r = annotatemore_msg_lookup(mailbox2, 2, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NULL(val2.s);

    buf_free(&val2);
    r = annotatemore_msg_lookup(mailbox2, 3, COMMENT, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NULL(val2.s);

    CU_ASSERT_EQUAL(fexists(buf_cstring(&path1)), 0);
    CU_ASSERT_EQUAL(fexists(buf_cstring(&path2)), 0);
    buf_free(&path1);
    buf_free(&path2);

    annotatemore_close();

    strarray_fini(&entries);
    strarray_fini(&attribs);
    buf_free(&val);
    buf_free(&val2);
    mailbox_close(&mailbox1);
    mailbox_close(&mailbox2);
}

static void test_missing_definitions_file(void)
{
    set_annotation_definitions(NULL);
    CU_SYSLOG_MATCH("annotations\\.def: could not open.*No such file");

    annotate_init(NULL, NULL);
    /* if we got here, we didn't fatal() */

    /* but we did complain to syslog */
    CU_ASSERT_SYSLOG(/*all*/0, 1);
}

static void test_broken_definitions_file_1(void)
{
    set_annotation_definitions(
        EXENTRY",sXerver,string,backend,value.shared,\n");
    CU_SYSLOG_MATCH("invalid annotation scope.*'sXerver'");

    annotate_init(NULL, NULL);
    /* if we got here, we didn't fatal() */

    /* but we did complain to syslog */
    CU_ASSERT_SYSLOG(/*all*/0, 1);
}

static void test_broken_definitions_file_2(void)
{
    set_annotation_definitions(
        EXENTRY",server,stXring,backend,value.shared,\n");
    CU_SYSLOG_MATCH("invalid annotation type.*'stXring'");

    annotate_init(NULL, NULL);
    /* if we got here, we didn't fatal() */

    /* but we did complain to syslog */
    CU_ASSERT_SYSLOG(/*all*/0, 1);
}

static void test_broken_definitions_file_3(void)
{
    set_annotation_definitions(
        EXENTRY",server,string,bacXkend,value.shared,\n");
    CU_SYSLOG_MATCH("invalid annotation proxy type.*'bacXkend'");

    annotate_init(NULL, NULL);
    /* if we got here, we didn't fatal() */

    /* but we did complain to syslog */
    CU_ASSERT_SYSLOG(/*all*/0, 1);
}

static void test_broken_definitions_file_4(void)
{
    set_annotation_definitions(
        EXENTRY",server,string,backend,valuXe.shared,\n");
    CU_SYSLOG_MATCH("invalid annotation attributes.*'valuXe.shared'");

    annotate_init(NULL, NULL);
    /* if we got here, we didn't fatal() */

    /* but we did complain to syslog */
    CU_ASSERT_SYSLOG(/*all*/0, 1);
}

static void test_broken_definitions_file_5(void)
{
    set_annotation_definitions(
        "/flags/foobar,message,string,backend,value.shared,\n");
    CU_SYSLOG_MATCH("message entry under /flags/");

    annotate_init(NULL, NULL);
    /* if we got here, we didn't fatal() */

    /* but we did complain to syslog */
    CU_ASSERT_SYSLOG(/*all*/0, 1);
}

static void test_broken_definitions_file_6(void)
{
    set_annotation_definitions(
        "/vendor/cmu/cyrus-imapd/foobar,server,string,backend,value.shared,\n");
    CU_SYSLOG_MATCH("annotation under /vendor/cmu/cyrus-imapd/");

    annotate_init(NULL, NULL);
    /* if we got here, we didn't fatal() */

    /* but we did complain to syslog */
    CU_ASSERT_SYSLOG(/*all*/0, 1);
}

static void test_broken_definitions_file_7(void)
{
    set_annotation_definitions(
        EXENTRY",server,string,backend,value.shared,,,,\n");
    CU_SYSLOG_MATCH("junk at end of line");

    annotate_init(NULL, NULL);
    /* if we got here, we didn't fatal() */

    /* but we did complain to syslog */
    CU_ASSERT_SYSLOG(/*all*/0, 1);
}

static void test_broken_definitions_file_8(void)
{
    set_annotation_definitions(
        EXENTRY",server,string,\n");
    CU_SYSLOG_MATCH("invalid annotation attributes.*");

    annotate_init(NULL, NULL);
    /* if we got here, we didn't fatal() */

    /* but we did complain to syslog */
    /* Please note that this one complains to syslog twice */
    CU_ASSERT_SYSLOG(/*all*/0, 2);
}

static void test_broken_definitions_file_9(void)
{
    /* test that when parsing a bitfield, only the first
     * invalid name is reported in the error context */
    set_annotation_definitions(
        EXENTRY",server,string,backend,value valXue valYue,\n");
    CU_SYSLOG_MATCH("invalid annotation attributes.*'valXue'");

    annotate_init(NULL, NULL);
    /* if we got here, we didn't fatal() */

    /* but we did complain to syslog */
    CU_ASSERT_SYSLOG(/*all*/0, 1);
}

static void test_broken_definitions_file_10(void)
{
    set_annotation_definitions(
        EXENTRY",ser@ver,string,backend,value.shared,\n");
    CU_SYSLOG_MATCH("invalid character.*'@");

    annotate_init(NULL, NULL);
    /* if we got here, we didn't fatal() */

    /* but we did complain to syslog */
    CU_ASSERT_SYSLOG(/*all*/0, 1);
}

static void test_getset_server_undefined(void)
{
    int r;
    annotate_state_t *astate = NULL;
    strarray_t entries = STRARRAY_INITIALIZER;
    strarray_t attribs = STRARRAY_INITIALIZER;
    strarray_t results = STRARRAY_INITIALIZER;
    struct entryattlist *ealist = NULL;
    struct buf val = BUF_INITIALIZER;
    struct buf val2 = BUF_INITIALIZER;

    annotate_init(NULL, NULL);

    annotatemore_open();

    astate = annotate_state_new();
    r = annotate_state_set_server(astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    strarray_append(&entries, EXENTRY);
    strarray_append(&attribs, VALUE_SHARED);

    /* check that there is no value initially */

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" EXENTRY "\" " \
           VALUE_SHARED "=NIL"
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(/*mboxname*/"", EXENTRY, /*userid*/"", &val);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(val.s);

    /* setting a value should fail */

    buf_appendcstr(&val, VALUE0);
    setentryatt(&ealist, EXENTRY, VALUE_SHARED, &val);
    annotate_state_set_auth(astate, /*isadmin*/1, userid, auth_state);
    r = annotate_state_store(astate, ealist);
    annotate_state_set_auth(astate, /*isadmin*/0, userid, auth_state);
    CU_ASSERT_EQUAL(r, IMAP_PERMISSION_DENIED);
    freeentryatts(ealist);
    ealist = NULL;

    r = annotate_state_commit(&astate);
    CU_ASSERT_EQUAL(r, 0);

    /* check that there is no value */
    astate = annotate_state_new();
    r = annotate_state_set_server(astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, /*isadmin*/0, userid, auth_state);

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" EXENTRY "\" " \
           VALUE_SHARED "=NIL"
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    buf_free(&val);
    r = annotatemore_lookup(/*mboxname*/"", EXENTRY, /*userid*/"", &val);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(val.s);

    annotate_state_abort(&astate);
    annotatemore_close();

    strarray_fini(&entries);
    strarray_fini(&attribs);
    strarray_fini(&results);
    buf_free(&val);
    buf_free(&val2);
}


static void test_getset_server_defined(void)
{
    int r;
    annotate_state_t *astate = NULL;
    strarray_t entries = STRARRAY_INITIALIZER;
    strarray_t attribs = STRARRAY_INITIALIZER;
    strarray_t results = STRARRAY_INITIALIZER;
    struct entryattlist *ealist = NULL;
    struct buf val = BUF_INITIALIZER;
    struct buf val2 = BUF_INITIALIZER;

    set_annotation_definitions(
        EXENTRY",server,string,backend,value.shared,\n");
    annotate_init(NULL, NULL);

    annotatemore_open();

    astate = annotate_state_new();
    r = annotate_state_set_server(astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    strarray_append(&entries, EXENTRY);
    strarray_append(&attribs, VALUE_SHARED);
    strarray_append(&attribs, SIZE_SHARED);

    /* check that there is no value initially */

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" EXENTRY "\" " \
           VALUE_SHARED "=NIL " \
           SIZE_SHARED "=\"0\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(/*mboxname*/"", EXENTRY, /*userid*/"", &val);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(val.s);

    /* set a value */

    buf_appendcstr(&val, VALUE0);
    setentryatt(&ealist, EXENTRY, VALUE_SHARED, &val);
    annotate_state_set_auth(astate, /*isadmin*/1, userid, auth_state);
    r = annotate_state_store(astate, ealist);
    annotate_state_set_auth(astate, /*isadmin*/0, userid, auth_state);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    /* check that we can fetch the value back in the same txn */
    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" EXENTRY "\" " \
           VALUE_SHARED "=\"" VALUE0 "\" " \
           SIZE_SHARED "=\"" LENGTH0 "\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(/*mboxname*/"", EXENTRY, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    r = annotate_state_commit(&astate);
    CU_ASSERT_EQUAL(r, 0);

    /* check that we can fetch the value back in a new txn */
    astate = annotate_state_new();
    r = annotate_state_set_server(astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" EXENTRY "\" " \
           VALUE_SHARED "=\"" VALUE0 "\" " \
           SIZE_SHARED "=\"" LENGTH0 "\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(/*mboxname*/"", EXENTRY, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    annotate_state_abort(&astate);
    annotatemore_close();

    /* check that we can fetch the value back after close and re-open */

    annotatemore_open();

    astate = annotate_state_new();
    r = annotate_state_set_server(astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" EXENTRY "\" " \
           VALUE_SHARED "=\"" VALUE0 "\" " \
           SIZE_SHARED "=\"" LENGTH0 "\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(/*mboxname*/"", EXENTRY, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    buf_free(&val);
    setentryatt(&ealist, EXENTRY, VALUE_SHARED, &val);
    annotate_state_set_auth(astate, /*isadmin*/1, userid, auth_state);
    r = annotate_state_store(astate, ealist);
    annotate_state_set_auth(astate, /*isadmin*/0, userid, auth_state);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    r = annotate_state_commit(&astate);
    CU_ASSERT_EQUAL(r, 0);

    /* check that there is no value any more */
    astate = annotate_state_new();
    r = annotate_state_set_server(astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" EXENTRY "\" " \
           VALUE_SHARED "=NIL " \
           SIZE_SHARED "=\"0\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(/*mboxname*/"", EXENTRY, /*userid*/"", &val);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(val.s);


    annotate_state_abort(&astate);
    annotatemore_close();

    strarray_fini(&entries);
    strarray_fini(&attribs);
    strarray_fini(&results);
    buf_free(&val);
}

static void test_getset_server_defined_ucase(void)
{
    int r;
    annotate_state_t *astate = NULL;
    strarray_t entries = STRARRAY_INITIALIZER;
    strarray_t attribs = STRARRAY_INITIALIZER;
    strarray_t results = STRARRAY_INITIALIZER;
    struct entryattlist *ealist = NULL;
    struct buf val = BUF_INITIALIZER;
    struct buf val2 = BUF_INITIALIZER;

    /* set the definition in uppercase, as if pasted from rfc examples */
    set_annotation_definitions(
        UCEXENTRY",server,string,backend,value.shared,\n");

    annotate_init(NULL, NULL);

    annotatemore_open();

    astate = annotate_state_new();
    r = annotate_state_set_server(astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    /* should be able to set/get it in lowercase, because these are
     * case insensitive! */
    strarray_append(&entries, EXENTRY);
    strarray_append(&attribs, VALUE_SHARED);
    strarray_append(&attribs, SIZE_SHARED);

    /* check that there is no value initially */

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" EXENTRY "\" " \
           VALUE_SHARED "=NIL " \
           SIZE_SHARED "=\"0\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(/*mboxname*/"", EXENTRY, /*userid*/"", &val);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(val.s);

    /* set a value */

    buf_appendcstr(&val, VALUE0);
    setentryatt(&ealist, EXENTRY, VALUE_SHARED, &val);
    annotate_state_set_auth(astate, /*isadmin*/1, userid, auth_state);
    r = annotate_state_store(astate, ealist);
    annotate_state_set_auth(astate, /*isadmin*/0, userid, auth_state);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    /* check that we can fetch the value back in the same txn */
    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" EXENTRY "\" " \
           VALUE_SHARED "=\"" VALUE0 "\" " \
           SIZE_SHARED "=\"" LENGTH0 "\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(/*mboxname*/"", EXENTRY, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    r = annotate_state_commit(&astate);
    CU_ASSERT_EQUAL(r, 0);

    /* check that we can fetch the value back in a new txn */
    astate = annotate_state_new();
    r = annotate_state_set_server(astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" EXENTRY "\" " \
           VALUE_SHARED "=\"" VALUE0 "\" " \
           SIZE_SHARED "=\"" LENGTH0 "\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(/*mboxname*/"", EXENTRY, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    annotate_state_abort(&astate);
    annotatemore_close();

    /* check that we can fetch the value back after close and re-open */

    annotatemore_open();

    astate = annotate_state_new();
    r = annotate_state_set_server(astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" EXENTRY "\" " \
           VALUE_SHARED "=\"" VALUE0 "\" " \
           SIZE_SHARED "=\"" LENGTH0 "\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(/*mboxname*/"", EXENTRY, /*userid*/"", &val2);
    CU_ASSERT_EQUAL_FATAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL_FATAL(val2.s);
    CU_ASSERT_STRING_EQUAL(buf_cstring(&val2), VALUE0);
    buf_free(&val2);

    buf_free(&val);
    setentryatt(&ealist, EXENTRY, VALUE_SHARED, &val);
    annotate_state_set_auth(astate, /*isadmin*/1, userid, auth_state);
    r = annotate_state_store(astate, ealist);
    annotate_state_set_auth(astate, /*isadmin*/0, userid, auth_state);
    CU_ASSERT_EQUAL(r, 0);
    freeentryatts(ealist);
    ealist = NULL;

    r = annotate_state_commit(&astate);
    CU_ASSERT_EQUAL(r, 0);

    /* check that there is no value any more */
    astate = annotate_state_new();
    r = annotate_state_set_server(astate);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    r = annotate_state_fetch(astate,
                             &entries, &attribs,
                             fetch_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL_FATAL(results.count, 1);
#define EXPECTED \
           "mboxname=\"\" " \
           "uid=0 " \
           "entry=\"" EXENTRY "\" " \
           VALUE_SHARED "=NIL " \
           SIZE_SHARED "=\"0\""
    CU_ASSERT_STRING_EQUAL(results.data[0], EXPECTED);
#undef EXPECTED
    strarray_truncate(&results, 0);

    r = annotatemore_lookup(/*mboxname*/"", EXENTRY, /*userid*/"", &val);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(val.s);


    annotate_state_abort(&astate);
    annotatemore_close();

    strarray_fini(&entries);
    strarray_fini(&attribs);
    strarray_fini(&results);
    buf_free(&val);
}
static const char *stringifyea(const struct entryattlist *ea)
{
    const struct attvaluelist *av;
    static struct buf buf;
    buf_reset(&buf);

    for ( ; ea ; ea = ea->next) {
        buf_printf(&buf, "(%s", ea->entry);
        for (av = ea->attvalues ; av ; av = av->next) {
            buf_printf(&buf, "(%s\"", av->attrib);
            buf_appendmap(&buf, av->value.s, av->value.len);
            buf_appendcstr(&buf, "\")");
        }
        buf_putc(&buf, ')');
    }

    return buf_cstring(&buf);
}

static void test_setentryatt(void)
{
    struct entryattlist *eal = NULL;
    struct buf val = BUF_INITIALIZER;

    CU_ASSERT_PTR_NULL(eal);
    CU_ASSERT_STRING_EQUAL(stringifyea(eal), "");

    /* Test data courtesy http://hipsteripsum.me */

    /* append an ea and av */
    buf_init_ro(&val, "coffee", 6);
    setentryatt(&eal, "letterpress", "single-origin", &val);
    CU_ASSERT_PTR_NOT_NULL(eal);
    CU_ASSERT_STRING_EQUAL(stringifyea(eal),
                           "(letterpress(single-origin\"coffee\"))");

    /* append another ea and av */
    buf_init_ro(&val, "mustache", 8);
    setentryatt(&eal, "cosby", "sweater", &val);
    CU_ASSERT_PTR_NOT_NULL(eal);
    CU_ASSERT_STRING_EQUAL(stringifyea(eal),
                           "(letterpress(single-origin\"coffee\"))"
                           "(cosby(sweater\"mustache\"))");

    /* append a third ea and av */
    buf_init_ro(&val, "portland", 8);
    setentryatt(&eal, "cred", "artisan", &val);
    CU_ASSERT_PTR_NOT_NULL(eal);
    CU_ASSERT_STRING_EQUAL(stringifyea(eal),
                           "(letterpress(single-origin\"coffee\"))"
                           "(cosby(sweater\"mustache\"))"
                           "(cred(artisan\"portland\"))");

    /* replace the value in an av */
    buf_init_ro(&val, "shoreditch", 10);
    setentryatt(&eal, "cosby", "sweater", &val);
    CU_ASSERT_PTR_NOT_NULL(eal);
    CU_ASSERT_STRING_EQUAL(stringifyea(eal),
                           "(letterpress(single-origin\"coffee\"))"
                           "(cosby(sweater\"shoreditch\"))"
                           "(cred(artisan\"portland\"))");

    /* add an av to an existing ea */
    buf_init_ro(&val, "gluten-free", 11);
    setentryatt(&eal, "letterpress", "biodiesel", &val);
    CU_ASSERT_PTR_NOT_NULL(eal);
    CU_ASSERT_STRING_EQUAL(stringifyea(eal),
                           "(letterpress(single-origin\"coffee\")"
                           "(biodiesel\"gluten-free\"))"
                           "(cosby(sweater\"shoreditch\"))"
                           "(cred(artisan\"portland\"))");

    freeentryatts(eal);
    buf_free(&val);
}

static void test_clearentryatt(void)
{
    struct entryattlist *eal = NULL;
    struct buf val = BUF_INITIALIZER;

    /* Test data courtesy http://hipsteripsum.me */

    /* append an ea and av */
    buf_init_ro(&val, "coffee", 6);
    setentryatt(&eal, "letterpress", "single-origin", &val);
    /* add an av to an existing ea */
    buf_init_ro(&val, "gluten-free", 11);
    setentryatt(&eal, "letterpress", "biodiesel", &val);
    /* add another av to an existing ea */
    buf_init_ro(&val, "organic", 7);
    setentryatt(&eal, "letterpress", "keffiyeh", &val);
    /* append another ea and av */
    buf_init_ro(&val, "shoreditch", 10);
    setentryatt(&eal, "cosby", "sweater", &val);
    /* append a third ea and av */
    buf_init_ro(&val, "portland", 8);
    setentryatt(&eal, "cred", "artisan", &val);
    CU_ASSERT_PTR_NOT_NULL(eal);
    CU_ASSERT_STRING_EQUAL(stringifyea(eal),
                           "(letterpress(single-origin\"coffee\")"
                           "(biodiesel\"gluten-free\")"
                           "(keffiyeh\"organic\"))"
                           "(cosby(sweater\"shoreditch\"))"
                           "(cred(artisan\"portland\"))");

    clearentryatt(&eal, "cosby", "sweater");
    CU_ASSERT_PTR_NOT_NULL(eal);
    CU_ASSERT_STRING_EQUAL(stringifyea(eal),
                           "(letterpress(single-origin\"coffee\")"
                           "(biodiesel\"gluten-free\")"
                           "(keffiyeh\"organic\"))"
                           "(cred(artisan\"portland\"))");

    clearentryatt(&eal, "letterpress", "biodiesel");
    CU_ASSERT_PTR_NOT_NULL(eal);
    CU_ASSERT_STRING_EQUAL(stringifyea(eal),
                           "(letterpress(single-origin\"coffee\")"
                           "(keffiyeh\"organic\"))"
                           "(cred(artisan\"portland\"))");

    clearentryatt(&eal, "letterpress", "single-origin");
    CU_ASSERT_PTR_NOT_NULL(eal);
    CU_ASSERT_STRING_EQUAL(stringifyea(eal),
                           "(letterpress"
                           "(keffiyeh\"organic\"))"
                           "(cred(artisan\"portland\"))");

    clearentryatt(&eal, "letterpress", "keffiyeh");
    CU_ASSERT_PTR_NOT_NULL(eal);
    CU_ASSERT_STRING_EQUAL(stringifyea(eal),
                           "(cred(artisan\"portland\"))");

    clearentryatt(&eal, "cred", "artisan");
    CU_ASSERT_PTR_NULL(eal);
    CU_ASSERT_STRING_EQUAL(stringifyea(eal), "");

    freeentryatts(eal);
    buf_free(&val);
}

static int create_messages(struct mailbox *mailbox, int count)
{
    int i, r;

    for (i = 0; i < count; i++) {
        static const char msgtmpl[] =
            "From: Fred Bloggs <fbloggs@fastmail.fm>\r\n"
            "To: Sarah Jane Smith <sjsmith@gmail.com>\r\n"
            "Date: Wed, 27 Oct 2010 18:37:26 +1100\r\n"
            "Subject: Trivial testing email %d in mbox %s\r\n"
            "Message-ID: <fake800-%d@fastmail.fm>\r\n"
            "X-Mailer: Norman\r\n"
            "\r\n"
            "Hello, World from message %d in mailbox %s!\n";
        struct stagemsg *stage = NULL;
        struct appendstate as;
        quota_t qdiffs[QUOTA_NUMRESOURCES] = QUOTA_DIFFS_DONTCARE_INITIALIZER;
        FILE *fp;
        time_t internaldate = time(NULL);
        struct body *body = NULL;
        struct buf buf = BUF_INITIALIZER;

        /* Write the message to the filesystem */
        if (!(fp = append_newstage(mailbox_name(mailbox), internaldate, 0, &stage))) {
            fprintf(stderr, "append_newstage(%s) failed", mailbox_name(mailbox));
            return IMAP_IOERROR;
        }
        buf_printf(&buf, msgtmpl, i, mailbox_name(mailbox), i, i, mailbox_name(mailbox));
        fwrite(buf_base(&buf), 1, buf_len(&buf), fp);
        buf_free(&buf);
        if (fclose(fp)) {
            fprintf(stderr, "fclose failed: %s", strerror(errno));
            return IMAP_IOERROR;
        }

        /* Append the message to the mailbox */
        qdiffs[QUOTA_MESSAGE] = 1;
        r = append_setup_mbox(&as, mailbox, userid, auth_state,
                0, qdiffs, 0, 0, EVENT_MESSAGE_NEW);
        if (r) {
            fprintf(stderr, "append_setup_mbox(%s) failed: %s", mailbox_name(mailbox),
                    strerror(errno));
            return r;
        }
        r = append_fromstage(&as, &body, stage, internaldate, 0, NULL, 0, NULL);
        if (r) {
            fprintf(stderr, "append_fromstage(%s) failed: %s", mailbox_name(mailbox),
                    error_message(r));
            append_abort(&as);
            return r;
        }
        message_free_body(body);
        free(body);

        append_removestage(stage);
        r = append_commit(&as);
        if (r) {
            fprintf(stderr, "append_commit(%s) failed: %s", mailbox_name(mailbox),
                    error_message(r));
            return r;
        }
    }

    return 0;
}

static void test_canon_value(void)
{
    struct {
        const char *annot;
        const char *attrb;
        const char *value;
        int is_invalid;
    } tests[] = {{
        .annot = "/check",
        .attrb = "value.shared",
        .value = "true",
    }, {
        .annot = "/check",
        .attrb = "value.shared",
        .value = "false",
    }, {
        .annot = "/check",
        .attrb = "value.shared",
        .value = "yes",
        .is_invalid = 1,
    }, {
        .annot = "/vendor/cmu/cyrus-imapd/sortorder",
        .attrb = "value.priv",
        .value = "123",
    }, {
        .annot = "/vendor/cmu/cyrus-imapd/sortorder",
        .attrb = "value.priv",
        .value = "12298189749871298739829873",
        .is_invalid = 1
    }, {
        .annot = "/vendor/cmu/cyrus-imapd/sortorder",
        .attrb = "value.priv",
        .value = "-1",
        .is_invalid = 1
    }, {
        .annot = "/vendor/cmu/cyrus-imapd/expire",
        .attrb = "value.shared",
        .value = "1",
    }, {
        .annot = "/vendor/cmu/cyrus-imapd/expire",
        .attrb = "value.shared",
        .value = "1s",
    }, {
        .annot = "/vendor/cmu/cyrus-imapd/expire",
        .attrb = "value.shared",
        .value = "1x",
        .is_invalid = 1,
    }, {
        .annot = "/vendor/cmu/cyrus-imapd/archive",
        .attrb = "value.shared",
        .value = "1",
    }, {
        .annot = "/vendor/cmu/cyrus-imapd/archive",
        .attrb = "value.shared",
        .value = "1s",
    }, {
        .annot = "/vendor/cmu/cyrus-imapd/archive",
        .attrb = "value.shared",
        .value = "1x",
        .is_invalid = 1,
    }, {
        .annot = "/vendor/cmu/cyrus-imapd/delete",
        .attrb = "value.shared",
        .value = "1",
    }, {
        .annot = "/vendor/cmu/cyrus-imapd/delete",
        .attrb = "value.shared",
        .value = "1s",
    }, {
        .annot = "/vendor/cmu/cyrus-imapd/delete",
        .attrb = "value.shared",
        .value = "1x",
        .is_invalid = 1,
    }};

    struct mailbox *mailbox = NULL;
    annotate_state_t *astate = NULL;
    struct entryattlist *ealist = NULL;
    struct buf val = BUF_INITIALIZER;

    annotate_init(NULL, NULL);
    annotatemore_open();

    int r = mailbox_open_iwl(MBOXNAME1_INT, &mailbox);
    CU_ASSERT_EQUAL_FATAL(r, 0);

    astate = annotate_state_new();
    r = annotate_state_set_mailbox(astate, mailbox);
    CU_ASSERT_EQUAL(r, 0);
    annotate_state_set_auth(astate, isadmin, userid, auth_state);

    /* validate value */
    for (size_t i = 0; i < sizeof(tests)/sizeof(tests[0]); i++) {
        buf_setcstr(&val, tests[i].value);
        setentryatt(&ealist, tests[i].annot, tests[i].attrb, &val);
        r = annotate_state_store(astate, ealist);
        CU_ASSERT_EQUAL(r, tests[i].is_invalid ? IMAP_ANNOTATION_BADVALUE : 0);
        freeentryatts(ealist);
        ealist = NULL;
    }

    buf_free(&val);

    annotate_state_abort(&astate);
    mailbox_close(&mailbox);
    annotatemore_close();
}


static int set_up(void)
{
    int r;
    struct mboxlist_entry mbentry;
    struct mailbox *mailbox;
    const char * const *d;
    static const char * const dirs[] = {
        DBDIR,
        DBDIR"/db",
        DBDIR"/conf",
        DBDIR"/data",
        NULL
    };

    r = system("rm -rf " DBDIR);
    if (r)
        return r;
    r = fexists(DBDIR);
    if (r != -ENOENT)
        return ENOTDIR;

    for (d = dirs ; *d ; d++) {
        r = mkdir(*d, 0777);
        if (r < 0) {
            int e = errno;
            perror(*d);
            return e;
        }
    }

    libcyrus_config_setstring(CYRUSOPT_CONFIG_DIR, DBDIR);
    config_read_string(
        "configdirectory: "DBDIR"/conf\n"
        "defaultpartition: "PARTITION"\n"
        "partition-"PARTITION": "DBDIR"/data\n"
    );

    cyrusdb_init();
    config_mboxlist_db = "skiplist";
    config_annotation_db = "skiplist";
    config_quota_db = "skiplist";

    userid = "smurf";
    isadmin = 0;
    auth_state = auth_newstate(userid);

    quotadb_init(0);
    quotadb_open(NULL);

    mboxlist_init(0);
    mboxlist_open(NULL);

    struct mboxlock *namespacelock = mboxname_usernamespacelock(MBOXNAME1_INT);

    /* XXX API abuse? perhaps this should just call mboxlist_create,
     * XXX rather than the low level APIs. */
    r = mailbox_create(MBOXNAME1_INT, /*mbtype*/0, PARTITION, ACL,
                       makeuuid(),
                       /*options*/0, /*uidvalidity*/0,
                       /*createdmodseq*/0,
                       /*highestmodseq*/0, &mailbox);
    if (r)
        return r;

    memset(&mbentry, 0, sizeof(mbentry));
    mbentry.name = MBOXNAME1_INT;
    mbentry.uniqueid = (char *)mailbox_uniqueid(mailbox);
    mbentry.mbtype = 0;
    mbentry.partition = PARTITION;
    mbentry.acl = ACL;
    r = mboxlist_updatelock(&mbentry, /*localonly*/1);
    if (r)
        return r;

    r = create_messages(mailbox, MBOX1_MAXUID);
    if (r)
        return r;
    mailbox_close(&mailbox);

    mboxname_release(&namespacelock);

    namespacelock = mboxname_usernamespacelock(MBOXNAME2_INT);

    r = mailbox_create(MBOXNAME2_INT, /*mbtype*/0, PARTITION, ACL,
                       makeuuid(),
                       /*options*/0, /*uidvalidity*/0,
                       /*createdmodseq*/0,
                       /*highestmodseq*/0, &mailbox);
    if (r)
        return r;

    memset(&mbentry, 0, sizeof(mbentry));
    mbentry.name = MBOXNAME2_INT;
    mbentry.uniqueid = (char *)mailbox_uniqueid(mailbox);
    mbentry.mbtype = 0;
    mbentry.partition = PARTITION;
    mbentry.acl = ACL;
    r = mboxlist_updatelock(&mbentry, /*localonly*/1);
    if (r)
        return r;

    r = create_messages(mailbox, MBOX2_MAXUID);
    if (r)
        return r;
    mailbox_close(&mailbox);

    mboxname_release(&namespacelock);

    namespacelock = mboxname_usernamespacelock(MBOXNAME3_INT);

    r = mailbox_create(MBOXNAME3_INT, /*mbtype*/0, PARTITION, ACL,
                       makeuuid(),
                       /*options*/0, /*uidvalidity*/0,
                       /*createdmodseq*/0,
                       /*highestmodseq*/0, &mailbox);
    if (r)
        return r;

    memset(&mbentry, 0, sizeof(mbentry));
    mbentry.name = MBOXNAME3_INT;
    mbentry.uniqueid = (char *)mailbox_uniqueid(mailbox);
    mbentry.mbtype = 0;
    mbentry.partition = PARTITION;
    mbentry.acl = ACL;
    r = mboxlist_updatelock(&mbentry, /*localonly*/1);
    if (r)
        return r;

    mailbox_close(&mailbox);

    mboxname_release(&namespacelock);

    old_annotation_definitions =
        imapopts[IMAPOPT_ANNOTATION_DEFINITIONS].val.s;

    return 0;
}

static int tear_down(void)
{
    int r;

    mboxlist_close();
    mboxlist_done();

    quotadb_close();
    quotadb_done();

    annotate_done();

    imapopts[IMAPOPT_ANNOTATION_DEFINITIONS].val.s =
        old_annotation_definitions;

    auth_freestate(auth_state);

    cyrusdb_done();
    config_reset();
    config_mboxlist_db = NULL;
    config_annotation_db = NULL;

    r = system("rm -rf " DBDIR);
    if (r) r = -1;

    return r;
}
/* vim: set ft=c: */
