#include "cunit/cyrunit.h"
#include "strarray.h"
#include "util.h"
#include "hash.h"

static void count_cb(const char *key __attribute__((unused)),
                     void *data __attribute__((unused)),
                     void *rock)
{
    unsigned int *countp = (unsigned int *)rock;
    (*countp)++;
}

static void printer_cb(const char *key,
                       void *data,
                       void *rock)
{
    strarray_t *sa = (strarray_t *)rock;
    struct buf buf = BUF_INITIALIZER;
    buf_printf(&buf, "%s=%s", key, (char *)data);
    strarray_appendm(sa, buf_release(&buf));
}


#define KEY0    "Yale"
#define KEY1    "Lockwood"
#define KEY2    "Skeleton"
static const char * const values[] = {
    "Paper", "Glass", "Wood", "Diamond"
};
#define VALUE0      ((void *)values[0])
#define VALUE1      ((void *)values[1])
#define VALUE2      ((void *)values[2])
#define VALUE3      ((void *)values[3])

static void test_old(void)
{
    /* this is the old test code in lib/hash.c converted to CUnit */
    hash_table table;

    static const char * const strings[] = {
        "1","2","3","4","5","A decently long string",
        NULL
    };

    static const char * const junk[] = {
        "The first data",
        "The second data",
        "The third data",
        "The fourth data",
        "The fifth datum",
        "The sixth piece of data"
    };

    int i;
    void *j;
    strarray_t sa = STRARRAY_INITIALIZER;

    /* n.b. This test uses mpool, which coincidentally gets us 100% test
     * coverage of lib/mpool.c, which we do not otherwise test at all!
     */
    construct_hash_table(&table, 200, 1);

    for (i = 0 ; NULL != strings[i] ; i++ ) {
        j = hash_insert(strings[i], (void *)junk[i], &table);
        CU_ASSERT_PTR_EQUAL((void *)junk[i], j);
    }

    for (i = 0 ; NULL != strings[i] ; i++) {
        j = hash_lookup(strings[i], &table);
        CU_ASSERT_PTR_NOT_NULL(j);
        CU_ASSERT_PTR_EQUAL((void *)junk[i], j);
    }

    for (i = 0 ; NULL != strings[i] ; i++) {
        strarray_truncate(&sa, 0);
        hash_enumerate(&table, printer_cb, &sa);
        CU_ASSERT_EQUAL(6-i, sa.count);
        if (i < 1) CU_ASSERT(strarray_find(&sa, "1=The first data", 0) >= 0);
        if (i < 2) CU_ASSERT(strarray_find(&sa, "2=The second data", 0) >= 0);
        if (i < 3) CU_ASSERT(strarray_find(&sa, "3=The third data", 0) >= 0);
        if (i < 4) CU_ASSERT(strarray_find(&sa, "4=The fourth data", 0) >= 0);
        if (i < 5) CU_ASSERT(strarray_find(&sa, "5=The fifth datum", 0) >= 0);
        if (i < 6) CU_ASSERT(strarray_find(&sa, "A decently long string=The sixth piece of data", 0) >= 0);
        strarray_fini(&sa);

        j = hash_del((char *)strings[i], &table);
        CU_ASSERT_PTR_EQUAL((void *)junk[i], j);
    }

    for (i = 0 ; NULL != strings[i] ; i++) {
        j = hash_lookup(strings[i], &table);
        CU_ASSERT_PTR_NULL(j);
    }

    free_hash_table(&table, NULL);
}

static void test_empty(void)
{
    hash_table ht;
    hash_table *h;
    void *d;
    unsigned int count;

    /* construct an empty hash table */
    h = construct_hash_table(&ht, 1024, 0);
    CU_ASSERT_PTR_EQUAL(&ht, h);

    /* lookup the empty hash table */
    d = hash_lookup(KEY0, &ht);
    CU_ASSERT_PTR_NULL(d);

    /* delete from the empty hash table */
    d = hash_del(KEY0, &ht);
    CU_ASSERT_PTR_NULL(d);

    /* enumerate the empty hash table */
    count = 0;
    hash_enumerate(&ht, count_cb, &count);
    CU_ASSERT_EQUAL(0, count);

    /* check hash_numrecords */
    CU_ASSERT_EQUAL(0, hash_numrecords(&ht));

    /* free the hash table */
    free_hash_table(&ht, NULL);
}

static void test_reinsert(void)
{
    hash_table ht;
    hash_table *h;
    void *d;
    unsigned int count;

    /* construct an empty hash table */
    h = construct_hash_table(&ht, 1024, 0);
    CU_ASSERT_PTR_EQUAL(&ht, h);

    /* insert into the table */
    d = hash_insert(KEY0, VALUE0, &ht);
    /* no old data so hash_insert() returns the new data pointer */
    CU_ASSERT_PTR_EQUAL(VALUE0, d);

    /* lookup the hash table */
    d = hash_lookup(KEY0, &ht);
    CU_ASSERT_PTR_EQUAL(VALUE0, d);

    /* enumerate the hash table */
    count = 0;
    hash_enumerate(&ht, count_cb, &count);
    CU_ASSERT_EQUAL(1, count);

    /* check hash_numrecords */
    CU_ASSERT_EQUAL(1, hash_numrecords(&ht));

    /* re-insert into the hash table */
    d = hash_insert(KEY0, VALUE1, &ht);
    /* get the old value back */
    CU_ASSERT_PTR_EQUAL(VALUE0, d);

    /* lookup the hash table */
    d = hash_lookup(KEY0, &ht);
    CU_ASSERT_PTR_EQUAL(VALUE1, d);

    /* enumerate the hash table */
    count = 0;
    hash_enumerate(&ht, count_cb, &count);
    CU_ASSERT_EQUAL(1, count);

    /* check hash_numrecords */
    CU_ASSERT_EQUAL(1, hash_numrecords(&ht));

    /* delete from the hash table */
    d = hash_del(KEY0, &ht);
    CU_ASSERT_PTR_EQUAL(VALUE1, d);

    /* lookup the hash table */
    d = hash_lookup(KEY0, &ht);
    CU_ASSERT_PTR_NULL(d);

    /* enumerate the hash table */
    count = 0;
    hash_enumerate(&ht, count_cb, &count);
    CU_ASSERT_EQUAL(0, count);

    /* check hash_numrecords */
    CU_ASSERT_EQUAL(0, hash_numrecords(&ht));

    /* free the hash table */
    free_hash_table(&ht, NULL);
}

static const char *key(unsigned int i)
{
    static char buf[32];
    snprintf(buf, sizeof(buf), "%u", i);
    return buf;
}

static void *value(unsigned int i)
{
    return (void *)(unsigned long)(0xdead0000 + i);
}

static unsigned int freed_count = 0;
static void lincoln(void *x __attribute__((unused)))
{
    ++freed_count;
}

/* test overloading a hash table with more
 * entries than the configured number of buckets */
static void test_many(void)
{
    hash_table ht;
    hash_table *h;
    void *d;
    unsigned int count;
#define N 2048
    unsigned int i;

    /* construct an empty hash table */
    h = construct_hash_table(&ht, N/8, 0);
    CU_ASSERT_PTR_EQUAL(&ht, h);

    /* insert lots of entries into the table */
    for (i = 0 ; i < N ; i++) {
        d = hash_insert(key(i), value(i), &ht);
        CU_ASSERT_PTR_EQUAL(value(i), d);
    }

    /* lookup all the entries in the hash table */
    for (i = 0 ; i < N ; i++) {
        d = hash_lookup(key(i), &ht);
        CU_ASSERT_PTR_EQUAL(value(i), d);
    }

    /* lookup and delete entries that aren't there */
    for (i = N ; i < 2*N ; i++) {
        d = hash_lookup(key(i), &ht);
        CU_ASSERT_PTR_NULL(d);
        d = hash_del(key(i), &ht);
        CU_ASSERT_PTR_NULL(d);
    }
    d = hash_del("Not here please stop looking", &ht);
    CU_ASSERT_PTR_NULL(d);
    d = hash_lookup("Not here please stop looking", &ht);
    CU_ASSERT_PTR_NULL(d);

    /* enumerate the hash table */
    count = 0;
    hash_enumerate(&ht, count_cb, &count);
    CU_ASSERT_EQUAL(N, count);

    /* check hash_numrecords */
    CU_ASSERT_EQUAL(N, hash_numrecords(&ht));

    /* delete from the hash table */
    for (i = 0 ; i < N ; i++) {
        d = hash_del(key(i), &ht);
        CU_ASSERT_PTR_EQUAL(value(i), d);
    }

    /* all the entries should be gone */
    for (i = 0 ; i < N ; i++) {
        d = hash_lookup(key(i), &ht);
        CU_ASSERT_PTR_NULL(d);
    }

    /* enumerate the hash table: should be empty now */
    count = 0;
    hash_enumerate(&ht, count_cb, &count);
    CU_ASSERT_EQUAL(0, count);

    /* check hash_numrecords */
    CU_ASSERT_EQUAL(0, hash_numrecords(&ht));

    /* free the hash table */
    freed_count = 0;
    free_hash_table(&ht, lincoln);
    CU_ASSERT_EQUAL(0, freed_count);
}

static void test_freeing_nonempty(void)
{
    hash_table ht;
    hash_table *h;
    void *d;
    unsigned int count;
#define N 2048
    unsigned int i;

    /* construct an empty hash table */
    h = construct_hash_table(&ht, N/8, 0);
    CU_ASSERT_PTR_EQUAL(&ht, h);

    /* insert lots of entries into the table */
    for (i = 0 ; i < N ; i++) {
        d = hash_insert(key(i), value(i), &ht);
        CU_ASSERT_PTR_EQUAL(value(i), d);
    }

    /* enumerate the hash table */
    count = 0;
    hash_enumerate(&ht, count_cb, &count);
    CU_ASSERT_EQUAL(N, count);

    /* check hash_numrecords */
    CU_ASSERT_EQUAL(N, hash_numrecords(&ht));

    /* free the hash table */
    freed_count = 0;
    free_hash_table(&ht, lincoln);
    CU_ASSERT_EQUAL(N, freed_count);
}

static void test_iter(void)
{
    hash_table ht;

    /* construct an empty hash table */
    construct_hash_table(&ht, 16, 0);

    hash_insert("foo", (void*)1, &ht);
    hash_insert("bar", (void*)2, &ht);
    hash_insert("baz", (void*)3, &ht);

    hash_iter *iter = hash_table_iter(&ht);

    CU_ASSERT_PTR_NOT_NULL(iter);

    /* run twice to test hash_iter_reset at end of first loop */
    int i;
    for (i = 0; i < 2; i++) {
        strarray_t seen = STRARRAY_INITIALIZER;

        CU_ASSERT(hash_iter_has_next(iter));
        while (hash_iter_next(iter)) {
            const char *key = hash_iter_key(iter);
            CU_ASSERT_PTR_NOT_NULL(key);
            strarray_append(&seen, key);

            uintptr_t val = (uintptr_t) hash_iter_val(iter);
            if (!strcmp(key, "foo")) {
                CU_ASSERT_EQUAL(1, val);
            }
            else if (!strcmp(key, "bar")) {
                CU_ASSERT_EQUAL(2, val);
            }
            else if (!strcmp(key, "baz")) {
                CU_ASSERT_EQUAL(3, val);
            }
            else CU_ASSERT(0);

            if (strarray_size(&seen) < 3) {
                CU_ASSERT(hash_iter_has_next(iter));
            }
        }
        CU_ASSERT(!hash_iter_has_next(iter));

        CU_ASSERT_EQUAL(3, strarray_size(&seen));
        CU_ASSERT(strarray_find(&seen, "foo", 0) >= 0);
        CU_ASSERT(strarray_find(&seen, "bar", 0) >= 0);
        CU_ASSERT(strarray_find(&seen, "baz", 0) >= 0);
        strarray_fini(&seen);
        hash_iter_reset(iter);
    }

    hash_iter_free(&iter);
    free_hash_table(&ht, NULL);
}

static void test_load_factor_warning(void)
{
    const char *const words[] = {
        "id", "faucibus", "nisl", "tincidunt", "eget", "nullam", "non", "nisi",
        "est", "sit", "amet", "facilisis", "magna", "etiam", "tempor", "orci",
        "eu", "lobortis", "elementum", "nibh", "tellus", "molestie"
    };
    const size_t n_words = sizeof words / sizeof words[0]; /* 22 */
    const size_t n_buckets = n_words / 4; /* 5 */
    unsigned i;
    unsigned int syslog_index;

    hash_table ht;

    /* make sure numeric assumptions hold */
    CU_ASSERT_EQUAL_FATAL(22, n_words);
    CU_ASSERT_EQUAL_FATAL(5, n_buckets);

    construct_hash_table(&ht, n_buckets, 0);
    CU_ASSERT_EQUAL(n_buckets, ht.size);

    syslog_index = CU_SYSLOG_MATCH("hash table load factor exceeds 3.0");
    /* 5 buckets will hit load factor 3.0 after 15 insertions, 4.0 after 20
     * insertions, and it won't reach 5.0, so we should warn exactly twice */
    for (i = 0; i < n_words; i++) {
        hash_insert(words[i], NULL, &ht);
    }
    CU_ASSERT_EQUAL(n_words, hash_numrecords(&ht));
    CU_ASSERT_SYSLOG(syslog_index, 2);

    free_hash_table(&ht, NULL);
}

/* vim: set ft=c: */
