#include "config.h"

#include <pthread.h>

#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>

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

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>

#define NSS_WRAPPER_HOSTNAME_ENV "NSS_WRAPPER_HOSTNAME"

static void test_nwrap_gethostname(void **state)
{
	const char *hostname = "milliways";
	char sys_host[256] = {0};
	char host[16] = {0};
	int rc;

	(void) state; /* unused */

	rc = setenv(NSS_WRAPPER_HOSTNAME_ENV, hostname, 1);
	assert_int_equal(rc, 0);

	rc = gethostname(host, sizeof(host));
	assert_int_equal(rc, 0);

	assert_string_equal(host, hostname);

	rc = setenv(NSS_WRAPPER_HOSTNAME_ENV, "this_hostname_is_too_long", 1);
	assert_int_equal(rc, 0);

	rc = gethostname(host, sizeof(host));
	assert_int_equal(rc, -1);
	assert_int_equal(errno, ENAMETOOLONG);

	unsetenv(NSS_WRAPPER_HOSTNAME_ENV);

	rc = gethostname(sys_host, sizeof(sys_host));
	assert_int_equal(rc, 0);

}

static void *thread_test_gethostbyname(void *u)
{
	struct hostent *he;

	(void) u; /* unused */

	he = gethostbyname("magrathea");
	assert_non_null(he);
	assert_non_null(he->h_name);
	assert_string_equal(he->h_name, "magrathea.galaxy.site");
	pthread_exit(NULL);
}

static void test_nwrap_gethostbyname_thread(void **state)
{
	struct hostent *he;
	pthread_t th;

	(void) state; /* unused */

	he = gethostbyname("maximegalon.galaxy.site");
	assert_non_null(he);
	assert_non_null(he->h_name);
	assert_string_equal(he->h_name, "maximegalon.galaxy.site");

	pthread_create(&th, NULL, &thread_test_gethostbyname, NULL);
	pthread_join(th, NULL);

	assert_non_null(he);
	assert_non_null(he->h_name);
#ifdef BSD
	/*
	 * On *BSD (and Mac OS X) systems,
	 * data is stored in thread local storage.
	 */
	assert_string_equal(he->h_name, "maximegalon.galaxy.site");
#else
	/*
	 * Glibc doesn't store data in thread local storage, so calling
	 * gethostbyname from a thread overwrites the parent thread's data.
	 */
	assert_string_equal(he->h_name, "magrathea.galaxy.site");
#endif
}

static void test_nwrap_gethostbyname(void **state)
{
	char ip[INET_ADDRSTRLEN];
	struct hostent *he;
	const char *a;

	(void) state; /* unused */

	he = gethostbyname("magrathea.galaxy.site");
	assert_non_null(he);
	assert_non_null(he->h_name);
	assert_non_null(he->h_addr_list);

	assert_string_equal(he->h_name, "magrathea.galaxy.site");
	assert_int_equal(he->h_addrtype, AF_INET);

	a = inet_ntop(AF_INET, he->h_addr_list[0], ip, sizeof(ip));
	assert_non_null(a);

	assert_string_equal(ip, "127.0.0.11");
}

static void test_nwrap_gethostbyname_multiple(void **state)
{
	struct hostent *he;
	char **list;

	/* For inet_ntop call */
	char buf[4096];
	const char *result;
	char *p = buf;

	/* List of ips in hosts file - order matters */
	const char *const result_ips[] = { "127.0.0.11", "127.0.0.12", NULL };
	const char *actual_ip = result_ips[0];
	unsigned int ac;

	(void) state; /* unused */

	he = gethostbyname("magrathea.galaxy.site");
	assert_non_null(he);
	assert_non_null(he->h_name);
	assert_non_null(he->h_addr_list);

	list = he->h_addr_list;
	for (ac = 0; *list != NULL; ++ac, ++list) {
		actual_ip = result_ips[ac];
		/* When test fails here more records are returned */
		assert_non_null(actual_ip);
		result = inet_ntop(AF_INET, *list, p, 4096);
		assert_non_null(p);
		assert_string_equal(actual_ip, result);
	}
}

#ifdef HAVE_GETHOSTBYNAME2
static void test_nwrap_gethostbyname2(void **state)
{
	char ip[INET6_ADDRSTRLEN];
	struct hostent *he;
	const char *a;

	(void) state; /* unused */

	he = gethostbyname2("magrathea.galaxy.site", AF_INET6);
	assert_non_null(he);

	he = gethostbyname2("magrathea.galaxy.site", AF_INET);
	assert_non_null(he);

	/* Check ipv6 he */
	he = gethostbyname2("krikkit.galaxy.site", AF_INET6);
	assert_non_null(he);
	assert_non_null(he->h_name);
	assert_non_null(he->h_addr_list);

	assert_string_equal(he->h_name, "krikkit.galaxy.site");
	assert_int_equal(he->h_addrtype, AF_INET6);

	a = inet_ntop(AF_INET6, he->h_addr_list[0], ip, sizeof(ip));
	assert_non_null(a);

	assert_string_equal(ip, "::14");

	/* Check ipv4 he */
	he = gethostbyname2("krikkit.galaxy.site", AF_INET);
	assert_non_null(he);
	assert_non_null(he->h_name);
	assert_non_null(he->h_addr_list);

	assert_string_equal(he->h_name, "krikkit.galaxy.site");
	assert_int_equal(he->h_addrtype, AF_INET);

	a = inet_ntop(AF_INET, he->h_addr_list[0], ip, sizeof(ip));
	assert_non_null(a);

	assert_string_equal(ip, "127.0.0.14");
}
#endif /* HAVE_GETHOSTBYNAME2 */

static void test_nwrap_gethostbyaddr(void **state)
{
	struct hostent *he;
	struct in_addr in;
	int rc;

	(void) state; /* unused */

	rc = inet_aton("127.0.0.11", &in);
	assert_int_equal(rc, 1);

	he = gethostbyaddr(&in, sizeof(struct in_addr), AF_INET);
	assert_non_null(he);
	assert_non_null(he->h_name);
	assert_non_null(he->h_addr_list);

	assert_string_equal(he->h_name, "magrathea.galaxy.site");
	assert_int_equal(he->h_addrtype, AF_INET);
	assert_memory_equal(&in, he->h_addr_list[0], he->h_length);
}

#ifdef HAVE_GETHOSTBYNAME_R
static void test_nwrap_gethostbyname_r(void **state)
{
	char buf[1024] = {0};
	char ip[INET_ADDRSTRLEN];
	struct hostent hb, *he;
	const char *a;
	int herr = 0;
	int rc;

	(void) state; /* unused */

	rc = gethostbyname_r("magrathea.galaxy.site",
			     &hb,
			     buf, sizeof(buf),
			     &he,
			     &herr);
	assert_int_equal(rc, 0);
	assert_non_null(he);
	assert_non_null(he->h_name);
	assert_non_null(he->h_addr_list);

	assert_string_equal(he->h_name, "magrathea.galaxy.site");
	assert_int_equal(he->h_addrtype, AF_INET);

	a = inet_ntop(AF_INET, he->h_addr_list[0], ip, sizeof(ip));
	assert_non_null(a);

	assert_string_equal(ip, "127.0.0.11");
}

static void test_nwrap_gethostbyname_r_null(void **state)
{
	char buf[2];
	struct hostent hb, *he;
	int herr = 0;
	int rc;

	(void) state; /* unused */

	buf[0] = 'A';
	buf[1] = '\0';

	/* Check that the returned buffer is zeroed */
	rc = gethostbyname_r("wurst",
			     &hb,
			     buf, sizeof(buf),
			     &he,
			     &herr);
	assert_int_equal(rc, ENOENT);
	assert_null(he);
	assert_null(hb.h_name);
	assert_null(hb.h_addr_list);
	assert_string_equal(buf, "");
}
#endif

#ifdef HAVE_GETHOSTBYNAME2_R
static void test_nwrap_gethostbyname2_r_v4(void **state)
{
	char *buf = NULL;
	size_t buflen = 2;
	char ip[INET_ADDRSTRLEN];
	struct hostent hb, *he;
	const char *a;
	int herr = 0;
	int rc;

	(void) state; /* unused */

	for (rc = ERANGE, buflen = 2; rc == ERANGE; buflen *= 2) {
		if (buf != NULL) {
			free(buf);
		}
		buf = calloc(1, buflen);
		assert_non_null(buf);

		rc = gethostbyname2_r("magrathea.galaxy.site", AF_INET,
				      &hb,
				      buf, buflen,
				      &he,
				      &herr);
	}

	assert_int_equal(rc, 0);
	assert_int_equal(herr, 0);
	assert_non_null(he);
	assert_non_null(he->h_name);
	assert_non_null(he->h_addr_list);

	assert_string_equal(he->h_name, "magrathea.galaxy.site");
	assert_int_equal(he->h_addrtype, AF_INET);

	assert_non_null(he->h_addr_list[0]);
	a = inet_ntop(AF_INET, he->h_addr_list[0], ip, sizeof(ip));
	assert_non_null(a);
	assert_string_equal(a, "127.0.0.11");

	assert_non_null(he->h_addr_list[1]);
	a = inet_ntop(AF_INET, he->h_addr_list[1], ip, sizeof(ip));
	assert_non_null(a);
	assert_string_equal(a, "127.0.0.12");

	assert_null(he->h_addr_list[2]);

	free(buf);
}

static void test_nwrap_gethostbyname2_r_v6(void **state)
{
	char *buf = NULL;
	size_t buflen = 2;
	char ip[INET6_ADDRSTRLEN];
	struct hostent hb, *he;
	const char *a;
	int herr = 0;
	int rc;

	(void) state; /* unused */

	for (rc = ERANGE, buflen = 2; rc == ERANGE; buflen *= 2) {
		if (buf != NULL) {
			free(buf);
		}
		buf = calloc(1, buflen);
		assert_non_null(buf);

		rc = gethostbyname2_r("magrathea.galaxy.site", AF_INET6,
				      &hb,
				      buf, buflen,
				      &he,
				      &herr);
	}

	assert_int_equal(rc, 0);
	assert_int_equal(herr, 0);
	assert_non_null(he);
	assert_non_null(he->h_name);
	assert_non_null(he->h_addr_list);

	assert_string_equal(he->h_name, "magrathea.galaxy.site");
	assert_int_equal(he->h_addrtype, AF_INET6);

	assert_non_null(he->h_addr_list[0]);
	a = inet_ntop(AF_INET6, he->h_addr_list[0], ip, sizeof(ip));
	assert_non_null(a);
	assert_string_equal(a, "::29a");

	assert_null(he->h_addr_list[1]);

	free(buf);
}
#endif

#ifdef HAVE_GETHOSTBYADDR_R
static void test_nwrap_gethostbyaddr_r(void **state)
{
	char buf[1024] = {0};
	struct hostent hb, *he;
	struct in_addr in;
	int herr = 0;
	int rc;

	(void) state; /* unused */

	rc = inet_aton("127.0.0.11", &in);
	assert_int_equal(rc, 1);

	rc = gethostbyaddr_r(&in, sizeof(struct in_addr),
			     AF_INET,
			     &hb,
			     buf, sizeof(buf),
			     &he,
			     &herr);
	assert_int_equal(rc, 0);
	assert_non_null(he);
	assert_non_null(he->h_name);
	assert_non_null(he->h_addr_list);

	assert_string_equal(he->h_name, "magrathea.galaxy.site");
	assert_int_equal(he->h_addrtype, AF_INET);
	assert_memory_equal(&in, he->h_addr_list[0], he->h_length);
}
#endif

int main(void) {
	int rc;

	const struct CMUnitTest tests[] = {
		cmocka_unit_test(test_nwrap_gethostname),
		cmocka_unit_test(test_nwrap_gethostbyname),
		cmocka_unit_test(test_nwrap_gethostbyname_thread),
#ifdef HAVE_GETHOSTBYNAME2
		cmocka_unit_test(test_nwrap_gethostbyname2),
#endif
		cmocka_unit_test(test_nwrap_gethostbyaddr),
#ifdef HAVE_GETHOSTBYNAME_R
		cmocka_unit_test(test_nwrap_gethostbyname_r),
		cmocka_unit_test(test_nwrap_gethostbyname_r_null),
#endif
#ifdef HAVE_GETHOSTBYNAME2_R
		cmocka_unit_test(test_nwrap_gethostbyname2_r_v4),
		cmocka_unit_test(test_nwrap_gethostbyname2_r_v6),
#endif
#ifdef HAVE_GETHOSTBYADDR_R
		cmocka_unit_test(test_nwrap_gethostbyaddr_r),
#endif
		cmocka_unit_test(test_nwrap_gethostbyname_multiple),
	};

	rc = cmocka_run_group_tests(tests, NULL, NULL);

	return rc;
}

