#include "clar_libgit2.h"

#include "futils.h"
#include "repo/repo_helpers.h"

#define CLEAR_FOR_CORE_FILEMODE(M) ((M) &= ~0177)

static git_repository *_repo = NULL;
static mode_t g_umask = 0;
static git_str _global_path = GIT_STR_INIT;

static const char *fixture_repo;
static const char *fixture_templates;

void test_repo_template__initialize(void)
{
	_repo = NULL;

	/* load umask if not already loaded */
	if (!g_umask) {
		g_umask = p_umask(022);
		(void)p_umask(g_umask);
	}
}

void test_repo_template__cleanup(void)
{
	git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_GLOBAL,
		_global_path.ptr);
	git_str_dispose(&_global_path);

	cl_fixture_cleanup("tmp_global_path");

	if (fixture_repo) {
		cl_fixture_cleanup(fixture_repo);
		fixture_repo = NULL;
	}

	if (fixture_templates) {
		cl_fixture_cleanup(fixture_templates);
		fixture_templates = NULL;
	}

	git_repository_free(_repo);
	_repo = NULL;
}

static void assert_hooks_match(
	const char *template_dir,
	const char *repo_dir,
	const char *hook_path,
	bool core_filemode)
{
	git_str expected = GIT_STR_INIT;
	git_str actual = GIT_STR_INIT;
	struct stat expected_st, st;

	cl_git_pass(git_str_joinpath(&expected, template_dir, hook_path));
	cl_git_pass(git_fs_path_lstat(expected.ptr, &expected_st));

	cl_git_pass(git_str_joinpath(&actual, repo_dir, hook_path));
	cl_git_pass(git_fs_path_lstat(actual.ptr, &st));

	cl_assert(expected_st.st_size == st.st_size);

	if (GIT_MODE_TYPE(expected_st.st_mode) != GIT_FILEMODE_LINK) {
		mode_t expected_mode =
			GIT_MODE_TYPE(expected_st.st_mode) |
			(GIT_PERMS_FOR_WRITE(expected_st.st_mode) & ~g_umask);

		if (!core_filemode) {
			CLEAR_FOR_CORE_FILEMODE(expected_mode);
			CLEAR_FOR_CORE_FILEMODE(st.st_mode);
		}

		cl_assert_equal_i_fmt(expected_mode, st.st_mode, "%07o");
	}

	git_str_dispose(&expected);
	git_str_dispose(&actual);
}

static void assert_mode_seems_okay(
	const char *base, const char *path,
	git_filemode_t expect_mode, bool expect_setgid, bool core_filemode)
{
	git_str full = GIT_STR_INIT;
	struct stat st;

	cl_git_pass(git_str_joinpath(&full, base, path));
	cl_git_pass(git_fs_path_lstat(full.ptr, &st));
	git_str_dispose(&full);

	if (!core_filemode) {
		CLEAR_FOR_CORE_FILEMODE(expect_mode);
		CLEAR_FOR_CORE_FILEMODE(st.st_mode);
		expect_setgid = false;
	}

	if (S_ISGID != 0)
		cl_assert_equal_b(expect_setgid, (st.st_mode & S_ISGID) != 0);

	cl_assert_equal_b(
		GIT_PERMS_IS_EXEC(expect_mode), GIT_PERMS_IS_EXEC(st.st_mode));

	cl_assert_equal_i_fmt(
		GIT_MODE_TYPE(expect_mode), GIT_MODE_TYPE(st.st_mode), "%07o");
}

static void setup_repo(const char *name, git_repository_init_options *opts)
{
	cl_git_pass(git_repository_init_ext(&_repo, name, opts));
	fixture_repo = name;
}

static void setup_templates(const char *name, bool setup_globally)
{
	git_str path = GIT_STR_INIT;

	cl_fixture_sandbox("template");
	if (strcmp(name, "template"))
		cl_must_pass(p_rename("template", name));

	fixture_templates = name;

	/*
	 * Create a symlink from link.sample to update.sample if the filesystem
	 * supports it.
	 */
	cl_git_pass(git_str_join3(&path, '/', name, "hooks", "link.sample"));
#ifdef GIT_WIN32
	cl_git_mkfile(path.ptr, "#!/bin/sh\necho hello, world\n");
#else
	cl_must_pass(p_symlink("update.sample", path.ptr));
#endif

	git_str_clear(&path);

	/* Create a file starting with a dot */
	cl_git_pass(git_str_join3(&path, '/', name, "hooks", ".dotfile"));
	cl_git_mkfile(path.ptr, "something\n");

	git_str_clear(&path);

	if (setup_globally) {
		cl_git_pass(git_str_joinpath(&path, clar_sandbox_path(), name));
		create_tmp_global_config("tmp_global_path", "init.templatedir", path.ptr);
	}

	git_str_dispose(&path);
}

static void validate_templates(git_repository *repo, const char *template_path)
{
	git_str path = GIT_STR_INIT, expected = GIT_STR_INIT, actual = GIT_STR_INIT;
	int filemode;

	cl_git_pass(git_str_joinpath(&path, template_path, "description"));
	cl_git_pass(git_futils_readbuffer(&expected, path.ptr));

	git_str_clear(&path);

	cl_git_pass(git_str_joinpath(&path, git_repository_path(repo), "description"));
	cl_git_pass(git_futils_readbuffer(&actual, path.ptr));

	cl_assert_equal_s(expected.ptr, actual.ptr);

	filemode = cl_repo_get_bool(repo, "core.filemode");

	assert_hooks_match(
		template_path, git_repository_path(repo),
		"hooks/update.sample", filemode);
	assert_hooks_match(
		template_path, git_repository_path(repo),
		"hooks/link.sample", filemode);
	assert_hooks_match(
		template_path, git_repository_path(repo),
		"hooks/.dotfile", filemode);

	git_str_dispose(&expected);
	git_str_dispose(&actual);
	git_str_dispose(&path);
}

void test_repo_template__external_templates_specified_in_options(void)
{
	git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;

	opts.flags = GIT_REPOSITORY_INIT_MKPATH | GIT_REPOSITORY_INIT_BARE |
		GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE;
	opts.template_path = "template";

	setup_templates("template", false);
	setup_repo("templated.git", &opts);

	validate_templates(_repo, "template");
}

void test_repo_template__external_templates_specified_in_config(void)
{
	git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;

	opts.flags = GIT_REPOSITORY_INIT_MKPATH | GIT_REPOSITORY_INIT_BARE |
		GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE;

	setup_templates("template", true);
	setup_repo("templated.git", &opts);

	validate_templates(_repo, "template");
}

void test_repo_template__external_templates_with_leading_dot(void)
{
	git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;

	opts.flags = GIT_REPOSITORY_INIT_MKPATH | GIT_REPOSITORY_INIT_BARE |
		GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE;

	setup_templates(".template_with_leading_dot", true);
	setup_repo("templated.git", &opts);

	validate_templates(_repo, ".template_with_leading_dot");
}

void test_repo_template__extended_with_template_and_shared_mode(void)
{
	git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;
	const char *repo_path;
	int filemode;

	opts.flags = GIT_REPOSITORY_INIT_MKPATH;
	opts.template_path = "template";
	opts.mode = GIT_REPOSITORY_INIT_SHARED_GROUP;

	setup_templates("template", false);
	setup_repo("init_shared_from_tpl", &opts);

	filemode = cl_repo_get_bool(_repo, "core.filemode");

	repo_path = git_repository_path(_repo);
	assert_mode_seems_okay(repo_path, "hooks",
		GIT_FILEMODE_TREE | GIT_REPOSITORY_INIT_SHARED_GROUP, true, filemode);
	assert_mode_seems_okay(repo_path, "info",
		GIT_FILEMODE_TREE | GIT_REPOSITORY_INIT_SHARED_GROUP, true, filemode);
	assert_mode_seems_okay(repo_path, "description",
		GIT_FILEMODE_BLOB, false, filemode);

	validate_templates(_repo, "template");
}

void test_repo_template__templated_head_is_used(void)
{
	git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;
	git_str head = GIT_STR_INIT;

	opts.flags = GIT_REPOSITORY_INIT_MKPATH | GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE;

	setup_templates("template", true);
	cl_git_mkfile("template/HEAD", "foobar\n");
	setup_repo("repo", &opts);

	cl_git_pass(git_futils_readbuffer(&head, "repo/.git/HEAD"));
	cl_assert_equal_s("foobar\n", head.ptr);

	git_str_dispose(&head);
}

void test_repo_template__initial_head_option_overrides_template_head(void)
{
	git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;
	git_str head = GIT_STR_INIT;

	opts.flags = GIT_REPOSITORY_INIT_MKPATH | GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE;
	opts.initial_head = "manual";

	setup_templates("template", true);
	cl_git_mkfile("template/HEAD", "foobar\n");
	setup_repo("repo", &opts);

	cl_git_pass(git_futils_readbuffer(&head, "repo/.git/HEAD"));
	cl_assert_equal_s("ref: refs/heads/manual\n", head.ptr);

	git_str_dispose(&head);
}

void test_repo_template__empty_template_path(void)
{
	git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;

	opts.flags = GIT_REPOSITORY_INIT_MKPATH | GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE;
	opts.template_path = "";

	setup_repo("foo", &opts);
}

void test_repo_template__nonexistent_template_path(void)
{
	git_repository_init_options opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;

	opts.flags = GIT_REPOSITORY_INIT_MKPATH | GIT_REPOSITORY_INIT_EXTERNAL_TEMPLATE;
	opts.template_path = "/tmp/path/that/does/not/exist/for/libgit2/test";

	setup_repo("bar", &opts);
}
