/*
 * test-variable.c
 * Tests for the public libpkgconf variable API.
 *
 * 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/stdinc.h>
#include <libpkgconf/libpkgconf.h>
#include "test-api.h"

/*
 * Install a value on a variable by compiling it as bytecode.
 * Mirrors what the parser does when it reads a .pc field.
 */
static void
seed_variable(pkgconf_list_t *vars, const char *key, const char *value)
{
	pkgconf_variable_t *v = pkgconf_variable_get_or_create(vars, key);
	TEST_ASSERT_NONNULL(v);

	pkgconf_buffer_reset(&v->bcbuf);
	pkgconf_bytecode_compile(&v->bcbuf, value);
	pkgconf_bytecode_from_buffer(&v->bc, &v->bcbuf);
}

static void
test_variable_new_and_free(void)
{
	pkgconf_variable_t *v = pkgconf_variable_new("prefix");

	TEST_ASSERT_NONNULL(v);
	TEST_ASSERT_STRCMP_EQ(v->key, "prefix");

	pkgconf_variable_free(v);
}

static void
test_variable_get_or_create_creates(void)
{
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;

	pkgconf_variable_t *v = pkgconf_variable_get_or_create(&vars, "prefix");
	TEST_ASSERT_NONNULL(v);
	TEST_ASSERT_STRCMP_EQ(v->key, "prefix");

	pkgconf_variable_list_free(&vars);
}

static void
test_variable_get_or_create_returns_existing(void)
{
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;

	pkgconf_variable_t *first = pkgconf_variable_get_or_create(&vars, "libdir");
	TEST_ASSERT_NONNULL(first);

	// A second call with the same key should return the same object, not create a duplicate.
	pkgconf_variable_t *second = pkgconf_variable_get_or_create(&vars, "libdir");
	TEST_ASSERT_NONNULL(second);
	TEST_ASSERT_EQ(first, second);

	pkgconf_variable_list_free(&vars);
}

static void
test_variable_find_present(void)
{
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;

	pkgconf_variable_t *created = pkgconf_variable_get_or_create(&vars, "prefix");
	TEST_ASSERT_NONNULL(created);

	pkgconf_variable_t *found = pkgconf_variable_find(&vars, "prefix");
	TEST_ASSERT_NONNULL(found);
	TEST_ASSERT_EQ(created, found);

	pkgconf_variable_list_free(&vars);
}

static void
test_variable_find_absent(void)
{
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;

	pkgconf_variable_t *found = pkgconf_variable_find(&vars, "nonexistent");
	TEST_ASSERT_NULL(found);

	pkgconf_variable_list_free(&vars);
}

static void
test_variable_find_among_many(void)
{
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;

	pkgconf_variable_get_or_create(&vars, "prefix");
	pkgconf_variable_get_or_create(&vars, "exec_prefix");
	pkgconf_variable_get_or_create(&vars, "libdir");
	pkgconf_variable_get_or_create(&vars, "includedir");

	TEST_ASSERT_NONNULL(pkgconf_variable_find(&vars, "prefix"));
	TEST_ASSERT_NONNULL(pkgconf_variable_find(&vars, "exec_prefix"));
	TEST_ASSERT_NONNULL(pkgconf_variable_find(&vars, "libdir"));
	TEST_ASSERT_NONNULL(pkgconf_variable_find(&vars, "includedir"));
	TEST_ASSERT_NULL(pkgconf_variable_find(&vars, "notpresent"));

	pkgconf_variable_list_free(&vars);
}

static void
test_variable_delete(void)
{
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;

	pkgconf_variable_t *v = pkgconf_variable_get_or_create(&vars, "prefix");
	TEST_ASSERT_NONNULL(v);
	TEST_ASSERT_NONNULL(pkgconf_variable_find(&vars, "prefix"));

	pkgconf_variable_delete(&vars, v);
	TEST_ASSERT_NULL(pkgconf_variable_find(&vars, "prefix"));

	pkgconf_variable_list_free(&vars);
}

static void
test_variable_eval_str_plain(void)
{
	pkgconf_client_t *client = test_client_new();
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
	bool saw_sysroot = false;

	seed_variable(&vars, "version", "1.2.3");

	pkgconf_variable_t *v = pkgconf_variable_find(&vars, "version");
	TEST_ASSERT_NONNULL(v);

	char *out = pkgconf_variable_eval_str(client, &vars, v, &saw_sysroot);
	TEST_ASSERT_NONNULL(out);
	TEST_ASSERT_STRCMP_EQ(out, "1.2.3");

	free(out);
	pkgconf_variable_list_free(&vars);
	pkgconf_client_free(client);
}

static void
test_variable_eval_str_with_reference(void)
{
	pkgconf_client_t *client = test_client_new();
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
	bool saw_sysroot = false;

	// Standard pkg-config layout: libdir is derived from prefix.
	seed_variable(&vars, "prefix", "/opt/foo");
	seed_variable(&vars, "libdir", "${prefix}/lib");

	pkgconf_variable_t *libdir = pkgconf_variable_find(&vars, "libdir");
	TEST_ASSERT_NONNULL(libdir);

	char *out = pkgconf_variable_eval_str(client, &vars, libdir, &saw_sysroot);
	TEST_ASSERT_NONNULL(out);
	TEST_ASSERT_STRCMP_EQ(out, "/opt/foo/lib");

	free(out);
	pkgconf_variable_list_free(&vars);
	pkgconf_client_free(client);
}

static void
test_variable_eval_str_chained(void)
{
	pkgconf_client_t *client = test_client_new();
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;
	bool saw_sysroot = false;

	seed_variable(&vars, "prefix", "/usr/local");
	seed_variable(&vars, "exec_prefix", "${prefix}");
	seed_variable(&vars, "includedir", "${exec_prefix}/include");

	pkgconf_variable_t *includedir = pkgconf_variable_find(&vars, "includedir");
	TEST_ASSERT_NONNULL(includedir);

	char *out = pkgconf_variable_eval_str(client, &vars, includedir, &saw_sysroot);
	TEST_ASSERT_NONNULL(out);
	TEST_ASSERT_STRCMP_EQ(out, "/usr/local/include");

	free(out);
	pkgconf_variable_list_free(&vars);
	pkgconf_client_free(client);
}

static void
test_variable_eval_name_plain(void)
{
	pkgconf_client_t *client = test_client_new();
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;

	seed_variable(&vars, "version", "1.2.3");

	char *out = pkgconf_variable_eval_name(client, &vars, "version");
	TEST_ASSERT_NONNULL(out);
	TEST_ASSERT_STRCMP_EQ(out, "1.2.3");

	free(out);
	pkgconf_variable_list_free(&vars);
	pkgconf_client_free(client);
}

static void
test_variable_eval_name_with_reference(void)
{
	pkgconf_client_t *client = test_client_new();
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;

	seed_variable(&vars, "prefix", "/opt/foo");
	seed_variable(&vars, "libdir", "${prefix}/lib");

	char *out = pkgconf_variable_eval_name(client, &vars, "libdir");
	TEST_ASSERT_NONNULL(out);
	TEST_ASSERT_STRCMP_EQ(out, "/opt/foo/lib");

	free(out);
	pkgconf_variable_list_free(&vars);
	pkgconf_client_free(client);
}

static void
test_variable_eval_name_absent(void)
{
	pkgconf_client_t *client = test_client_new();
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;

	// Looking up a variable that was never defined yields no value.
	char *out = pkgconf_variable_eval_name(client, &vars, "nonexistent");
	TEST_ASSERT_NULL(out);

	pkgconf_variable_list_free(&vars);
	pkgconf_client_free(client);
}

static void
test_variable_eval_name_sysroot_prepended(void)
{
	pkgconf_client_t *client = test_client_new();
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;

	pkgconf_client_set_sysroot_dir(client, "/sysroot");
	seed_variable(&vars, "libdir", "/opt/foo/lib");

	// A plausible path that does not already begin with sysroot gets it prepended.
	char *out = pkgconf_variable_eval_name(client, &vars, "libdir");
	TEST_ASSERT_NONNULL(out);
	TEST_ASSERT_STRCMP_EQ(out, "/sysroot/opt/foo/lib");

	free(out);
	pkgconf_variable_list_free(&vars);
	pkgconf_client_free(client);
}

static void
test_variable_eval_name_sysroot_not_doubled(void)
{
	pkgconf_client_t *client = test_client_new();
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;

	pkgconf_client_set_sysroot_dir(client, "/sysroot");
	seed_variable(&vars, "libdir", "/sysroot/opt/foo/lib");

	// A value that already begins with sysroot must not have it prepended twice.
	char *out = pkgconf_variable_eval_name(client, &vars, "libdir");
	TEST_ASSERT_NONNULL(out);
	TEST_ASSERT_STRCMP_EQ(out, "/sysroot/opt/foo/lib");

	free(out);
	pkgconf_variable_list_free(&vars);
	pkgconf_client_free(client);
}

static void
test_variable_eval_name_non_path_no_sysroot(void)
{
	pkgconf_client_t *client = test_client_new();
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;

	pkgconf_client_set_sysroot_dir(client, "/sysroot");
	seed_variable(&vars, "version", "1.2.3");

	// A non-path value is left untouched even when sysroot is set.
	char *out = pkgconf_variable_eval_name(client, &vars, "version");
	TEST_ASSERT_NONNULL(out);
	TEST_ASSERT_STRCMP_EQ(out, "1.2.3");

	free(out);
	pkgconf_variable_list_free(&vars);
	pkgconf_client_free(client);
}

static void
test_variable_list_free_handles_empty(void)
{
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;

	// Freeing an empty list should be a no-op and not crash. Mostly an ASan/leak smoke test.
	pkgconf_variable_list_free(&vars);
}

static void
test_variable_list_free_handles_many(void)
{
	pkgconf_list_t vars = PKGCONF_LIST_INITIALIZER;

	/* Add a mix of bare and value-carrying variables, then free the whole list at once.
	 * Mostly an ASan/leak smoke test. */
	pkgconf_variable_get_or_create(&vars, "a");
	seed_variable(&vars, "b", "/path/b");
	pkgconf_variable_get_or_create(&vars, "c");
	seed_variable(&vars, "d", "${b}/sub");

	pkgconf_variable_list_free(&vars);
}

int
main(int argc, char *argv[])
{
	(void) argc;
	const char *basename = pkgconf_path_find_basename(argv[0]);

	TEST_RUN(basename, test_variable_new_and_free);
	TEST_RUN(basename, test_variable_get_or_create_creates);
	TEST_RUN(basename, test_variable_get_or_create_returns_existing);
	TEST_RUN(basename, test_variable_find_present);
	TEST_RUN(basename, test_variable_find_absent);
	TEST_RUN(basename, test_variable_find_among_many);
	TEST_RUN(basename, test_variable_delete);
	TEST_RUN(basename, test_variable_eval_str_plain);
	TEST_RUN(basename, test_variable_eval_str_with_reference);
	TEST_RUN(basename, test_variable_eval_str_chained);
	TEST_RUN(basename, test_variable_eval_name_plain);
	TEST_RUN(basename, test_variable_eval_name_with_reference);
	TEST_RUN(basename, test_variable_eval_name_absent);
	TEST_RUN(basename, test_variable_eval_name_sysroot_prepended);
	TEST_RUN(basename, test_variable_eval_name_sysroot_not_doubled);
	TEST_RUN(basename, test_variable_eval_name_non_path_no_sysroot);
	TEST_RUN(basename, test_variable_list_free_handles_empty);
	TEST_RUN(basename, test_variable_list_free_handles_many);

	return EXIT_SUCCESS;
}
