/*
 * SPDX-License-Identifier: ISC
 *
 * Copyright (C) 2018-2021 Michael Drake <tlsa@netsurf-browser.org>
 */

#include <stdbool.h>
#include <assert.h>
#include <string.h>
#include <stdio.h>

#include <cyaml/cyaml.h>

#include "../../src/mem.h"
#include "../../src/util.h"

#include "ttest.h"
#include "test.h"

/**
 * Test cyaml memory functions.
 *
 * \param[in]  report  The test report context.
 * \param[in]  config  The CYAML config to use for the test.
 * \return true if test passes, false otherwise.
 */
static bool test_util_memory_funcs(
		ttest_report_ctx_t *report,
		const cyaml_config_t *config)
{
	ttest_ctx_t tc;
	unsigned char *mem, *tmp;

	if (!ttest_start(report, __func__, NULL, NULL, &tc)) return true;

	/* Create test allocation */
	mem = cyaml__alloc(config, 0xff, true);
	if (mem == NULL) {
		return ttest_fail(&tc, "Memory allocation failed.");
	}

	/* Check allocation was zeroed. */
	for (unsigned i = 0; i < 0x7f; i++) {
		if (mem[i] != 0) {
			return ttest_fail(&tc, "Allocation not cleaned.");
		}
	}

	/* Set our own known values */
	for (unsigned i = 0; i < 0xff; i++) {
		mem[i] = 0xff;
	}

	/* Shrink allocation */
	tmp = cyaml__realloc(config, mem, 0xff, 0x7f, true);
	if (tmp == NULL) {
		return ttest_fail(&tc, "Allocation shrink failed.");
	}
	mem = tmp;

	/* Check for our own values. */
	for (unsigned i = 0; i < 0x7f; i++) {
		if (mem[i] != 0xff) {
			return ttest_fail(&tc, "Data trampled by realloc.");
		}
	}

	/* Free test allocation. */
	cyaml__free(config, mem);

	return ttest_pass(&tc);
}

/**
 * Test cyaml memory functions.
 *
 * \param[in]  report  The test report context.
 * \param[in]  config  The CYAML config to use for the test.
 * \return true if test passes, false otherwise.
 */
static bool test_util_strdup(
		ttest_report_ctx_t *report,
		const cyaml_config_t *config)
{
	ttest_ctx_t tc;
	const char *orig = "Hello!";
	char *copy;

	if (!ttest_start(report, __func__, NULL, NULL, &tc)) return true;

	/* Create test allocation */
	copy = cyaml__strdup(config, orig, NULL);
	if (copy == NULL) {
		return ttest_fail(&tc, "Memory allocation failed.");
	}

	/* Confirm string duplication. */
	if (strcmp(orig, copy) != 0) {
		return ttest_fail(&tc, "String not copied correctly.");
	}

	/* Free test allocation. */
	cyaml__free(config, copy);

	return ttest_pass(&tc);
}

/**
 * Test cyaml memory functions.
 *
 * \param[in]  report  The test report context.
 * \param[in]  config  The CYAML config to use for the test.
 * \return true if test passes, false otherwise.
 */
static bool test_util_strdup_len(
		ttest_report_ctx_t *report,
		const cyaml_config_t *config)
{
	ttest_ctx_t tc;
	const char *orig = "Hello!";
	char *copy;
	size_t len;

	if (!ttest_start(report, __func__, NULL, NULL, &tc)) return true;

	/* Create test allocation */
	copy = cyaml__strdup(config, orig, &len);
	if (copy == NULL) {
		return ttest_fail(&tc, "Memory allocation failed.");
	}

	/* Confirm string duplication. */
	if (strcmp(orig, copy) != 0) {
		return ttest_fail(&tc, "String not copied correctly.");
	}

	/* Confirm string length. */
	if (strlen(orig) != len) {
		return ttest_fail(&tc, "Bad string length.");
	}

	/* Free test allocation. */
	cyaml__free(config, copy);

	return ttest_pass(&tc);
}

/**
 * Test invalid state machine state.
 *
 * \param[in]  report  The test report context.
 * \return true if test passes, false otherwise.
 */
static bool test_util_state_invalid(
		ttest_report_ctx_t *report)
{
	ttest_ctx_t tc;

	if (!ttest_start(report, __func__, NULL, NULL, &tc)) return true;

	if (strcmp(cyaml__state_to_str(CYAML_STATE__COUNT),
			"<invalid>") != 0) {
		return ttest_fail(&tc, "Wrong string for invalid state.");
	}

	if (strcmp(cyaml__state_to_str(CYAML_STATE__COUNT + 1),
			"<invalid>") != 0) {
		return ttest_fail(&tc, "Wrong string for invalid state.");
	}

	return ttest_pass(&tc);
}

/**
 * Test CYAML_OK error code.
 *
 * \param[in]  report  The test report context.
 * \return true if test passes, false otherwise.
 */
static bool test_util_err_success_zero(
		ttest_report_ctx_t *report)
{
	ttest_ctx_t tc;

	if (!ttest_start(report, __func__, NULL, NULL, &tc)) return true;

	if (CYAML_OK != 0) {
		return ttest_fail(&tc, "CYAML_OK value not zero");
	}

	return ttest_pass(&tc);
}

/**
 * Test valid cyaml_strerror strings.
 *
 * \param[in]  report  The test report context.
 * \return true if test passes, false otherwise.
 */
static bool test_util_err_strings_valid(
		ttest_report_ctx_t *report)
{
	ttest_ctx_t tc;

	if (!ttest_start(report, __func__, NULL, NULL, &tc)) return true;

	if (cyaml_strerror(CYAML_OK) == NULL) {
		return ttest_fail(&tc, "CYAML_OK string is NULL");
	}

	if (strcmp(cyaml_strerror(CYAML_OK), "Success") != 0) {
		return ttest_fail(&tc, "CYAML_OK string not 'Success'");
	}

	for (unsigned i = 1; i <= CYAML_ERR__COUNT; i++) {
		if (cyaml_strerror(i) == NULL) {
			return ttest_fail(&tc, "Error code %u string is NULL",
					i);
		}

		for (unsigned j = 0; j < i; j++) {
			if (strcmp(cyaml_strerror(j), cyaml_strerror(i)) == 0) {
				return ttest_fail(&tc, "Two error codes "
						"have same string (%u and %u)",
						i, j);
			}
		}
	}

	return ttest_pass(&tc);
}

/**
 * Test invalid cyaml_strerror strings.
 *
 * \param[in]  report  The test report context.
 * \return true if test passes, false otherwise.
 */
static bool test_util_err_strings_invalid(
		ttest_report_ctx_t *report)
{
	ttest_ctx_t tc;

	if (!ttest_start(report, __func__, NULL, NULL, &tc)) return true;

	if (strcmp(cyaml_strerror(CYAML_ERR__COUNT),
			"Invalid error code") != 0) {
		return ttest_fail(&tc, "CYAML_ERR__COUNT string not "
				"'Invalid error code'");
	}

	if (strcmp(cyaml_strerror(CYAML_ERR__COUNT + 1),
			"Invalid error code") != 0) {
		return ttest_fail(&tc, "CYAML_ERR__COUNT + 1 string not "
				"'Invalid error code'");
	}

	if (strcmp(cyaml_strerror((cyaml_err_t)-1), "Invalid error code") != 0) {
		return ttest_fail(&tc, "-1 string not 'Invalid error code'");
	}

	return ttest_pass(&tc);
}

/**
 * Run the CYAML util unit tests.
 *
 * \param[in]  rc         The ttest report context.
 * \param[in]  log_level  CYAML log level.
 * \param[in]  log_fn     CYAML logging function, or NULL.
 * \return true iff all unit tests pass, otherwise false.
 */
bool util_tests(
		ttest_report_ctx_t *rc,
		cyaml_log_t log_level,
		cyaml_log_fn_t log_fn)
{
	bool pass = true;
	cyaml_config_t config = {
		.log_fn = log_fn,
		.mem_fn = cyaml_mem,
		.log_level = log_level,
		.flags = CYAML_CFG_DEFAULT,
	};

	ttest_heading(rc, "Memory utility functions");

	pass &= test_util_strdup(rc, &config);
	pass &= test_util_strdup_len(rc, &config);
	pass &= test_util_memory_funcs(rc, &config);

	ttest_heading(rc, "Error code tests");

	pass &= test_util_state_invalid(rc);
	pass &= test_util_err_success_zero(rc);
	pass &= test_util_err_strings_valid(rc);
	pass &= test_util_err_strings_invalid(rc);

	return pass;
}
