#include <stic.h>

#include <unistd.h> /* chdir() */

#include <stddef.h> /* size_t */
#include <stdlib.h> /* free() */
#include <string.h> /* memset() strdup() strlen() */

#include <test-utils.h>

#include "../../src/cfg/config.h"
#include "../../src/compat/fs_limits.h"
#include "../../src/int/ext_edit.h"
#include "../../src/ui/ui.h"
#include "../../src/utils/fs.h"
#include "../../src/utils/macros.h"
#include "../../src/utils/str.h"
#include "../../src/utils/string_array.h"
#include "../../src/fops_common.h"
#include "../../src/fops_rename.h"

static void check_editing(char *orig[], int orig_len, const char template[],
		const char edited[], char *expected[], int expected_len);

static ext_edit_t ext_edit;

SETUP_ONCE()
{
	curr_view = &lwin;
}

TEARDOWN()
{
	ext_edit_discard(&ext_edit);
}

TEST(names_less_than_files)
{
	char *src[] = { "a", "b" };
	char *dst[] = { "a" };
	char *error = NULL;
	assert_false(fops_is_name_list_ok(ARRAY_LEN(src), ARRAY_LEN(dst), src,
				&error));
	assert_string_equal("Not enough file names (1/2)", error);
	free(error);
}

TEST(names_greater_that_files_fail)
{
	char *src[] = { "a" };
	char *dst[] = { "a", "b" };
	char *error = NULL;
	assert_false(fops_is_name_list_ok(ARRAY_LEN(src), ARRAY_LEN(dst), src,
				&error));
	assert_string_equal("Too many file names (2/1)", error);
	free(error);
}

TEST(move_is_allowed)
{
	char src1[] = "a", src2[] = "b";
	char *src[] = { src1, src2 };
	char dst1[] = "../a", dst2[] = "b";
	char *dst[] = { dst1, dst2 };

	char *error = NULL;

	assert_true(fops_is_name_list_ok(ARRAY_LEN(src), ARRAY_LEN(dst), src,
				&error));
	assert_string_equal(NULL, error);

	assert_int_equal(1, fops_check_moves_on_rename(ARRAY_LEN(dst), dst, src,
				&error));
	assert_string_equal(NULL, error);

#ifdef _WIN32
	char dst3[] = "..\\a";
	dst[0] = dst3;

	assert_true(fops_is_name_list_ok(ARRAY_LEN(src), ARRAY_LEN(dst), src,
				&error));
	assert_string_equal(NULL, error);

	assert_int_equal(1, fops_check_moves_on_rename(ARRAY_LEN(dst), dst, src,
				&error));
	assert_string_equal(NULL, error);
#endif

	free(error);
}

TEST(move_check_handles_empty_lines)
{
	char src1[] = "a";
	char *src[] = { src1 };
	char dst1[] = "";
	char *dst[] = { dst1 };

	char *error = NULL;

	assert_int_equal(0, fops_check_moves_on_rename(ARRAY_LEN(dst), dst, src,
				&error));
	assert_string_equal(NULL, error);

	free(error);
}

TEST(move_check_detects_identical_prefixes)
{
	char src1[] = "././a";
	char *src[] = { src1 };
	char dst1[] = "a";
	char *dst[] = { dst1 };

	char *error = NULL;

	assert_int_equal(0, fops_check_moves_on_rename(ARRAY_LEN(dst), dst, src,
				&error));
	assert_string_equal(NULL, error);

	free(error);
}

TEST(move_check_handles_empty_target_prefix)
{
	char src1[] = "b/a";
	char *src[] = { src1 };
	char dst1[] = "a";
	char *dst[] = { dst1 };

	char *error = NULL;

	assert_int_equal(1, fops_check_moves_on_rename(ARRAY_LEN(dst), dst, src,
				&error));
	assert_string_equal(NULL, error);

	free(error);
}

TEST(move_check_detects_missing_target_parent)
{
	char src1[] = "a";
	char *src[] = { src1 };
	char dst1[] = "b/a";
	char *dst[] = { dst1 };

	char *error = NULL;

	assert_int_equal(-1, fops_check_moves_on_rename(ARRAY_LEN(dst), dst, src,
				&error));
	assert_string_equal("Won't create directory for \"b/a\"", error);

	free(error);
}

TEST(rename_inside_subdir_ok)
{
	char *src[] = { "../a", "b" };
	char *dst[] = { "../a_a", "b" };
	char *error = NULL;
	assert_true(fops_is_name_list_ok(ARRAY_LEN(src), ARRAY_LEN(dst), src,
				&error));
	assert_string_equal(NULL, error);

#ifdef _WIN32
	src[0] = "..\\a";
	dst[0] = "..\\a_a";
	assert_true(fops_is_name_list_ok(ARRAY_LEN(src), ARRAY_LEN(dst), src,
			&error));
	assert_string_equal(NULL, error);
#endif

	free(error);
}

TEST(incdec_leaves_zeros)
{
	assert_string_equal("1", incdec_name("0", 1));
	assert_string_equal("01", incdec_name("00", 1));
	assert_string_equal("00", incdec_name("01", -1));
	assert_string_equal("-01", incdec_name("00", -1));
	assert_string_equal("002", incdec_name("001", 1));
	assert_string_equal("012", incdec_name("005", 7));
	assert_string_equal("008", incdec_name("009", -1));
	assert_string_equal("010", incdec_name("009", 1));
	assert_string_equal("100", incdec_name("099", 1));
	assert_string_equal("-08", incdec_name("-09", 1));
	assert_string_equal("-10", incdec_name("-09", -1));
	assert_string_equal("-14", incdec_name("-09", -5));
	assert_string_equal("a01.", incdec_name("a00.", 1));
}

TEST(incdec_handles_large_numbers)
{
	/* 2**31-1 / 2**31 / 2**31+1 */
	assert_string_equal("2147483648", incdec_name("2147483647", 1));
	assert_string_equal("2147483649", incdec_name("2147483648", 1));
	assert_string_equal("2147483650", incdec_name("2147483649", 1));

	/* 2**32-1 / 2**32 / 2**32+1 */
	assert_string_equal("4294967296", incdec_name("4294967295", 1));
	assert_string_equal("4294967297", incdec_name("4294967296", 1));
	assert_string_equal("4294967298", incdec_name("4294967297", 1));
}

TEST(single_file_rename)
{
	char *error = NULL;

	assert_success(chdir(TEST_DATA_PATH "/rename"));
	assert_true(fops_check_file_rename(".", "a", "a", &error) < 0);
	assert_string_equal(NULL, error);
	assert_true(fops_check_file_rename(".", "a", "", &error) < 0);
	assert_string_equal(NULL, error);
	assert_true(fops_check_file_rename(".", "a", "b", &error) > 0);
	assert_string_equal(NULL, error);
	assert_true(fops_check_file_rename(".", "a", "aa", &error) == 0);
	assert_string_equal("File \"aa\" already exists", error);
#ifdef _WIN32
	update_string(&error, NULL);
	assert_true(fops_check_file_rename(".", "a", "A", &error) > 0);
	assert_string_equal(NULL, error);
#endif

	free(error);
}

TEST(rename_list_checks)
{
	size_t i;

	char src1[] = "a", src2[] = "aa", src3[] = "aaa";
	char *list[] = { src1, src2, src3 };
	char dst1[] = "", dst2[] = "aa", dst3[] = "bbb";
	char *files[] = { dst1, dst2, dst3 };
	ARRAY_GUARD(files, ARRAY_LEN(list));

	char dup[ARRAY_LEN(files)] = {};
	char *error = NULL;

	assert_true(fops_is_rename_list_ok(files, dup, ARRAY_LEN(list), list,
				&error));
	assert_string_equal(NULL, error);
	free(error);

	for(i = 0; i < ARRAY_LEN(list); ++i)
	{
		assert_false(dup[i]);
	}
}

TEST(rename_list_checks_normalized_paths)
{
	char src1[] = "aa/../bb", src2[] = "ccc/ccc", src3[] = "./a";
	char *src[] = { src1, src2, src3 };
	char dst1[] = "a", dst2[] = "bb", dst3[] = "ccc//ccc";
	char *dst[] = { dst1, dst2, dst3 };
	ARRAY_GUARD(src, ARRAY_LEN(dst));

	char dup[ARRAY_LEN(src)] = {};
	char *error = NULL;

	/* This is to avoid error due to missing target path. */
	assert_success(chdir(SANDBOX_PATH));
	create_dir("ccc");

	assert_true(fops_is_rename_list_ok(src, dup, ARRAY_LEN(dst), dst, &error));
	assert_string_equal(NULL, error);

	assert_int_equal(2, fops_check_moves_on_rename(ARRAY_LEN(dst), src, dst,
				&error));
	assert_string_equal(NULL, error);

	free(error);

	size_t i;
	for(i = 0; i < ARRAY_LEN(dst); ++i)
	{
		assert_true(dup[i]);
	}

	remove_dir("ccc");
}

TEST(file_name_list_can_be_reread)
{
	char long_name[PATH_MAX + NAME_MAX + 1];
	char *list[] = { "aaa", long_name };
	int nlines;
	char **new_list;

	memset(long_name, '1', sizeof(long_name) - 1U);
	long_name[sizeof(long_name) - 1U] = '\0';

#ifndef _WIN32
	replace_string(&cfg.shell, "/bin/sh");
	update_string(&cfg.vi_command, "echo > /dev/null");
#else
	replace_string(&cfg.shell, "cmd");
	update_string(&cfg.vi_command, "echo > NUL");
#endif
	stats_update_shell_type(cfg.shell);

	curr_stats.exec_env_type = EET_EMULATOR;

	ext_edit_t ext_edit = {};

	new_list = edit_list(&ext_edit, ARRAY_LEN(list), list, &nlines, 1);
	assert_int_equal(2, nlines);
	if(nlines >= 2)
	{
		assert_string_equal(list[0], new_list[0]);
		assert_string_equal(list[1], new_list[1]);
	}
	free_string_array(new_list, nlines);

	ext_edit_discard(&ext_edit);

	update_string(&cfg.vi_command, NULL);

	update_string(&cfg.shell, NULL);
	stats_update_shell_type("/bin/sh");
}

TEST(file_name_list_can_be_changed, IF(not_windows))
{
	char *orig[] = { "aaa" };
	const char *edited = "bbb";
	char *final[] = { "bbb" };
	check_editing(orig, ARRAY_LEN(orig), NULL, edited, final, ARRAY_LEN(final));
}

TEST(leading_chars_and_comments, IF(not_windows))
{
	char *orig[] = { "#aa", "\\escapeme" };
	const char *edited = "\\\\aa\n"
	                     "#commenthere\n"
	                     "\\#escapeme\n"
	                     "# and some\n"
	                     "# more here\n";
	char *final[] = { "\\aa", "#escapeme" };
	check_editing(orig, ARRAY_LEN(orig), NULL, edited, final, ARRAY_LEN(final));
}

TEST(re_editing, IF(not_windows))
{
	char *orig[] = { "first" };
	const char *edited = "second";
	char *final[] = { "second" };
	check_editing(orig, ARRAY_LEN(orig), NULL, edited, final, ARRAY_LEN(final));
	check_editing(orig, ARRAY_LEN(orig), NULL, NULL, final, ARRAY_LEN(final));
}

TEST(re_editing_cancellation, IF(not_windows))
{
	char *orig[] = { "first" };
	const char *edited = "second";
	const char *cancel = "#only comments";
	char *modified[] = { "second" };
	check_editing(orig, ARRAY_LEN(orig), NULL, edited, modified,
			ARRAY_LEN(modified));
	check_editing(orig, ARRAY_LEN(orig), NULL, cancel, NULL, 0);
	check_editing(orig, ARRAY_LEN(orig), NULL, NULL, orig, ARRAY_LEN(orig));
}

TEST(re_editing_unchanged_skips_caching, IF(not_windows))
{
	char *orig[] = { "a", "b" };
	const char *same = "a\nb";
	check_editing(orig, ARRAY_LEN(orig), same, same, NULL, 0);
	check_editing(orig, ARRAY_LEN(orig), same, same, NULL, 0);
}

TEST(last_rename_error_is_displayed, IF(not_windows))
{
	char *orig[] = { "a", "b" };
	const char *template = "# Last error:\n"
	                       "#  test error\n"
	                       "# Original names:\n"
	                       "#  a\n"
	                       "#  b\n"
	                       "# Edited names:\n"
	                       "a\n"
	                       "a\n";
	const char *edited = "a\na";
	char *modified[] = { "a", "a" };
	check_editing(orig, ARRAY_LEN(orig), NULL, edited, modified,
			ARRAY_LEN(modified));
	update_string(&ext_edit.last_error, "test error");
	check_editing(orig, ARRAY_LEN(orig), template, edited, modified,
			ARRAY_LEN(modified));
}

TEST(unchanged_list, IF(not_windows))
{
	char *orig[] = { "aaa" };
	const char *edited = "aaa";
	check_editing(orig, ARRAY_LEN(orig), NULL, edited, NULL, 0);
}

static void
check_editing(char *orig[], int orig_len, const char template[],
		const char edited[], char *expected[], int expected_len)
{
	char *saved_cwd = save_cwd();
	assert_success(chdir(SANDBOX_PATH));

	update_string(&cfg.shell, "/bin/sh");
	stats_update_shell_type(cfg.shell);

	create_executable("script");
	if(edited != NULL)
	{
		make_file("edited", edited);
		make_file("script", "#!/bin/sh\n"
		                    "mv $2 template\n"
		                    "mv edited $2\n");
	}

	curr_stats.exec_env_type = EET_EMULATOR;
	update_string(&cfg.vi_command, "./script");

	int actual_len;
	char **actual = edit_list(&ext_edit, orig_len, orig, &actual_len,
			edited == NULL);
	assert_int_equal(expected_len, actual_len);

	int i;
	for(i = 0; i < MIN(expected_len, actual_len); ++i)
	{
		assert_string_equal(expected[i], actual[i]);
	}

	free_string_array(actual, actual_len);

	update_string(&cfg.vi_command, NULL);
	update_string(&cfg.shell, NULL);

	if(template != NULL)
	{
		char *template_copy = strdup(template);
		int nlines;
		char **lines = break_into_lines(template_copy, strlen(template_copy),
				&nlines, 0);
		file_is("template", (const char **)lines, nlines);
		free_string_array(lines, nlines);
		free(template_copy);
	}
	if(edited != NULL)
	{
		remove_file("template");
	}

	remove_file("script");

	restore_cwd(saved_cwd);
}

/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
/* vim: set cinoptions+=t0 filetype=c : */
