#define BOOST_TEST_MODULE GitFS_Core
#include <boost/test/data/test_case.hpp>
#include <boost/test/unit_test.hpp>

#include "mockDefs.h"
#include "sys/fcntl.h"
#include "sys/stat.h"

using namespace GitFS;
using namespace GitFS::Test;

namespace std {
	std::ostream &
	operator<<(std::ostream & strm, const std::tuple<std::string, int> &)
	{
		return strm;
	}

	template<typename T>
	std::ostream &
	operator<<(std::ostream & strm, const std::vector<T> & v)
	{
		strm << "[ ";
		for (const auto & e : v) {
			if (&e != &v.front()) {
				strm << ", ";
			}
			strm << e;
		}
		strm << " ]";
		return strm;
	}
}

#define BOOST_CHECK_THROW_SYSTEMERROR(CODE, ERRNO) \
	try { \
		BOOST_TEST_CHECKPOINT(""); \
		[&]() { \
			CODE; \
		}(); \
		BOOST_ERROR("No exception thrown, NetFS::SystemError expected"); \
	} \
	catch (const NetFS::SystemError & se) { \
		BOOST_CHECK_EQUAL(se.syserrno, ERRNO); \
	} \
	catch (...) { \
		BOOST_ERROR("NetFS::SystemError not thrown"); \
	}

BOOST_TEST_GLOBAL_FIXTURE(Service);

BOOST_FIXTURE_TEST_SUITE(volume, VolumeClient);

BOOST_AUTO_TEST_CASE(unsupported_rofs_ops)
{
	BOOST_CHECK_THROW_SYSTEMERROR(v->create(env, {}, {}, {}), EROFS);
	BOOST_CHECK_THROW_SYSTEMERROR(v->truncate(env, {}, {}), EROFS);
	BOOST_CHECK_THROW_SYSTEMERROR(v->unlink(env, {}), EROFS);
	BOOST_CHECK_THROW_SYSTEMERROR(v->mkdir(env, {}, {}), EROFS);
	BOOST_CHECK_THROW_SYSTEMERROR(v->rmdir(env, {}), EROFS);
	BOOST_CHECK_THROW_SYSTEMERROR(v->mknod(env, {}, {}, {}), EROFS);
	BOOST_CHECK_THROW_SYSTEMERROR(v->symlink(env, {}, {}), EROFS);
	BOOST_CHECK_THROW_SYSTEMERROR(v->link(env, {}, {}), EROFS);
	BOOST_CHECK_THROW_SYSTEMERROR(v->rename(env, {}, {}, {}), EROFS);
	BOOST_CHECK_THROW_SYSTEMERROR(v->chmod(env, {}, {}), EROFS);
	BOOST_CHECK_THROW_SYSTEMERROR(v->chown(env, {}, {}, {}), EROFS);
	BOOST_CHECK_THROW_SYSTEMERROR(v->utimens(env, {}, {}, {}, {}, {}), EROFS);
}

BOOST_AUTO_TEST_CASE(statfs)
{
	// Don't know what this should return, but it shouldn't error given a valid path
	BOOST_CHECK_THROW_SYSTEMERROR(v->statfs(env, ""), EINVAL);
	BOOST_CHECK_NO_THROW(v->statfs(env, "/"));
}

namespace btdata = boost::unit_test::data;
const auto INVALIDPATHS = btdata::make({""});
const auto BADPATHS = btdata::make({
		"/.",
		"/../",
		".",
		"..",
		"../",
});
const auto DIRPATHS = btdata::make({
		"/",
		"/src",
		"/unittests",
		"/unittests/fixtures",
});
const auto REGPATHS = btdata::make({
		"/.gitignore",
		"/Jamroot.jam",
		"/src/Jamfile.jam",
		"/unittests/Jamfile.jam",
});
const auto EXECPATHS = btdata::make({
		"/unittests/fixtures/executable",
});
const auto LINKPATHS = btdata::make({
		"/unittests/fixtures/symlink",
});
const auto MISSINGPATHS = btdata::make({
		"/.missing",
		"/missing",
		"/src/missing",
		"/unittests/fixtures/missing",
});

BOOST_DATA_TEST_CASE(
		accessWrite, INVALIDPATHS + DIRPATHS + REGPATHS + EXECPATHS + LINKPATHS + MISSINGPATHS + BADPATHS, path)
{
	BOOST_CHECK_EQUAL(EACCES, v->access(env, path, W_OK));
}
BOOST_DATA_TEST_CASE(accessDirs, DIRPATHS, path)
{
	BOOST_CHECK_EQUAL(0, v->access(env, path, R_OK));
	BOOST_CHECK_EQUAL(0, v->access(env, path, X_OK));
}
BOOST_DATA_TEST_CASE(accessRead, REGPATHS, path)
{
	BOOST_CHECK_EQUAL(0, v->access(env, path, R_OK));
	BOOST_CHECK_EQUAL(EACCES, v->access(env, path, X_OK));
}
BOOST_DATA_TEST_CASE(accessLink, LINKPATHS, path)
{
	BOOST_CHECK_EQUAL(0, v->access(env, path, R_OK));
	BOOST_CHECK_EQUAL(0, v->access(env, path, X_OK));
}
BOOST_DATA_TEST_CASE(accessExec, EXECPATHS, path)
{
	BOOST_CHECK_EQUAL(0, v->access(env, path, R_OK));
	BOOST_CHECK_EQUAL(0, v->access(env, path, X_OK));
}
BOOST_DATA_TEST_CASE(accessInval, INVALIDPATHS * btdata::make({R_OK, X_OK}), path, mode)
{
	BOOST_CHECK_EQUAL(EINVAL, v->access(env, path, mode));
}
BOOST_DATA_TEST_CASE(accessBad, BADPATHS * btdata::make({R_OK, X_OK}), path, mode)
{
	BOOST_CHECK_EQUAL(ENOENT, v->access(env, path, mode));
}
BOOST_DATA_TEST_CASE(statInval, INVALIDPATHS, path)
{
	BOOST_CHECK_THROW_SYSTEMERROR(v->getattr(env, path), EINVAL);
}
BOOST_DATA_TEST_CASE(statBad, BADPATHS, path)
{
	BOOST_CHECK_THROW_SYSTEMERROR(v->getattr(env, path), ENOENT);
}

const auto DIRMODE = S_IFDIR | S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH;
const auto FILEMODE = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;
const auto EXECMODE = FILEMODE | S_IXUSR | S_IXGRP | S_IXOTH;
const auto LINKMODE = S_IFLNK | S_IRUSR | S_IRGRP | S_IROTH;
const time_t COMMIT_TIME = 1563621030;
const std::string USER = "root";
const std::string GROUP = "root";

BOOST_DATA_TEST_CASE(statDirs, DIRPATHS, path)
{
	const auto attr = v->getattr(env, path);
	BOOST_CHECK_EQUAL(DIRMODE, attr.mode);
	BOOST_CHECK_EQUAL(COMMIT_TIME, attr.mtime);
	BOOST_CHECK_EQUAL(COMMIT_TIME, attr.ctime);
	BOOST_CHECK_EQUAL(COMMIT_TIME, attr.atime);
	BOOST_CHECK_EQUAL(0, attr.size);
	BOOST_CHECK_EQUAL(USER, attr.uid);
	BOOST_CHECK_EQUAL(GROUP, attr.gid);
}
BOOST_DATA_TEST_CASE(statFiles, REGPATHS, path)
{
	const auto attr = v->getattr(env, path);
	BOOST_CHECK_EQUAL(FILEMODE, attr.mode);
	BOOST_CHECK_EQUAL(COMMIT_TIME, attr.mtime);
	BOOST_CHECK_EQUAL(COMMIT_TIME, attr.ctime);
	BOOST_CHECK_EQUAL(COMMIT_TIME, attr.atime);
	BOOST_CHECK_LE(4, attr.size);
	BOOST_CHECK_EQUAL(USER, attr.uid);
	BOOST_CHECK_EQUAL(GROUP, attr.gid);
}
BOOST_DATA_TEST_CASE(statExecs, EXECPATHS, path)
{
	const auto attr = v->getattr(env, path);
	BOOST_CHECK_EQUAL(EXECMODE, attr.mode);
	BOOST_CHECK_EQUAL(COMMIT_TIME, attr.mtime);
	BOOST_CHECK_EQUAL(COMMIT_TIME, attr.ctime);
	BOOST_CHECK_EQUAL(COMMIT_TIME, attr.atime);
	BOOST_CHECK_LE(4, attr.size);
	BOOST_CHECK_EQUAL(USER, attr.uid);
	BOOST_CHECK_EQUAL(GROUP, attr.gid);
}
BOOST_DATA_TEST_CASE(statSymlink, LINKPATHS, path)
{
	const auto attr = v->getattr(env, path);
	BOOST_CHECK_EQUAL(LINKMODE, attr.mode);
	BOOST_CHECK_EQUAL(COMMIT_TIME, attr.mtime);
	BOOST_CHECK_EQUAL(COMMIT_TIME, attr.ctime);
	BOOST_CHECK_EQUAL(COMMIT_TIME, attr.atime);
	BOOST_CHECK_EQUAL(0, attr.size);
	BOOST_CHECK_EQUAL(USER, attr.uid);
	BOOST_CHECK_EQUAL(GROUP, attr.gid);
}

BOOST_DATA_TEST_CASE(readlinkInval, INVALIDPATHS + DIRPATHS + REGPATHS + EXECPATHS, path)
{
	BOOST_CHECK_THROW_SYSTEMERROR(v->readlink(env, path), EINVAL);
}
BOOST_DATA_TEST_CASE(readlinkBad, BADPATHS + MISSINGPATHS, path)
{
	BOOST_CHECK_THROW_SYSTEMERROR(v->readlink(env, path), ENOENT);
}
BOOST_DATA_TEST_CASE(readlink, LINKPATHS ^ btdata::make({"executable"}), path, target)
{
	BOOST_CHECK_EQUAL(target, v->readlink(env, path));
}

BOOST_DATA_TEST_CASE(openDirInval, INVALIDPATHS, path)
{
	BOOST_CHECK_THROW_SYSTEMERROR(v->opendir(env, path), EINVAL);
}
BOOST_DATA_TEST_CASE(openDirBad, BADPATHS + MISSINGPATHS, path)
{
	BOOST_CHECK_THROW_SYSTEMERROR(v->opendir(env, path), ENOENT);
}
BOOST_DATA_TEST_CASE(openDirNotDir, REGPATHS + LINKPATHS + EXECPATHS, path)
{
	BOOST_CHECK_THROW_SYSTEMERROR(v->opendir(env, path), ENOTDIR);
}
const auto DIRCONTENTS = btdata::make<std::vector<std::string>>({
		{".gitignore", "Jamroot.jam", "src", "unittests"},
		{"Jamfile.jam", "blob.cpp", "blob.h", "dir.cpp", "dir.h", "git.cpp", "git.h", "main.cpp", "repo.cpp", "repo.h",
				"repoList.cpp", "repoList.h"},
		{"Jamfile.jam", "core.cpp", "fixtures", "mockDefs.cpp", "mockDefs.h"},
		{"executable", "symlink"},
});
BOOST_DATA_TEST_CASE(openDirRead, DIRPATHS ^ DIRCONTENTS, path, contents)
{
	auto dir = v->opendir(env, path);
	BOOST_REQUIRE(dir);
	auto names = dir->readdir();
	std::sort(names.begin(), names.end());
	BOOST_CHECK_EQUAL_COLLECTIONS(names.begin(), names.end(), contents.begin(), contents.end());
	dir->close();
}
const auto DIRCONTENTMODES = btdata::make<std::vector<std::tuple<std::string, int>>>({
		{{".gitignore", FILEMODE}, {"Jamroot.jam", FILEMODE}, {"src", DIRMODE}, {"unittests", DIRMODE}},
		{{"Jamfile.jam", FILEMODE}, {"blob.cpp", FILEMODE}, {"blob.h", FILEMODE}, {"dir.cpp", FILEMODE},
				{"dir.h", FILEMODE}, {"git.cpp", FILEMODE}, {"git.h", FILEMODE}, {"main.cpp", FILEMODE},
				{"repo.cpp", FILEMODE}, {"repo.h", FILEMODE}, {"repoList.cpp", FILEMODE}, {"repoList.h", FILEMODE}},
		{{"Jamfile.jam", FILEMODE}, {"core.cpp", FILEMODE}, {"fixtures", DIRMODE}, {"mockDefs.cpp", FILEMODE},
				{"mockDefs.h", FILEMODE}},
		{{"executable", EXECMODE}, {"symlink", LINKMODE}},
});
BOOST_DATA_TEST_CASE(openDirList, DIRPATHS ^ DIRCONTENTMODES, path, contents)
{
	auto dir = v->opendir(env, path);
	BOOST_REQUIRE(dir);
	auto list = dir->listdir();
	BOOST_REQUIRE_EQUAL(contents.size(), list.size());
	for (const auto & c : contents) {
		auto li = list.find(std::get<0>(c));
		BOOST_REQUIRE(li != list.end());
		BOOST_CHECK_EQUAL(li->second.mode, std::get<1>(c));
		BOOST_CHECK_EQUAL(COMMIT_TIME, li->second.mtime);
		BOOST_CHECK_EQUAL(COMMIT_TIME, li->second.ctime);
		BOOST_CHECK_EQUAL(COMMIT_TIME, li->second.atime);
		BOOST_CHECK_EQUAL(USER, li->second.uid);
		BOOST_CHECK_EQUAL(GROUP, li->second.gid);
		if (S_ISREG(li->second.mode)) {
			BOOST_CHECK_LE(4, li->second.size);
		}
	}
	dir->close();
}

BOOST_DATA_TEST_CASE(openInval, INVALIDPATHS, path)
{
	BOOST_CHECK_THROW_SYSTEMERROR(v->open(env, path, O_RDONLY), EINVAL);
}
BOOST_DATA_TEST_CASE(openBad, BADPATHS + MISSINGPATHS, path)
{
	BOOST_CHECK_THROW_SYSTEMERROR(v->open(env, path, O_RDONLY), ENOENT);
}
BOOST_DATA_TEST_CASE(openNotFileDir, DIRPATHS, path)
{
	BOOST_CHECK_THROW_SYSTEMERROR(v->open(env, path, O_RDONLY), EISDIR);
}
BOOST_DATA_TEST_CASE(openNotFileLink, LINKPATHS, path)
{
	BOOST_CHECK_THROW_SYSTEMERROR(v->open(env, path, O_RDONLY), ELOOP);
}
BOOST_DATA_TEST_CASE(openFileROFSOps, REGPATHS + EXECPATHS, path)
{
	auto f = v->open(env, path, O_RDONLY);
	BOOST_REQUIRE(f);
	BOOST_CHECK_THROW_SYSTEMERROR(f->ftruncate(env, {}), EROFS);
	BOOST_CHECK_THROW_SYSTEMERROR(f->write({}, {}, {}), EROFS);
	f->close();
}

BOOST_DATA_TEST_CASE(openFileGetAttr, REGPATHS + EXECPATHS, path)
{
	auto f = v->open(env, path, O_RDONLY);
	BOOST_REQUIRE(f);
	const auto attr = f->fgetattr(env);
	BOOST_CHECK(S_ISREG(attr.mode));
	BOOST_CHECK_EQUAL(attr.size, attr.blocks);
	BOOST_CHECK_EQUAL(1, attr.blockSize);
	BOOST_CHECK_LE(4, attr.size);
	BOOST_CHECK_EQUAL(USER, attr.uid);
	BOOST_CHECK_EQUAL(GROUP, attr.gid);
	f->close();
}

BOOST_DATA_TEST_CASE(openFileRead, REGPATHS + EXECPATHS, path)
{
	auto f = v->open(env, path, O_RDONLY);
	BOOST_REQUIRE(f);

	const auto attr = f->fgetattr(env);

	const auto readAll = f->read(0, attr.size);
	BOOST_CHECK_EQUAL(attr.size, readAll.size());

	const auto readTrunc = f->read(0, BUFSIZ);
	BOOST_CHECK_EQUAL(attr.size, readTrunc.size());

	const auto readBeyond = f->read(BUFSIZ, BUFSIZ);
	BOOST_CHECK(readBeyond.empty());

	f->close();
}

BOOST_AUTO_TEST_SUITE_END();