/*
 * test-runner.c
 * test harness
 *
 * SPDX-License-Identifier: pkgconf
 *
 * Copyright (c) 2025 pkgconf authors (see AUTHORS).
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * This software is provided 'as is' and without any warranty, express or
 * implied.  In no event shall the authors be liable for any damages arising
 * from the use of this software.
 */

#include <libpkgconf/config.h>
#include <libpkgconf/libpkgconf.h>
#include <libpkgconf/stdinc.h>
#include <tests/win-shim.h>
#include <sys/types.h>
#ifndef _WIN32
#	include <sys/wait.h>
#endif
#include <cli/core.h>
#include <cli/getopt_long.h>
#include <limits.h>
#include <assert.h>

#ifndef PKGCONF_LITE
#	if !defined(_WIN32) && !defined(__HAIKU__)
#		define PKGCONF_TEST_PLATFORM "unix"
#	elif !defined(_WIN32)
#		define PKGCONF_TEST_PLATFORM "haiku"
#	else
#		define PKGCONF_TEST_PLATFORM "windows"
#	endif
#else // PKGCONF_LITE
#	define PKGCONF_TEST_PLATFORM "lite"
#endif // PKGCONF_LITE

static void test_parser_warn(void *p, const char *fmt, ...) PRINTFLIKE(2, 3);
static void handle_substs(pkgconf_buffer_t *dest, const pkgconf_buffer_t *src, const char *pwd);

static pkgconf_buffer_t test_fixtures_dir = PKGCONF_BUFFER_INITIALIZER;
static pkgconf_buffer_t test_tool_dir = PKGCONF_BUFFER_INITIALIZER;
static bool debug = false;

typedef enum test_match_strategy_
{
	MATCH_EXACT = 0,
	MATCH_PARTIAL,
	MATCH_EMPTY,
} pkgconf_test_match_strategy_t;

typedef struct test_case_
{
	char *name;
	char *testfile_dir;

	pkgconf_list_t search_path;
	pkgconf_buffer_t query;

	pkgconf_list_t expected_stdout;
	pkgconf_test_match_strategy_t match_stdout;

	pkgconf_list_t expected_stderr;
	pkgconf_test_match_strategy_t match_stderr;

	pkgconf_buffer_t expected_stdout_file;

	int exitcode;
	uint64_t wanted_flags;

	pkgconf_list_t env_vars;

	pkgconf_buffer_t want_env_prefix;
	pkgconf_buffer_t want_variable;
	pkgconf_buffer_t fragment_filter;

	pkgconf_buffer_t skip_platforms;
	bool require_utf8_locale;

	pkgconf_list_t define_variables;

	int verbosity;

	pkgconf_buffer_t atleast_version;
	pkgconf_buffer_t exact_version;
	pkgconf_buffer_t max_version;

	pkgconf_buffer_t tool;
	pkgconf_buffer_t tool_args; // TODO: tool-specific flags

	pkgconf_list_t mkdirs;
#ifndef _WIN32
	pkgconf_list_t symlinks;
#endif
	pkgconf_list_t copies;

#ifndef PKGCONF_LITE
	pkgconf_buffer_t want_personality;
#endif
} pkgconf_test_case_t;

typedef struct test_state_
{
	pkgconf_cli_state_t cli_state;
	const pkgconf_test_case_t *testcase;
} pkgconf_test_state_t;

typedef struct test_environ_
{
	pkgconf_node_t node;
	char *key;
	char *value;
} pkgconf_test_environ_t;

typedef struct test_output_
{
	pkgconf_output_t output;

	pkgconf_buffer_t o_stdout;
	pkgconf_buffer_t o_stderr;
} pkgconf_test_output_t;

typedef struct test_flag_pair_
{
	const char *name;
	uint64_t flag;
} pkgconf_test_flag_pair_t;

typedef void (*test_keyword_func_t)(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value);

typedef struct test_keyword_pair_
{
	const char *keyword;
	const test_keyword_func_t func;
	const ptrdiff_t offset;
} pkgconf_test_keyword_pair_t;

static void
test_environment_push(pkgconf_test_case_t *testcase, const char *key, const char *value)
{
	pkgconf_test_environ_t *env = calloc(1, sizeof(*env));
	if (env == NULL)
		return;

	env->key = strdup(key);
	env->value = strdup(value);
	pkgconf_node_insert_tail(&env->node, env, &testcase->env_vars);
}

static void
test_environment_free(pkgconf_list_t *env_list)
{
	pkgconf_node_t *iter, *iter_next;

	PKGCONF_FOREACH_LIST_ENTRY_SAFE(env_list->head, iter_next, iter)
	{
		pkgconf_test_environ_t *env = iter->data;

		pkgconf_node_delete(&env->node, env_list);

		free(env->key);
		free(env->value);
		free(env);
	}
}

static const char *
environ_lookup_handler(const pkgconf_client_t *client, const char *key)
{
	pkgconf_test_state_t *state = client->client_data;
	pkgconf_node_t *iter;

	PKGCONF_FOREACH_LIST_ENTRY(state->testcase->env_vars.head, iter)
	{
		pkgconf_test_environ_t *env = iter->data;

		if (!strcmp(key, env->key))
		{
			char cwd[PATH_MAX] = {0};
			const char *pwd = getcwd(cwd, sizeof(cwd));

			pkgconf_buffer_t expanded = PKGCONF_BUFFER_INITIALIZER;
			handle_substs(&expanded, PKGCONF_BUFFER_FROM_STR(env->value), pwd);

			free(env->value);
			env->value = strdup(pkgconf_buffer_str_or_empty(&expanded));
			pkgconf_buffer_finalize(&expanded);

			return env->value;
		}
	}

	return NULL;
}

#ifndef PKGCONF_LITE
static bool
debug_handler(const char *msg, const pkgconf_client_t *client, void *data)
{
	(void) client;
	(void) data;
	fprintf(stderr, "%s", msg);
	return true;
}
#endif // PKGCONF_LITE

static bool
error_handler(const char *msg, const pkgconf_client_t *client, void *data)
{
	(void) data;
	pkgconf_test_state_t *state = client->client_data;
	pkgconf_output_fmt(client->output, state->testcase->wanted_flags & PKG_ERRORS_ON_STDOUT ? PKGCONF_OUTPUT_STDOUT : PKGCONF_OUTPUT_STDERR, "%s", msg);
	return true;
}

static bool
write_handler(pkgconf_output_t *output, pkgconf_output_stream_t stream, const pkgconf_buffer_t *buffer)
{
	pkgconf_test_output_t *out = (pkgconf_test_output_t *) output;
	pkgconf_buffer_t *dest = stream == PKGCONF_OUTPUT_STDERR ? &out->o_stderr : &out->o_stdout;

	pkgconf_buffer_append(dest, pkgconf_buffer_str(buffer));
	return true;
}

static pkgconf_output_t *
test_output(void)
{
	static pkgconf_test_output_t output =
	{
		.output.write = write_handler,
	};

	return &output.output;
}

static void
test_output_reset(pkgconf_test_output_t *out)
{
	pkgconf_buffer_reset(&out->o_stdout);
	pkgconf_buffer_reset(&out->o_stderr);
}

/*
 * handle_substs: expand %TEST_FIXTURES_DIR%, %DIR_SEP%, and %PWD%
 * in src into dest. pwd may be NULL, in which case %PWD% is left as-is
 * (it should only appear in fields that are re-expanded after tmp_dir creation).
 */
static void
handle_substs(pkgconf_buffer_t *dest, const pkgconf_buffer_t *src, const char *pwd)
{
	struct subst_pair
	{
		const char *key;
		const char *value;
	} subst_pairs[] =
	{
		{"%TEST_FIXTURES_DIR%",	pkgconf_buffer_str(&test_fixtures_dir)},
		{"%DIR_SEP%",		PKG_CONFIG_PATH_SEP_S},
		{"%PWD%",		pwd != NULL ? pwd : "%PWD%"},
	};

	pkgconf_buffer_t workbuf_src = PKGCONF_BUFFER_INITIALIZER;
	pkgconf_buffer_t workbuf_dest = PKGCONF_BUFFER_INITIALIZER;

	if (!pkgconf_buffer_len(src))
		return;

	pkgconf_buffer_append(&workbuf_dest, pkgconf_buffer_str(src));

	for (size_t i = 0; i < PKGCONF_ARRAY_SIZE(subst_pairs); i++)
	{
		pkgconf_buffer_reset(&workbuf_src);
		pkgconf_buffer_append(&workbuf_src, pkgconf_buffer_str(&workbuf_dest));

		pkgconf_buffer_reset(&workbuf_dest);
		pkgconf_buffer_subst(&workbuf_dest, &workbuf_src, subst_pairs[i].key, subst_pairs[i].value);
	}

	pkgconf_buffer_append(dest, pkgconf_buffer_str(&workbuf_dest));

	pkgconf_buffer_finalize(&workbuf_src);
	pkgconf_buffer_finalize(&workbuf_dest);
}

static int
test_keyword_pair_cmp(const void *key, const void *ptr)
{
	const pkgconf_test_keyword_pair_t *pair = ptr;
	return strcasecmp(key, pair->keyword);
}

static void
test_keyword_set_int(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value)
{
	(void) keyword;
	(void) warnprefix;

	int *dest = (int *)((char *) testcase + offset);
	*dest = atoi(value);
}

static void
test_keyword_set_bool(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value)
{
	(void) keyword;
	(void) warnprefix;

	bool *dest = (bool *)((char *) testcase + offset);
	*dest = !strcasecmp(value, "true");
}

static void
test_keyword_set_buffer(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value)
{
	(void) keyword;
	(void) warnprefix;

	pkgconf_buffer_t *dest = (pkgconf_buffer_t *)((char *) testcase + offset);
	handle_substs(dest, PKGCONF_BUFFER_FROM_STR((char *) value), NULL);
}

static void
test_keyword_extend_bufferset(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value)
{
	(void) keyword;
	(void) warnprefix;

	pkgconf_list_t *dest = (pkgconf_list_t *)((char *) testcase + offset);
	pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;

	handle_substs(&buf, PKGCONF_BUFFER_FROM_STR((char *) value), NULL);
	pkgconf_bufferset_extend(dest, &buf);
	pkgconf_buffer_finalize(&buf);
}

static int
test_flag_pair_cmp(const void *key, const void *ptr)
{
	const pkgconf_test_flag_pair_t *pair = ptr;
	return strcasecmp(key, pair->name);
}

static const pkgconf_test_flag_pair_t test_flag_pairs[] =
{
	{"cflags",			PKG_CFLAGS},
	{"cflags-only-i",		PKG_CFLAGS_ONLY_I},
	{"cflags-only-other",		PKG_CFLAGS_ONLY_OTHER},
	{"debug",			PKG_DEBUG},
	{"define-prefix",		PKG_DEFINE_PREFIX},
	{"digraph",			PKG_DIGRAPH},
	{"dont-define-prefix",		PKG_DONT_DEFINE_PREFIX},
	{"dont-relocate-paths",		PKG_DONT_RELOCATE_PATHS},
	{"dump-license",		PKG_DUMP_LICENSE},
	{"dump-license-file",		PKG_DUMP_LICENSE_FILE},
	{"dump-personality",		PKG_DUMP_PERSONALITY},
	{"dump-source",			PKG_DUMP_SOURCE},
	{"env-only",			PKG_ENV_ONLY},
	{"errors-on-stdout",		PKG_ERRORS_ON_STDOUT},
	{"exists",			PKG_EXISTS},
	{"exists-cflags",		PKG_EXISTS_CFLAGS},
	{"fragment-tree",		PKG_FRAGMENT_TREE},
	{"ignore-conflicts",		PKG_IGNORE_CONFLICTS},
	{"internal-cflags",		PKG_INTERNAL_CFLAGS},
	{"keep-system-cflags",		PKG_KEEP_SYSTEM_CFLAGS},
	{"keep-system-libs",		PKG_KEEP_SYSTEM_LIBS},
	{"libs",			PKG_LIBS},
	{"libs-only-ldpath",		PKG_LIBS_ONLY_LDPATH},
	{"libs-only-libname",		PKG_LIBS_ONLY_LIBNAME},
	{"libs-only-other",		PKG_LIBS_ONLY_OTHER},
	{"link-abi",			PKG_LINK_ABI},
	{"list",			PKG_LIST},
	{"list-package-names",		PKG_LIST_PACKAGE_NAMES},
	{"modversion",			PKG_MODVERSION},
	{"msvc-syntax",			PKG_MSVC_SYNTAX},
	{"newlines",			PKG_NEWLINES},
	{"no-cache",			PKG_NO_CACHE},
	{"no-provides",			PKG_NO_PROVIDES},
	{"no-uninstalled",		PKG_NO_UNINSTALLED},
	{"path",			PKG_PATH},
	{"print-digraph-query-nodes",	PKG_PRINT_DIGRAPH_QUERY_NODES},
	{"print-errors",		PKG_PRINT_ERRORS},
	{"print-provides",		PKG_PROVIDES},
	{"print-requires",		PKG_REQUIRES},
	{"print-requires-private",	PKG_REQUIRES_PRIVATE},
	{"print-variables",		PKG_VARIABLES},
	{"pure",			PKG_PURE},
	{"shared",			PKG_SHARED},
	{"short-errors",		PKG_SHORT_ERRORS},
	{"silence-errors",		PKG_SILENCE_ERRORS},
	{"simulate",			PKG_SIMULATE},
	{"solution",			PKG_SOLUTION},
	{"static",			PKG_STATIC},
	{"uninstalled",			PKG_UNINSTALLED},
	{"validate",			PKG_VALIDATE},
};

static void
test_keyword_set_wanted_flags(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value)
{
	int i;
	int flagcount;
	char **flags = NULL;

	(void) keyword;
	(void) warnprefix;
	(void) offset;

	pkgconf_argv_split(value, &flagcount, &flags);

	for (i = 0; i < flagcount; i++)
	{
		const char *flag = flags[i];
		const pkgconf_test_flag_pair_t *pair = bsearch(flag,
			test_flag_pairs, PKGCONF_ARRAY_SIZE(test_flag_pairs),
			sizeof(*pair), test_flag_pair_cmp);

		if (pair == NULL)
			continue;

		testcase->wanted_flags |= pair->flag;
	}

	pkgconf_argv_free(flags);
}

static size_t
prefixed_path_split(const char *text, pkgconf_list_t *dirlist, const char *prefix)
{
	size_t count = 0;
	char *workbuf, *p, *iter;

	if (text == NULL)
		return 0;

	iter = workbuf = strdup(text);
	while ((p = strtok(iter, PKG_CONFIG_PATH_SEP_S)) != NULL)
	{
		pkgconf_buffer_t pathbuf = PKGCONF_BUFFER_INITIALIZER;

		pkgconf_buffer_append(&pathbuf, prefix);
		pkgconf_buffer_push_byte(&pathbuf, '/');
		pkgconf_buffer_append(&pathbuf, p);
		pkgconf_path_add(pkgconf_buffer_str(&pathbuf), dirlist, false);
		pkgconf_buffer_finalize(&pathbuf);

		count++, iter = NULL;
	}
	free(workbuf);

	return count;
}

static void
test_keyword_set_path_list(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value)
{
	(void) keyword;
	(void) warnprefix;

	pkgconf_list_t *dest = (pkgconf_list_t *)((char *) testcase + offset);
	prefixed_path_split(value, dest, pkgconf_buffer_str(&test_fixtures_dir));
}

static void
test_keyword_set_match_strategy(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value)
{
	(void) keyword;
	(void) warnprefix;

	pkgconf_test_match_strategy_t *dest = (pkgconf_test_match_strategy_t *)((char *) testcase + offset);

	if (!strcasecmp(value, "partial"))
		*dest = MATCH_PARTIAL;

	if (!strcasecmp(value, "empty"))
		*dest = MATCH_EMPTY;
}

static void
test_keyword_set_environment(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value)
{
	(void) keyword;
	(void) offset;

	char *eq = strchr(value, '=');
	if (eq == NULL)
	{
		fprintf(stderr, "%s: malformed Environment entry: %s\n", warnprefix, value);
		return;
	}

	*eq++ = '\0';

	// store raw, vars are expanded at run time
	test_environment_push(testcase, value, eq);
}

#ifdef _WIN32
static void
test_keyword_disabled(pkgconf_test_case_t *testcase, const char *keyword, const char *warnprefix, const ptrdiff_t offset, const char *value)
{
	(void) testcase;
	(void) keyword;
	(void) warnprefix;
	(void) offset;
	(void) value;
}
#endif

static const pkgconf_test_keyword_pair_t test_keyword_pairs[] =
{
	{"AtLeastVersion",	test_keyword_set_buffer,		offsetof(pkgconf_test_case_t, atleast_version)},
	{"DefineVariable",	test_keyword_extend_bufferset,		offsetof(pkgconf_test_case_t, define_variables)},
	{"Environment",		test_keyword_set_environment,		offsetof(pkgconf_test_case_t, env_vars)},
	{"ExactVersion",	test_keyword_set_buffer,		offsetof(pkgconf_test_case_t, exact_version)},
	{"ExpectedExitCode",	test_keyword_set_int,			offsetof(pkgconf_test_case_t, exitcode)},
	{"ExpectedStderr",	test_keyword_extend_bufferset,		offsetof(pkgconf_test_case_t, expected_stderr)},
	{"ExpectedStdout",	test_keyword_extend_bufferset,		offsetof(pkgconf_test_case_t, expected_stdout)},
	{"ExpectedStdoutFile",	test_keyword_set_buffer,		offsetof(pkgconf_test_case_t, expected_stdout_file)},
	{"FragmentFilter",	test_keyword_set_buffer,		offsetof(pkgconf_test_case_t, fragment_filter)},
	{"MatchStderr",		test_keyword_set_match_strategy,	offsetof(pkgconf_test_case_t, match_stderr)},
	{"MatchStdout",		test_keyword_set_match_strategy,	offsetof(pkgconf_test_case_t, match_stdout)},
	{"MaxVersion",		test_keyword_set_buffer,		offsetof(pkgconf_test_case_t, max_version)},
	{"PackageSearchPath",	test_keyword_set_path_list,		offsetof(pkgconf_test_case_t, search_path)},
	{"Query",		test_keyword_set_buffer,		offsetof(pkgconf_test_case_t, query)},
	{"RequireUtf8Locale",	test_keyword_set_bool,			offsetof(pkgconf_test_case_t, require_utf8_locale)},
	{"SetupCopy",		test_keyword_extend_bufferset,		offsetof(pkgconf_test_case_t, copies)},
	{"SetupMkdir",		test_keyword_extend_bufferset,		offsetof(pkgconf_test_case_t, mkdirs)},
#ifdef _WIN32
	{"SetupSymlink",	test_keyword_disabled,			0},
#else
	{"SetupSymlink",	test_keyword_extend_bufferset,		offsetof(pkgconf_test_case_t, symlinks)},
#endif
	{"SkipPlatforms",	test_keyword_set_buffer,		offsetof(pkgconf_test_case_t, skip_platforms)},
	{"Tool",		test_keyword_set_buffer,		offsetof(pkgconf_test_case_t, tool)},
	{"ToolArgs",		test_keyword_set_buffer,		offsetof(pkgconf_test_case_t, tool_args)},
	{"VerbosityLevel",	test_keyword_set_int,			offsetof(pkgconf_test_case_t, verbosity)},
	{"WantedFlags",		test_keyword_set_wanted_flags,		offsetof(pkgconf_test_case_t, wanted_flags)},
	{"WantEnvPrefix",	test_keyword_set_buffer,		offsetof(pkgconf_test_case_t, want_env_prefix)},
#ifndef PKGCONF_LITE
	{"WantPersonality",	test_keyword_set_buffer,		offsetof(pkgconf_test_case_t, want_personality)},
#endif
	{"WantVariable",	test_keyword_set_buffer,		offsetof(pkgconf_test_case_t, want_variable)},
};

static void
test_keyword_set(void *data, const char *warnprefix, const char *keyword, const char *value)
{
	pkgconf_test_case_t *testcase = data;
	const pkgconf_test_keyword_pair_t *pair = bsearch(keyword,
		test_keyword_pairs, PKGCONF_ARRAY_SIZE(test_keyword_pairs),
		sizeof(*pair), test_keyword_pair_cmp);

	if (pair == NULL || pair->func == NULL)
		return;

	pair->func(testcase, warnprefix, keyword, pair->offset, value);
}

static const pkgconf_parser_operand_func_t test_parser_ops[256] =
{
	[':'] = (pkgconf_parser_operand_func_t) test_keyword_set,
};

static void
test_parser_warn(void *p, const char *fmt, ...)
{
	va_list va;

	(void) p;

	va_start(va, fmt);
	vfprintf(stderr, fmt, va);
	va_end(va);
}

static pkgconf_test_case_t *
load_test_case(char *testfile)
{
	FILE *testf = fopen(testfile, "r");
	if (testf == NULL)
		return NULL;

	pkgconf_test_case_t *out = calloc(1, sizeof(*out));
	if (out == NULL)
		goto cleanup;

	char *nameptr;
	if ((nameptr = strrchr(testfile, '/')) != NULL)
		nameptr++;
	else
		nameptr = testfile;

	out->name = strdup(nameptr);

	// store directory containing the test file for ExpectedStdoutFile resolution
	{
		char *dirend = strrchr(testfile, '/');
		if (dirend != NULL)
		{
			size_t dirlen = (size_t)(dirend - testfile);
			out->testfile_dir = calloc(1, dirlen + 1);
			if (out->testfile_dir != NULL)
				memcpy(out->testfile_dir, testfile, dirlen);
		}
		else
			out->testfile_dir = strdup(".");
	}

	pkgconf_parser_parse(testf, out, test_parser_ops, test_parser_warn, testfile);

cleanup:
	fclose(testf);
	return out;
}

// we use a custom personality to ensure the tests are fully hermetic
static pkgconf_cross_personality_t *
personality_for_test(const pkgconf_test_case_t *testcase)
{
#ifndef PKGCONF_LITE
	if (pkgconf_buffer_len(&testcase->want_personality))
		return pkgconf_cross_personality_find(pkgconf_buffer_str(&testcase->want_personality));
#endif

	pkgconf_cross_personality_t *pers = calloc(1, sizeof(*pers));
	if (pers == NULL)
		return NULL;

	pers->name = strdup("test");
	pkgconf_path_copy_list(&pers->dir_list, &testcase->search_path);
	pkgconf_path_add("/test/sysroot/include", &pers->filter_includedirs, false);
	pkgconf_path_add("/test/sysroot/lib", &pers->filter_libdirs, false);

	return pers;
}

static bool
report_failure(pkgconf_test_match_strategy_t match, const pkgconf_buffer_t *expected, const pkgconf_buffer_t *actual, const char *buffername)
{
	fprintf(stderr,
		"================================================================================\n"
		"%s did not%s match:\n"
		"  expected: [%s]\n"
		"  actual: [%s]\n"
		"================================================================================\n",
		buffername, match == MATCH_PARTIAL ? " partially" : "",
		pkgconf_buffer_str_or_empty(expected),
		pkgconf_buffer_str_or_empty(actual));

	return false;
}

static bool
test_match_buffer(pkgconf_test_match_strategy_t match, const pkgconf_buffer_t *expected, const pkgconf_buffer_t *actual, const char *buffername)
{
	if (!pkgconf_buffer_len(expected) && match != MATCH_EMPTY)
		return true;

	if (!pkgconf_buffer_len(actual))
	{
		if (match == MATCH_EMPTY)
			return true;

		return report_failure(match, expected, actual, buffername);
	}

	if (match == MATCH_PARTIAL)
		return pkgconf_buffer_contains(actual, expected) ? true : report_failure(match, expected, actual, buffername);

	return pkgconf_buffer_match(actual, expected) ? true : report_failure(match, expected, actual, buffername);
}

static bool
read_file_into_buffer(FILE *f, pkgconf_buffer_t *buf)
{
	char tmp[4096] = {0};
	size_t n;
	while ((n = fread(tmp, 1, sizeof(tmp), f)) > 0)
		pkgconf_buffer_append_slice(buf, tmp, n);

	if (ferror(f))
		return false;

	return true;
}

static bool
open_file_into_buffer(const char *path, pkgconf_buffer_t *buf)
{
	FILE *f = fopen(path, "r");
	if (f == NULL)
		return false;

	bool ok = read_file_into_buffer(f, buf);
	fclose(f);
	return ok;
}

static bool
copy_file(const char *dst, const char *src)
{
	FILE *fsrc = fopen(src, "rb");
	if (!fsrc)
		return false;

	FILE *fdst = fopen(dst, "wb");
	if (!fdst)
	{
		int errno_save = errno;
		fclose(fsrc);
		errno = errno_save;
		return false;
	}

	bool ok = true;
	char buf[4096] = {0};
	size_t nr;
	while ((nr = fread(buf, 1, sizeof(buf), fsrc)) > 0)
	{
		if (fwrite(buf, 1, nr, fdst) != nr)
		{
			ok = false;
			break;
		}
	}

	if (ferror(fsrc) || ferror(fdst))
		ok = false;

	int errno_save = errno;
	fclose(fsrc);
	fclose(fdst);
	errno = errno_save;

	return ok;
}

/*
 * Recursively remove a directory tree.
 */
static void
rmdir_recursive(const char *path)
{
	DIR *dir = opendir(path);
	if (dir == NULL)
		return;

	struct dirent *ent;
	while ((ent = readdir(dir)) != NULL)
	{
		if (!strcmp(ent->d_name, ".") || !strcmp(ent->d_name, ".."))
			continue;

		pkgconf_buffer_t child = PKGCONF_BUFFER_INITIALIZER;
		pkgconf_buffer_append(&child, path);
		pkgconf_buffer_push_byte(&child, '/');
		pkgconf_buffer_append(&child, ent->d_name);

#ifdef _WIN32
		if (_access(pkgconf_buffer_str(&child), 0) == 0)
		{
			// Get required buffer size
			int size = MultiByteToWideChar(CP_ACP, 0, pkgconf_buffer_str(&child), -1, NULL, 0);

			// Allocate and convert
			wchar_t* wide_path = calloc(size, sizeof(wchar_t));
			assert(wide_path != NULL);
			MultiByteToWideChar(CP_ACP, 0, pkgconf_buffer_str(&child), -1, wide_path, size);

			DWORD attrs = GetFileAttributesW(wide_path);

			free(wide_path);

			if (attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY))
				rmdir_recursive(pkgconf_buffer_str(&child));
			else
				unlink(pkgconf_buffer_str(&child));
		}
#else
		struct stat st;
		if (lstat(pkgconf_buffer_str(&child), &st) == 0)
		{
			if (S_ISDIR(st.st_mode))
				rmdir_recursive(pkgconf_buffer_str(&child));
			else
				unlink(pkgconf_buffer_str(&child));
		}
#endif

		pkgconf_buffer_finalize(&child);
	}

	closedir(dir);
	rmdir(path);
}

/*
 * Recursively make a directory tree.
 */
static bool
mkdir_recursive(const char *path)
{
	if (!path)
		return false;

	const char *tmpstr = NULL;
	pkgconf_buffer_t buf = PKGCONF_BUFFER_INITIALIZER;
	size_t i = 0;

	while (path[i])
	{
		// Append characters up to the next separator
		size_t start = i;
		while (path[i] && path[i] != '/')
			i++;

		pkgconf_buffer_append_slice(&buf, path + start, i - start);

		// If we hit a separator, try to create this component
		if (path[i] == '/')
		{
			pkgconf_buffer_push_byte(&buf, '/');
			tmpstr = pkgconf_buffer_str(&buf);
			if (tmpstr && mkdir(tmpstr, 0755) != 0 && errno != EEXIST)
			{
				pkgconf_buffer_finalize(&buf);
				return false;
			}
			i++; // skip the separator
		}
	}

	// Make the final directory (handles paths without trailing separator)
	tmpstr = pkgconf_buffer_str(&buf);
	bool ok = true;
	if (tmpstr && strlen(tmpstr) > 0)
	{
		if (mkdir(tmpstr, 0755) != 0 && errno != EEXIST)
			ok = false;
	}

	pkgconf_buffer_finalize(&buf);
	return ok;
}

// Returns true if we need tmp_dir
static bool
needs_tmp_dir(const pkgconf_test_case_t *testcase)
{
#ifdef _WIN32
	return testcase->mkdirs.head != NULL || testcase->copies.head != NULL;
#else // _WIN32
	return testcase->mkdirs.head != NULL || testcase->copies.head != NULL || testcase->symlinks.head != NULL;
#endif // _WIN32
}

static int
run_tool(const pkgconf_test_case_t *testcase, pkgconf_buffer_t *o_stdout, pkgconf_buffer_t *o_stderr)
{
	(void) o_stderr; // TODO: external tool stderr goes to real stderr for now

	pkgconf_buffer_t cmdbuf = PKGCONF_BUFFER_INITIALIZER;

	// build: <tool-dir>/<tool> <tool_args>
	if (pkgconf_buffer_len(&test_tool_dir))
	{
		pkgconf_buffer_append(&cmdbuf, pkgconf_buffer_str(&test_tool_dir));
		pkgconf_buffer_push_byte(&cmdbuf, '/');
	}

	pkgconf_buffer_append(&cmdbuf, pkgconf_buffer_str(&testcase->tool));

	if (pkgconf_buffer_len(&testcase->tool_args))
	{
		pkgconf_buffer_append(&cmdbuf, " ");
		pkgconf_buffer_append(&cmdbuf, pkgconf_buffer_str(&testcase->tool_args));
	}

	// Inject Environment vars for the child process
	char tool_cwd[PATH_MAX] = {0};
	const char *pwd = getcwd(tool_cwd, sizeof(tool_cwd));

	pkgconf_node_t *iter;
	PKGCONF_FOREACH_LIST_ENTRY(testcase->env_vars.head, iter)
	{
		pkgconf_test_environ_t *env = iter->data;
		pkgconf_buffer_t expanded = PKGCONF_BUFFER_INITIALIZER;
		handle_substs(&expanded, PKGCONF_BUFFER_FROM_STR(env->value), pwd);
		setenv(env->key, pkgconf_buffer_str_or_empty(&expanded), 1);
		pkgconf_buffer_finalize(&expanded);
	}

	FILE *pipe = popen(pkgconf_buffer_str(&cmdbuf), "r");
	pkgconf_buffer_finalize(&cmdbuf);

	PKGCONF_FOREACH_LIST_ENTRY(testcase->env_vars.head, iter)
	{
		pkgconf_test_environ_t *env = iter->data;
		unsetenv(env->key);
	}

	if (pipe == NULL)
	{
		fprintf(stderr, "popen failed for tool '%s': %s\n",
			pkgconf_buffer_str(&testcase->tool), strerror(errno));
		return -1;
	}

	bool ok = read_file_into_buffer(pipe, o_stdout);
	int saved_errno = errno; // pclose() will clobber errno, save it
	int status = pclose(pipe);
	if (!ok)
	{
		fprintf(stderr, "read failed into buffer for command '%s': %s",
			pkgconf_buffer_str(&testcase->tool), strerror(saved_errno));
		return -1;
	}

	if (status == -1)
	{
		fprintf(stderr, "pclose failed for command '%s': %s\n",
			pkgconf_buffer_str(&testcase->tool), strerror(errno));
		return -1;
	}

#if defined(WIFEXITED) && defined(WEXITSTATUS)
	if (WIFEXITED(status))
		return WEXITSTATUS(status);

	fprintf(stderr, "command '%s' did not exit normally\n",
		pkgconf_buffer_str(&testcase->tool));
	return -1;
#else
	return status;
#endif
}

/*
 * Split a bufferset entry on the first space into left and right halves.
 * Caller must free *left_out and *right_out.
 */
static bool
split_pair(const char *entry, char **left_out, char **right_out)
{
	if (entry == NULL)
		return false;

	const char *sp = strchr(entry, ' ');
	if (sp == NULL)
		return false;

	*left_out = pkgconf_strndup(entry, (size_t)(sp - entry));
	*right_out = strdup(sp + 1);
	return true;
}

/*
 * run_setup: execute mkdirs, copies, and symlinks in order.
 * Must be called after chdir() into the tmp_dir.
 */
static bool
run_setup(const pkgconf_test_case_t *testcase, const char *pwd)
{
	pkgconf_node_t *iter;

	// mkdirs: each entry is a single path, relative to tmp_dir
	PKGCONF_FOREACH_LIST_ENTRY(testcase->mkdirs.head, iter)
	{
		pkgconf_bufferset_t *set = iter->data;

		pkgconf_buffer_t path = PKGCONF_BUFFER_INITIALIZER;
		handle_substs(&path, &set->buffer, pwd);

		bool ok = mkdir_recursive(pkgconf_buffer_str(&path));
		pkgconf_buffer_finalize(&path);

		if (!ok && errno != EEXIST)
		{
			fprintf(stderr, "SetupMkdir: mkdir '%s' failed: %s\n",
				pkgconf_buffer_str_or_empty(&set->buffer), strerror(errno));
			return false;
		}
	}

	// copies: "src dst", src relative to TEST_FIXTURES_DIR, dst relative to tmp_dir
	PKGCONF_FOREACH_LIST_ENTRY(testcase->copies.head, iter)
	{
		pkgconf_bufferset_t *set = iter->data;

		pkgconf_buffer_t expanded = PKGCONF_BUFFER_INITIALIZER;
		handle_substs(&expanded, &set->buffer, pwd);

		char *left = NULL, *right = NULL;
		if (!split_pair(pkgconf_buffer_str(&expanded), &left, &right))
		{
			fprintf(stderr, "SetupCopy: malformed entry (expected 'src dst'): %s\n",
				pkgconf_buffer_str_or_empty(&set->buffer));
			pkgconf_buffer_finalize(&expanded);
			return false;
		}
		pkgconf_buffer_finalize(&expanded);

		pkgconf_buffer_t srcpath = PKGCONF_BUFFER_INITIALIZER;
		pkgconf_buffer_append(&srcpath, pkgconf_buffer_str(&test_fixtures_dir));
		pkgconf_buffer_push_byte(&srcpath, '/');
		pkgconf_buffer_append(&srcpath, left);

		bool ok = copy_file(right, pkgconf_buffer_str(&srcpath));
		pkgconf_buffer_finalize(&srcpath);
		if (!ok)
		{
			fprintf(stderr, "SetupCopy: failed to copy file '%s' to '%s': %s\n", left, right, strerror(errno));
			free(left);
			free(right);
			return false;
		}

		free(left);
		free(right);
	}

#ifndef _WIN32
	// symlinks: "target linkpath" — both may be relative to tmp_dir or absolute after %PWD% expansion
	PKGCONF_FOREACH_LIST_ENTRY(testcase->symlinks.head, iter)
	{
		pkgconf_bufferset_t *set = iter->data;

		pkgconf_buffer_t expanded = PKGCONF_BUFFER_INITIALIZER;
		handle_substs(&expanded, &set->buffer, pwd);

		char *target = NULL, *linkpath = NULL;
		if (!split_pair(pkgconf_buffer_str(&expanded), &target, &linkpath))
		{
			fprintf(stderr, "SetupSymlink: malformed entry (expected 'target linkpath'): %s\n",
				pkgconf_buffer_str_or_empty(&set->buffer));
			pkgconf_buffer_finalize(&expanded);
			return false;
		}
		pkgconf_buffer_finalize(&expanded);

		unlink(linkpath);

		if (symlink(target, linkpath) != 0)
		{
			fprintf(stderr, "SetupSymlink: symlink('%s', '%s') failed: %s\n",
				target, linkpath, strerror(errno));
			free(target);
			free(linkpath);
			return false;
		}

		free(target);
		free(linkpath);
	}
#endif // _WIN32

	return true;
}

static void
annotate_result(const pkgconf_test_case_t *testcase, int ret, const pkgconf_test_output_t *out)
{
	pkgconf_buffer_t search_path_buf = PKGCONF_BUFFER_INITIALIZER;
	const pkgconf_node_t *iter;

	PKGCONF_FOREACH_LIST_ENTRY(testcase->search_path.head, iter)
	{
		const pkgconf_path_t *path = iter->data;

		if (pkgconf_buffer_len(&search_path_buf))
			pkgconf_buffer_push_byte(&search_path_buf, ' ');

		pkgconf_buffer_append(&search_path_buf, path->path);
	}

	pkgconf_buffer_t wanted_flags_buf = PKGCONF_BUFFER_INITIALIZER;

	for (size_t i = 0; i < PKGCONF_ARRAY_SIZE(test_flag_pairs); i++)
	{
		const pkgconf_test_flag_pair_t *pair = &test_flag_pairs[i];

		if ((testcase->wanted_flags & pair->flag) == pair->flag)
		{
			if (pkgconf_buffer_len(&wanted_flags_buf))
				pkgconf_buffer_push_byte(&wanted_flags_buf, ' ');

			pkgconf_buffer_append(&wanted_flags_buf, pair->name);
		}
	}

	pkgconf_buffer_t env_buf = PKGCONF_BUFFER_INITIALIZER;

	PKGCONF_FOREACH_LIST_ENTRY(testcase->env_vars.head, iter)
	{
		const pkgconf_test_environ_t *env = iter->data;

		if (pkgconf_buffer_len(&env_buf))
			pkgconf_buffer_append(&env_buf, "\n  ");

		pkgconf_buffer_append_fmt(&env_buf, "%s: %s", env->key, env->value);
	}

	fprintf(stderr,
		"--------------------------------------------------------------------------------\n"
		"search-path: <%s>\n"
		"wanted-flags: <%s>\n"
		"environment:\n"
		"  %s\n"
		"query: [%s]\n"
		"exit-code: %d\n"
		"verbosity: %d\n",
		pkgconf_buffer_str_or_empty(&search_path_buf),
		pkgconf_buffer_str_or_empty(&wanted_flags_buf),
		pkgconf_buffer_str_or_empty(&env_buf),
		pkgconf_buffer_str_or_empty(&testcase->query),
		ret,
		testcase->verbosity);

	if (pkgconf_buffer_len(&testcase->tool))
		fprintf(stderr, "tool: [%s] tool-args: [%s]\n",
			pkgconf_buffer_str_or_empty(&testcase->tool),
			pkgconf_buffer_str_or_empty(&testcase->tool_args));

	fprintf(stderr, "stdout: [%s]\n",
		pkgconf_buffer_str_or_empty(&out->o_stdout));

	PKGCONF_FOREACH_LIST_ENTRY(testcase->expected_stdout.head, iter)
	{
		pkgconf_bufferset_t *set = iter->data;

		fprintf(stderr,
			"expected-stdout: [%s] (%s)\n",
			pkgconf_buffer_str_or_empty(&set->buffer),
			testcase->match_stdout == MATCH_PARTIAL ? "partial" : "exact");
	}

	if (pkgconf_buffer_len(&testcase->expected_stdout_file))
		fprintf(stderr, "expected-stdout-file: [%s]\n",
			pkgconf_buffer_str_or_empty(&testcase->expected_stdout_file));

	fprintf(stderr, "stderr: [%s]\n",
		pkgconf_buffer_str_or_empty(&out->o_stderr));

	PKGCONF_FOREACH_LIST_ENTRY(testcase->expected_stderr.head, iter)
	{
		pkgconf_bufferset_t *set = iter->data;

		fprintf(stderr,
			"expected-stderr: [%s] (%s)\n",
			pkgconf_buffer_str_or_empty(&set->buffer),
			testcase->match_stderr == MATCH_PARTIAL ? "partial" : "exact");
	}

	PKGCONF_FOREACH_LIST_ENTRY(testcase->define_variables.head, iter)
	{
		pkgconf_bufferset_t *set = iter->data;
		fprintf(stderr, "define-variable: [%s]\n", pkgconf_buffer_str_or_empty(&set->buffer));
	}

	fprintf(stderr,
		"want-env-prefix: [%s]\n"
		"fragment-filter: [%s]\n"
		"--------------------------------------------------------------------------------\n",
		pkgconf_buffer_str_or_empty(&testcase->want_env_prefix),
		pkgconf_buffer_str_or_empty(&testcase->fragment_filter));

	pkgconf_buffer_finalize(&search_path_buf);
	pkgconf_buffer_finalize(&wanted_flags_buf);
	pkgconf_buffer_finalize(&env_buf);
}

static bool
run_test_case(const pkgconf_test_case_t *testcase)
{
	bool passed = true;

	const pkgconf_buffer_t *our_platform = PKGCONF_BUFFER_FROM_STR(PKGCONF_TEST_PLATFORM);
	if (pkgconf_buffer_contains(&testcase->skip_platforms, our_platform))
	{
		printf("# test skipped on %s\nSKIP: %s\n",
			pkgconf_buffer_str(our_platform), testcase->name);
		return true;
	}

	if (testcase->require_utf8_locale && !pkgconf_is_locale_utf8())
	{
		printf("# test skipped: requires a UTF-8 locale\nSKIP: %s\n", testcase->name);
		return true;
	}

	// If the test has setup steps, create a new tmp_dir and chdir into it.
	char original_cwd[PATH_MAX] = {0};
	char *tmp_dir = NULL;

	if (getcwd(original_cwd, sizeof(original_cwd)) == NULL)
	{
		fprintf(stderr, "FAIL: getcwd failed: %s\n", strerror(errno));
		return false;
	}

	if (needs_tmp_dir(testcase))
	{
		pkgconf_buffer_t tmp_buf = PKGCONF_BUFFER_INITIALIZER;
		pkgconf_buffer_append_fmt(&tmp_buf, "%s/pkgconf-test-XXXXXX", original_cwd);
		tmp_dir = pkgconf_buffer_freeze(&tmp_buf);

		if (mkdtemp(tmp_dir) == NULL)
		{
			fprintf(stderr, "FAIL: mkdtemp failed: %s\n", strerror(errno));
			free(tmp_dir);
			return false;
		}

		if (chdir(tmp_dir) != 0)
		{
			fprintf(stderr, "FAIL: chdir('%s') failed: %s\n", tmp_dir, strerror(errno));
			rmdir(tmp_dir);
			free(tmp_dir);
			return false;
		}

		if (!run_setup(testcase, tmp_dir))
		{
			fprintf(stderr, "FAIL: %s (setup failed)\n", testcase->name);
			chdir(original_cwd);
			rmdir_recursive(tmp_dir);
			free(tmp_dir);
			return false;
		}
	}

	pkgconf_test_output_t *out = (pkgconf_test_output_t *) test_output();
	int ret;

	if (pkgconf_buffer_len(&testcase->tool))
	{
		ret = run_tool(testcase, &out->o_stdout, &out->o_stderr);
	}
	else
	{
		pkgconf_cross_personality_t *personality = personality_for_test(testcase);
		pkgconf_test_state_t state =
		{
			.cli_state.want_flags = testcase->wanted_flags,
			.cli_state.want_env_prefix = pkgconf_buffer_str(&testcase->want_env_prefix),
			.cli_state.want_variable = pkgconf_buffer_str(&testcase->want_variable),
			.cli_state.want_fragment_filter = pkgconf_buffer_str(&testcase->fragment_filter),
			.cli_state.required_module_version = pkgconf_buffer_str(&testcase->atleast_version),
			.cli_state.required_exact_module_version = pkgconf_buffer_str(&testcase->exact_version),
			.cli_state.required_max_module_version = pkgconf_buffer_str(&testcase->max_version),
			.cli_state.verbosity = testcase->verbosity,
			.testcase = testcase,
		};

		pkgconf_client_init(&state.cli_state.pkg_client, error_handler, NULL, personality, &state, environ_lookup_handler);
		pkgconf_client_set_output(&state.cli_state.pkg_client, &out->output);

		pkgconf_node_t *iter;
		PKGCONF_FOREACH_LIST_ENTRY(testcase->define_variables.head, iter)
		{
			pkgconf_bufferset_t *set = iter->data;
			pkgconf_tuple_define_global(&state.cli_state.pkg_client, pkgconf_buffer_str_or_empty(&set->buffer));
		}

		/*
		 * Re-expand Query now that %PWD% is known (if we have a tmp_dir).
		 * For tests without a tmp_dir this is a no-op since %PWD% won't appear.
		 */
		char query_cwd[PATH_MAX] = {0};
		const char *query_pwd = getcwd(query_cwd, sizeof(query_cwd));

		pkgconf_buffer_t query_expanded = PKGCONF_BUFFER_INITIALIZER;
		handle_substs(&query_expanded, &testcase->query, query_pwd);

		pkgconf_buffer_t arg_buf = PKGCONF_BUFFER_INITIALIZER;
		int test_argc = 0;
		char **test_argv = NULL;

		if (pkgconf_buffer_len(&query_expanded))
			pkgconf_buffer_append_fmt(&arg_buf, "pkgconf %s", pkgconf_buffer_str(&query_expanded));
		else
			pkgconf_buffer_append(&arg_buf, "pkgconf");

		pkgconf_argv_split(pkgconf_buffer_str(&arg_buf), &test_argc, &test_argv);
		pkgconf_buffer_finalize(&arg_buf);
		pkgconf_buffer_finalize(&query_expanded);

		pkgconf_client_set_warn_handler(&state.cli_state.pkg_client, error_handler, NULL);

#ifndef PKGCONF_LITE
		if (debug)
			pkgconf_client_set_trace_handler(&state.cli_state.pkg_client, debug_handler, NULL);
#endif // PKGCONF_LITE

		ret = pkgconf_cli_run(&state.cli_state, test_argc, test_argv, 1);
		pkgconf_argv_free(test_argv);
	}

	if (pkgconf_buffer_len(&out->o_stdout))
		pkgconf_buffer_trim_byte(&out->o_stdout);

	if (pkgconf_buffer_len(&out->o_stderr))
		pkgconf_buffer_trim_byte(&out->o_stderr);

	pkgconf_node_t *iter;

	PKGCONF_FOREACH_LIST_ENTRY(testcase->expected_stdout.head, iter)
	{
		pkgconf_bufferset_t *set = iter->data;

		char expected_cwd[PATH_MAX] = {0};
		const char *expected_pwd = getcwd(expected_cwd, sizeof(expected_cwd));

		pkgconf_buffer_t expected_expanded = PKGCONF_BUFFER_INITIALIZER;
		handle_substs(&expected_expanded, &set->buffer, expected_pwd);

		if (!test_match_buffer(testcase->match_stdout, &expected_expanded, &out->o_stdout, "stdout"))
			passed = false;

		pkgconf_buffer_finalize(&expected_expanded);
	}

	// ExpectedStdoutFile: load file relative to the .test file's directory
	if (pkgconf_buffer_len(&testcase->expected_stdout_file))
	{
		pkgconf_buffer_t filepath = PKGCONF_BUFFER_INITIALIZER;
		pkgconf_buffer_append(&filepath, testcase->testfile_dir);
		pkgconf_buffer_push_byte(&filepath, '/');
		pkgconf_buffer_append(&filepath, pkgconf_buffer_str(&testcase->expected_stdout_file));

		pkgconf_buffer_t file_contents = PKGCONF_BUFFER_INITIALIZER;
		if (!open_file_into_buffer(pkgconf_buffer_str(&filepath), &file_contents))
		{
			fprintf(stderr, "ExpectedStdoutFile: failed to open '%s': %s\n", pkgconf_buffer_str(&filepath), strerror(errno));
			passed = false;
		}
		else
		{
			if (pkgconf_buffer_len(&file_contents))
				pkgconf_buffer_trim_byte(&file_contents);

			if (!test_match_buffer(testcase->match_stdout, &file_contents, &out->o_stdout, "stdout (file)"))
				passed = false;
		}

		pkgconf_buffer_finalize(&file_contents);
		pkgconf_buffer_finalize(&filepath);
	}

	PKGCONF_FOREACH_LIST_ENTRY(testcase->expected_stderr.head, iter)
	{
		pkgconf_bufferset_t *set = iter->data;

		if (!test_match_buffer(testcase->match_stderr, &set->buffer, &out->o_stderr, "stderr"))
			passed = false;
	}

	if (ret != testcase->exitcode)
	{
		fprintf(stderr, "exitcode %d does not match expected %d\n", ret, testcase->exitcode);
		passed = false;
	}

	printf("%s: %s\n", passed ? "PASS" : "FAIL", testcase->name);

	if (!passed)
		annotate_result(testcase, ret, out);

	test_output_reset(out);

	// Restore cwd and clean up tmp_dir if we created one
	if (tmp_dir && strcmp(tmp_dir, original_cwd) != 0)
	{
		chdir(original_cwd);
		rmdir_recursive(tmp_dir);
	}

	free(tmp_dir);
	return passed;
}

static void
free_test_case(pkgconf_test_case_t *testcase)
{
	pkgconf_bufferset_free(&testcase->define_variables);
	pkgconf_bufferset_free(&testcase->expected_stderr);
	pkgconf_bufferset_free(&testcase->expected_stdout);
	pkgconf_bufferset_free(&testcase->mkdirs);
#ifndef _WIN32
	pkgconf_bufferset_free(&testcase->symlinks);
#endif // _WIN32
	pkgconf_bufferset_free(&testcase->copies);

	test_environment_free(&testcase->env_vars);
	pkgconf_path_free(&testcase->search_path);

	pkgconf_buffer_finalize(&testcase->query);
	pkgconf_buffer_finalize(&testcase->want_env_prefix);
	pkgconf_buffer_finalize(&testcase->want_variable);
	pkgconf_buffer_finalize(&testcase->fragment_filter);
	pkgconf_buffer_finalize(&testcase->skip_platforms);
	pkgconf_buffer_finalize(&testcase->atleast_version);
	pkgconf_buffer_finalize(&testcase->exact_version);
	pkgconf_buffer_finalize(&testcase->max_version);
	pkgconf_buffer_finalize(&testcase->expected_stdout_file);
	pkgconf_buffer_finalize(&testcase->tool);
	pkgconf_buffer_finalize(&testcase->tool_args);

#ifndef PKGCONF_LITE
	pkgconf_buffer_finalize(&testcase->want_personality);
#endif

	free(testcase->name);
	free(testcase->testfile_dir);
	free(testcase);
}

static bool
process_test_case(char *testcase_file)
{
	pkgconf_test_case_t *testcase = load_test_case(testcase_file);
	bool ret;

	if (testcase == NULL)
	{
		fprintf(stderr, "test %s failed to load\n", testcase_file);
		return false;
	}

	ret = run_test_case(testcase);
	free_test_case(testcase);

	return ret;
}

static inline bool
str_has_suffix(const char *str, const char *suffix)
{
	size_t str_len = strlen(str);
	size_t suf_len = strlen(suffix);

	if (str_len < suf_len)
		return false;

	return !strncasecmp(str + str_len - suf_len, suffix, suf_len);
}

static int
path_sort_cmp(const void *a, const void *b)
{
	return strcmp(*(const char **) a, *(const char **) b);
}

static bool
process_test_directory(char *dirpath)
{
	bool ret = true;
	DIR *dir = opendir(dirpath);
	if (dir == NULL)
	{
		fprintf(stderr, "failed to open test directory %s\n", dirpath);
		return false;
	}
	char **paths = NULL;
	size_t numpaths = 0;

	struct dirent *dirent;
	for (dirent = readdir(dir); dirent != NULL; dirent = readdir(dir))
	{
		pkgconf_buffer_t pathbuf = PKGCONF_BUFFER_INITIALIZER;

		pkgconf_buffer_append(&pathbuf, dirpath);
		pkgconf_buffer_push_byte(&pathbuf, '/');
		pkgconf_buffer_append(&pathbuf, dirent->d_name);

		char *pathstr = pkgconf_buffer_freeze(&pathbuf);
		if (pathstr == NULL)
			continue;

		if (!str_has_suffix(pathstr, ".test"))
		{
			free(pathstr);
			continue;
		}

		paths = pkgconf_reallocarray(paths, ++numpaths, sizeof(void *));
		paths[numpaths - 1] = pathstr;
	}

	qsort(paths, numpaths, sizeof(void *), path_sort_cmp);

	for (size_t i = 0; i < numpaths; i++)
	{
		char *pathstr = paths[i];

		ret = process_test_case(pathstr);
		if (!ret)
			break;
	}

	for (size_t i = 0; i < numpaths; i++)
		free(paths[i]);

	free(paths);
	closedir(dir);
	return ret;
}

static void
usage(void)
{
	fprintf(stderr, "usage: test-runner --test-fixtures <path-to-fixtures> [--tool-dir <path>] <path-to-tests>\n");
	exit(EXIT_FAILURE);
}

int
main(int argc, char *argv[])
{
	int ret;
	char *test_fixtures_dir_arg = NULL;
	char *test_tool_dir_arg = NULL;

	struct pkg_option options[] =
	{
		{"test-fixtures",	required_argument,	NULL,	1},
		{"debug",		no_argument,		NULL,	2},
		{"test-case",		required_argument,	NULL,	3},
		{"tool-dir",		required_argument,	NULL,	4},
		{NULL,			0,			NULL,	0},
	};
	char *testcase = NULL;

	while ((ret = pkg_getopt_long_only(argc, argv, "", options, NULL)) != -1)
	{
		switch (ret)
		{
		case 1:
			test_fixtures_dir_arg = pkg_optarg;
			break;
		case 2:
			debug = true;
			break;
		case 3:
			testcase = pkg_optarg;
			break;
		case 4:
			test_tool_dir_arg = pkg_optarg;
			break;
		}
	}

	if (test_fixtures_dir_arg == NULL)
		usage();

	{
		char test_fixtures_dir_abs[PATH_MAX] = {0};
		if (!realpath(test_fixtures_dir_arg, test_fixtures_dir_abs))
		{
			fprintf(stderr, "realpath failed: %s\n", strerror(errno));
			return EXIT_FAILURE;
		}
		const pkgconf_buffer_t *test_fixtures_dir_arg_buf = PKGCONF_BUFFER_FROM_STR_NONNULL(test_fixtures_dir_abs);
		pkgconf_buffer_subst(&test_fixtures_dir, test_fixtures_dir_arg_buf, "\\", "/");
	}

	if (test_tool_dir_arg != NULL)
	{
		const pkgconf_buffer_t *test_tool_dir_arg_buf = PKGCONF_BUFFER_FROM_STR(test_tool_dir_arg);
		pkgconf_buffer_subst(&test_tool_dir, test_tool_dir_arg_buf, "\\", "/");
	}

	if (testcase != NULL)
		return process_test_case(testcase) ? EXIT_SUCCESS : EXIT_FAILURE;

	if (argv[pkg_optind] == NULL)
		usage();

	return process_test_directory(argv[pkg_optind]) ? EXIT_SUCCESS : EXIT_FAILURE;
}
