/*
 * Amanda, The Advanced Maryland Automatic Network Disk Archiver
 * Copyright (c) 1991-1998 University of Maryland at College Park
 * Copyright (c) 2007-2012 Zmanda, Inc.  All Rights Reserved.
 * Copyright (c) 2013-2016 Carbonite, Inc.  All Rights Reserved.
 * All Rights Reserved.
 *
 * Permission to use, copy, modify, distribute, and sell this software and its
 * documentation for any purpose is hereby granted without fee, provided that
 * the above copyright notice appear in all copies and that both that
 * copyright notice and this permission notice appear in supporting
 * documentation, and that the name of U.M. not be used in advertising or
 * publicity pertaining to distribution of the software without specific,
 * written prior permission.  U.M. makes no representations about the
 * suitability of this software for any purpose.  It is provided "as is"
 * without express or implied warranty.
 *
 * U.M. DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL U.M.
 * BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * Authors: the Amanda Development Team.  Its members are listed in a
 * file named AUTHORS, in the root directory of this distribution.
 */

#include "amanda.h"
#include "testutils.h"
#include "glib-util.h"

/* from amflock.c */
extern amflock_impl_t *amflock_impls[];

#define TEST_FILENAME "./amflocktest.file"

/* Test all amflock implementations available for basic 
 * functionality
 */
static gboolean
test_old_impls(void)
{
    amflock_impl_t **imp = amflock_impls;
    char *resource = "rez";
    int fd;
    int lock_ro;

    /* set lnlock's lock directory to the current directory */
    extern char *_lnlock_dir;
    _lnlock_dir = ".";

    while (*imp) {
	tu_dbg("Testing amflock-%s\n", (*imp)->impl_name);

	for (lock_ro = 0; lock_ro < 2; lock_ro++) { /* false (0) or true (1) */
	    if (unlink(TEST_FILENAME) == -1 && errno != ENOENT) {
		perror("unlink");
		return FALSE;
	    }

	    if ((fd = open(TEST_FILENAME, O_RDWR | O_CREAT | O_EXCL, 0600)) == -1) {
		perror("open");
		return FALSE;
	    }

	    if (lock_ro) {
		if ((*imp)->amroflock_impl(fd, resource) != 0) {
		    perror("amroflock");
		    close(fd);
		    return FALSE;
		}
	    } else {
		if ((*imp)->amflock_impl(fd, resource) != 0) {
		    perror("amflock");
		    close(fd);
		    return FALSE;
		}
	    }

	    if ((*imp)->amfunlock_impl(fd, resource) != 0) {
		perror("amfunlock");
		close(fd);
		return FALSE;
	    }

	    close(fd); /* ignore error */
	    unlink(TEST_FILENAME); /* ignore error */
	}

	fprintf(stderr, "  PASS amflock-%s\n", (*imp)->impl_name);

	imp++;
    }

    return TRUE;
}

/*
 * Test lock and write_and_unlock
 */
static gboolean
inc_counter(file_lock *lock)
{
    char old_val = 'a';
    char new_val;

    if (lock->len) {
	old_val = lock->data[0];
    }

    g_assert(old_val < 'z');

    new_val = old_val + 1;
    if (file_lock_write(lock, &new_val, 1) == -1) {
	g_fprintf(stderr, "file_lock_write: %s\n",
			strerror(errno));
	return FALSE;
    }

    return TRUE;
}

#define pipeget(fd, cp) \
    (read((fd), (cp), 1) == 1)

#define pipeput(fd, s) \
    g_assert(write((fd), s, 1) == 1);

static void
locking_slave(int in_fd, int out_fd)
{
    char cmd;
    int rv;
    file_lock *lock = file_lock_new(TEST_FILENAME);
    gboolean locked = 0;

    while (1) {
	if (!pipeget(in_fd, &cmd))
	    cmd = 'q';

	switch (cmd) {
	    case 'q': /* q = quit */
		tu_dbg("slave: quitting\n");
		file_lock_free(lock);
		lock = NULL;
		return;

	    case 'l': /* l = try locking; reply with 'y' or 'n' */
		g_assert(!locked);
		rv = file_lock_lock(lock);
		if (rv == -1) {
		    g_fprintf(stderr, "file_lock_lock: %s\n",
				    strerror(errno));
		    return;
		}
		tu_dbg("slave: lock attempt => %s\n", (rv == 1)? "n" : "y");
		pipeput(out_fd, (rv == 1)? "n" : "y");
		if (rv != 1)
		    locked = 1;
		break;

	    case 'i': /* i = increment counter, reply with new value */
		g_assert(locked);
		if (!inc_counter(lock))
		    return;
		tu_dbg("slave: inc'd to %c\n", lock->data[0]);
		pipeput(out_fd, lock->data);
		break;

	    case 'u': /* u = try unlocking; reply with 'k' */
		g_assert(locked);
		rv = file_lock_unlock(lock);
		if (rv != 0) {
		    g_fprintf(stderr, "file_lock_unlock: %s\n",
			    strerror(errno));
		    return;
		}
		tu_dbg("slave: unlocked\n");
		pipeput(out_fd, "k");
		locked = 0;
		break;

	    default:
		return;
	}
    }
}

static int
locking_master(int in_fd, int out_fd)
{
    file_lock *lock = file_lock_new(TEST_FILENAME);
    int rv;
    char slaveres;

    /* start by locking here and incrementing the value */
    rv = file_lock_lock(lock);
    if (rv == -1) {
	g_fprintf(stderr, "file_lock_lock: %s\n", strerror(errno));
	return 0;
    }
    g_assert(rv != 1); /* not already locked */
    tu_dbg("master: locked\n");

    if (!inc_counter(lock))
	return 0;

    g_assert(lock->data[0] == 'b');
    tu_dbg("master: inc'd to b\n");

    /* unlock and re-lock */
    rv = file_lock_unlock(lock);
    if (rv != 0) {
	g_fprintf(stderr, "file_lock_unlock: %s\n", strerror(errno));
	return 0;
    }
    tu_dbg("master: unlocked\n");

    rv = file_lock_lock(lock);
    if (rv == -1) {
	g_fprintf(stderr, "file_lock_lock: %s\n", strerror(errno));
	return 0;
    }
    g_assert(rv != 1); /* not already locked */
    tu_dbg("master: locked\n");

    /* inc it again */
    g_assert(lock->data[0] == 'b');
    inc_counter(lock);
    g_assert(lock->data[0] == 'c');
    tu_dbg("master: inc'd to c\n");

    /* the slave should fail to get a lock now */
    pipeput(out_fd, "l");
    g_assert(pipeget(in_fd, &slaveres));
    g_assert(slaveres == 'n');

    /* and, finally unlock */
    rv = file_lock_unlock(lock);
    if (rv != 0) {
	g_fprintf(stderr, "file_lock_unlock: %s\n", strerror(errno));
	return 0;
    }
    tu_dbg("master: unlocked\n");

    /* the slave should succeed now */
    pipeput(out_fd, "l");
    g_assert(pipeget(in_fd, &slaveres));
    g_assert(slaveres == 'y');

    pipeput(out_fd, "i");
    g_assert(pipeget(in_fd, &slaveres));
    g_assert(slaveres == 'd');

    /* master shouldn't be able to lock now */
    rv = file_lock_lock(lock);
    if (rv == -1) {
	g_fprintf(stderr, "file_lock_lock: %s\n", strerror(errno));
	return 0;
    }
    g_assert(rv == 1); /* already locked */
    tu_dbg("master: lock attempt failed (as expected)\n");

    pipeput(out_fd, "i");
    g_assert(pipeget(in_fd, &slaveres));
    g_assert(slaveres == 'e');

    /* get the slave to unlock */
    pipeput(out_fd, "u");
    g_assert(pipeget(in_fd, &slaveres));
    g_assert(slaveres == 'k');

    /* we should get a lock now */
    rv = file_lock_lock(lock);
    if (rv == -1) {
	g_fprintf(stderr, "file_lock_lock: %s\n", strerror(errno));
	return 0;
    }
    g_assert(rv != 1); /* not already locked */
    tu_dbg("master: lock attempt succeeded\n");

    g_assert(lock->data[0] == 'e');

    /* leave it unlocked, just to see what happens */

    return 1;
}

static gpointer
test_intra_proc_locking_thd(gpointer *fdptr)
{
    int *fds = (int *)fdptr;
    locking_slave(fds[0], fds[1]);
    return NULL;
}

static gboolean
test_intra_proc_locking(void)
{
    GThread *thd;
    int outpipe[2], inpipe[2];
    int thd_fds[2];
    int rv;

    unlink(TEST_FILENAME);

    g_assert(pipe(outpipe) == 0);
    g_assert(pipe(inpipe) == 0);

    thd_fds[0] = outpipe[0];
    thd_fds[1] = inpipe[1];
    thd = g_thread_create((GThreadFunc)test_intra_proc_locking_thd, (gpointer)thd_fds, TRUE, NULL);

    rv = locking_master(inpipe[0], outpipe[1]);

    /* close the write end of the outgoing pipe, which should trigger an EOF on
     * the slave if it's still running */
    close(outpipe[1]);
    g_thread_join(thd);
    unlink(TEST_FILENAME);

    /* caller will kill the remaining files */

    return rv;
}

static gboolean
test_inter_proc_locking(void)
{
    int outpipe[2], inpipe[2];
    int pid;
    int rv;

    unlink(TEST_FILENAME);

    g_assert(pipe(outpipe) == 0);
    g_assert(pipe(inpipe) == 0);

    if ((pid = fork()) == 0) {
	close(outpipe[1]);
	close(inpipe[0]);
	locking_slave(outpipe[0], inpipe[1]);
	exit(0);
    }

    close(outpipe[0]);
    close(inpipe[1]);

    rv = locking_master(inpipe[0], outpipe[1]);

    /* close the write end of the outgoing pipe, which should trigger an EOF on
     * the slave if it's still running */
    close(outpipe[1]);
    waitpid(pid, NULL, 0);
    unlink(TEST_FILENAME);

    /* caller will kill the remaining files */

    return rv;
}

int
main(int argc, char **argv)
{
    static TestUtilsTest tests[] = {
	TU_TEST(test_old_impls, 90),
	TU_TEST(test_inter_proc_locking, 60),
	TU_TEST(test_intra_proc_locking, 60),
	TU_END()
    };

    glib_init();
    return testutils_run_tests(argc, argv, tests);
}
