// Copyright © 2024 Rot127 <unisono@quyllur.org>
// SPDX-License-Identifier: BSD-3

#include "test_run.h"
#include "test_case.h"
#include "test_mapping.h"
#include "../../../utils.h"
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include "cmocka.h"
#include <capstone/capstone.h>
#include <stdbool.h>
#include <stdio.h>

static TestRunResult get_test_run_result(TestRunStats *stats)
{
	if (stats->tc_total !=
	    stats->successful + stats->failed + stats->skipped) {
		fprintf(stderr,
			"[!] Inconsistent statistics: total != successful + failed + skipped\n");
		stats->errors++;
		return TEST_RUN_ERROR;
	}

	if (stats->errors != 0) {
		return TEST_RUN_ERROR;
	} else if (stats->failed != 0) {
		return TEST_RUN_FAILURE;
	}
	return TEST_RUN_SUCCESS;
}

/// Extract all test cases from the given test files.
static TestFile **parse_test_files(char **tf_paths, uint32_t path_count,
				   TestRunStats *stats)
{
	TestFile **files = NULL;
	stats->tc_total = 0;

	for (size_t i = 0; i < path_count; ++i) {
		TestFile *test_file_data = NULL;
		cyaml_err_t err = cyaml_load_file(
			tf_paths[i], &cyaml_config, &test_file_schema,
			(cyaml_data_t **)&test_file_data, NULL);

		if (err != CYAML_OK || !test_file_data) {
			fprintf(stderr, "[!] Failed to parse test file '%s'\n",
				tf_paths[i]);
			fprintf(stderr, "[!] Error: '%s'\n",
				!test_file_data && err == CYAML_OK ?
					"Empty file" :
					cyaml_strerror(err));
			stats->invalid_files++;
			stats->errors++;
			continue;
		}

		size_t k = stats->valid_test_files++;
		// Copy all test cases of a test file
		files = cs_mem_realloc(files, sizeof(TestFile *) *
						      stats->valid_test_files);

		files[k] = test_file_clone(test_file_data);
		assert(files[k]);
		stats->tc_total += files[k]->test_cases_count;
		files[k]->filename = strrchr(tf_paths[i], '/') ?
					     strdup(strrchr(tf_paths[i], '/')) :
					     strdup(tf_paths[i]);

		err = cyaml_free(&cyaml_config, &test_file_schema,
				 test_file_data, 0);
		if (err != CYAML_OK) {
			fprintf(stderr, "[!] Error: '%s'\n",
				cyaml_strerror(err));
			stats->errors++;
			continue;
		}
	}

	return files;
}

/// Parses the @input and saves the results in the other arguments.
static bool parse_input_options(const TestInput *input, cs_arch *arch,
				cs_mode *mode, cs_opt *opt_arr,
				size_t opt_arr_size, size_t *opt_set)
{
	assert(input && arch && mode && opt_arr);
	bool arch_found = false;
	const char *opt_str = input->arch;

	int val = enum_map_bin_search(test_arch_map, ARR_SIZE(test_arch_map),
				      opt_str, &arch_found);
	if (arch_found) {
		*arch = val;
	} else {
		fprintf(stderr,
			"[!] '%s' is not mapped to a capstone architecture.\n",
			input->arch);
		return false;
	}

	*mode = 0;
	bool mode_found = false;
	size_t opt_idx = 0;
	char **options = input->options;
	for (size_t i = 0; i < input->options_count; ++i) {
		opt_str = options[i];
		val = enum_map_bin_search(test_mode_map,
					  ARR_SIZE(test_mode_map), opt_str,
					  &mode_found);
		if (mode_found) {
			*mode |= val;
			goto next_option;
		}

		// Might be an option descriptor
		for (size_t k = 0; k < ARR_SIZE(test_option_map); k++) {
			if (strings_match(opt_str, test_option_map[k].str)) {
				if (opt_idx >= opt_arr_size) {
					fprintf(stderr,
						"Too many options given in: '%s'. Maximum is: %" PRId64
						"\n",
						opt_str, opt_arr_size);
					return false;
				}
				opt_arr[opt_idx++] = test_option_map[k].opt;
				goto next_option;
			}
		}
		fprintf(stderr, "[!] Option: '%s' not used\n", opt_str);
next_option:
		continue;
	}
	*opt_set = opt_idx;
	return true;
}

/// Parses the options for cs_open/cs_option and initializes the handle.
/// Returns true for success and false otherwise.
static bool open_cs_handle(UnitTestState *ustate)
{
	cs_arch arch = 0;
	cs_mode mode = 0;
	cs_opt options[8] = { 0 };
	size_t options_set = 0;

	if (!parse_input_options(ustate->tcase->input, &arch, &mode, options, 8,
				 &options_set)) {
		char *tc_str = test_input_stringify(ustate->tcase->input, "");
		fprintf(stderr, "Could not parse options: %s\n", tc_str);
		cs_mem_free(tc_str);
		return false;
	}

	cs_err err = cs_open(arch, mode, &ustate->handle);
	if (err != CS_ERR_OK) {
		char *tc_str = test_input_stringify(ustate->tcase->input, "");
		fprintf(stderr,
			"[!] cs_open() failed with: '%s'. TestInput: %s\n",
			cs_strerror(err), tc_str);
		cs_mem_free(tc_str);
		return false;
	}

	// The bit mode must be set, otherwise the numbers are
	// not normalized correctly in the asm-test comparison step.
	if (arch == CS_ARCH_AARCH64 || mode & CS_MODE_64) {
		ustate->arch_bits = 64;
	} else if (mode & CS_MODE_16) {
		ustate->arch_bits = 16;
	} else {
		ustate->arch_bits = 32;
	}
	if (err != CS_ERR_OK) {
		goto option_error;
	}

	if (err != CS_ERR_OK) {
		goto option_error;
	}
	for (size_t i = 0; i < options_set; ++i) {
		err = cs_option(ustate->handle, options[i].type,
				options[i].val);
		if (err != CS_ERR_OK) {
			goto option_error;
		}
	}
	return true;

option_error: {
	char *tc_str = test_input_stringify(ustate->tcase->input, "");
	fprintf(stderr, "[!] cs_option() failed with: '%s'. TestInput: %s\n",
		cs_strerror(err), tc_str);
	cs_mem_free(tc_str);
	cs_close(&ustate->handle);
	return false;
}
}

static int cstest_unit_test_setup(void **state)
{
	assert(state);
	UnitTestState *ustate = *state;
	assert(ustate->tcase);
	if (!open_cs_handle(ustate)) {
		fail_msg("Failed to initialize Capstone with given options.");
		return -1;
	}
	return 0;
}

static int cstest_unit_test_teardown(void **state)
{
	if (!state) {
		return 0;
	}
	UnitTestState *ustate = *state;
	if (ustate->handle) {
		cs_err err = cs_close(&ustate->handle);
		if (err != CS_ERR_OK) {
			fail_msg("cs_close() failed with: '%s'.",
				 cs_strerror(err));
			return -1;
		}
	}
	return 0;
}

static void cstest_unit_test(void **state)
{
	assert(state);
	UnitTestState *ustate = *state;
	assert(ustate);
	assert(ustate->handle);
	assert(ustate->tcase);
	csh handle = ustate->handle;
	TestCase *tcase = ustate->tcase;

	cs_insn *insns = NULL;
	size_t insns_count = cs_disasm(handle, tcase->input->bytes,
				       tcase->input->bytes_count,
				       tcase->input->address, 0, &insns);
	test_expected_compare(&ustate->handle, tcase->expected, insns,
			      insns_count, ustate->arch_bits);
	cs_free(insns, insns_count);
}

static void eval_test_cases(TestFile **test_files, TestRunStats *stats)
{
	assert(test_files && stats);
	// CMocka's API doesn't allow to init a CMUnitTest with a partially initialized state
	// (which is later initialized in the test setup).
	// So we do it manually here.
	struct CMUnitTest *utest_table =
		cs_mem_calloc(sizeof(struct CMUnitTest),
			      stats->tc_total); // Number of test cases.

	char utest_id[128] = { 0 };

	size_t tci = 0;
	for (size_t i = 0; i < stats->valid_test_files; ++i) {
		TestCase **test_cases = test_files[i]->test_cases;
		const char *filename = test_files[i]->filename ?
					       test_files[i]->filename :
					       NULL;

		for (size_t k = 0; k < test_files[i]->test_cases_count;
		     ++k, ++tci) {
			cs_snprintf(utest_id, sizeof(utest_id),
				    "%s - TC #%" PRIx32 ": ", filename, k);
			if (test_cases[k]->skip) {
				char *tc_name = test_input_stringify(
					test_cases[k]->input, utest_id);
				fprintf(stderr, "SKIP: %s\nReason: %s\n",
					tc_name, test_cases[k]->skip_reason);
				cs_mem_free(tc_name);
				stats->skipped++;
				continue;
			}

			UnitTestState *ut_state =
				cs_mem_calloc(sizeof(UnitTestState), 1);
			ut_state->tcase = test_cases[k];
			utest_table[tci].name = test_input_stringify(
				ut_state->tcase->input, utest_id);
			utest_table[tci].initial_state = ut_state;
			utest_table[tci].setup_func = cstest_unit_test_setup;
			utest_table[tci].teardown_func =
				cstest_unit_test_teardown;
			utest_table[tci].test_func = cstest_unit_test;
		}
	}
	// Use private function here, because the API takes only constant tables.
	int failed_tests = _cmocka_run_group_tests(
		"All test cases", utest_table, stats->tc_total, NULL, NULL);
	for (size_t i = 0; i < stats->tc_total; ++i) {
		cs_mem_free((char *)utest_table[i].name);
		cs_mem_free(utest_table[i].initial_state);
	}
	cs_mem_free(utest_table);
	stats->failed += failed_tests;
	stats->successful += stats->tc_total - failed_tests - stats->skipped;
}

/// Runs runs all valid tests in the given @test_files
/// and returns the result as well as statistics in @stats.
TestRunResult cstest_run_tests(char **test_file_paths, uint32_t path_count,
			       TestRunStats *stats)
{
	TestFile **files = parse_test_files(test_file_paths, path_count, stats);
	if (!files) {
		return get_test_run_result(stats);
	}
	eval_test_cases(files, stats);
	for (size_t i = 0; i < stats->valid_test_files; ++i) {
		test_file_free(files[i]);
	}
	cs_mem_free(files);

	return get_test_run_result(stats);
}
