#include <unistd.h>
#include <stdlib.h>
#include "config.h"
#include "cunit/cyrunit.h"
#include "imap/duplicate.h"
#include "xmalloc.h"
#include "retry.h"
#include "imap/global.h"
#include "imap/imap_err.h"
#include "cyrusdb.h"
#include "libcyr_cfg.h"
#include "libconfig.h"

#define DBDIR                   "test-mb-dbdir"

struct result
{
    struct result *next;
    char *id;
    char *to;
    char *date;
    time_t mark;
    unsigned long uid;
};

#define GOTRESULT(expid, expto, expdate, expmark, expuid) \
{ \
    struct result *actual = results; \
    CU_ASSERT_PTR_NOT_NULL_FATAL(results); \
    results = results->next; \
    CU_ASSERT_STRING_EQUAL(expid, actual->id); \
    CU_ASSERT_STRING_EQUAL(expto, actual->to); \
    CU_ASSERT_STRING_EQUAL(expdate, actual->date); \
    CU_ASSERT_EQUAL(expmark, actual->mark); \
    CU_ASSERT_EQUAL(expuid, actual->uid); \
    free(actual->id); \
    free(actual->to); \
    free(actual->date); \
    free(actual); \
}

static void test_getset(void)
{
    duplicate_key_t dkey = DUPLICATE_INITIALIZER;
    time_t t;
    time_t now;
    static const char MSGID1[] = "<fake0999@fastmail.fm>";
    static const char MSGID2[] = "<fake1001@fastmail.fm>";
    static const char FOLDER[] = "user.smurf";
    static const char DATE[] = "Wed, 27 Oct 2010 18:37:26 +1100";
    static unsigned long UID = 42;

    /* an invalid key returns 0 */
    dkey.id = NULL;
    dkey.to = NULL;
    dkey.date = NULL;
    t = duplicate_check(&dkey);
    CU_ASSERT_EQUAL(t, 0);

    /* a valid but missing key returns 0 */
    dkey.id = MSGID1;
    dkey.to = FOLDER;
    dkey.date = DATE;
    t = duplicate_check(&dkey);
    CU_ASSERT_EQUAL(t, 0);

    /* the other one too */
    dkey.id = MSGID2;
    dkey.to = FOLDER;
    dkey.date = DATE;
    t = duplicate_check(&dkey);
    CU_ASSERT_EQUAL(t, 0);

    /* mark appears to succeed */
    dkey.id = MSGID1;
    dkey.to = FOLDER;
    dkey.date = DATE;
    now = time(NULL);
    duplicate_mark(&dkey, now, UID);

    /* an invalid key still returns 0 */
    dkey.id = NULL;
    dkey.to = NULL;
    dkey.date = NULL;
    t = duplicate_check(&dkey);
    CU_ASSERT_EQUAL(t, 0);

    /* a valid and present key returns non-zero */
    dkey.id = MSGID1;
    dkey.to = FOLDER;
    dkey.date = DATE;
    t = duplicate_check(&dkey);
    CU_ASSERT_NOT_EQUAL(t, 0);
    CU_ASSERT_EQUAL(t, now);

    /* a valid but missing key still returns 0 */
    dkey.id = MSGID2;
    dkey.to = FOLDER;
    dkey.date = DATE;
    t = duplicate_check(&dkey);
    CU_ASSERT_EQUAL(t, 0);
}

static int finder(const duplicate_key_t *dkey, time_t mark,
                  unsigned long uid, void *rock)
{
    struct result **head = (struct result **)rock;
    struct result **tail;
    struct result *res;

//     if (verbose)
//      fprintf(stderr, "found: id=\"%s\" to=\"%s\" date=\"%s\" "
//                      "mark=%lu uid=%lu\n",
//                      dkey->id, dkey->to, dkey->date,
//                      (unsigned long)mark, (unsigned long)uid);

    /* remember arguments for later perusal */
    res = xzmalloc(sizeof(*res));
    res->id = xstrdup(dkey->id);
    res->to = xstrdup(dkey->to);
    res->date = xstrdup(dkey->date);
    res->mark = mark;
    res->uid = uid;

    /* append to the list.  yes, it's inefficient. */
    for (tail = head ; *tail ; tail = &(*tail)->next)
        ;
    *tail = res;

    return 0;
}

static void test_find(void)
{
    duplicate_key_t dkey = DUPLICATE_INITIALIZER;
    struct result *results = NULL;
    static const char MSGID1[] = "<fake0999@fastmail.fm>";
    static const char MSGID2[] = "<fake1001@fastmail.fm>";
    /* test data from hipsteripsum.me */
    static const char FOLDER1[] = "user.vegan";
    static const char FOLDER2[] = "user.irony";
    static const char FOLDER3[] = "user.single.origin";
    static const char FOLDER4[] = "user.loko";
    static const char FOLDER5[] = "user.biodiesel";
    static const char DATE1[] = "Wed, 27 Oct 2010 18:01:02 +1100";
    static const char DATE2[] = "Wed, 27 Oct 2010 18:03:04 +1100";
    static const char DATE3[] = "Wed, 27 Oct 2010 18:05:06 +1100";
    static const char DATE4[] = "Wed, 27 Oct 2010 18:07:08 +1100";
    static const char DATE5[] = "Wed, 27 Oct 2010 18:09:10 +1100";
    static time_t MARK1 = 1319088235;
    static time_t MARK2 = 1319088235;
    static time_t MARK3 = 1319088236;
    static time_t MARK4 = 1319088735;
    static time_t MARK5 = 1319089235;
    static unsigned long UID1 = 23;
    static unsigned long UID2 = 37;
    static unsigned long UID3 = 42;
    static unsigned long UID4 = 1007;
    static unsigned long UID5 = 314159;
    int r;

    /* find on an empty db returns nothing */
    r = duplicate_find(MSGID1, finder, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(results);
    r = duplicate_find(MSGID2, finder, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(results);

    /* add one entry */
    dkey.id = MSGID1;
    dkey.to = FOLDER1;
    dkey.date = DATE1;
    duplicate_mark(&dkey, MARK1, UID1);

    /* find returns the only entry */
    r = duplicate_find(MSGID1, finder, &results);
    CU_ASSERT_EQUAL(r, 0);
    GOTRESULT(MSGID1, FOLDER1, DATE1, MARK1, UID1);
    CU_ASSERT_PTR_NULL(results);

    /* find of another msgid returns nothing */
    r = duplicate_find(MSGID2, finder, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(results);

    /* add some more entries */
    dkey.id = MSGID1;
    dkey.to = FOLDER2;
    dkey.date = DATE2;
    duplicate_mark(&dkey, MARK2, UID2);

    dkey.id = MSGID1;
    dkey.to = FOLDER3;
    dkey.date = DATE3;
    duplicate_mark(&dkey, MARK3, UID3);

    /* find returns all the entries in lexical order */
    r = duplicate_find(MSGID1, finder, &results);
    CU_ASSERT_EQUAL(r, 0);
    GOTRESULT(MSGID1, FOLDER2, DATE2, MARK2, UID2);
    GOTRESULT(MSGID1, FOLDER3, DATE3, MARK3, UID3);
    GOTRESULT(MSGID1, FOLDER1, DATE1, MARK1, UID1);
    CU_ASSERT_PTR_NULL(results);

    /* find of another msgid returns nothing */
    r = duplicate_find(MSGID2, finder, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NULL(results);

    /* add some entries for another msgid */
    dkey.id = MSGID2;
    dkey.to = FOLDER4;
    dkey.date = DATE4;
    duplicate_mark(&dkey, MARK4, UID4);

    dkey.id = MSGID2;
    dkey.to = FOLDER5;
    dkey.date = DATE5;
    duplicate_mark(&dkey, MARK5, UID5);

    /* find returns all the entries in lexical order */
    r = duplicate_find(MSGID1, finder, &results);
    CU_ASSERT_EQUAL(r, 0);
    GOTRESULT(MSGID1, FOLDER2, DATE2, MARK2, UID2);
    GOTRESULT(MSGID1, FOLDER3, DATE3, MARK3, UID3);
    GOTRESULT(MSGID1, FOLDER1, DATE1, MARK1, UID1);
    CU_ASSERT_PTR_NULL(results);

    /* find of another msgid all it's entries in lexical order */
    r = duplicate_find(MSGID2, finder, &results);
    CU_ASSERT_EQUAL(r, 0);
    GOTRESULT(MSGID2, FOLDER5, DATE5, MARK5, UID5);
    GOTRESULT(MSGID2, FOLDER4, DATE4, MARK4, UID4);
    CU_ASSERT_PTR_NULL(results);

    /* find with msgid="" returns all the entries for all msgids */
    r = duplicate_find("", finder, &results);
    CU_ASSERT_EQUAL(r, 0);
    GOTRESULT(MSGID1, FOLDER2, DATE2, MARK2, UID2);
    GOTRESULT(MSGID1, FOLDER3, DATE3, MARK3, UID3);
    GOTRESULT(MSGID1, FOLDER1, DATE1, MARK1, UID1);
    GOTRESULT(MSGID2, FOLDER5, DATE5, MARK5, UID5);
    GOTRESULT(MSGID2, FOLDER4, DATE4, MARK4, UID4);
    CU_ASSERT_PTR_NULL(results);

    /* likewise msgid=NULL */
    r = duplicate_find(NULL, finder, &results);
    CU_ASSERT_EQUAL(r, 0);
    GOTRESULT(MSGID1, FOLDER2, DATE2, MARK2, UID2);
    GOTRESULT(MSGID1, FOLDER3, DATE3, MARK3, UID3);
    GOTRESULT(MSGID1, FOLDER1, DATE1, MARK1, UID1);
    GOTRESULT(MSGID2, FOLDER5, DATE5, MARK5, UID5);
    GOTRESULT(MSGID2, FOLDER4, DATE4, MARK4, UID4);
    CU_ASSERT_PTR_NULL(results);
}


static void config_read_string(const char *s)
{
    char *fname = xstrdup("/tmp/cyrus-cunit-configXXXXXX");
    int fd = mkstemp(fname);
    retry_write(fd, s, strlen(s));
    config_reset();
    config_read(fname, 0);
    unlink(fname);
    free(fname);
    close(fd);
}

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

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

    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"
    );

    cyrusdb_init();
    config_duplicate_db = "skiplist";

    duplicate_init(0);

    return 0;
}

static int tear_down(void)
{
    int r;

    duplicate_done();
    cyrusdb_done();
    config_duplicate_db = NULL;
    config_reset();

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

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