// SPDX-License-Identifier: LGPL-2.0-only
// SPDX-FileCopyrightText: 2023 Oleksij Rempel <linux@rempel-privat.de>

#include <errno.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>
#include <time.h>

#include "isobusfs_cli.h"
#include "isobusfs_cmn.h"
#include "isobusfs_cmn_fa.h"

size_t current_test;
bool test_running;
struct timespec test_start_time;

struct isobusfs_cli_test_case {
	int (*test_func)(struct isobusfs_priv *priv, bool *complete);
	const char *test_description;
};

static int isobusfs_cli_test_connect(struct isobusfs_priv *priv, bool *complete)
{
	struct timespec current_time;
	int ret;

	clock_gettime(CLOCK_MONOTONIC, &current_time);

	switch (priv->state) {
	case ISOBUSFS_CLI_STATE_SELFTEST:
		test_start_time = current_time;
		priv->state = ISOBUSFS_CLI_STATE_CONNECTING;
		/* fall through */
	case ISOBUSFS_CLI_STATE_CONNECTING:
		if (priv->fs_is_active) {
			*complete = true;
			break;
		}

		if (current_time.tv_sec - test_start_time.tv_sec >= 5) {
			ret = -ETIMEDOUT;
			goto test_fail;
		}
		break;
	default:
		pr_err("%s:%i: unknown state: %d", __func__, __LINE__, priv->state);
		ret = -EINVAL;
		goto test_fail;
	}

	return 0;

test_fail:
	/* without server all other tests make no sense */
	priv->run_selftest = false;
	*complete = true;

	return ret;
}

static int isobusfs_cli_test_property_req(struct isobusfs_priv *priv, bool *complete)
{
	struct timespec current_time;
	int ret;

	clock_gettime(CLOCK_MONOTONIC, &current_time);

	switch (priv->state) {
	case ISOBUSFS_CLI_STATE_SELFTEST:
		test_start_time = current_time;

		ret = isobusfs_cli_property_req(priv);
		if (ret)
			goto test_fail;

		break;
	case ISOBUSFS_CLI_STATE_WAIT_FS_PROPERTIES:
		if (current_time.tv_sec - test_start_time.tv_sec >= 5) {
			ret = -ETIMEDOUT;
			goto test_fail;
		}
		break;
	case ISOBUSFS_CLI_STATE_GET_FS_PROPERTIES_DONE:
		*complete = true;
		break;
	default:
		pr_err("%s:%i: unknown state: %d", __func__, __LINE__, priv->state);
		ret = -EINVAL;
		goto test_fail;
	}

	return 0;

test_fail:
	*complete = true;

	return ret;
}

static int isobusfs_cli_test_volume_status_req(struct isobusfs_priv *priv, bool *complete)
{
	static const char volume_name[] = "\\\\vol1";
	struct timespec current_time;
	int ret;

	clock_gettime(CLOCK_MONOTONIC, &current_time);

	switch (priv->state) {
	case ISOBUSFS_CLI_STATE_SELFTEST:
		test_start_time = current_time;

		ret = isobusfs_cli_volume_status_req(priv, 0,
				       sizeof(volume_name) - 1, volume_name);
		if (ret)
			goto test_fail;

		break;
	case ISOBUSFS_CLI_STATE_WAIT_VOLUME_STATUS:
		if (current_time.tv_sec - test_start_time.tv_sec >= 5) {
			ret = -ETIMEDOUT;
			goto test_fail;
		}
		break;
	case ISOBUSFS_CLI_STATE_VOLUME_STATUS_DONE:
		*complete = true;
		break;
	default:
		pr_err("%s:%i: unknown state: %d", __func__, __LINE__, priv->state);
		ret = -EINVAL;
		goto test_fail;
	}

	return 0;

test_fail:
	*complete = true;

	return ret;
}

static int isobusfs_cli_test_current_dir_req(struct isobusfs_priv *priv,
					     bool *complete)
{
	struct timespec current_time;
	int ret;

	clock_gettime(CLOCK_MONOTONIC, &current_time);

	switch (priv->state) {
	case ISOBUSFS_CLI_STATE_SELFTEST:
		test_start_time = current_time;

		ret = isobusfs_cli_get_current_dir_req(priv);
		if (ret)
			goto test_fail;
		break;
	case ISOBUSFS_CLI_STATE_WAIT_CURRENT_DIR:
		if (current_time.tv_sec - test_start_time.tv_sec >= 5) {
			ret = -ETIMEDOUT;
			goto test_fail;
		}
		break;
	case ISOBUSFS_CLI_STATE_GET_CURRENT_DIR_DONE:
		*complete = true;
		break;
	default:
		pr_err("%s:%i: unknown state: %d", __func__, __LINE__, priv->state);
		ret = -EINVAL;
		goto test_fail;
	}

	return 0;

test_fail:
	*complete = true;

	return ret;
}

struct isobusfs_cli_test_dir_path {
	const char *dir_name;
	bool expect_pass;
};

static struct isobusfs_cli_test_dir_path test_dir_patterns[] = {
	/* expected result \\vol1\dir1\ */
	{ "\\\\vol1\\dir1", true },
	/* expected result \\vol1\dir1\dir2\ */
	{ "\\\\vol1\\dir1\\dir2", true },
	/* expected result \\vol1\dir1\dir2\dir3\dir4\ */
	{ ".\\dir3\\dir4", true },
	/* expected result \\vol1\dir1\dir2\dir3\dir5\ */
	{ "..\\dir5", true },
	/* expected result \\vol1\ */
	{ "..\\..\\..\\..\\..\\..\\vol1", true },
	/* expected result \\vol1\~\ */
	{ "~\\", true },
	/* expected result \\vol1\~\msd_dir1\msd_dir2\ */
	{ "~\\msd_dir1\\msd_dir2", true },
	/* expected result \\vol1\~\ */
	{ "\\\\vol1\\~\\", true },
	/* expected result \\vol1\~\msd_dir1\msd_dir2\ */
	{ "\\\\vol1\\~\\msd_dir1\\msd_dir2", true },
	/* expected result \\vol1\~\msd_dir1\msd_dir2\~\ */
	{ ".\\~\\", true },
	/* expected result \\vol1\~\msd_dir1\msd_dir2\~\~tilde_dir */
	{ "~tilde_dir", true },
	/* expected result \\vol1\dir1\~\ */
	{ "\\\\vol1\\dir1\\~", true },
	/* expected result \\vol1\~\ not clear if it is manufacture speficic dir */
	{ "\\~\\", true },
	/* expected result \\~\ */
	{ "\\\\~\\", false },
	/* expected result: should fail */
	{ "\\\\\\\\\\\\\\\\", false },
	/* Set back to dir1 for other test. Expected result \\vol1\dir1\ */
	{ "\\\\vol1\\dir1", true },

	/* Initialize server path to root: Expected initial state: root */
	{ "\\\\vol1\\dir1", true }, /* Set server path to \\vol1\dir1\ */

	/* Test absolute paths: Expected state: \\vol1\dir1\ */
	{ "\\\\vol1\\dir1\\dir2", true }, /* Changes to \\vol1\dir1\dir2\ */
	{ "\\\\vol1\\dir1", true }, /* Changes back to \\vol1\dir1\ */

	/* Test relative path .\ : Expected state: \\vol1\dir1\ */
	{ ".\\dir2\\dir3\\dir4", true }, /* Changes to \\vol1\dir1\dir2\dir3\dir4\ */
	{ "..\\dir5", true }, /* Changes to \\vol1\dir1\dir2\dir3\dir5\ */
	{ "..\\..\\..\\..\\..\\..\\vol1", true }, /* Changes to \\vol1\ */
	{ ".\\dir1\\dir2", true }, /* Changes back to \\vol1\dir1\dir2 */

	/* Test relative path ..\ with multiple backslashes:
	 * Expected state: \\vol1\dir1\dir2\
	 */
	{ "..\\\\\\", true }, /* Changes to \\vol1\dir1\ */
	{ ".\\dir2", true }, /* Changes back to \\vol1\dir1\dir2\ */
	{ "..\\\\\\\\\\\\\\", true }, /* Changes to \\vol1\dir1\ */
	{ ".\\dir2", true }, /* Changes back to \\vol1\dir1\dir2 */
	{ "\\\\vol1\\dir1", true }, /* Changes back to \\vol1\dir1\ */

	/* Test relative path .\ with multiple backslashes: Expected state:
	 * \\vol1\dir1\
	 */
	{ ".\\\\\\", true }, /* Remains at \\vol1\dir1\ */
	{ ".\\dir2", true }, /* Changes back to \\vol1\dir1\dir2 */
	{ "..\\", true }, /* Changes to \\vol1\dir1\ */
	{ ".\\\\\\\\\\\\\\", true }, /* Remains at \\vol1\dir1\ */
	{ ".\\dir2", true }, /* Changes back to \\vol1\dir1\dir2 */
	{ "..\\", true }, /* Changes to \\vol1\dir1\ */

	/* Test navigating up and down: Expected state: \\vol1\dir1\ */
	{ "..\\..\\..\\..\\..\\..\\vol1", true }, /* Changes to \\vol1\ */

	/* prepare for tilde tests */
	{ "\\\\vol1\\", true }, /* Set server path to \\vol1\ */
	/* Tilde used correctly at the beginning of a path */
	{ "~\\", true }, /* Replace with the manufacturer-specific directory on
			  * the current volume
			  */
	/* Tilde used correctly after a volume name */
	{ "\\\\vol1\\~\\", true }, /* Replace ~ with the manufacturer-specific
				    * directory on vol1
				    */

	/* Tilde used in non-root locations, treated as a regular directory */
	{ "\\\\vol1\\dir1\\~", true }, /* Treated as a regular directory named
					* '~' under \\vol1\dir1\
					*/
	{ ".\\~\\", true }, /* Treated as a regular directory named '~' in the
			     * current directory: \\vol1\dir1\~\
			     */

	/* Tilde used with a specific manufacturer directory at the root */
	{ "~\\msd_dir1\\msd_dir2", true }, /* Replace ~ and append rest of the
					    * path on the current volume
					    */
	{ "\\\\vol1\\~\\msd_dir1\\msd_dir2", true },
					/* Replace ~ and append rest of the
					 * path on vol1
					 */
	{ ".\\~\\", true }, /* Treated as a regular directory named '~' in the
			     * current directory: \\vol1\~\msd_dir1\msd_dir2\~\
			     */
	{ "~tilde_dir", true }, /* Treated as a regular directory named
				 * '~tilde_dir' in the current directory:
				 * \\vol1\~\msd_dir1\msd_dir2\~\~tilde_dir\
				 */
	/* Invalid usage of tilde at non-root locations (as a
	 * manufacturer-specific directory)
	 */
	{ "\\~\\", false }, /* Incorrect usage of tilde at non-root, expected
			     * to fail
			     */ 
	{ "\\\\~\\", false }, /* Incorrect usage of tilde at non-root, expected
			       * to fail
			       */

	/* Test invalid or ambiguous paths: Expected state: \\vol1\dir1\ */
	{ "\\\\\\\\\\\\\\\\", false }, /* Invalid path, should fail */

	/* Set back to dir1 for other tests: Expected state: \\vol1\dir1\ */
	{ "\\\\vol1\\dir1", true }, /* Ensure server path is set to \\vol1\dir1\ */
};

size_t current_dir_pattern_test;

static int isobusfs_cli_test_ccd_req(struct isobusfs_priv *priv, bool *complete)
{
	size_t num_patterns = ARRAY_SIZE(test_dir_patterns);
	struct isobusfs_cli_test_dir_path *tp =
		&test_dir_patterns[current_dir_pattern_test];
	struct timespec current_time;
	bool fail = false;
	int ret;

	clock_gettime(CLOCK_MONOTONIC, &current_time);

	switch (priv->state) {
	case ISOBUSFS_CLI_STATE_SELFTEST:
		test_start_time = current_time;
		pr_info("Start pattern test: %s", tp->dir_name);
		ret = isobusfs_cli_ccd_req(priv, tp->dir_name,
					   strlen(tp->dir_name));
		if (ret)
			goto test_fail;
		break;
	case ISOBUSFS_CLI_STATE_WAIT_CCD_RESP:
		if (current_time.tv_sec - test_start_time.tv_sec >= 5) {
			ret = -ETIMEDOUT;
			goto test_fail;
		}
		break;
	case ISOBUSFS_CLI_STATE_CCD_FAIL:
		fail = true;
		/* fallthrough */
	case ISOBUSFS_CLI_STATE_CCD_DONE:
		if (tp->expect_pass && fail) {
			pr_err("pattern test failed: %s", tp->dir_name);
			ret = -EINVAL;
			goto test_fail;
		} else if (!tp->expect_pass && !fail) {
			pr_err("pattern test failed: %s", tp->dir_name);
			ret = -EINVAL;
			goto test_fail;
		}
		current_dir_pattern_test++;

		if (current_dir_pattern_test >= num_patterns) {
			*complete = true;
			break;
		}

		priv->state = ISOBUSFS_CLI_STATE_SELFTEST;
		break;
	default:
		pr_err("%s:%i: unknown state: %d", __func__, __LINE__, priv->state);
		ret = -EINVAL;
		goto test_fail;
	}

	return 0;

test_fail:
	*complete = true;
	return ret;
}

struct isobusfs_cli_test_of_path {
	const char *path_name;
	uint8_t flags;
	bool expect_pass;
};

static struct isobusfs_cli_test_of_path test_of_patterns[] = {
	/* expected result \\vol1\dir1\dir2\ */
	{ "\\\\vol1\\dir1\\dir2", 0, false },
	{ "\\\\vol1\\dir1\\dir2\\file0", 0, true },
};

size_t current_of_pattern_test;

static int isobusfs_cli_test_of_req(struct isobusfs_priv *priv, bool *complete)
{
	size_t num_patterns = ARRAY_SIZE(test_of_patterns);
	struct isobusfs_cli_test_of_path *tp =
		&test_of_patterns[current_of_pattern_test];
	struct timespec current_time;
	int ret;

	clock_gettime(CLOCK_MONOTONIC, &current_time);

	switch (priv->state) {
	case ISOBUSFS_CLI_STATE_SELFTEST:
		test_start_time = current_time;
		pr_info("Start pattern test: %s", tp->path_name);
		ret = isobusfs_cli_fa_of_req(priv, tp->path_name,
					     strlen(tp->path_name), tp->flags);
		if (ret)
			goto test_fail;

		break;
	case ISOBUSFS_CLI_STATE_WAIT_OF_RESP:
		if (current_time.tv_sec - test_start_time.tv_sec >= 5) {
			ret = -ETIMEDOUT;
			goto test_fail;
		}
		break;
	case ISOBUSFS_CLI_STATE_OF_FAIL:
		if (tp->expect_pass) {
			pr_err("pattern test failed: %s", tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		}

		priv->state = ISOBUSFS_CLI_STATE_TEST_DONE;
		break;
	case ISOBUSFS_CLI_STATE_OF_DONE:
		if (!tp->expect_pass) {
			pr_err("pattern test failed: %s", tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		}

		if (priv->handle != ISOBUSFS_FILE_HANDLE_ERROR)
			isobusfs_cli_fa_cf_req(priv, priv->handle);
		else
			priv->state = ISOBUSFS_CLI_STATE_TEST_DONE;

		break;
	case ISOBUSFS_CLI_STATE_CF_FAIL:
		pr_err("failed to close file: %s", tp->path_name);
		ret = -EINVAL;
		goto test_fail;
	case ISOBUSFS_CLI_STATE_CF_DONE:
	case ISOBUSFS_CLI_STATE_TEST_DONE:
		current_of_pattern_test++;

		if (current_of_pattern_test >= num_patterns)
			*complete = true;
		else
			priv->state = ISOBUSFS_CLI_STATE_SELFTEST;

		break;
	default:
		pr_err("%s:%i: unknown state: %d", __func__, __LINE__, priv->state);
		ret = -EINVAL;
		goto test_fail;
	}

	return 0;

test_fail:
	*complete = true;
	return ret;
}

struct isobusfs_cli_test_sf_path {
	const char *path_name;
	uint8_t flags;
	uint32_t offset;
	uint32_t read_size;
	bool expect_pass;
};

static struct isobusfs_cli_test_sf_path test_sf_patterns[] = {
	/* expected result \\vol1\dir1\dir2\ */
	{ "\\\\vol1\\dir1\\dir2\\file1k", 0, 0, 0, true },
	{ "\\\\vol1\\dir1\\dir2\\file1k", 0, 10, 0, true },
};

size_t current_sf_pattern_test;

static int isobusfs_cli_test_sf_req(struct isobusfs_priv *priv, bool *complete)
{
	size_t num_patterns = ARRAY_SIZE(test_sf_patterns);
	struct isobusfs_cli_test_sf_path *tp =
		&test_sf_patterns[current_sf_pattern_test];
	struct timespec current_time;
	int ret;

	clock_gettime(CLOCK_MONOTONIC, &current_time);

	switch (priv->state) {
	case ISOBUSFS_CLI_STATE_SELFTEST:
		test_start_time = current_time;
		pr_info("Start pattern test: %s", tp->path_name);
		ret = isobusfs_cli_fa_of_req(priv, tp->path_name,
					     strlen(tp->path_name), tp->flags);
		if (ret)
			goto test_fail;

		break;
	case ISOBUSFS_CLI_STATE_WAIT_OF_RESP:
		if (current_time.tv_sec - test_start_time.tv_sec >= 5) {
			ret = -ETIMEDOUT;
			goto test_fail;
		}
		break;
	case ISOBUSFS_CLI_STATE_OF_FAIL:
		if (tp->expect_pass) {
			pr_err("pattern test failed: %s", tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		}

		priv->state = ISOBUSFS_CLI_STATE_TEST_DONE;
		break;
	case ISOBUSFS_CLI_STATE_OF_DONE:
		if (!tp->expect_pass) {
			pr_err("pattern test failed: %s", tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		}

		ret = isobusfs_cli_fa_sf_req(priv, priv->handle,
					     ISOBUSFS_FA_SEEK_SET, tp->offset);
		if (ret)
			goto test_fail;
		break;
	case ISOBUSFS_CLI_STATE_SF_FAIL:
		if (tp->expect_pass) {
			pr_err("pattern test failed: %s", tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		}

		priv->state = ISOBUSFS_CLI_STATE_TEST_DONE;
		break;
	case ISOBUSFS_CLI_STATE_SF_DONE:
		if (!tp->expect_pass) {
			pr_err("pattern test failed: %s", tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		}

		if (priv->read_offset != tp->offset) {
			pr_err("Not expected read offset: %s", tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		}

		ret = isobusfs_cli_fa_rf_req(priv, priv->handle,
			   tp->read_size);
		if (ret)
			goto test_fail;
		break;
	case ISOBUSFS_CLI_STATE_RF_FAIL:
		if (tp->expect_pass) {
			pr_err("pattern test failed: %s", tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		}

		priv->state = ISOBUSFS_CLI_STATE_TEST_CLEANUP;
		break;
	case ISOBUSFS_CLI_STATE_RF_DONE:
		if (!tp->expect_pass) {
			pr_err("pattern test failed: %s", tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		}

		/* fall troth */
	case ISOBUSFS_CLI_STATE_TEST_CLEANUP:
		if (priv->handle != ISOBUSFS_FILE_HANDLE_ERROR)
			isobusfs_cli_fa_cf_req(priv, priv->handle);
		else
			priv->state = ISOBUSFS_CLI_STATE_TEST_DONE;

		break;
	case ISOBUSFS_CLI_STATE_CF_FAIL:
		pr_err("failed to close file: %s", tp->path_name);
		ret = -EINVAL;
		goto test_fail;
	case ISOBUSFS_CLI_STATE_CF_DONE:
	case ISOBUSFS_CLI_STATE_TEST_DONE:
		current_of_pattern_test++;

		if (current_of_pattern_test >= num_patterns)
			*complete = true;
		else
			priv->state = ISOBUSFS_CLI_STATE_SELFTEST;
		break;
	default:
		pr_err("%s:%i: unknown state: %d", __func__, __LINE__, priv->state);
		ret = -EINVAL;
		goto test_fail;
	}

	return 0;

test_fail:
	*complete = true;
	return ret;
}

struct isobusfs_cli_test_rf_path {
	const char *path_name;
	uint8_t flags;
	uint32_t offset;
	uint32_t read_size;
	bool expect_pass;
};

static struct isobusfs_cli_test_rf_path test_rf_patterns[] = {
	/* expected result \\vol1\dir1\dir2\ */
	{ "\\\\vol1\\dir1\\dir2\\file1k", 0, 0, 0, true },
	{ "\\\\vol1\\dir1\\dir2\\file1k", 0, 0, 1, true },
	{ "\\\\vol1\\dir1\\dir2\\file1k", 0, 1, 1, true },
	{ "\\\\vol1\\dir1\\dir2\\file1k", 0, 2, 1, true },
	{ "\\\\vol1\\dir1\\dir2\\file1k", 0, 3, 1, true },
	{ "\\\\vol1\\dir1\\dir2\\file1m", 0, 0, 8, true },
	{ "\\\\vol1\\dir1\\dir2\\file1m", 0, 0, 8 * 100, true },
	{ "\\\\vol1\\dir1\\dir2\\file1m", 0, 100, 8 * 100, true },
	{ "\\\\vol1\\dir1\\dir2\\file1m", 0, 0, ISOBUSFS_MAX_DATA_LENGH, true },
	{ "\\\\vol1\\dir1\\dir2\\file1m", 0, 0, (ISOBUSFS_MAX_DATA_LENGH & ~3) + 16, true },
	{ "\\\\vol1\\dir1\\dir2\\file1m", 0, 0, ISOBUSFS_MAX_DATA_LENGH + 1, true },
	{ "\\\\vol1\\dir1\\dir2\\file1m", 0, 0, -1, true },
};

size_t current_rf_pattern_test;

static uint32_t isobusfs_cli_calculate_sum(uint8_t *data, size_t size,
					   uint32_t offset)
{
	const uint8_t xor_pattern[] = {0xde, 0xad, 0xbe, 0xef};
	uint32_t actual_sum = 0;
	uint32_t current_value = 0;
	uint8_t byte_offset = 0;
	size_t idx;

	byte_offset = offset % 4;

	for (idx = 0; idx < size; idx++) {
		uint8_t byte;

		if (data) {
			byte = data[idx] ^ xor_pattern[byte_offset];
			current_value |= (byte << ((3 - byte_offset) * 8));
		} else {
			uint32_t value_at_offset;

			/* if no data is provided, generate the data based on
			 * offset
			 */

			value_at_offset = (offset + idx) / 4;
			byte_offset = (offset + idx) % 4;
			byte = (value_at_offset >> ((3 - byte_offset) * 8)) & 0xff;
			current_value |= (byte << ((3 - byte_offset) * 8));
		}

		if (byte_offset == 3) {
			actual_sum += current_value;
			current_value = 0;
		}

		byte_offset = (byte_offset + 1) % 4;

		/* if this is the last byte in the buffer but it's not aligned,
		 * add the partial uint32_t to the sum
		 */
		if (idx == size - 1 && byte_offset != 0)
			actual_sum += current_value;
	}

	return actual_sum;
}

static int isobusfs_cli_test_rf_req(struct isobusfs_priv *priv, bool *complete)
{
	size_t num_patterns = ARRAY_SIZE(test_rf_patterns);
	struct isobusfs_cli_test_rf_path *tp =
		&test_rf_patterns[current_rf_pattern_test];
	uint32_t actual_sum, expected_sum;
	struct timespec current_time;
	ssize_t remaining_size, read_size;
	int ret;

	clock_gettime(CLOCK_MONOTONIC, &current_time);

	switch (priv->state) {
	case ISOBUSFS_CLI_STATE_SELFTEST:
		test_start_time = current_time;
		pr_info("Start read test. Path: %s, size: %d, offset: %d, flags: %x",
			tp->path_name, tp->read_size, tp->offset, tp->flags);
		ret = isobusfs_cli_fa_of_req(priv, tp->path_name,
					     strlen(tp->path_name), tp->flags);
		if (ret)
			goto test_fail;

		break;
	case ISOBUSFS_CLI_STATE_WAIT_OF_RESP:
		if (current_time.tv_sec - test_start_time.tv_sec >= 5) {
			ret = -ETIMEDOUT;
			goto test_fail;
		}
		break;
	case ISOBUSFS_CLI_STATE_OF_FAIL:
		if (tp->expect_pass) {
			pr_err("pattern test failed: %s", tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		}

		priv->state = ISOBUSFS_CLI_STATE_TEST_DONE;
		break;
	case ISOBUSFS_CLI_STATE_OF_DONE:
		if (!tp->expect_pass) {
			pr_err("pattern test failed: %s", tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		}

		ret = isobusfs_cli_fa_sf_req(priv, priv->handle,
					     ISOBUSFS_FA_SEEK_SET, tp->offset);
		if (ret)
			goto test_fail;
		break;
	case ISOBUSFS_CLI_STATE_SF_FAIL:
		if (tp->expect_pass) {
			pr_err("pattern test failed: %s", tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		}

		priv->state = ISOBUSFS_CLI_STATE_TEST_DONE;
		break;
	case ISOBUSFS_CLI_STATE_SF_DONE:
		if (!tp->expect_pass) {
			pr_err("pattern test failed: %s", tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		}

		if (priv->read_offset != tp->offset) {
			pr_err("Not expected read offset: %s", tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		}

		if (tp->read_size > 0xffff)
			read_size = 0xffff;
		else
			read_size = tp->read_size;

		ret = isobusfs_cli_fa_rf_req(priv, priv->handle,
			   read_size);
		if (ret)
			goto test_fail;
		test_start_time = current_time;
		break;
	case ISOBUSFS_CLI_STATE_WAIT_RF_RESP:
		if (current_time.tv_sec - test_start_time.tv_sec >= 5) {
			ret = -ETIMEDOUT;
			goto test_fail;
		}
		break;
	case ISOBUSFS_CLI_STATE_RF_FAIL:
		if (tp->expect_pass) {
			pr_err("pattern test failed: %s", tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		}

		priv->state = ISOBUSFS_CLI_STATE_TEST_CLEANUP;
		break;
	case ISOBUSFS_CLI_STATE_RF_DONE:
		if (!tp->expect_pass) {
			pr_err("pattern test failed: %s", tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		}

		pr_info("read file size: %zu", priv->read_data_len);
		actual_sum = isobusfs_cli_calculate_sum(priv->read_data,
							priv->read_data_len,
							priv->read_offset);
		expected_sum = isobusfs_cli_calculate_sum(NULL,
							  priv->read_data_len,
							  priv->read_offset);
		if (actual_sum != expected_sum) {
			pr_err("pattern test failed: incorrect sum in %s. Sum got: %d, expected: %d",
			       tp->path_name, actual_sum, expected_sum);
			isobusfs_cmn_dump_last_x_bytes(priv->read_data,
						       priv->read_data_len, 16);

			ret = -EINVAL;
			goto test_fail;
		} else {
			pr_info("pattern test passed: %s. Sum got: %d, expected: %d",
				tp->path_name, actual_sum, expected_sum);
			isobusfs_cmn_dump_last_x_bytes(priv->read_data,
						       priv->read_data_len, 16);
		}

		free(priv->read_data);
		priv->read_data = NULL;

		remaining_size = (tp->offset + tp->read_size) -
				(priv->read_offset + priv->read_data_len);
		pr_debug("remaining_size: %zd, read_offset: %zu, read_data_len: %zu, test read size: %zu, test offset %zu",
			 remaining_size, priv->read_offset, priv->read_data_len,
			 tp->read_size, tp->offset);
		if (remaining_size < 0) {
			pr_err("pattern test failed: %s. Read size is too big",
			       tp->path_name);
			ret = -EINVAL;
			goto test_fail;
		} else if (remaining_size > 0 && priv->read_data_len != 0) {
			priv->read_offset += priv->read_data_len;

			if (remaining_size > 0xffff)
				read_size = 0xffff;
			else
				read_size = remaining_size;

			ret = isobusfs_cli_fa_rf_req(priv, priv->handle,
						     read_size);
			if (ret)
				goto test_fail;
			test_start_time = current_time;
			break;
		} else if (remaining_size > 0 && priv->read_data_len == 0 && tp->expect_pass) {
			pr_err("read test failed: %s. Read size is zero, but expected more data: %zd",
			       tp->path_name, remaining_size);
			ret = -EINVAL;
			goto test_fail;
		}

		/* fall troth */
	case ISOBUSFS_CLI_STATE_TEST_CLEANUP:
		if (priv->handle != ISOBUSFS_FILE_HANDLE_ERROR) {
			isobusfs_cli_fa_cf_req(priv, priv->handle);
			test_start_time = current_time;
		} else {
			priv->state = ISOBUSFS_CLI_STATE_TEST_DONE;
		}

		break;
	case ISOBUSFS_CLI_STATE_WAIT_CF_RESP:
		if (current_time.tv_sec - test_start_time.tv_sec >= 5) {
			ret = -ETIMEDOUT;
			goto test_fail;
		}
		break;
	case ISOBUSFS_CLI_STATE_CF_FAIL:
		pr_err("failed to close file: %s", tp->path_name);
		ret = -EINVAL;
		goto test_fail;
	case ISOBUSFS_CLI_STATE_CF_DONE:
	case ISOBUSFS_CLI_STATE_TEST_DONE:
		current_rf_pattern_test++;

		if (current_rf_pattern_test >= num_patterns)
			*complete = true;
		else
			priv->state = ISOBUSFS_CLI_STATE_SELFTEST;

		break;
	default:
		pr_err("%s:%i: unknown state: %d", __func__, __LINE__,
		       priv->state);
		ret = -EINVAL;
		goto test_fail;
	}

	return 0;

test_fail:
	*complete = true;
	return ret;
}

struct isobusfs_cli_test_case test_cases[] = {
	{ isobusfs_cli_test_connect, "Server connection" },
	{ isobusfs_cli_test_property_req, "Server property request" },
	{ isobusfs_cli_test_volume_status_req, "Volume status request" },
	{ isobusfs_cli_test_current_dir_req, "Get current dir request" },
	{ isobusfs_cli_test_ccd_req, "Change current dir request" },
	{ isobusfs_cli_test_of_req, "Open File request" },
	{ isobusfs_cli_test_sf_req, "Seek File request" },
	{ isobusfs_cli_test_rf_req, "Read File request" },
};

void isobusfs_cli_run_self_tests(struct isobusfs_priv *priv)
{
	if (priv->run_selftest) {
		size_t num_tests = ARRAY_SIZE(test_cases);

		if (current_test < num_tests) {
			bool test_complete = false;
			int ret;

			if (!test_running) {
				pr_int("Executing test %zu: %s\n", current_test + 1, test_cases[current_test].test_description);
				test_running = true;
				priv->state = ISOBUSFS_CLI_STATE_SELFTEST;
			}

			ret = test_cases[current_test].test_func(priv, &test_complete);

			if (test_complete) {
				test_running = false;
				pr_int("Test %zu: %s.\n", current_test + 1, ret ? "FAILED" : "PASSED");
				current_test++;
				priv->state = ISOBUSFS_CLI_STATE_SELFTEST;
			}
		} else {
			pr_int("All tests completed.\n");
			priv->run_selftest = false;
			current_test = 0;
			priv->state = ISOBUSFS_CLI_STATE_IDLE;
		}
	}
}
