#include "clar_libgit2.h"

#include "git2/checkout.h"
#include "repository.h"

static git_repository *g_repo;
static git_checkout_opts g_opts;

static void reset_index_to_treeish(git_object *treeish)
{
	git_object *tree;
	git_index *index;

	cl_git_pass(git_object_peel(&tree, treeish, GIT_OBJ_TREE));

	cl_git_pass(git_repository_index(&index, g_repo));
	cl_git_pass(git_index_read_tree(index, (git_tree *)tree));
	cl_git_pass(git_index_write(index));

	git_object_free(tree);
	git_index_free(index);
}

void test_checkout_index__initialize(void)
{
	git_tree *tree;

	GIT_INIT_STRUCTURE(&g_opts, GIT_CHECKOUT_OPTS_VERSION);
	g_opts.checkout_strategy = GIT_CHECKOUT_SAFE;

	g_repo = cl_git_sandbox_init("testrepo");

	cl_git_pass(git_repository_head_tree(&tree, g_repo));

	reset_index_to_treeish((git_object *)tree);
	git_tree_free(tree);

	cl_git_rewritefile(
		"./testrepo/.gitattributes",
		"* text eol=lf\n");
}

void test_checkout_index__cleanup(void)
{
	cl_git_sandbox_cleanup();
}

static void test_file_contents(const char *path, const char *expectedcontents)
{
	int fd;
	char buffer[1024] = {0};
	size_t expectedlen, actuallen;

	fd = p_open(path, O_RDONLY);
	cl_assert(fd >= 0);

	expectedlen = strlen(expectedcontents);
	actuallen = p_read(fd, buffer, 1024);
	cl_git_pass(p_close(fd));

	cl_assert_equal_sz(actuallen, expectedlen);
	cl_assert_equal_s(buffer, expectedcontents);
}

void test_checkout_index__cannot_checkout_a_bare_repository(void)
{
	test_checkout_index__cleanup();

	GIT_INIT_STRUCTURE(&g_opts, GIT_CHECKOUT_OPTS_VERSION);
	g_repo = cl_git_sandbox_init("testrepo.git");

	cl_git_fail(git_checkout_index(g_repo, NULL, NULL));
}

void test_checkout_index__can_create_missing_files(void)
{
	cl_assert_equal_i(false, git_path_isfile("./testrepo/README"));
	cl_assert_equal_i(false, git_path_isfile("./testrepo/branch_file.txt"));
	cl_assert_equal_i(false, git_path_isfile("./testrepo/new.txt"));

	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));

	test_file_contents("./testrepo/README", "hey there\n");
	test_file_contents("./testrepo/branch_file.txt", "hi\nbye!\n");
	test_file_contents("./testrepo/new.txt", "my new file\n");
}

void test_checkout_index__can_remove_untracked_files(void)
{
	git_futils_mkdir("./testrepo/dir/subdir/subsubdir", NULL, 0755, GIT_MKDIR_PATH);
	cl_git_mkfile("./testrepo/dir/one", "one\n");
	cl_git_mkfile("./testrepo/dir/subdir/two", "two\n");

	cl_assert_equal_i(true, git_path_isdir("./testrepo/dir/subdir/subsubdir"));

	g_opts.checkout_strategy |= GIT_CHECKOUT_REMOVE_UNTRACKED;
	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));

	cl_assert_equal_i(false, git_path_isdir("./testrepo/dir"));
}

void test_checkout_index__honor_the_specified_pathspecs(void)
{
	char *entries[] = { "*.txt" };

	g_opts.paths.strings = entries;
	g_opts.paths.count = 1;

	cl_assert_equal_i(false, git_path_isfile("./testrepo/README"));
	cl_assert_equal_i(false, git_path_isfile("./testrepo/branch_file.txt"));
	cl_assert_equal_i(false, git_path_isfile("./testrepo/new.txt"));

	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));

	cl_assert_equal_i(false, git_path_isfile("./testrepo/README"));
	test_file_contents("./testrepo/branch_file.txt", "hi\nbye!\n");
	test_file_contents("./testrepo/new.txt", "my new file\n");
}

static void set_config_entry_to(const char *entry_name, bool value)
{
	git_config *cfg;

	cl_git_pass(git_repository_config(&cfg, g_repo));
	cl_git_pass(git_config_set_bool(cfg, entry_name, value));

	git_config_free(cfg);
}

static void set_core_autocrlf_to(bool value)
{
	set_config_entry_to("core.autocrlf", value);
}

void test_checkout_index__honor_the_gitattributes_directives(void)
{
	const char *attributes =
		"branch_file.txt text eol=crlf\n"
		"new.txt text eol=lf\n";

	cl_git_mkfile("./testrepo/.gitattributes", attributes);
	set_core_autocrlf_to(false);

	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));

	test_file_contents("./testrepo/README", "hey there\n");
	test_file_contents("./testrepo/new.txt", "my new file\n");
	test_file_contents("./testrepo/branch_file.txt", "hi\r\nbye!\r\n");
}

void test_checkout_index__honor_coreautocrlf_setting_set_to_true(void)
{
#ifdef GIT_WIN32
	const char *expected_readme_text = "hey there\r\n";

	cl_git_pass(p_unlink("./testrepo/.gitattributes"));
	set_core_autocrlf_to(true);

	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));

	test_file_contents("./testrepo/README", expected_readme_text);
#endif
}

static void set_repo_symlink_handling_cap_to(bool value)
{
	set_config_entry_to("core.symlinks", value);
}

void test_checkout_index__honor_coresymlinks_setting_set_to_true(void)
{
	set_repo_symlink_handling_cap_to(true);

	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));

#ifdef GIT_WIN32
	test_file_contents("./testrepo/link_to_new.txt", "new.txt");
#else
	{
		char link_data[1024];
		size_t link_size = 1024;

		link_size = p_readlink("./testrepo/link_to_new.txt", link_data, link_size);
		link_data[link_size] = '\0';
		cl_assert_equal_i(link_size, strlen("new.txt"));
		cl_assert_equal_s(link_data, "new.txt");
		test_file_contents("./testrepo/link_to_new.txt", "my new file\n");
	}
#endif
}

void test_checkout_index__honor_coresymlinks_setting_set_to_false(void)
{
	set_repo_symlink_handling_cap_to(false);

	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));

	test_file_contents("./testrepo/link_to_new.txt", "new.txt");
}

void test_checkout_index__donot_overwrite_modified_file_by_default(void)
{
	cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!");

	/* set this up to not return an error code on conflicts, but it
	 * still will not have permission to overwrite anything...
	 */
	g_opts.checkout_strategy = GIT_CHECKOUT_ALLOW_CONFLICTS;

	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));

	test_file_contents("./testrepo/new.txt", "This isn't what's stored!");
}

void test_checkout_index__can_overwrite_modified_file(void)
{
	cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!");

	g_opts.checkout_strategy |= GIT_CHECKOUT_UPDATE_MODIFIED;

	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));

	test_file_contents("./testrepo/new.txt", "my new file\n");
}

void test_checkout_index__options_disable_filters(void)
{
	cl_git_mkfile("./testrepo/.gitattributes", "*.txt text eol=crlf\n");

	g_opts.disable_filters = false;
	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));

	test_file_contents("./testrepo/new.txt", "my new file\r\n");

	p_unlink("./testrepo/new.txt");

	g_opts.disable_filters = true;
	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));

	test_file_contents("./testrepo/new.txt", "my new file\n");
}

void test_checkout_index__options_dir_modes(void)
{
#ifndef GIT_WIN32
	struct stat st;
	git_oid oid;
	git_commit *commit;

	cl_git_pass(git_reference_name_to_id(&oid, g_repo, "refs/heads/dir"));
	cl_git_pass(git_commit_lookup(&commit, g_repo, &oid));

	reset_index_to_treeish((git_object *)commit);

	g_opts.dir_mode = 0701;
	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));

	cl_git_pass(p_stat("./testrepo/a", &st));
	cl_assert_equal_i(st.st_mode & 0777, 0701);

	/* File-mode test, since we're on the 'dir' branch */
	cl_git_pass(p_stat("./testrepo/a/b.txt", &st));
	cl_assert_equal_i(st.st_mode & 0777, 0755);

	git_commit_free(commit);
#endif
}

void test_checkout_index__options_override_file_modes(void)
{
#ifndef GIT_WIN32
	struct stat st;

	g_opts.file_mode = 0700;

	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));

	cl_git_pass(p_stat("./testrepo/new.txt", &st));
	cl_assert_equal_i(st.st_mode & 0777, 0700);
#endif
}

void test_checkout_index__options_open_flags(void)
{
	cl_git_mkfile("./testrepo/new.txt", "hi\n");

	g_opts.file_open_flags = O_CREAT | O_RDWR | O_APPEND;

	g_opts.checkout_strategy |= GIT_CHECKOUT_UPDATE_MODIFIED;
	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));

	test_file_contents("./testrepo/new.txt", "hi\nmy new file\n");
}

struct conflict_data {
	const char *file;
	const char *sha;
};

static int conflict_cb(
	const char *conflict_file,
	const git_oid *blob_oid,
	unsigned int index_mode,
	unsigned int wd_mode,
	void *payload)
{
	struct conflict_data *expectations = (struct conflict_data *)payload;

	GIT_UNUSED(index_mode);
	GIT_UNUSED(wd_mode);

	cl_assert_equal_s(expectations->file, conflict_file);
	cl_assert_equal_i(0, git_oid_streq(blob_oid, expectations->sha));

	return 0;
}

void test_checkout_index__can_notify_of_skipped_files(void)
{
	struct conflict_data data;

	cl_git_mkfile("./testrepo/new.txt", "This isn't what's stored!");

	/*
	 * $ git ls-tree HEAD
	 * 100644 blob a8233120f6ad708f843d861ce2b7228ec4e3dec6    README
	 * 100644 blob 3697d64be941a53d4ae8f6a271e4e3fa56b022cc    branch_file.txt
	 * 100644 blob a71586c1dfe8a71c6cbf6c129f404c5642ff31bd    new.txt
	 */
	data.file = "new.txt";
	data.sha = "a71586c1dfe8a71c6cbf6c129f404c5642ff31bd";

	g_opts.checkout_strategy |= GIT_CHECKOUT_ALLOW_CONFLICTS;
	g_opts.conflict_cb = conflict_cb;
	g_opts.conflict_payload = &data;

	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));
}

static int dont_conflict_cb(
	const char *conflict_file,
	const git_oid *blob_oid,
	unsigned int index_mode,
	unsigned int wd_mode,
	void *payload)
{
	GIT_UNUSED(conflict_file);
	GIT_UNUSED(blob_oid);
	GIT_UNUSED(index_mode);
	GIT_UNUSED(wd_mode);
	GIT_UNUSED(payload);

	cl_assert(false);

	return 0;
}

void test_checkout_index__wont_notify_of_expected_line_ending_changes(void)
{
	cl_git_pass(p_unlink("./testrepo/.gitattributes"));
	set_core_autocrlf_to(true);

	cl_git_mkfile("./testrepo/new.txt", "my new file\r\n");

	g_opts.checkout_strategy |= GIT_CHECKOUT_ALLOW_CONFLICTS;
	g_opts.conflict_cb = dont_conflict_cb;
	g_opts.conflict_payload = NULL;

	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));
}

static void progress(const char *path, size_t cur, size_t tot, void *payload)
{
	bool *was_called = (bool*)payload;
	GIT_UNUSED(path); GIT_UNUSED(cur); GIT_UNUSED(tot);
	*was_called = true;
}

void test_checkout_index__calls_progress_callback(void)
{
	bool was_called = 0;
	g_opts.progress_cb = progress;
	g_opts.progress_payload = &was_called;

	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));
	cl_assert_equal_i(was_called, true);
}

void test_checkout_index__can_overcome_name_clashes(void)
{
	git_index *index;

	cl_git_pass(git_repository_index(&index, g_repo));
	git_index_clear(index);

	cl_git_mkfile("./testrepo/path0", "content\r\n");
	cl_git_pass(p_mkdir("./testrepo/path1", 0777));
	cl_git_mkfile("./testrepo/path1/file1", "content\r\n");

	cl_git_pass(git_index_add_from_workdir(index, "path0"));
	cl_git_pass(git_index_add_from_workdir(index, "path1/file1"));


	cl_git_pass(p_unlink("./testrepo/path0"));
	cl_git_pass(git_futils_rmdir_r(
		"./testrepo/path1", NULL, GIT_RMDIR_REMOVE_FILES));

	cl_git_mkfile("./testrepo/path1", "content\r\n");
	cl_git_pass(p_mkdir("./testrepo/path0", 0777));
	cl_git_mkfile("./testrepo/path0/file0", "content\r\n");

	cl_assert(git_path_isfile("./testrepo/path1"));
	cl_assert(git_path_isfile("./testrepo/path0/file0"));

	g_opts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_ALLOW_CONFLICTS;
	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));

	cl_assert(git_path_isfile("./testrepo/path1"));
	cl_assert(git_path_isfile("./testrepo/path0/file0"));

	g_opts.checkout_strategy = GIT_CHECKOUT_FORCE;
	cl_git_pass(git_checkout_index(g_repo, NULL, &g_opts));

	cl_assert(git_path_isfile("./testrepo/path0"));
	cl_assert(git_path_isfile("./testrepo/path1/file1"));

	git_index_free(index);
}

void test_checkout_index__validates_struct_version(void)
{
	const git_error *err;

	g_opts.version = 1024;
	cl_git_fail(git_checkout_index(g_repo, NULL, &g_opts));

	err = giterr_last();
	cl_assert_equal_i(err->klass, GITERR_INVALID);

	g_opts.version = 0;
	giterr_clear();
	cl_git_fail(git_checkout_index(g_repo, NULL, &g_opts));

	err = giterr_last();
	cl_assert_equal_i(err->klass, GITERR_INVALID);
}
