#include <stic.h>

#include <sys/stat.h> /* chmod() */
#include <unistd.h> /* chdir() unlink() */

#include <limits.h> /* INT_MAX */
#include <stddef.h> /* NULL */
#include <stdlib.h> /* free() */
#include <string.h> /* strcpy() strdup() */

#include <test-utils.h>

#include "../../src/cfg/config.h"
#include "../../src/compat/fs_limits.h"
#include "../../src/utils/dynarray.h"
#include "../../src/utils/fs.h"
#include "../../src/utils/macros.h"
#include "../../src/utils/path.h"
#include "../../src/utils/str.h"
#include "../../src/background.h"
#include "../../src/filelist.h"
#include "../../src/fops_cpmv.h"
#include "../../src/trash.h"

static void check_directory_clash(int parent_to_child, CopyMoveLikeOp op);

static char *saved_cwd;

SETUP()
{
	saved_cwd = save_cwd();
	assert_success(chdir(SANDBOX_PATH));

	/* lwin */
	view_setup(&lwin);
	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), SANDBOX_PATH, "",
			saved_cwd);
	lwin.list_rows = 1;
	lwin.list_pos = 0;
	/* An extra item is to simplify test that needs two files. */
	lwin.dir_entry = dynarray_cextend(NULL, 2*sizeof(*lwin.dir_entry));
	lwin.dir_entry[0].name = strdup("file");
	lwin.dir_entry[0].origin = &lwin.curr_dir[0];

	/* rwin */
	view_setup(&rwin);
	make_abs_path(rwin.curr_dir, sizeof(rwin.curr_dir), SANDBOX_PATH, "",
			saved_cwd);
	rwin.filtered = 0;
	rwin.list_pos = 0;

	curr_view = &lwin;
	other_view = &rwin;
}

TEARDOWN()
{
	view_teardown(&lwin);
	view_teardown(&rwin);

	restore_cwd(saved_cwd);
}

TEST(move_file)
{
	char new_fname[] = "new_name";
	char *list[] = { &new_fname[0] };

	create_file(lwin.dir_entry[0].name);

	lwin.dir_entry[0].marked = 1;
	(void)fops_cpmv(&lwin, list, ARRAY_LEN(list), CMLO_MOVE, CMLF_NONE);

	restore_cwd(saved_cwd);
	saved_cwd = save_cwd();

	assert_false(path_exists_at(SANDBOX_PATH, lwin.dir_entry[0].name, DEREF));
	assert_success(unlink(SANDBOX_PATH "/new_name"));
}

TEST(make_relative_link, IF(not_windows))
{
	char link_name[] = "link";
	char *list[] = { &link_name[0] };
	char link_value[PATH_MAX + 1];

	strcpy(lwin.curr_dir, "/fake/absolute/path");

	lwin.dir_entry[0].marked = 1;
	(void)fops_cpmv(&lwin, list, ARRAY_LEN(list), CMLO_LINK_REL, CMLF_NONE);

	assert_true(path_exists(link_name, NODEREF));

	assert_success(get_link_target(link_name, link_value, sizeof(link_value)));
	assert_false(is_path_absolute(link_value));

	(void)unlink(link_name);
}

TEST(make_absolute_link, IF(not_windows))
{
	char link_name[] = "link";
	char *list[] = { &link_name[0] };
	char link_value[PATH_MAX + 1];

	strcpy(lwin.curr_dir, "/fake/absolute/path");

	lwin.dir_entry[0].marked = 1;
	(void)fops_cpmv(&lwin, list, ARRAY_LEN(list), CMLO_LINK_ABS, CMLF_NONE);

	assert_true(path_exists(link_name, NODEREF));

	assert_success(get_link_target(link_name, link_value, sizeof(link_value)));
	assert_true(is_path_absolute(link_value));

	(void)unlink(link_name);
}

TEST(refuse_to_copy_or_move_to_source_files_with_the_same_name)
{
	assert_false(flist_custom_active(&rwin));

	restore_cwd(saved_cwd);
	saved_cwd = save_cwd();

	strcpy(rwin.curr_dir, saved_cwd);
	flist_custom_start(&rwin, "test");
	flist_custom_add(&rwin, TEST_DATA_PATH "/existing-files/a");
	flist_custom_add(&rwin, TEST_DATA_PATH "/rename/a");
	assert_true(flist_custom_finish(&rwin, CV_REGULAR, 0) == 0);
	assert_int_equal(2, rwin.list_rows);

	curr_view = &rwin;
	other_view = &lwin;

	rwin.dir_entry[0].marked = 1;
	rwin.dir_entry[1].marked = 1;
	rwin.pending_marking = 1;

	check_marking(curr_view, 0, NULL);

	(void)fops_cpmv(&rwin, NULL, 0, CMLO_COPY, CMLF_NONE);
	(void)fops_cpmv(&rwin, NULL, 0, CMLO_COPY, CMLF_FORCE);
	(void)fops_cpmv(&rwin, NULL, 0, CMLO_MOVE, CMLF_NONE);
	(void)fops_cpmv(&rwin, NULL, 0, CMLO_MOVE, CMLF_FORCE);

	assert_false(path_exists("a", NODEREF));
}

TEST(operation_with_rename_of_identically_named_files_is_allowed)
{
	assert_false(flist_custom_active(&rwin));

	restore_cwd(saved_cwd);
	saved_cwd = save_cwd();

	strcpy(rwin.curr_dir, saved_cwd);
	flist_custom_start(&rwin, "test");
	flist_custom_add(&rwin, TEST_DATA_PATH "/existing-files/a");
	flist_custom_add(&rwin, TEST_DATA_PATH "/rename/a");
	assert_true(flist_custom_finish(&rwin, CV_REGULAR, 0) == 0);
	assert_int_equal(2, rwin.list_rows);

	curr_view = &rwin;
	other_view = &lwin;

	rwin.dir_entry[0].marked = 1;
	rwin.dir_entry[1].marked = 1;
	rwin.pending_marking = 1;

	check_marking(curr_view, 0, NULL);

	char *list[] = { "a", "b" };
	char a_path[PATH_MAX + 1], b_path[PATH_MAX + 1];
	make_abs_path(a_path, sizeof(a_path), SANDBOX_PATH, "a", saved_cwd);
	make_abs_path(b_path, sizeof(b_path), SANDBOX_PATH, "b", saved_cwd);

	/* Not testing moving for simplicity. */
	(void)fops_cpmv(&rwin, list, ARRAY_LEN(list), CMLO_COPY, CMLF_NONE);
	remove_file(a_path);
	remove_file(b_path);
	(void)fops_cpmv(&rwin, list, ARRAY_LEN(list), CMLO_COPY, CMLF_FORCE);
	remove_file(a_path);
	remove_file(b_path);
}

TEST(cpmv_crash_on_wrong_list_access)
{
	char *list[] = { "." };

	view_teardown(&lwin);
	view_setup(&lwin);

	restore_cwd(saved_cwd);
	saved_cwd = save_cwd();

	if(is_path_absolute(TEST_DATA_PATH))
	{
		snprintf(lwin.curr_dir, sizeof(lwin.curr_dir), "%s/existing-files",
				TEST_DATA_PATH);
	}
	else
	{
		snprintf(lwin.curr_dir, sizeof(lwin.curr_dir), "%s/%s/existing-files",
				saved_cwd, TEST_DATA_PATH);
	}

	lwin.list_rows = 3;
	lwin.list_pos = 0;
	lwin.dir_entry = dynarray_cextend(NULL,
			lwin.list_rows*sizeof(*lwin.dir_entry));
	lwin.dir_entry[0].name = strdup("a");
	lwin.dir_entry[0].origin = &lwin.curr_dir[0];
	lwin.dir_entry[0].selected = 1;
	lwin.dir_entry[1].name = strdup("b");
	lwin.dir_entry[1].origin = &lwin.curr_dir[0];
	lwin.dir_entry[1].selected = 1;
	lwin.dir_entry[2].name = strdup("c");
	lwin.dir_entry[2].origin = &lwin.curr_dir[0];
	lwin.dir_entry[2].selected = 1;
	lwin.selected_files = 3;

	check_marking(&lwin, 0, NULL);

	/* cpmv used to use presence of the argument as indication of availability of
	 * file list and access memory beyond array boundaries. */
	assert_failure(fops_cpmv(&lwin, list, ARRAY_LEN(list), CMLO_COPY, CMLF_NONE));
	assert_failure(fops_cpmv(&lwin, list, ARRAY_LEN(list), CMLO_COPY,
				CMLF_FORCE));

	assert_success(remove(SANDBOX_PATH "/a"));
	assert_success(remove(SANDBOX_PATH "/b"));
	assert_success(remove(SANDBOX_PATH "/c"));
}

TEST(cpmv_considers_tree_structure)
{
	char new_fname[] = "new_name";
	char *list[] = { &new_fname[0] };

	cfg.dot_dirs = DD_TREE_LEAFS_PARENT;
	create_dir("dir");

	/* Move from tree root to nested dir. */
	create_file("file");
	flist_load_tree(&rwin, rwin.curr_dir, INT_MAX);
	rwin.list_pos = 1;
	lwin.dir_entry[0].marked = 1;
	(void)fops_cpmv(&lwin, list, 1, CMLO_MOVE, CMLF_NONE);
	assert_success(unlink("dir/new_name"));

	/* Move back. */
	curr_view = &rwin;
	other_view = &lwin;
	create_file("dir/file");
	flist_load_tree(&lwin, flist_get_dir(&lwin), INT_MAX);
	flist_load_tree(&rwin, flist_get_dir(&rwin), INT_MAX);
	lwin.list_pos = 0;
	rwin.dir_entry[1].marked = 1;
	(void)fops_cpmv(&rwin, NULL, 0, CMLO_MOVE, CMLF_NONE);
	assert_success(unlink("file"));

	assert_success(rmdir("dir"));
}

TEST(cpmv_can_move_files_from_and_out_of_trash_at_the_same_time)
{
	int bg;

	strcat(lwin.curr_dir, "/trash");
	trash_set_specs(lwin.curr_dir);
	assert_success(rmdir("trash"));
	remove_last_path_component(lwin.curr_dir);

	strcat(lwin.curr_dir, "/dir");

	curr_view = &rwin;
	other_view = &lwin;

	for(bg = 0; bg < 2; ++bg)
	{
		create_dir("trash");
		create_file("trash/000_a");
		create_dir("trash/nested");
		create_file("trash/nested/000_file");
		create_dir("dir");
		create_file("000_b");

		flist_custom_start(&rwin, "test");
		flist_custom_add(&rwin, "trash/000_a");
		flist_custom_add(&rwin, "000_b");
		flist_custom_add(&rwin, "trash/nested/000_file");
		assert_true(flist_custom_finish(&rwin, CV_REGULAR, 0) == 0);
		assert_int_equal(3, rwin.list_rows);

		rwin.dir_entry[0].marked = 1;
		rwin.dir_entry[1].marked = 1;
		rwin.dir_entry[2].marked = 1;

		if(!bg)
		{
			(void)fops_cpmv(&rwin, NULL, 0, CMLO_MOVE, CMLF_NONE);
		}
		else
		{
			(void)fops_cpmv_bg(&rwin, NULL, 0, CMLO_MOVE, CMLF_NONE);
			wait_for_bg();
		}

		assert_success(unlink("dir/a"));
		assert_success(unlink("dir/000_b"));
		assert_success(unlink("dir/000_file"));
		assert_success(rmdir("dir"));
		assert_success(rmdir("trash/nested"));
		assert_success(rmdir("trash"));
	}
}

TEST(child_overwrite_is_prevented_on_abs_link, IF(not_windows))
{
	check_directory_clash(1, CMLO_LINK_ABS);
}

TEST(parent_overwrite_is_prevented_on_abs_link, IF(not_windows))
{
	check_directory_clash(0, CMLO_LINK_ABS);
}

TEST(child_overwrite_is_prevented_on_rel_link, IF(not_windows))
{
	check_directory_clash(1, CMLO_LINK_REL);
}

TEST(parent_overwrite_is_prevented_on_rel_link, IF(not_windows))
{
	check_directory_clash(0, CMLO_LINK_REL);
}

TEST(child_overwrite_is_prevented_on_file_copy)
{
	check_directory_clash(1, CMLO_COPY);
}

TEST(parent_overwrite_is_prevented_on_file_copy)
{
	check_directory_clash(0, CMLO_COPY);
}

TEST(child_overwrite_is_prevented_on_file_move)
{
	check_directory_clash(1, CMLO_MOVE);
}

TEST(parent_overwrite_is_prevented_on_file_move)
{
	check_directory_clash(0, CMLO_MOVE);
}

TEST(copying_is_aborted_if_we_can_not_read_a_file, IF(regular_unix_user))
{
	create_file("can-read");
	create_file("can-not-read");
	assert_success(chmod("can-not-read", 0000));
	populate_dir_list(&lwin, 0);

	lwin.dir_entry[0].marked = 1;
	lwin.dir_entry[1].marked = 1;
	assert_int_equal(0, fops_cpmv(&lwin, NULL, 0, CMLO_COPY, CMLF_NONE));

	assert_success(unlink("can-read"));
	assert_success(unlink("can-not-read"));
}

TEST(bail_out_if_source_matches_destination)
{
	create_file("file");

	populate_dir_list(&lwin, 0);
	populate_dir_list(&rwin, 0);

	lwin.dir_entry[0].marked = 1;

	(void)fops_cpmv(&lwin, NULL, 0, CMLO_COPY, CMLF_NONE);
	(void)fops_cpmv(&lwin, NULL, 0, CMLO_COPY, CMLF_FORCE);
	(void)fops_cpmv(&lwin, NULL, 0, CMLO_MOVE, CMLF_NONE);
	(void)fops_cpmv(&lwin, NULL, 0, CMLO_MOVE, CMLF_FORCE);
	(void)fops_cpmv(&lwin, NULL, 0, CMLO_LINK_REL, CMLF_NONE);
	(void)fops_cpmv(&lwin, NULL, 0, CMLO_LINK_REL, CMLF_FORCE);
	(void)fops_cpmv(&lwin, NULL, 0, CMLO_LINK_ABS, CMLF_NONE);
	(void)fops_cpmv(&lwin, NULL, 0, CMLO_LINK_ABS, CMLF_FORCE);

	(void)fops_cpmv_bg(&lwin, NULL, 0, /*move=*/0, CMLF_NONE);
	wait_for_bg();
	(void)fops_cpmv_bg(&lwin, NULL, 0, /*move=*/0, CMLF_FORCE);
	wait_for_bg();
	(void)fops_cpmv_bg(&lwin, NULL, 0, /*move=*/1, CMLF_NONE);
	wait_for_bg();
	(void)fops_cpmv_bg(&lwin, NULL, 0, /*move=*/1, CMLF_FORCE);
	wait_for_bg();

	remove_file("file");
}

TEST(cpmv_can_copy_or_move_files_to_a_subdirectory)
{
	char dir[] = "dir";
	char *list[] = { dir };

	int bg;
	for(bg = 0; bg < 2; ++bg)
	{
		create_dir("dir");
		create_file("file1");
		create_file("file2");

		populate_dir_list(&lwin, 0);
		assert_int_equal(3, lwin.list_rows);
		assert_string_equal("file1", lwin.dir_entry[1].name);
		assert_string_equal("file2", lwin.dir_entry[2].name);

		/* Copy. */

		lwin.dir_entry[1].marked = 1;
		lwin.dir_entry[2].marked = 1;

		if(!bg)
		{
			(void)fops_cpmv(&lwin, list, ARRAY_LEN(list), CMLO_COPY, CMLF_NONE);
		}
		else
		{
			(void)fops_cpmv_bg(&lwin, list, ARRAY_LEN(list), CMLO_COPY, CMLF_NONE);
			wait_for_bg();
		}

		assert_success(unlink("dir/file1"));
		assert_success(unlink("dir/file2"));

		/* Move. */

		lwin.dir_entry[1].marked = 1;
		lwin.dir_entry[2].marked = 1;

		if(!bg)
		{
			(void)fops_cpmv(&lwin, list, ARRAY_LEN(list), CMLO_MOVE, CMLF_NONE);
		}
		else
		{
			(void)fops_cpmv_bg(&lwin, list, ARRAY_LEN(list), CMLO_MOVE, CMLF_NONE);
			wait_for_bg();
		}

		assert_success(unlink("dir/file1"));
		assert_success(unlink("dir/file2"));
		assert_success(rmdir("dir"));
	}
}

TEST(can_skip_existing_files)
{
	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), SANDBOX_PATH, "dir",
			saved_cwd);

	int bg;
	for(bg = 0; bg < 2; ++bg)
	{
		create_dir("dir");
		make_file("dir/file", "aaa");
		create_file("file");

		CopyMoveLikeOp ops[] = {
			CMLO_COPY,
			CMLO_MOVE,
			CMLO_LINK_REL,
			CMLO_LINK_ABS,
		};

		size_t op;
		for(op = 0; op < ARRAY_LEN(ops); ++op)
		{
			lwin.dir_entry[0].marked = 1;

			if(!bg)
			{
				(void)fops_cpmv(&lwin, NULL, 0, ops[op], CMLF_SKIP);
			}
			else
			{
				(void)fops_cpmv_bg(&lwin, NULL, 0, ops[op], CMLF_SKIP);
				wait_for_bg();
			}

			assert_int_equal(0, get_file_size("file"));
		}

		remove_file("file");
		remove_file("dir/file");
		remove_dir("dir");
	}
}

TEST(bg_jog_has_correct_title)
{
	/* To test the use of ~. */
	make_abs_path(cfg.home_dir, sizeof(cfg.home_dir), SANDBOX_PATH, "/",
			saved_cwd);

	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), SANDBOX_PATH, "dir",
			saved_cwd);

	create_dir("dir");
	create_file("dir/file");
	create_file("dir/file2");

	lwin.dir_entry[0].marked = 1;

	lwin.list_rows = 2;
	lwin.dir_entry[1].name = strdup("file2");
	lwin.dir_entry[1].origin = &lwin.curr_dir[0];
	lwin.dir_entry[1].marked = 1;

	wait_for_all_bg();
	(void)fops_cpmv_bg(&lwin, NULL, 0, CMLO_COPY, CMLF_SKIP);
	wait_for_bg();
	assert_string_equal("2/2  ~/dir/file2", bg_jobs->bg_op.descr);

	remove_file("file");
	remove_file("file2");
	remove_file("dir/file");
	remove_file("dir/file2");
	remove_dir("dir");

	cfg.home_dir[0] = '\0';
}

TEST(broken_link_behaves_like_a_regular_file_on_conflict, IF(not_windows))
{
	make_abs_path(lwin.curr_dir, sizeof(lwin.curr_dir), SANDBOX_PATH, "src",
			saved_cwd);
	replace_string(&lwin.dir_entry[0].name, "symlink");

	int bg;
	for(bg = 0; bg < 2; ++bg)
	{
		create_dir("src");
		create_file("src/symlink");
		assert_success(make_symlink("notarget", "symlink"));

		lwin.dir_entry[0].marked = 1;

		if(!bg)
		{
			(void)fops_cpmv(&lwin, NULL, 0, CMLO_MOVE, CMLF_SKIP);
		}
		else
		{
			(void)fops_cpmv_bg(&lwin, NULL, 0, CMLO_MOVE, CMLF_SKIP);
			wait_for_bg();
		}

		remove_file("src/symlink");
		remove_dir("src");
		remove_file("symlink");
	}
}

static void
check_directory_clash(int parent_to_child, CopyMoveLikeOp op)
{
	create_dir("dir");
	create_dir("dir/dir");
	create_file("dir/dir/file");

	strcat(parent_to_child ? rwin.curr_dir : lwin.curr_dir, "/dir");

	populate_dir_list(&lwin, 0);
	populate_dir_list(&rwin, 0);

	lwin.dir_entry[0].marked = 1;
	assert_string_equal("dir", lwin.dir_entry[0].name);
	(void)fops_cpmv(&lwin, NULL, 0, CMLO_MOVE, CMLF_FORCE);

	restore_cwd(saved_cwd);
	saved_cwd = save_cwd();

	assert_success(remove(SANDBOX_PATH "/dir/dir/file"));
	assert_success(rmdir(SANDBOX_PATH "/dir/dir"));
	assert_success(rmdir(SANDBOX_PATH "/dir"));
}

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