#if HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>
#include <stdlib.h>
#include <unistd.h>

#include "cunit/cyrunit.h"
#include "lib/hash.h"
#include "lib/libconfig.h"
#include "lib/proc.h"
#include "lib/util.h"

static char *myconfigdir = NULL;

/* copied declarations: these must match same in lib/proc.c */
struct proc_handle {
    pid_t pid;
    char *fname;
};
#define FNAME_PROCDIR "/proc"
/* end copied declarations */

static int set_up(void)
{
    char myconfigdir_template[] = "/tmp/cyrus-cunit-proctestc-XXXXXX";
    char *dir;
    struct buf myconfig = BUF_INITIALIZER;

    dir = mkdtemp(myconfigdir_template);
    if (!dir) return errno;

    myconfigdir = xstrdup(dir);
    buf_printf(&myconfig, "configdirectory: %s\n", myconfigdir);

    config_read_string(buf_cstring(&myconfig));

    buf_free(&myconfig);
    return 0;
}

static int tear_down(void)
{
    int r = 0;

    config_reset();

    if (myconfigdir && myconfigdir[0]) {
        struct buf rm_cmd = BUF_INITIALIZER;

        buf_printf(&rm_cmd, "rm -rf %s", myconfigdir);

        r = system(buf_cstring(&rm_cmd));
        if (r) r = -1;

        buf_free(&rm_cmd);
    }

    xzfree(myconfigdir);
    return r;
}

static const char *predict_handle_fname(pid_t pid)
{
    static char buf[1024];

    memset(buf, 0, sizeof buf);
    snprintf(buf, sizeof buf, "%s%s/%u", myconfigdir, FNAME_PROCDIR, pid);

    return buf;
}

static void test_register_self(void)
{
    struct proc_handle *handle = NULL;
    struct proc_handle *savedptr;
    int r;

    /* first call must create a valid handle */
    r = proc_register(&handle, 0,
                      "servicename",
                      "clienthost",
                      "userid",
                      "mailbox",
                      "cmd");
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL(handle);
    CU_ASSERT_NOT_EQUAL(handle->pid, 0);
    CU_ASSERT_EQUAL(handle->pid, getpid());
    CU_ASSERT_PTR_NOT_NULL(handle->fname);
    CU_ASSERT_STRING_EQUAL(handle->fname, predict_handle_fname(getpid()));
    savedptr = handle;

    /* must be okay to re-register (and keep same handle) */
    r = proc_register(&handle, 0,
                      "new_servicename",
                      "new_clienthost",
                      "new_userid",
                      "new_mailbox",
                      "new_cmd");
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL(handle);
    CU_ASSERT_PTR_EQUAL(handle, savedptr);
    CU_ASSERT_NOT_EQUAL(handle->pid, 0);
    CU_ASSERT_EQUAL(handle->pid, getpid());
    CU_ASSERT_PTR_NOT_NULL(handle->fname);
    CU_ASSERT_STRING_EQUAL(handle->fname, predict_handle_fname(getpid()));

    proc_cleanup(&handle);
}

static void test_register_other(void)
{
    struct proc_handle *handle = NULL;
    struct proc_handle *savedptr;
    pid_t pid;
    int r;

    /* choose some random pid > 0 */
    do {
        pid = 1 + rand();
    } while (pid <= 0 || pid == getpid()); /* whoops, got ours! try again */

    /* first call must create a handle */
    r = proc_register(&handle, pid,
                      "servicename",
                      "clienthost",
                      "userid",
                      "mailbox",
                      "cmd");
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL(handle);
    CU_ASSERT_NOT_EQUAL(handle->pid, 0);
    CU_ASSERT_NOT_EQUAL(handle->pid, getpid());
    CU_ASSERT_EQUAL(handle->pid, pid);
    CU_ASSERT_PTR_NOT_NULL(handle->fname);
    CU_ASSERT_STRING_EQUAL(handle->fname, predict_handle_fname(pid));
    savedptr = handle;

    /* must be okay to re-register (pid argument must be ignored) */
    r = proc_register(&handle, 0,
                      "new_servicename",
                      "new_clienthost",
                      "new_userid",
                      "new_mailbox",
                      "new_cmd");
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL(handle);
    CU_ASSERT_PTR_EQUAL(handle, savedptr);
    CU_ASSERT_NOT_EQUAL(handle->pid, 0);
    CU_ASSERT_NOT_EQUAL(handle->pid, getpid());
    CU_ASSERT_EQUAL(handle->pid, pid);
    CU_ASSERT_PTR_NOT_NULL(handle->fname);
    CU_ASSERT_STRING_EQUAL(handle->fname, predict_handle_fname(pid));

    proc_cleanup(&handle);
}

static void test_cleanup(void)
{
    struct proc_handle *handle = NULL;
    int r;

    /* gotta register something to clean it up... */
    r = proc_register(&handle, 1,
                      "servicename",
                      "clienthost",
                      "userid",
                      "mailbox",
                      "cmd");
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL(handle);
    CU_ASSERT_EQUAL(handle->pid, 1);
    CU_ASSERT_PTR_NOT_NULL(handle->fname);
    CU_ASSERT_STRING_EQUAL(handle->fname, predict_handle_fname(1));

    /* cleanup had better discard that handle */
    proc_cleanup(&handle);
    CU_ASSERT_PTR_NULL(handle);

    /* re-register after cleanup must create a new handle */
    r = proc_register(&handle, 2,
                      "servicename",
                      "clienthost",
                      "userid",
                      "mailbox",
                      "cmd");
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_PTR_NOT_NULL(handle);
    CU_ASSERT_EQUAL(handle->pid, 2);
    CU_ASSERT_PTR_NOT_NULL(handle->fname);
    CU_ASSERT_STRING_EQUAL(handle->fname, predict_handle_fname(2));

    /* cleanup had better discard that one too */
    proc_cleanup(&handle);
    CU_ASSERT_PTR_NULL(handle);
}

struct procdata_fields {
    char *servicename;
    char *clienthost;
    char *userid;
    char *mailbox;
    char *cmd;
};

static void free_procdata_fields(void *p)
{
    struct procdata_fields *f = (struct procdata_fields *) p;

    free(f->servicename);
    free(f->clienthost);
    free(f->userid);
    free(f->mailbox);
    free(f->cmd);
    free(f);
}

static int collect_procs_cb(pid_t pid,
                            const char *servicename,
                            const char *clienthost,
                            const char *userid,
                            const char *mailbox,
                            const char *cmd,
                            void *rock)
{
    char pid_str[32] = {0};
    hash_table *results = (hash_table *) rock;

    snprintf(pid_str, sizeof pid_str, "%u", pid);

    struct procdata_fields *fields = xmalloc(sizeof *fields);
    fields->servicename = xstrdupnull(servicename);
    fields->clienthost = xstrdupnull(clienthost);
    fields->userid = xstrdupnull(userid);
    fields->mailbox = xstrdupnull(mailbox);
    fields->cmd = xstrdupnull(cmd);

    /* better not have seen this pid already! */
    CU_ASSERT_PTR_NULL(hash_lookup(pid_str, results));

    hash_insert(pid_str, fields, results);
    return 0;
}

static void test_proc_foreach(void)
{
    struct {
        struct proc_handle *handle;
        struct procdata_fields fields;
    } tests[] = {
        { NULL, { "sn0", "ch0", "ui0", "mb0", "cm0" } },
        { NULL, { "sn1", "ch1", "ui1", "mb1", "cm1" } },
        { NULL, { "sn2", "ch2", "ui2", "mb2", "cm2" } },
        { NULL, { "sn3", "ch3", "ui3", "mb3", "cm3" } },
        { NULL, { "sn4", "ch4", "ui4", "mb4", "cm4" } },
        { NULL, { "sn5", "ch5", "ui5", "mb5", "cm5" } },
    };
    const size_t n_tests = sizeof(tests) / sizeof(tests[0]);
    const pid_t mypid = getpid();
    hash_table results = HASH_TABLE_INITIALIZER;
    strarray_t *keys = NULL;
    int i, r;

    /* register our "processes" */
    for (i = 0; (unsigned) i < n_tests; i++) {
        r = proc_register(&tests[i].handle,
                          i, /* use test index as pid */
                          tests[i].fields.servicename,
                          tests[i].fields.clienthost,
                          tests[i].fields.userid,
                          tests[i].fields.mailbox,
                          tests[i].fields.cmd);
        CU_ASSERT_EQUAL(r, 0);
        CU_ASSERT_PTR_NOT_NULL(&tests[i].handle);
        if (i == 0) {
            CU_ASSERT_EQUAL(tests[i].handle->pid, mypid);
        }
        else {
            CU_ASSERT_EQUAL(tests[i].handle->pid, i);
        }
    }

    /* let's see if it finds everything */
    construct_hash_table(&results, n_tests, 0);
    r = proc_foreach(&collect_procs_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    for (i = 0; (unsigned) i < n_tests; i++) {
        char pid_str[32] = {0};
        struct procdata_fields *fields;

        snprintf(pid_str, sizeof pid_str, "%d", i ? i : mypid);

        fields = hash_lookup(pid_str, &results);
        CU_ASSERT_PTR_NOT_NULL(fields);
        CU_ASSERT_STRING_EQUAL(fields->servicename, tests[i].fields.servicename);
        CU_ASSERT_STRING_EQUAL(fields->clienthost, tests[i].fields.clienthost);
        CU_ASSERT_STRING_EQUAL(fields->userid, tests[i].fields.userid);
        CU_ASSERT_STRING_EQUAL(fields->mailbox, tests[i].fields.mailbox);
        CU_ASSERT_STRING_EQUAL(fields->cmd, tests[i].fields.cmd);
    }

    /* better not have found anything extra! */
    keys = hash_keys(&results);
    for (i = 0; i < strarray_size(keys); i++) {
        int found_pid = atoi(strarray_nth(keys, i));

        /* real process pid will be out of range but is legit */
        if (found_pid == mypid) continue;

        /* better not see a pid 0! */
        CU_ASSERT(found_pid > 0);

        /* better not see anything higher than those we created */
        CU_ASSERT((unsigned) found_pid < n_tests);
    }
    strarray_free(keys);

    /* reset results */
    free_hash_table(&results, &free_procdata_fields);

    /* reregistering with different strings should work */
    for (i = 0; (unsigned) i < n_tests; i++) {
        r = proc_register(&tests[i].handle,
                          i, /* use test index as pid */
                          "new servicename",
                          "new clienthost",
                          "new userid",
                          "new mailbox",
                          "new cmd");
        CU_ASSERT_EQUAL(r, 0);
        CU_ASSERT_PTR_NOT_NULL(&tests[i].handle);
        if (i == 0) {
            CU_ASSERT_EQUAL(tests[i].handle->pid, mypid);
        }
        else {
            CU_ASSERT_EQUAL(tests[i].handle->pid, i);
        }
    }

    /* let's see if it finds everything */
    construct_hash_table(&results, n_tests, 0);
    r = proc_foreach(&collect_procs_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    for (i = 0; (unsigned) i < n_tests; i++) {
        char pid_str[32] = {0};
        struct procdata_fields *fields;

        snprintf(pid_str, sizeof pid_str, "%u", i ? i : mypid);

        fields = hash_lookup(pid_str, &results);
        CU_ASSERT_PTR_NOT_NULL(fields);
        CU_ASSERT_STRING_EQUAL(fields->servicename, "new servicename");
        CU_ASSERT_STRING_EQUAL(fields->clienthost, "new clienthost");
        CU_ASSERT_STRING_EQUAL(fields->userid, "new userid");
        CU_ASSERT_STRING_EQUAL(fields->mailbox, "new mailbox");
        CU_ASSERT_STRING_EQUAL(fields->cmd, "new cmd");
    }

    /* better not have found anything extra! */
    keys = hash_keys(&results);
    for (i = 0; i < strarray_size(keys); i++) {
        int found_pid = atoi(strarray_nth(keys, i));

        /* real process pid will be out of range but is legit */
        if (found_pid == mypid) continue;

        /* better not see a pid 0! */
        CU_ASSERT(found_pid > 0);

        /* better not see anything higher than those we created */
        CU_ASSERT((unsigned) found_pid < n_tests);
    }
    strarray_free(keys);

    /* reset results */
    free_hash_table(&results, &free_procdata_fields);

    /* cleanup our "processes" */
    for (i = 0; (unsigned ) i < n_tests; i++) {
        proc_cleanup(&tests[i].handle);
        CU_ASSERT_PTR_NULL(tests[i].handle);
    }

    /* shouldn't find anything this time */
    construct_hash_table(&results, n_tests, 0);
    r = proc_foreach(&collect_procs_cb, &results);
    CU_ASSERT_EQUAL(r, 0);
    CU_ASSERT_EQUAL(hash_numrecords(&results), 0);

    /* and we're finished */
    free_hash_table(&results, &free_procdata_fields);
}

/* vim: set ft=c: */
