#include "deucegate.h"

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

#include "dirwrap.h"

#ifdef _WIN32
#include <process.h>
#include <windows.h>
#define getpid _getpid
#else
#include <unistd.h>
#endif

static int failures;

#define CHECK(expr) do { if (!(expr)) { \
	fprintf(stderr, "%s:%d: check failed: %s\n", __FILE__, __LINE__, #expr); failures++; \
} } while (0)

static void
test_cp437(void)
{
	dg_decoder_t decoder;
	uint8_t utf8[32], cp[32];
	const uint8_t input[] = {0x41, 0xb3, 0xdb};
	dg_decoder_init(&decoder, DG_CP437);
	size_t n = dg_decode(&decoder, input, sizeof(input), utf8, sizeof(utf8), true);
	CHECK(n == 7);
	CHECK(dg_encode(DG_CP437, utf8, n, cp, sizeof(cp)) == sizeof(input));
	CHECK(memcmp(cp, input, sizeof(input)) == 0);
}

static void
test_encoding_matrix(void)
{
	static const uint8_t cp437[] = {'A', 0xb3, 0xdb, '\r', '\n'};
	dg_decoder_t client_decoder, door_decoder;
	uint8_t canonical[64], edge[64], returned[64], client_out[64];
	size_t canonical_len, edge_len, returned_len, client_len;
	for (int client_enc = DG_CP437; client_enc <= DG_UTF8; client_enc++) {
		for (int door_enc = DG_CP437; door_enc <= DG_UTF8; door_enc++) {
			dg_decoder_init(&client_decoder, DG_CP437);
			canonical_len = dg_decode(&client_decoder, cp437, sizeof(cp437), canonical, sizeof(canonical), true);
			/* Model the caller edge in its selected encoding, then the door edge. */
			client_len = dg_encode((dg_encoding_t)client_enc, canonical, canonical_len, client_out, sizeof(client_out));
			dg_decoder_init(&client_decoder, (dg_encoding_t)client_enc);
			canonical_len = dg_decode(&client_decoder, client_out, client_len, canonical, sizeof(canonical), true);
			edge_len = dg_encode((dg_encoding_t)door_enc, canonical, canonical_len, edge, sizeof(edge));
			dg_decoder_init(&door_decoder, (dg_encoding_t)door_enc);
			returned_len = dg_decode(&door_decoder, edge, edge_len, returned, sizeof(returned), true);
			client_len = dg_encode((dg_encoding_t)client_enc, returned, returned_len, client_out, sizeof(client_out));
			if (client_enc == DG_CP437) {
				CHECK(client_len == sizeof(cp437));
				CHECK(memcmp(client_out, cp437, sizeof(cp437)) == 0);
			}
			else {
				CHECK(client_len == returned_len);
				CHECK(memcmp(client_out, returned, returned_len) == 0);
			}
		}
	}
}

static void
test_utf8_stream(void)
{
	dg_decoder_t decoder;
	uint8_t out[32];
	const uint8_t first[] = {0xe2, 0x94};
	const uint8_t second[] = {0x82};
	dg_decoder_init(&decoder, DG_UTF8);
	CHECK(dg_decode(&decoder, first, sizeof(first), out, sizeof(out), false) == 0);
	CHECK(dg_decode(&decoder, second, sizeof(second), out, sizeof(out), false) == 3);
	CHECK(memcmp(out, "\xe2\x94\x82", 3) == 0);
}

static void
test_language_tags(void)
{
	char tag[DG_LANGUAGE_TAG_MAX + 1];

	CHECK(dg_language_tag_valid("en"));
	CHECK(dg_language_tag_valid("fr-CA"));
	CHECK(dg_language_tag_valid("sr-Latn-RS"));
	CHECK(dg_language_tag_valid("x-dgate"));
	CHECK(!dg_language_tag_valid("en_US"));
	CHECK(!dg_language_tag_valid("en--US"));
	CHECK(!dg_language_tag_valid("UTF-8"));
	CHECK(dg_language_from_locale("fr_CA.UTF-8", tag, sizeof(tag)) && strcmp(tag, "fr-CA") == 0);
	CHECK(dg_language_from_locale("sr_RS@latin", tag, sizeof(tag)) && strcmp(tag, "sr-RS") == 0);
	CHECK(!dg_language_from_locale("C.UTF-8", tag, sizeof(tag)));
	CHECK(!dg_language_from_locale("POSIX", tag, sizeof(tag)));
}

static void
test_mci(void)
{
	dg_config_t cfg = {0};
	dg_user_t user = {.access_level = 10};
	dg_mci_context_t ctx = {.config = &cfg, .user = &user, .node = 2, .seconds_left = 310};
	char out[256];
	strcpy(cfg.bbs_name, "Test BBS");
	strcpy(user.alias, "rumpelstiltskin");
	dg_mci_expand("{ALIAS10}|{10ALIAS}|{NODE}|{TIMELEFT}|{UNKNOWN}", out, sizeof(out), &ctx);
	CHECK(strcmp(out, "rumpelstil|lstiltskin|2|00:05:10|{UNKNOWN}") == 0);
}

static void
test_legacy_password(void)
{
	dg_config_t cfg = {0};
	dg_user_t user = {0};
	static const char expected[] =
	    "V9NISNAVEi2u9QewAgiQuFiim1HxNhHOy9H5Xxi3bucgzOw8DXyzp0cjb3D/1j77uuhjGe0fSOGWpBHakw2y9A==";
	strcpy(cfg.password_pepper, "pepper");
	strcpy(user.password_salt, "salt");
	strcpy(user.password_hash, expected);
	CHECK(dg_password_matches(&cfg, &user, (const uint8_t *)"secret", 6));
	CHECK(!dg_password_matches(&cfg, &user, (const uint8_t *)"wrong", 5));
	strcpy(cfg.password_pepper, "DISABLE");
	strcpy(user.password_hash, "plain");
	CHECK(dg_password_matches(&cfg, &user, (const uint8_t *)"plain", 5));
}

static void
test_resource_limit_defaults(const char *fixture)
{
	dg_config_t cfg;
	char err[256];
	CHECK(dg_config_load(fixture, &cfg, err, sizeof(err)));
	CHECK(cfg.ssh_max_connections == 3);
	CHECK(cfg.ssh_max_connections_per_ip == 3);
	CHECK(cfg.ssh_input_byte_limit == 16 * 1024 * 1024);
	CHECK(cfg.ssh_output_byte_limit == 64 * 1024 * 1024);
	CHECK(cfg.ssh_idle_timeout_seconds == 900);
	CHECK(cfg.auth_tarpit_base_milliseconds == 250);
	CHECK(cfg.auth_tarpit_max_milliseconds == 8000);
	CHECK(cfg.auth_tarpit_decay_seconds == 900);
}

static bool
nth_line(const uint8_t *data, size_t len, unsigned wanted, char *out, size_t outsz)
{
	size_t start = 0;
	unsigned current = 1;
	for (size_t pos = 0; pos <= len; pos++) {
		if (pos != len && data[pos] != '\r' && data[pos] != '\n') continue;
		if (current == wanted) {
			size_t count = pos - start;
			if (count >= outsz) count = outsz - 1;
			memcpy(out, data + start, count);
			out[count] = 0;
			return true;
		}
		if (pos < len && data[pos] == '\r' && pos + 1 < len && data[pos + 1] == '\n') pos++;
		start = pos + 1;
		current++;
	}
	return false;
}

static void
test_expand_user_macros(void)
{
	dg_config_t cfg = {0};
	dg_client_t client = {0};
	char command[DG_PATH_MAX * 4], params[DG_PATH_MAX * 2];
	str_list_t exports = strListInit();
	strcpy(cfg.root, "/srv/gamesrv");
	cfg.time_per_call = 60;
	client.config = &cfg;
	client.node = 1;
	client.started = time(NULL);
	strcpy(client.user.alias, "x; $(touch /tmp/pwned) ***PASSWORD");
	strcpy(client.user.password_hash, "hash");
	strcpy(command, "doors/***ALIAS/run");
	strcpy(params, "-user \"***ALIAS\" -node *NODE");
	CHECK(dg_expand_command(&client, 0, true, command, sizeof(command), params, sizeof(params), &exports));
	CHECK(strcmp(command, "doors/${DEUCEGATE_ALIAS}/run") == 0);
	CHECK(strcmp(params, "-user \"${DEUCEGATE_ALIAS}\" -node 1") == 0);
	CHECK(strListCount(exports) == 1);
	CHECK(exports[0] != NULL && strcmp(exports[0], "DEUCEGATE_ALIAS=x; $(touch /tmp/pwned) ***PASSWORD") == 0);
	strListFree(&exports);

	strcpy(command, "door");
	strcpy(params, "-user ***ALIAS");
	CHECK(dg_expand_command(&client, 0, false, command, sizeof(command), params, sizeof(params), NULL));
	CHECK(strcmp(params, "-user x; $(touch /tmp/pwned) ***PASSWORD") == 0);
}

static void
test_drop_files(void)
{
	dg_config_t cfg = {0};
	dg_client_t client = {0};
	char root[DG_PATH_MAX], node_dir[DG_PATH_MAX], path[DG_PATH_MAX], line[64];
	uint8_t *data = NULL;
	size_t len = 0;
	static const char *files[] = {
		"door.sys", "door32.sys", "doorfile.sr", "dorinfo.def", "dorinfo1.def", "dorinfo42.def", "chain.txt",
		"BBSDEV.DRP", NULL
	};
#ifdef _WIN32
	{
		char temp[DG_PATH_MAX];
		DWORD count = GetTempPathA(sizeof(temp), temp);
		CHECK(count > 0 && count < sizeof(temp));
		snprintf(root, sizeof(root), "%sdeucegate-chain-%ld", temp, (long)getpid());
	}
#else
	snprintf(root, sizeof(root), "/tmp/deucegate-chain-%ld", (long)getpid());
#endif
	CHECK(mkpath(root) == 0 || dg_dir_exists(root));
	snprintf(cfg.root, sizeof(cfg.root), "%s", root);
	strcpy(cfg.bbs_name, "Drop Test");
	strcpy(cfg.sysop_first_name, "Test");
	strcpy(cfg.sysop_last_name, "Sysop");
	strcpy(cfg.sysop_name, "Test Sysop");
	cfg.time_per_call = 60;
	client.config = &cfg;
	client.node = 42;
	client.cols = 132;
	client.rows = 50;
	client.terminal = DG_TERM_ANSI;
	client.started = time(NULL) - 60;
	client.user.id = 7;
	client.user.access_level = 20;
	strcpy(client.user.alias, "Chain User");
	CHECK(dg_create_drop_files(&client, "socket", (SOCKET)123, DG_UTF8, node_dir, sizeof(node_dir)));
	CHECK(dg_path_join(path, sizeof(path), node_dir, "chain.txt"));
	CHECK(dg_read_file(path, &data, &len));
	if (data != NULL) {
		CHECK(nth_line(data, len, 4, line, sizeof(line)) && strcmp(line, "") == 0);
		CHECK(nth_line(data, len, 9, line, sizeof(line)) && strcmp(line, "132") == 0);
		CHECK(nth_line(data, len, 10, line, sizeof(line)) && strcmp(line, "50") == 0);
		CHECK(nth_line(data, len, 30, line, sizeof(line)) && strcmp(line, "8N1") == 0);
		free(data);
	}
	data = NULL; len = 0;
	CHECK(dg_path_join(path, sizeof(path), node_dir, "BBSDEV.DRP"));
	CHECK(dg_read_file(path, &data, &len));
	if (data != NULL) {
		unsigned lines = 0;
		for (size_t i = 0; i < len; i++) {
			if (data[i] == '\r') CHECK(i + 1 < len && data[i + 1] == '\n');
			if (data[i] == '\n') {
				CHECK(i > 0 && data[i - 1] == '\r');
				lines++;
			}
		}
		CHECK(lines == 19);
		CHECK(len >= 2 && data[len - 2] == '\r' && data[len - 1] == '\n');
		CHECK(nth_line(data, len, 1, line, sizeof(line)) && strcmp(line, "1.0") == 0);
		CHECK(nth_line(data, len, 2, line, sizeof(line)) && strcmp(line, "socket") == 0);
		CHECK(nth_line(data, len, 3, line, sizeof(line)) && strcmp(line, "123") == 0);
		CHECK(nth_line(data, len, 4, line, sizeof(line)) && strcmp(line, "Chain User") == 0);
		CHECK(nth_line(data, len, 5, line, sizeof(line)) && strcmp(line, "7") == 0);
		CHECK(nth_line(data, len, 6, line, sizeof(line)) && strcmp(line, "132") == 0);
		CHECK(nth_line(data, len, 7, line, sizeof(line)) && strcmp(line, "50") == 0);
		CHECK(nth_line(data, len, 8, line, sizeof(line)) && strcmp(line, "Y") == 0);
		CHECK(nth_line(data, len, 9, line, sizeof(line)) && strcmp(line, "N") == 0);
		CHECK(nth_line(data, len, 10, line, sizeof(line)) && strcmp(line, "") == 0);
		CHECK(nth_line(data, len, 11, line, sizeof(line)) && strlen(line) == 20
		    && line[4] == '-' && line[7] == '-' && line[10] == 'T' && line[13] == ':'
		    && line[16] == ':' && line[19] == 'Z');
		CHECK(nth_line(data, len, 12, line, sizeof(line)) && strcmp(line, "UTF-8") == 0);
		CHECK(nth_line(data, len, 13, line, sizeof(line)) && strcmp(line, "en") == 0);
		CHECK(nth_line(data, len, 14, line, sizeof(line)) && strcmp(line, "DeuceGate 0.1.0") == 0);
		CHECK(nth_line(data, len, 15, line, sizeof(line)) && strcmp(line, "Drop Test") == 0);
		CHECK(nth_line(data, len, 16, line, sizeof(line)) && strcmp(line, "Test Sysop") == 0);
		CHECK(nth_line(data, len, 17, line, sizeof(line)) && strcmp(line, "20") == 0);
		CHECK(nth_line(data, len, 18, line, sizeof(line)) && strcmp(line, "42") == 0);
		CHECK(nth_line(data, len, 19, line, sizeof(line)) && strcmp(line, "N") == 0);
		free(data);
	}
	CHECK(dg_create_drop_files(&client, "fossil", 0, DG_CP437, node_dir, sizeof(node_dir)));
	data = NULL; len = 0;
	CHECK(dg_read_file(path, &data, &len));
	if (data != NULL) {
		CHECK(nth_line(data, len, 2, line, sizeof(line)) && strcmp(line, "fossil") == 0);
		CHECK(nth_line(data, len, 3, line, sizeof(line)) && strcmp(line, "0") == 0);
		free(data);
	}
	strcpy(client.language_tag, "fr-CA");
	CHECK(dg_create_drop_files(&client, "stdio", 0, DG_CP437, node_dir, sizeof(node_dir)));
	data = NULL; len = 0;
	CHECK(dg_read_file(path, &data, &len));
	if (data != NULL) {
		CHECK(nth_line(data, len, 2, line, sizeof(line)) && strcmp(line, "stdio") == 0);
		CHECK(nth_line(data, len, 3, line, sizeof(line)) && strcmp(line, "") == 0);
		CHECK(nth_line(data, len, 12, line, sizeof(line)) && strcmp(line, "IBM437") == 0);
		CHECK(nth_line(data, len, 13, line, sizeof(line)) && strcmp(line, "fr-CA") == 0);
		free(data);
	}
	for (size_t i = 0; files[i] != NULL; i++) {
		if (dg_path_join(path, sizeof(path), node_dir, files[i])) remove(path);
	}
	rmdir(node_dir);
	rmdir(root);
}

int
main(int argc, char **argv)
{
	if (argc != 2) {
		fprintf(stderr, "usage: %s fixture-directory\n", argv[0]);
		return 2;
	}
	test_cp437();
	test_encoding_matrix();
	test_utf8_stream();
	test_language_tags();
	test_mci();
	test_legacy_password();
	test_resource_limit_defaults(argv[1]);
	test_drop_files();
	test_expand_user_macros();
	if (failures != 0) return 1;
	puts("All DeuceGate tests passed.");
	return 0;
}
