/*
 * Copyright 2018 Bloomberg Finance LP
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "buildboxcommontest_utils.h"
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_permissions.h>
#include <buildboxcommon_tempconstants.h>
#include <buildboxcommon_temporarydirectory.h>
#include <buildboxcommon_temporaryfile.h>
#include <buildboxcommon_timeutils.h>

#include <chrono>
#include <gtest/gtest.h>

// To create tempfiles
#include <fstream>
#include <iostream>

#include <dirent.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <system_error>
#include <unistd.h>

using namespace buildboxcommon;

class CreateDirectoryTestFixture : public ::testing::TestWithParam<mode_t> {
  protected:
    CreateDirectoryTestFixture() {}
};

mode_t getUmask()
{
    mode_t mask = umask(0); // Get the current umask value
    umask(mask);            // Restore the umask value
    return mask;
}

mode_t getExpectedPermissions(mode_t basePermissions)
{
    return basePermissions & ~getUmask();
}

mode_t getDefaultDirectoryPermissions()
{
    return getExpectedPermissions(0777);
}

mode_t dirPermissions(const std::string &path)
{
    struct stat stat_buf = {};
    const int stat_status = stat(path.c_str(), &stat_buf);
    if (stat_status != 0) {
        throw std::system_error(errno, std::system_category(),
                                "Error calling stat(" + path + ")");
    }

    return stat_buf.st_mode & 0777;
}
INSTANTIATE_TEST_SUITE_P(CreateDirectoryTests, CreateDirectoryTestFixture,
                         ::testing::Values(0755, 0700));

TEST_P(CreateDirectoryTestFixture, DirectoryTests)
{
    TemporaryDirectory tmpdir;

    const std::string pathStr = std::string(tmpdir.name()) + "/foodir/";
    const char *path = pathStr.c_str();

    EXPECT_FALSE(buildboxcommontest::TestUtils::pathExists(path));
    EXPECT_FALSE(FileUtils::isDirectory(path));
    EXPECT_FALSE(FileUtils::isRegularFile(path));

    const mode_t mode = GetParam();
    FileUtils::createDirectory(path, mode);

    EXPECT_TRUE(buildboxcommontest::TestUtils::pathExists(path));
    EXPECT_TRUE(FileUtils::isDirectory(path));
    EXPECT_FALSE(FileUtils::isRegularFile(path));
    EXPECT_EQ(dirPermissions(path), mode);

    FileUtils::deleteDirectory(path);

    EXPECT_FALSE(buildboxcommontest::TestUtils::pathExists(path));
    EXPECT_FALSE(FileUtils::isDirectory(path));
    EXPECT_FALSE(FileUtils::isRegularFile(path));
}

TEST(CreateDirectoryTestFixture, CreateDirectoryDefaultMode)
{
    const mode_t default_mode = getDefaultDirectoryPermissions();
    TemporaryDirectory dir;

    const std::string path = std::string(dir.name()) + "/subdir";

    EXPECT_FALSE(FileUtils::isDirectory(path.c_str()));
    FileUtils::createDirectory(path.c_str());
    EXPECT_TRUE(FileUtils::isDirectory(path.c_str()));

    EXPECT_EQ(dirPermissions(path), default_mode);
}

TEST_P(CreateDirectoryTestFixture, CreateDirectorySingleLevel)
{
    TemporaryDirectory dir;

    const std::string path = std::string(dir.name()) + "/subdir";

    EXPECT_FALSE(FileUtils::isDirectory(path.c_str()));

    const mode_t mode = GetParam();
    FileUtils::createDirectory(path.c_str(), mode);

    EXPECT_TRUE(FileUtils::isDirectory(path.c_str()));
    EXPECT_EQ(dirPermissions(path), mode);
}

TEST_P(CreateDirectoryTestFixture, CreateDirectoryPlusItsParents)
{
    const mode_t mode = GetParam();

    TemporaryDirectory dir;
    EXPECT_EQ(chmod(dir.name(), mode), 0);

    const std::string root_directory = dir.name();
    const std::string path = root_directory + "/dir1/dir2/dir3/";

    EXPECT_FALSE(FileUtils::isDirectory(path.c_str()));

    FileUtils::createDirectory(path.c_str(), mode);
    EXPECT_TRUE(FileUtils::isDirectory(path.c_str()));
    EXPECT_EQ(dirPermissions(path), mode);

    // The subdirectories were created with the same mode:
    EXPECT_EQ(dirPermissions(root_directory + "/dir1"), mode);
    EXPECT_EQ(dirPermissions(root_directory + "/dir1/dir2/"), mode);
}

TEST_P(CreateDirectoryTestFixture, CreateDirectoryDotDot)
{
    const mode_t mode = GetParam();

    TemporaryDirectory dir;
    EXPECT_EQ(chmod(dir.name(), mode), 0);

    const std::string root_directory = dir.name();
    const std::string path = root_directory + "/dir1/../dir2";

    EXPECT_FALSE(FileUtils::isDirectory(path.c_str()));

    FileUtils::createDirectory(path.c_str(), mode);
    EXPECT_TRUE(FileUtils::isDirectory(path.c_str()));
    EXPECT_EQ(dirPermissions(path), mode);
}

TEST_P(CreateDirectoryTestFixture, CreateDirectoryDotDotLastExists)
{
    const mode_t mode = GetParam();

    TemporaryDirectory dir;
    EXPECT_EQ(chmod(dir.name(), mode), 0);

    const std::string root_directory = dir.name();
    const std::string path_without_dotdot = root_directory + "/dir2";
    const std::string path_with_dotdot = root_directory + "/dir1/../dir2";

    EXPECT_FALSE(FileUtils::isDirectory(path_without_dotdot.c_str()));
    EXPECT_FALSE(FileUtils::isDirectory(path_with_dotdot.c_str()));

    FileUtils::createDirectory(path_without_dotdot.c_str(), mode);
    EXPECT_TRUE(FileUtils::isDirectory(path_without_dotdot.c_str()));
    EXPECT_EQ(dirPermissions(path_without_dotdot), mode);

    FileUtils::createDirectory(path_with_dotdot.c_str(), mode);
    EXPECT_TRUE(FileUtils::isDirectory(path_with_dotdot.c_str()));
    EXPECT_EQ(dirPermissions(path_with_dotdot), mode);
}

TEST(FileUtilsTest, CreateExistingDirectory)
{
    TemporaryDirectory dir;
    EXPECT_NO_THROW(FileUtils::createDirectory(dir.name()));
}

TEST(FileUtilsTests, IsFile)
{
    TemporaryDirectory tmpdir;
    std::string pathStr = std::string(tmpdir.name()) + "/foo.txt";
    const char *path = pathStr.c_str();

    buildboxcommontest::TestUtils::touchFile(path);

    EXPECT_TRUE(buildboxcommontest::TestUtils::pathExists(path));

    EXPECT_TRUE(FileUtils::isRegularFile(path));
    EXPECT_FALSE(FileUtils::isDirectory(path));
}

TEST(FileUtilsTests, IsFileFD)
{
    TemporaryFile file;

    EXPECT_NE(file.fd(), -1);
    EXPECT_FALSE(FileUtils::isDirectory(file.fd()));
}

TEST(FileUtilsTests, IsNotFileFD)
{
    TemporaryDirectory dir;

    const int dir_fd = open(dir.name(), O_RDONLY);
    EXPECT_NE(dir_fd, -1);

    EXPECT_TRUE(FileUtils::isDirectory(dir_fd));
}

TEST(FileUtilsTests, IsDirectoryBadFdReturnsFalse)
{
    const int bad_fd = -1;
    EXPECT_THROW(FileUtils::isDirectory(bad_fd), std::system_error);
}

TEST(FileUtilsTests, ExecutableTests)
{
    TemporaryDirectory tmpdir;
    std::string pathStr = std::string(tmpdir.name()) + "/foo.sh";
    const char *path = pathStr.c_str();

    EXPECT_FALSE(buildboxcommontest::TestUtils::pathExists(path));
    EXPECT_FALSE(FileUtils::isExecutable(path));

    buildboxcommontest::TestUtils::touchFile(path);

    EXPECT_TRUE(buildboxcommontest::TestUtils::pathExists(path));
    EXPECT_TRUE(FileUtils::isRegularFile(path));
    EXPECT_FALSE(FileUtils::isExecutable(path));

    FileUtils::makeExecutable(path);

    EXPECT_TRUE(buildboxcommontest::TestUtils::pathExists(path));
    EXPECT_TRUE(FileUtils::isRegularFile(path));
    EXPECT_TRUE(FileUtils::isExecutable(path));
}

TEST(FileUtilsTests, IsSymlink)
{
    TemporaryDirectory dir;
    const auto file_in_dir =
        buildboxcommontest::TestUtils::createFileInDirectory("file1",
                                                             dir.name());
    EXPECT_FALSE(FileUtils::isSymlink(file_in_dir.c_str()));
    const auto symlink_path = dir.strname() + "/symlink";
    EXPECT_EQ(symlink(file_in_dir.c_str(), symlink_path.c_str()), 0);
    EXPECT_TRUE(FileUtils::isSymlink(symlink_path.c_str()));
}

TEST(FileUtilsTest, DirectoryIsEmptyTest)
{
    TemporaryDirectory dir;

    EXPECT_TRUE(FileUtils::directoryIsEmpty(dir.name()));
}

TEST(FileUtilsTest, DirectoryIsNotEmptyTest)
{
    TemporaryDirectory dir;

    const std::string file_path = std::string(dir.name()) + "/file.txt";

    std::ofstream file(file_path);
    file.close();

    EXPECT_FALSE(FileUtils::directoryIsEmpty(dir.name()));
}

TEST(FileUtilsTest, RemoveSymlinkToDirectory)
{
    TemporaryDirectory dir;
    TemporaryDirectory dir2;
    EXPECT_TRUE(FileUtils::isDirectory(dir.name()));
    EXPECT_TRUE(FileUtils::isDirectory(dir2.name()));

    // Create a symlink to dir from a subdirectory in dir2
    const auto symlink_to_dir =
        std::string(dir2.name()) + "/" + "symlink_to_dir";
    EXPECT_EQ(symlink(dir.name(), symlink_to_dir.c_str()), 0);

    const auto file_in_dir = std::string(dir.name()) + "/" + "file_in_dir.txt";
    buildboxcommontest::TestUtils::touchFile(file_in_dir.c_str());
    EXPECT_TRUE(FileUtils::isRegularFile(file_in_dir.c_str()));

    // Assert it is a symlink
    EXPECT_FALSE(FileUtils::isDirectoryNoFollow(symlink_to_dir.c_str()));
    EXPECT_TRUE(FileUtils::isDirectory(symlink_to_dir.c_str()));
    EXPECT_FALSE(FileUtils::directoryIsEmpty(dir.name()));

    // Clear dir2
    FileUtils::clearDirectory(dir2.name());
    EXPECT_TRUE(FileUtils::directoryIsEmpty(dir2.name()));

    // Check that dir still exists
    EXPECT_TRUE(FileUtils::isDirectory(dir.name()));
    // Assert file exists in dir
    EXPECT_TRUE(FileUtils::isRegularFile(file_in_dir.c_str()));
    EXPECT_FALSE(FileUtils::directoryIsEmpty(dir.name()));
}

TEST(FileUtilsTests, ClearDirectoryTest)
{
    TemporaryDirectory directory;

    // Populating the directory with a subdirectory and a file:
    const std::string subdirectory_path =
        std::string(directory.name()) + "/subdir";
    FileUtils::createDirectory(subdirectory_path.c_str());

    EXPECT_TRUE(FileUtils::isDirectory(subdirectory_path.c_str()));

    const std::string file_in_subdirectory_path =
        subdirectory_path + "/file1.txt";
    buildboxcommontest::TestUtils::touchFile(
        file_in_subdirectory_path.c_str());

    // Create a symlink in the subdir directory to the test file
    const auto symlink_in_subdir = subdirectory_path + "/file2.txt";
    EXPECT_EQ(0, symlink(file_in_subdirectory_path.c_str(),
                         symlink_in_subdir.c_str()));
    // Assert it is a symlink
    EXPECT_FALSE(FileUtils::isRegularFileNoFollow(symlink_in_subdir.c_str()));
    EXPECT_TRUE(FileUtils::isRegularFile(symlink_in_subdir.c_str()));

    EXPECT_FALSE(FileUtils::directoryIsEmpty(directory.name()));
    FileUtils::clearDirectory(directory.name());

    EXPECT_TRUE(buildboxcommontest::TestUtils::pathExists(directory.name()));
    EXPECT_TRUE(FileUtils::directoryIsEmpty(directory.name()));
}

TEST(FileUtilsTests, ClearDirectoryTestReadOnlyDir)
{
    TemporaryDirectory directory;

    // Populating the directory with a subdirectory and a file:
    const std::string subdirectory_path =
        std::string(directory.name()) + "/subdir";
    FileUtils::createDirectory(subdirectory_path.c_str());

    EXPECT_TRUE(FileUtils::isDirectory(subdirectory_path.c_str()));

    const std::string file_in_subdirectory_path =
        subdirectory_path + "/file1.txt";
    buildboxcommontest::TestUtils::touchFile(
        file_in_subdirectory_path.c_str());

    // Create a symlink in the subdir directory to the test file
    const auto symlink_in_subdir = subdirectory_path + "/file2.txt";
    EXPECT_EQ(0, symlink(file_in_subdirectory_path.c_str(),
                         symlink_in_subdir.c_str()));
    // Assert it is a symlink
    EXPECT_FALSE(FileUtils::isRegularFileNoFollow(symlink_in_subdir.c_str()));
    EXPECT_TRUE(FileUtils::isRegularFile(symlink_in_subdir.c_str()));

    // Restrict the permission of the subdirectory to remove write permissions
    // and ensure that the directory can still be cleared
    chmod(subdirectory_path.c_str(), 0555);
    EXPECT_FALSE(FileUtils::directoryIsEmpty(directory.name()));
    FileUtils::clearDirectory(directory.name());

    EXPECT_TRUE(buildboxcommontest::TestUtils::pathExists(directory.name()));
    EXPECT_TRUE(FileUtils::directoryIsEmpty(directory.name()));
}

TEST(FileUtilsTests, ClearDirectoryTestReadOnlyRootDir)
{
    TemporaryDirectory directory;

    // Populating the directory with a subdirectory and a file:
    const std::string subdirectory_path =
        std::string(directory.name()) + "/subdir";
    FileUtils::createDirectory(subdirectory_path.c_str());

    EXPECT_TRUE(FileUtils::isDirectory(subdirectory_path.c_str()));

    const std::string file_in_subdirectory_path =
        subdirectory_path + "/file1.txt";
    buildboxcommontest::TestUtils::touchFile(
        file_in_subdirectory_path.c_str());

    const std::string file_in_root =
        std::string(directory.name()) + "/root_file.txt";
    buildboxcommontest::TestUtils::touchFile(file_in_root.c_str());

    // Restrict the permission of the root directory to remove write
    // permissions and ensure that the directory can still be cleared
    chmod(directory.name(), 0555);
    const mode_t original_mode = dirPermissions(directory.name());

    EXPECT_FALSE(FileUtils::directoryIsEmpty(directory.name()));
    FileUtils::clearDirectory(directory.name());

    EXPECT_TRUE(buildboxcommontest::TestUtils::pathExists(directory.name()));
    EXPECT_TRUE(FileUtils::directoryIsEmpty(directory.name()));

    // Verify that the original permissions were restored
    EXPECT_EQ(dirPermissions(directory.name()), original_mode);
}

TEST(FileUtilsTests, DeleteDirectoryTestReadOnlyRootDir)
{
    TemporaryDirectory parentDir;

    const std::string root_directory_path =
        std::string(parentDir.name()) + "/root_dir";
    FileUtils::createDirectory(root_directory_path.c_str());

    // Populating the directory with a subdirectory and a file:
    const std::string subdirectory_path = root_directory_path + "/subdir";
    FileUtils::createDirectory(subdirectory_path.c_str());

    EXPECT_TRUE(FileUtils::isDirectory(subdirectory_path.c_str()));

    const std::string file_in_subdirectory_path =
        subdirectory_path + "/file1.txt";
    buildboxcommontest::TestUtils::touchFile(
        file_in_subdirectory_path.c_str());

    const std::string file_in_root = root_directory_path + "/root_file.txt";
    buildboxcommontest::TestUtils::touchFile(file_in_root.c_str());

    // Restrict the permission of the root directory to remove write
    // permissions and ensure that the directory can still be deleted
    chmod(root_directory_path.c_str(), 0555);

    EXPECT_TRUE(FileUtils::isDirectory(root_directory_path.c_str()));
    FileUtils::deleteDirectory(root_directory_path.c_str());

    EXPECT_FALSE(buildboxcommontest::TestUtils::pathExists(
        root_directory_path.c_str()));
}

TEST(FileUtilsTests, ClearDirectoryTestNestedReadOnlyDirsWithReadOnlyRoot)
{
    TemporaryDirectory directory;

    // Create a nested structure with multiple read-only directories
    const std::string subdirectory_path =
        std::string(directory.name()) + "/subdir";
    FileUtils::createDirectory(subdirectory_path.c_str());

    const std::string nested_dir_path = subdirectory_path + "/nested";
    FileUtils::createDirectory(nested_dir_path.c_str());

    const std::string file_in_nested = nested_dir_path + "/file.txt";
    buildboxcommontest::TestUtils::touchFile(file_in_nested.c_str());

    // Make all directories read-only
    chmod(nested_dir_path.c_str(), 0555);
    chmod(subdirectory_path.c_str(), 0555);
    chmod(directory.name(), 0555);
    const mode_t original_mode = dirPermissions(directory.name());

    EXPECT_FALSE(FileUtils::directoryIsEmpty(directory.name()));
    FileUtils::clearDirectory(directory.name());

    EXPECT_TRUE(buildboxcommontest::TestUtils::pathExists(directory.name()));
    EXPECT_TRUE(FileUtils::directoryIsEmpty(directory.name()));

    // Verify that the root directory permissions were restored
    EXPECT_EQ(dirPermissions(directory.name()), original_mode);
}

TEST(ContainsDotDotTest, PathsWithDotDot)
{
    EXPECT_TRUE(FileUtils::containsDotDot("../foo"));
    EXPECT_TRUE(FileUtils::containsDotDot("foo/../bar"));
    EXPECT_TRUE(FileUtils::containsDotDot("/a/b/../c"));
    EXPECT_TRUE(FileUtils::containsDotDot(".."));
    EXPECT_TRUE(FileUtils::containsDotDot("foo/.."));
    EXPECT_TRUE(FileUtils::containsDotDot("/.."));
}

TEST(ContainsDotDotTest, PathsWithoutDotDot)
{
    EXPECT_FALSE(FileUtils::containsDotDot("foo/bar"));
    EXPECT_FALSE(FileUtils::containsDotDot("foo/./bar"));
    EXPECT_FALSE(FileUtils::containsDotDot("/usr/bin"));
    EXPECT_FALSE(FileUtils::containsDotDot("."));
    EXPECT_FALSE(FileUtils::containsDotDot("/a/b/c"));
    EXPECT_FALSE(FileUtils::containsDotDot(""));
}

TEST(ContainsDotDotTest, DotDotInFileName)
{
    EXPECT_FALSE(FileUtils::containsDotDot("foo..bar"));
    EXPECT_FALSE(FileUtils::containsDotDot("..foo"));
    EXPECT_FALSE(FileUtils::containsDotDot("foo.."));
}

TEST(NormalizePathTest, AlreadyNormalPaths)
{
    EXPECT_EQ("test.txt", FileUtils::normalizePath("test.txt"));
    EXPECT_EQ("subdir/hello", FileUtils::normalizePath("subdir/hello"));
    EXPECT_EQ("/usr/bin/gcc", FileUtils::normalizePath("/usr/bin/gcc"));
    EXPECT_EQ(".", FileUtils::normalizePath("."));
}

TEST(NormalizePathTest, RemoveEmptySegments)
{
    EXPECT_EQ("subdir/hello", FileUtils::normalizePath("subdir///hello//"));
    EXPECT_EQ("/usr/bin/gcc", FileUtils::normalizePath("/usr/bin/./gcc"));
}

TEST(NormalizePathTest, RemoveUnneededDots)
{
    EXPECT_EQ("subdir/hello",
              FileUtils::normalizePath("subdir/subsubdir/../hello"));
    EXPECT_EQ("/usr/bin/gcc",
              FileUtils::normalizePath("/usr/local/lib/../../bin/.//gcc"));
    EXPECT_EQ("/usr/bin/gcc", FileUtils::normalizePath("/../usr/bin/gcc"));
    EXPECT_EQ("/usr/bin/gcc",
              FileUtils::normalizePath("/usr/../../usr/bin/gcc"));
    EXPECT_EQ("/b/c", FileUtils::normalizePath("/a/../b/c/"));
    EXPECT_EQ("b/c", FileUtils::normalizePath("a/../b/c/"));
    EXPECT_EQ("gcc", FileUtils::normalizePath("./gcc"));
}

TEST(NormalizePathTest, KeepNeededDotDot)
{
    EXPECT_EQ("../dir/hello", FileUtils::normalizePath("../dir/hello"));
    EXPECT_EQ("../dir/hello",
              FileUtils::normalizePath("subdir/../../dir/hello"));
    EXPECT_EQ("../../dir/hello",
              FileUtils::normalizePath("subdir/../../../dir/hello"));
}

TEST(NormalizePathTest, AlwaysRemoveTrailingSlash)
{
    EXPECT_EQ("/usr/bin", FileUtils::normalizePath("/usr/bin"));
    EXPECT_EQ("/usr/bin", FileUtils::normalizePath("/usr/bin/"));
    EXPECT_EQ(".", FileUtils::normalizePath("./"));
}

TEST(NormalizePathTest, CurrentDirectory)
{
    EXPECT_EQ(".", FileUtils::normalizePath("foo/.."));
    EXPECT_EQ(".", FileUtils::normalizePath("foo/bar/../.."));
    EXPECT_EQ(".", FileUtils::normalizePath("foo/../bar/.."));
}

TEST(MakePathAbsoluteTest, CwdNotAbsoluteThrows)
{
    EXPECT_THROW(FileUtils::FileUtils::makePathAbsolute("a/b/", "a/b"),
                 std::runtime_error);

    EXPECT_THROW(FileUtils::FileUtils::makePathAbsolute("/a/b/c", ""),
                 std::runtime_error);

    EXPECT_THROW(FileUtils::FileUtils::makePathAbsolute("", "a/b"),
                 std::runtime_error);
}

TEST(MakePathAbsoluteTest, SimplePaths)
{
    EXPECT_EQ("/a/b/c/d", FileUtils::makePathAbsolute("d", "/a/b/c/"));
    EXPECT_EQ("/a/b/c/d/", FileUtils::makePathAbsolute("d/", "/a/b/c/"));
    EXPECT_EQ("/a/b", FileUtils::makePathAbsolute("..", "/a/b/c/"));
    EXPECT_EQ("/a/b/", FileUtils::makePathAbsolute("../", "/a/b/c/"));
    EXPECT_EQ("/a/b", FileUtils::makePathAbsolute("..", "/a/b/c"));
    EXPECT_EQ("/a/b/", FileUtils::makePathAbsolute("../", "/a/b/c"));

    EXPECT_EQ("/a/b/c", FileUtils::makePathAbsolute(".", "/a/b/c/"));
    EXPECT_EQ("/a/b/c/", FileUtils::makePathAbsolute("./", "/a/b/c/"));
    EXPECT_EQ("/a/b/c", FileUtils::makePathAbsolute(".", "/a/b/c"));
    EXPECT_EQ("/a/b/c/", FileUtils::makePathAbsolute("./", "/a/b/c"));
}

TEST(MakePathAbsoluteTest, MoreComplexPaths)
{
    EXPECT_EQ("/a/b/d", FileUtils::makePathAbsolute("../d", "/a/b/c"));
    EXPECT_EQ("/a/b/d", FileUtils::makePathAbsolute("../d", "/a/b/c/"));
    EXPECT_EQ("/a/b/d/", FileUtils::makePathAbsolute("../d/", "/a/b/c"));
    EXPECT_EQ("/a/b/d/", FileUtils::makePathAbsolute("../d/", "/a/b/c/"));

    EXPECT_EQ("/a/b/d", FileUtils::makePathAbsolute("./.././d", "/a/b/c"));
    EXPECT_EQ("/a/b/d", FileUtils::makePathAbsolute("./.././d", "/a/b/c/"));
    EXPECT_EQ("/a/b/d/", FileUtils::makePathAbsolute("./.././d/", "/a/b/c"));
    EXPECT_EQ("/a/b/d/", FileUtils::makePathAbsolute("./.././d/", "/a/b/c/"));
}

TEST(MakePathAbsoluteTest, AbsolutePaths)
{
    EXPECT_EQ("/x/y/z", FileUtils::makePathAbsolute("/x/y/z", "/a/b/c"));

    // verify that the path still gets normalized
    EXPECT_EQ("/x/y/m",
              FileUtils::makePathAbsolute("/x/y/z/.././m", "/a/b/c"));
}

TEST(JoinPathSegmentsTest, JoinPaths)
{
    // Relative path first segment
    EXPECT_EQ("/b", FileUtils::joinPathSegments("a", "/b"));
    EXPECT_EQ("/b", FileUtils::joinPathSegments("a/", "/b"));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a", "b"));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a/", "b"));

    EXPECT_EQ("/b", FileUtils::joinPathSegments("a", "/b/"));
    EXPECT_EQ("/b", FileUtils::joinPathSegments("a/", "/b/"));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a", "b/"));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a/", "b/"));

    EXPECT_EQ("/c", FileUtils::joinPathSegments("a/b", "/c"));
    EXPECT_EQ("/c", FileUtils::joinPathSegments("a/b/", "/c"));
    EXPECT_EQ("a/b/c", FileUtils::joinPathSegments("a/b", "c"));
    EXPECT_EQ("a/b/c", FileUtils::joinPathSegments("a/b/", "c"));

    EXPECT_EQ("/b/c", FileUtils::joinPathSegments("a", "/b/c"));
    EXPECT_EQ("/b/c", FileUtils::joinPathSegments("a/", "/b/c"));
    EXPECT_EQ("a/b/c", FileUtils::joinPathSegments("a", "b/c"));
    EXPECT_EQ("a/b/c", FileUtils::joinPathSegments("a/", "b/c"));

    // Absolute path first segment
    EXPECT_EQ("/a/b", FileUtils::joinPathSegments("/a", "b"));
    EXPECT_EQ("/a/b", FileUtils::joinPathSegments("/a/", "b"));
    EXPECT_EQ("/b", FileUtils::joinPathSegments("/a", "/b"));
    EXPECT_EQ("/b", FileUtils::joinPathSegments("/a/", "/b"));

    EXPECT_EQ("/a/b", FileUtils::joinPathSegments("/a", "b/"));
    EXPECT_EQ("/a/b", FileUtils::joinPathSegments("/a/", "b/"));
    EXPECT_EQ("/b", FileUtils::joinPathSegments("/a", "/b/"));
    EXPECT_EQ("/b", FileUtils::joinPathSegments("/a/", "/b/"));

    EXPECT_EQ("/a/b/c", FileUtils::joinPathSegments("/a/b", "c"));
    EXPECT_EQ("/a/b/c", FileUtils::joinPathSegments("/a/b/", "c"));
    EXPECT_EQ("/c", FileUtils::joinPathSegments("/a/b", "/c"));
    EXPECT_EQ("/c", FileUtils::joinPathSegments("/a/b/", "/c"));

    EXPECT_EQ("/a/b/c", FileUtils::joinPathSegments("/a", "b/c"));
    EXPECT_EQ("/a/b/c", FileUtils::joinPathSegments("/a/", "b/c"));
    ;
    EXPECT_EQ("/b/c", FileUtils::joinPathSegments("/a/", "/b/c"));

    // paths containing '.'
    EXPECT_EQ("/a", FileUtils::joinPathSegments("/a", "."));
    EXPECT_EQ("/a", FileUtils::joinPathSegments("/a/", "."));
    EXPECT_EQ("a", FileUtils::joinPathSegments("a", "."));
    EXPECT_EQ("a", FileUtils::joinPathSegments("a/", "."));

    EXPECT_EQ("/a", FileUtils::joinPathSegments("/a", "./"));
    EXPECT_EQ("/a", FileUtils::joinPathSegments("/a/", "./"));
    EXPECT_EQ("a", FileUtils::joinPathSegments("a", "./"));
    EXPECT_EQ("a", FileUtils::joinPathSegments("a/", "./"));

    EXPECT_EQ("/b", FileUtils::joinPathSegments("/./a/.", "/./b"));
    EXPECT_EQ("/b", FileUtils::joinPathSegments("/a/.", "/./b"));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("./a", "./b"));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a/./", "./b"));

    EXPECT_EQ("/b", FileUtils::joinPathSegments("a", "/./b"));
    EXPECT_EQ("/b", FileUtils::joinPathSegments("a/", "/./b"));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a", "./b"));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a/", "./b"));

    EXPECT_EQ("/b", FileUtils::joinPathSegments("a/.", "/./b"));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("./a/", "./b"));

    // paths containing '..' (Escapes allowed)
    EXPECT_EQ("/a/c/d/e/f", FileUtils::joinPathSegments("/a/b/../c", "d/e/f"));
    EXPECT_EQ("/b/c", FileUtils::joinPathSegments("/a", "../b/c"));
    EXPECT_EQ("/c", FileUtils::joinPathSegments("/a", "/b/../c"));

    // Base dir escapes
    EXPECT_EQ("../b",
              FileUtils::FileUtils::joinPathSegmentsNoEscape("a/../..", "b"));
    EXPECT_EQ("../b", FileUtils::FileUtils::joinPathSegmentsNoEscape(
                          "a/b/../../..", "b"));
}

TEST(JoinPathSegmentsTest, JoinPathsForceSecondSegmentRelative)
{
    // Relative path first segment
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a", "/b", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a/", "/b", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a", "b", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a/", "b", true));

    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a", "/b/", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a/", "/b/", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a", "b/", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a/", "b/", true));

    EXPECT_EQ("a/b/c", FileUtils::joinPathSegments("a/b", "/c", true));
    EXPECT_EQ("a/b/c", FileUtils::joinPathSegments("a/b/", "/c", true));
    EXPECT_EQ("a/b/c", FileUtils::joinPathSegments("a/b", "c", true));
    EXPECT_EQ("a/b/c", FileUtils::joinPathSegments("a/b/", "c", true));

    EXPECT_EQ("a/b/c", FileUtils::joinPathSegments("a", "/b/c", true));
    EXPECT_EQ("a/b/c", FileUtils::joinPathSegments("a/", "/b/c", true));
    EXPECT_EQ("a/b/c", FileUtils::joinPathSegments("a", "b/c", true));
    EXPECT_EQ("a/b/c", FileUtils::joinPathSegments("a/", "b/c", true));

    // Absolute path first segment
    EXPECT_EQ("/a/b", FileUtils::joinPathSegments("/a", "b", true));
    EXPECT_EQ("/a/b", FileUtils::joinPathSegments("/a/", "b", true));
    EXPECT_EQ("/a/b", FileUtils::joinPathSegments("/a", "/b", true));
    EXPECT_EQ("/a/b", FileUtils::joinPathSegments("/a/", "/b", true));

    EXPECT_EQ("/a/b", FileUtils::joinPathSegments("/a", "b/", true));
    EXPECT_EQ("/a/b", FileUtils::joinPathSegments("/a/", "b/", true));
    EXPECT_EQ("/a/b", FileUtils::joinPathSegments("/a", "/b/", true));
    EXPECT_EQ("/a/b", FileUtils::joinPathSegments("/a/", "/b/", true));

    EXPECT_EQ("/a/b/c", FileUtils::joinPathSegments("/a/b", "c", true));
    EXPECT_EQ("/a/b/c", FileUtils::joinPathSegments("/a/b/", "c", true));
    EXPECT_EQ("/a/b/c", FileUtils::joinPathSegments("/a/b", "/c", true));
    EXPECT_EQ("/a/b/c", FileUtils::joinPathSegments("/a/b/", "/c", true));

    EXPECT_EQ("/a/b/c", FileUtils::joinPathSegments("/a", "b/c", true));
    EXPECT_EQ("/a/b/c", FileUtils::joinPathSegments("/a/", "b/c", true));
    ;
    EXPECT_EQ("/a/b/c", FileUtils::joinPathSegments("/a/", "/b/c", true));

    // paths containing '.'
    EXPECT_EQ("/a", FileUtils::joinPathSegments("/a", ".", true));
    EXPECT_EQ("/a", FileUtils::joinPathSegments("/a/", ".", true));
    EXPECT_EQ("a", FileUtils::joinPathSegments("a", ".", true));
    EXPECT_EQ("a", FileUtils::joinPathSegments("a/", ".", true));

    EXPECT_EQ("/a", FileUtils::joinPathSegments("/a", "./", true));
    EXPECT_EQ("/a", FileUtils::joinPathSegments("/a/", "./", true));
    EXPECT_EQ("a", FileUtils::joinPathSegments("a", "./", true));
    EXPECT_EQ("a", FileUtils::joinPathSegments("a/", "./", true));

    EXPECT_EQ("/a/b", FileUtils::joinPathSegments("/./a/.", "/./b", true));
    EXPECT_EQ("/a/b", FileUtils::joinPathSegments("/a/.", "/./b", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("./a", "./b", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a/./", "./b", true));

    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a", "/./b", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a/", "/./b", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a", "./b", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a/", "./b", true));

    EXPECT_EQ("a/b", FileUtils::joinPathSegments("a/.", "/./b", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegments("./a/", "./b", true));

    // paths containing '..' (Escapes allowed)
    EXPECT_EQ("/a/c/d/e/f",
              FileUtils::joinPathSegments("/a/b/../c", "d/e/f", true));
    EXPECT_EQ("/b/c", FileUtils::joinPathSegments("/a", "../b/c", true));
    EXPECT_EQ("/a/c", FileUtils::joinPathSegments("/a", "/b/../c", true));
}

TEST(JoinPathSegmentsTestInvalidArgs, JoinPaths)
{
    EXPECT_THROW(FileUtils::FileUtils::joinPathSegments("", ""),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::FileUtils::joinPathSegments("a/b", ""),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::FileUtils::joinPathSegments("", "a/b"),
                 std::runtime_error);
}

TEST(JoinPathSegmentsNoEscapeTest, JoinPathsNoEscape)
{
    // Relative path first segment
    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a", "b"));
    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a/", "b"));

    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a", "b/"));
    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a/", "b/"));

    EXPECT_EQ("a/b/c", FileUtils::joinPathSegmentsNoEscape("a/b", "c"));
    EXPECT_EQ("a/b/c", FileUtils::joinPathSegmentsNoEscape("a/b/", "c"));

    EXPECT_EQ("a/b/c", FileUtils::joinPathSegmentsNoEscape("a", "b/c"));
    EXPECT_EQ("a/b/c", FileUtils::joinPathSegmentsNoEscape("a/", "b/c"));

    // Absolute path first segment
    EXPECT_EQ("/a/b", FileUtils::joinPathSegmentsNoEscape("/a", "b"));
    EXPECT_EQ("/a/b", FileUtils::joinPathSegmentsNoEscape("/a/", "b"));

    EXPECT_EQ("/a/b", FileUtils::joinPathSegmentsNoEscape("/a", "b/"));
    EXPECT_EQ("/a/b", FileUtils::joinPathSegmentsNoEscape("/a/", "b/"));

    EXPECT_EQ("/a/b/c", FileUtils::joinPathSegmentsNoEscape("/a/b", "c"));
    EXPECT_EQ("/a/b/c", FileUtils::joinPathSegmentsNoEscape("/a/b/", "c"));

    EXPECT_EQ("/a/b/c", FileUtils::joinPathSegmentsNoEscape("/a", "b/c"));
    EXPECT_EQ("/a/b/c", FileUtils::joinPathSegmentsNoEscape("/a/", "b/c"));

    // paths containing '.'
    EXPECT_EQ("/a", FileUtils::joinPathSegmentsNoEscape("/a", "."));
    EXPECT_EQ("/a", FileUtils::joinPathSegmentsNoEscape("/a/", "."));
    EXPECT_EQ("a", FileUtils::joinPathSegmentsNoEscape("a", "."));
    EXPECT_EQ("a", FileUtils::joinPathSegmentsNoEscape("a/", "."));

    EXPECT_EQ("/a", FileUtils::joinPathSegmentsNoEscape("/a", "./"));
    EXPECT_EQ("/a", FileUtils::joinPathSegmentsNoEscape("/a/", "./"));
    EXPECT_EQ("a", FileUtils::joinPathSegmentsNoEscape("a", "./"));
    EXPECT_EQ("a", FileUtils::joinPathSegmentsNoEscape("a/", "./"));

    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("./a", "./b"));
    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a/./", "./b"));

    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a", "./b"));
    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a/", "./b"));

    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("./a/", "./b"));

    // paths containing '..' (Escapes outside first dir NOT allowed)
    EXPECT_EQ("/a", FileUtils::joinPathSegmentsNoEscape("/a", "b/c/../../"));

    EXPECT_EQ("/a/b/f",
              FileUtils::joinPathSegmentsNoEscape("/a", "b/c/../d/../e/../f"));
    EXPECT_EQ("/a/c/d/e/f",
              FileUtils::joinPathSegmentsNoEscape("/a/b/../c", "d/e/f"));
    EXPECT_EQ("/c/d/e",
              FileUtils::joinPathSegmentsNoEscape("/a/../", "c/d/e"));
}

TEST(JoinPathSegmentsNoEscapeTest,
     JoinPathsNoEscapeForceRelativePathWithinBaseDir)
{
    // Relative path first segment
    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a", "b", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a/", "b", true));

    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a", "b/", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a/", "b/", true));

    EXPECT_EQ("a/b/c", FileUtils::joinPathSegmentsNoEscape("a/b", "c", true));
    EXPECT_EQ("a/b/c", FileUtils::joinPathSegmentsNoEscape("a/b/", "c", true));

    EXPECT_EQ("a/b/c", FileUtils::joinPathSegmentsNoEscape("a", "b/c", true));
    EXPECT_EQ("a/b/c", FileUtils::joinPathSegmentsNoEscape("a/", "b/c", true));

    // Absolute path first segment
    EXPECT_EQ("/a/b", FileUtils::joinPathSegmentsNoEscape("/a", "b", true));
    EXPECT_EQ("/a/b", FileUtils::joinPathSegmentsNoEscape("/a/", "b", true));

    EXPECT_EQ("/a/b", FileUtils::joinPathSegmentsNoEscape("/a", "b/", true));
    EXPECT_EQ("/a/b", FileUtils::joinPathSegmentsNoEscape("/a/", "b/", true));

    EXPECT_EQ("/a/b/c",
              FileUtils::joinPathSegmentsNoEscape("/a/b", "c", true));
    EXPECT_EQ("/a/b/c",
              FileUtils::joinPathSegmentsNoEscape("/a/b/", "c", true));

    EXPECT_EQ("/a/b/c",
              FileUtils::joinPathSegmentsNoEscape("/a", "b/c", true));
    EXPECT_EQ("/a/b/c",
              FileUtils::joinPathSegmentsNoEscape("/a/", "b/c", true));

    // paths containing '.'
    EXPECT_EQ("/a", FileUtils::joinPathSegmentsNoEscape("/a", ".", true));
    EXPECT_EQ("/a", FileUtils::joinPathSegmentsNoEscape("/a/", ".", true));
    EXPECT_EQ("a", FileUtils::joinPathSegmentsNoEscape("a", ".", true));
    EXPECT_EQ("a", FileUtils::joinPathSegmentsNoEscape("a/", ".", true));

    EXPECT_EQ("/a", FileUtils::joinPathSegmentsNoEscape("/a", "./", true));
    EXPECT_EQ("/a", FileUtils::joinPathSegmentsNoEscape("/a/", "./", true));
    EXPECT_EQ("a", FileUtils::joinPathSegmentsNoEscape("a", "./", true));
    EXPECT_EQ("a", FileUtils::joinPathSegmentsNoEscape("a/", "./", true));

    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("./a", "./b", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a/./", "./b", true));

    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a", "./b", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a/", "./b", true));

    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("./a/", "./b", true));

    // paths containing '..' (Escapes outside first dir NOT allowed)
    EXPECT_EQ("/a",
              FileUtils::joinPathSegmentsNoEscape("/a", "b/c/../../", true));
    ;
    EXPECT_EQ("/a/b/f", FileUtils::joinPathSegmentsNoEscape(
                            "/a", "b/c/../d/../e/../f", true));
    EXPECT_EQ("/a/c/d/e/f",
              FileUtils::joinPathSegmentsNoEscape("/a/b/../c", "d/e/f", true));
    // Directory name with dots in
    EXPECT_EQ("/a/c/d/e../f..", FileUtils::joinPathSegmentsNoEscape(
                                    "/a/b/../c", "d/e../f..", true));
    EXPECT_EQ("/..a/c/..d/..e../..f",
              FileUtils::joinPathSegmentsNoEscape("/..a/..b/../c",
                                                  "..d/..e../..f", true));
    // paths containing '..' as part of a filename
    EXPECT_EQ("/a/b/c/file..txt",
              FileUtils::joinPathSegmentsNoEscape("/a/b", "c/file..txt"));
    EXPECT_EQ("a/b/file..txt",
              FileUtils::joinPathSegmentsNoEscape("a", "b/file..txt"));
    EXPECT_EQ("a/file..txt",
              FileUtils::joinPathSegmentsNoEscape("a", "b/../file..txt"));
    EXPECT_EQ("/a/file..txt",
              FileUtils::joinPathSegmentsNoEscape("/a", "/a/b/../file..txt"));

    EXPECT_EQ("/c/d/e",
              FileUtils::joinPathSegmentsNoEscape("/a/../", "c/d/e", true));

    // Not escaping due to forceRelativePathWithinBaseDir
    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a", "/b", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a", "/b/", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a/", "/b/", true));
    EXPECT_EQ("/a/b", FileUtils::joinPathSegmentsNoEscape("/a/", "/b", true));
    EXPECT_EQ("/a/b", FileUtils::joinPathSegmentsNoEscape("/a/", "/b/", true));

    EXPECT_EQ("a/b/c", FileUtils::joinPathSegmentsNoEscape("a/b", "/c", true));
    EXPECT_EQ("a/b/c",
              FileUtils::joinPathSegmentsNoEscape("a/b/", "/c", true));

    EXPECT_EQ("a/b/c", FileUtils::joinPathSegmentsNoEscape("a", "/b/c", true));
    EXPECT_EQ("a/b/c",
              FileUtils::joinPathSegmentsNoEscape("a/", "/b/c", true));

    EXPECT_EQ("/a/b", FileUtils::joinPathSegmentsNoEscape("/a", "/b", true));
    EXPECT_EQ("/a/b", FileUtils::joinPathSegmentsNoEscape("/a/", "/b", true));

    EXPECT_EQ("/a/b/c",
              FileUtils::joinPathSegmentsNoEscape("/a/b", "/c", true));
    EXPECT_EQ("/a/b/c",
              FileUtils::joinPathSegmentsNoEscape("/a/b/", "/c", true));

    EXPECT_EQ("/a/b/c",
              FileUtils::joinPathSegmentsNoEscape("/a", "/b/c", true));
    EXPECT_EQ("/a/b/c",
              FileUtils::joinPathSegmentsNoEscape("/a/", "/b/c", true));

    EXPECT_EQ("/a/b",
              FileUtils::joinPathSegmentsNoEscape("/./a/.", "/./b", true));
    EXPECT_EQ("/a/b",
              FileUtils::joinPathSegmentsNoEscape("/a/.", "/./b", true));

    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a", "/./b", true));
    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a/", "/./b", true));

    EXPECT_EQ("a/b", FileUtils::joinPathSegmentsNoEscape("a/.", "/./b", true));
    EXPECT_EQ("/a/c",
              FileUtils::joinPathSegmentsNoEscape("/a", "/b/../c", true));

    // paths containing '..' as part of a filename
    EXPECT_EQ("/a/b/c/file..txt",
              FileUtils::joinPathSegments("/a/b", "c/file..txt", true));
    EXPECT_EQ("a/b/file..txt",
              FileUtils::joinPathSegments("a", "b/file..txt", true));
    EXPECT_EQ("a/file..txt",
              FileUtils::joinPathSegments("a", "/b/../file..txt", true));
    EXPECT_EQ("/a/file..txt",
              FileUtils::joinPathSegments("/a", "/b/../file..txt", true));

    // Base dir escapes
    EXPECT_EQ("../b", FileUtils::FileUtils::joinPathSegmentsNoEscape(
                          "a/../..", "/b", true));
    EXPECT_EQ("../b", FileUtils::FileUtils::joinPathSegmentsNoEscape(
                          "a/../..", "b", true));
    EXPECT_EQ("../b", FileUtils::FileUtils::joinPathSegmentsNoEscape(
                          "a/b/../../..", "/b", true));
    EXPECT_EQ("../b", FileUtils::FileUtils::joinPathSegmentsNoEscape(
                          "a/b/../../..", "b", true));
    EXPECT_EQ("c/d/e",
              FileUtils::joinPathSegmentsNoEscape("a/../", "c/d/e", true));
}

TEST(JoinPathSegmentsNoEscapeTestEscapesThrow, JoinPathsNoEscape)
{
    // Base dir escapes
    EXPECT_THROW(
        FileUtils::FileUtils::joinPathSegmentsNoEscape("a/../..", "/b"),
        std::runtime_error);
    EXPECT_THROW(
        FileUtils::FileUtils::joinPathSegmentsNoEscape("a/b/../../..", "/b"),
        std::runtime_error);

    // Path within basedir escapes
    EXPECT_THROW(FileUtils::FileUtils::joinPathSegmentsNoEscape("/a", "../b"),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::FileUtils::joinPathSegmentsNoEscape("/a", "/../b"),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::FileUtils::joinPathSegmentsNoEscape("/a", "/b"),
                 std::runtime_error);
    EXPECT_THROW(
        FileUtils::FileUtils::joinPathSegmentsNoEscape("/a", "b/c/../../../"),
        std::runtime_error);
    EXPECT_THROW(FileUtils::FileUtils::joinPathSegmentsNoEscape("/a/", "../b"),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::FileUtils::joinPathSegmentsNoEscape("/a/", "/b"),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::FileUtils::joinPathSegmentsNoEscape("/a/b/c/",
                                                                "d/../../e/f"),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::FileUtils::joinPathSegmentsNoEscape("a", "/b"),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("/a", "../b/c"),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("/a", "/b/../c"),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("/a", "/b/.."),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("/a", ".."),
                 std::runtime_error);

    // Escaping due to absolute paths
    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("a", "/b/"),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("a/", "/b/"),
                 std::runtime_error);

    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("a/b", "/c"),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("a/b/", "/c"),
                 std::runtime_error);

    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("a", "/b/c"),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("a/", "/b/c"),
                 std::runtime_error);

    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("/a", "/b"),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("/a/", "/b"),
                 std::runtime_error);

    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("/a/b", "/c"),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("/a/b/", "/c"),
                 std::runtime_error);

    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("/a", "/b/c"),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("/a/", "/b/c"),
                 std::runtime_error);

    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("/./a/.", "/./b"),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("/a/.", "/./b"),
                 std::runtime_error);

    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("a", "/./b"),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("a/", "/./b"),
                 std::runtime_error);

    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("a/.", "/./b"),
                 std::runtime_error);
}

TEST(JoinPathSegmentsNoEscapeTestForceRelativePathWithinBaseDirEscapesThrow,
     JoinPathsNoEscape)
{
    // Path within basedir escapes
    EXPECT_THROW(
        FileUtils::FileUtils::joinPathSegmentsNoEscape("/a", "../b", true),
        std::runtime_error);

    EXPECT_THROW(FileUtils::FileUtils::joinPathSegmentsNoEscape(
                     "/a", "b/c/../../../", true),
                 std::runtime_error);
    EXPECT_THROW(
        FileUtils::FileUtils::joinPathSegmentsNoEscape("/a/", "../b", true),
        std::runtime_error);
    EXPECT_THROW(FileUtils::FileUtils::joinPathSegmentsNoEscape(
                     "/a/b/c/", "d/../../e/f", true),
                 std::runtime_error);
    EXPECT_THROW(FileUtils::joinPathSegmentsNoEscape("/a", "../b/c", true),
                 std::runtime_error);
}

TEST(FileUtilsTests, WriteFileAtomically)
{
    TemporaryDirectory output_directory;

    const std::string output_path =
        std::string(output_directory.name()) + "/data.txt";

    EXPECT_FALSE(FileUtils::isRegularFile(output_path.c_str()));

    std::vector<char> raw_data = {'H', 'e', 'l', 'l', 'o',  '\0', 'W',
                                  'o', 'r', 'l', 'd', '\0', '!'};
    const std::string data_string(raw_data.cbegin(), raw_data.cend());

    EXPECT_NO_THROW(FileUtils::writeFileAtomically(output_path, data_string));

    // Data is correct:
    std::ifstream file(output_path, std::ifstream::binary);
    std::stringstream read_data;
    read_data << file.rdbuf();

    EXPECT_EQ(read_data.str(), data_string);

    // Default mode is 0600:
    struct stat stat_buf = {};
    const int stat_status = stat(output_path.c_str(), &stat_buf);
    EXPECT_EQ(stat_status, 0);

    EXPECT_TRUE(S_ISREG(stat_buf.st_mode));

    const auto file_permissions = stat_buf.st_mode & 0777;
    EXPECT_EQ(file_permissions, 0600);
}

TEST(FileUtilsTests, WriteFileAtomicallyReplacesFile)
{
    TemporaryDirectory output_directory;

    const std::string output_path =
        std::string(output_directory.name()) + "/output.txt";

    EXPECT_FALSE(FileUtils::isRegularFile(output_path.c_str()));

    EXPECT_NO_THROW(FileUtils::writeFileAtomically(output_path, "1"));
    EXPECT_EQ(FileUtils::getFileContents(output_path.c_str()), "1");

    EXPECT_NO_THROW(FileUtils::writeFileAtomically(output_path, "2"));
    EXPECT_EQ(FileUtils::getFileContents(output_path.c_str()), "2");
}

TEST(FileUtilsTests, GetFileContentsOfEmptyFile)
{
    TemporaryDirectory output_directory;

    const std::string output_path =
        std::string(output_directory.name()) + "/empty.txt";

    EXPECT_NO_THROW(FileUtils::writeFileAtomically(output_path, ""));
    EXPECT_EQ(FileUtils::getFileContents(output_path.c_str()), "");
}

TEST(FileUtilsTests, GetFileContentsOffsetLimit)
{
    TemporaryDirectory output_directory;

    const std::string output_path =
        std::string(output_directory.name()) + "/output.txt";

    const std::string data = "0123456789";
    EXPECT_NO_THROW(FileUtils::writeFileAtomically(output_path, data));

    FileDescriptor fd(open(output_path.c_str(), O_RDONLY));

    EXPECT_EQ(FileUtils::getFileContents(fd.get(), 0, 5), "01234");
    EXPECT_EQ(FileUtils::getFileContents(fd.get(), 5, 5), "56789");
}

TEST(FileUtilsTests, WriteFileAtomicallyPermissions)
{
    TemporaryDirectory output_directory;

    const std::string output_path =
        std::string(output_directory.name()) + "/executable.sh";

    EXPECT_FALSE(FileUtils::isRegularFile(output_path.c_str()));

    const std::string data = "#!/bin/bash";
    EXPECT_NO_THROW(FileUtils::writeFileAtomically(output_path, data, 0740));

    struct stat stat_buf = {};
    const int stat_status = stat(output_path.c_str(), &stat_buf);
    EXPECT_EQ(stat_status, 0);

    EXPECT_TRUE(S_ISREG(stat_buf.st_mode));

    const auto file_permissions = stat_buf.st_mode & 0777;
    EXPECT_EQ(file_permissions, 0740);

    // Validate that a file with read-only permissions can be created
    const std::string readonly_path =
        std::string(output_directory.name()) + "/readonly.out";

    EXPECT_FALSE(FileUtils::isRegularFile(readonly_path.c_str()));

    const std::string readonlyData = "helloWorld!";
    ASSERT_NO_THROW(
        FileUtils::writeFileAtomically(readonly_path, readonlyData, 0444));

    struct stat readonly_stat_buf = {};
    const int readonly_stat_status =
        stat(readonly_path.c_str(), &readonly_stat_buf);
    EXPECT_EQ(readonly_stat_status, 0);

    EXPECT_TRUE(S_ISREG(readonly_stat_buf.st_mode));

    const auto readonly_file_permissions = readonly_stat_buf.st_mode & 0777;
    EXPECT_EQ(readonly_file_permissions, 0444);
}

TEST(FileUtilsTests, WriteFileAtomicallyMtime)
{
    TemporaryDirectory output_directory;

    const std::string output_path =
        std::string(output_directory.name()) + "/output.txt";

    const auto mtime = std::chrono::system_clock::time_point{};
    FileUtils::writeFileAtomically(output_path, "1", PERMISSION_RWUSR, "",
                                   TempDefaults::DEFAULT_TMP_PREFIX, mtime);
    EXPECT_EQ(FileUtils::getFileContents(output_path.c_str()), "1");
    EXPECT_EQ(FileUtils::getFileMtime(output_path.c_str())
                  .time_since_epoch()
                  .count(),
              mtime.time_since_epoch().count());
}

TEST(FileUtilsTests, WriteFileAtomicallyTemporaryDirectory)
{
    TemporaryDirectory output_directory;
    TemporaryDirectory intermediate_directory;

    const std::string output_path =
        std::string(output_directory.name()) + "/test.txt";

    EXPECT_FALSE(FileUtils::isRegularFile(output_path.c_str()));

    const auto data = "some data...";
    FileUtils::writeFileAtomically(output_path, data, 0600,
                                   intermediate_directory.name());

    EXPECT_TRUE(FileUtils::isRegularFile(output_path.c_str()));

    // Data is correct:
    std::ifstream file(output_path, std::ifstream::binary);
    std::stringstream read_data;
    read_data << file.rdbuf();

    EXPECT_EQ(read_data.str(), data);
}

TEST(FileUtilsTests, WriteFileAtomicallyIntermediateFileIsDeleted)
{
    TemporaryDirectory test_directory;
    const std::string test_directory_path(test_directory.name());
    const auto output_path = test_directory_path + "/out.txt";

    const auto intermediate_directory = test_directory_path + "/intermediate";
    FileUtils::createDirectory(intermediate_directory.c_str());

    FileUtils::writeFileAtomically(output_path, "data: 12345", 0600,
                                   intermediate_directory);
    EXPECT_TRUE(FileUtils::isRegularFile(output_path.c_str()));

    // The intermediate file was deleted:
    EXPECT_TRUE(FileUtils::directoryIsEmpty(intermediate_directory.c_str()));
}

TEST(FileUtilsTests, PathBasenameTests)
{
    EXPECT_EQ("hello", FileUtils::pathBasename("a/b/hello"));
    EXPECT_EQ("hello.txt", FileUtils::pathBasename("a/b/hello.txt"));
    EXPECT_EQ("hello", FileUtils::pathBasename("//hello/a/b/hello"));
    EXPECT_EQ("hello", FileUtils::pathBasename("a/b/../../hello"));
    EXPECT_EQ("hello", FileUtils::pathBasename("a/b/hello/"));
    EXPECT_EQ("hello", FileUtils::pathBasename("/a/hello/"));
    EXPECT_EQ("hello", FileUtils::pathBasename("/a/hello//"));
    EXPECT_EQ("hello", FileUtils::pathBasename("hello"));
    EXPECT_EQ("hello", FileUtils::pathBasename("hello/"));
    EXPECT_EQ("hello", FileUtils::pathBasename("hello//"));
    EXPECT_EQ("", FileUtils::pathBasename("/"));
    EXPECT_EQ("", FileUtils::pathBasename("//"));
}

TEST(FileUtilsTests, GetFileSize)
{
    TemporaryDirectory tmpdir;

    const std::string file_path = std::string(tmpdir.name()) + "/data.txt";

    const std::string s("hello, world\n");

    std::ofstream file(file_path);
    file << s;
    file.close();

    EXPECT_EQ(FileUtils::getFileSize(file_path.c_str()), s.size());
}

TEST(FileUtilsTests, GetFileMtime)
{
    TemporaryDirectory tmpdir;
    std::string pathStr = std::string(tmpdir.name()) + "/foo.sh";
    const char *path = pathStr.c_str();

    // this should be fast enough
    EXPECT_FALSE(buildboxcommontest::TestUtils::pathExists(path));
    buildboxcommontest::TestUtils::touchFile(path);
    std::chrono::system_clock::time_point now =
        std::chrono::system_clock::now();
    EXPECT_TRUE(buildboxcommontest::TestUtils::pathExists(path));

    std::chrono::system_clock::time_point mtime =
        FileUtils::getFileMtime(path);
    std::chrono::seconds timediff =
        std::chrono::duration_cast<std::chrono::seconds>(now - mtime);
    EXPECT_EQ(timediff.count(), 0);

    const int fd = open(path, O_RDONLY);
    mtime = FileUtils::getFileMtime(fd);
    timediff = std::chrono::duration_cast<std::chrono::seconds>(now - mtime);
    EXPECT_EQ(timediff.count(), 0);
}

TEST(FileUtilsTests, ModifyFileTimestamp)
{
    // Depends upon ParseTimePoint, ParseTimeStamp and GetFileMtime
    TemporaryDirectory tmpdir;
    std::string pathStr = std::string(tmpdir.name()) + "/foo.sh";
    const char *path = pathStr.c_str();

    EXPECT_FALSE(buildboxcommontest::TestUtils::pathExists(path));
    buildboxcommontest::TestUtils::touchFile(path);
    EXPECT_TRUE(buildboxcommontest::TestUtils::pathExists(path));

    // try e2e test of file timestamps
    // get the original time
    auto orig_time = FileUtils::getFileMtime(path);
    auto orig_count = std::chrono::duration_cast<std::chrono::microseconds>(
                          orig_time.time_since_epoch())
                          .count();
    // get a new time to set and sanity check it
    const long int exp_count = 1325586092000000;
    EXPECT_NE(exp_count, orig_count);

    const std::string new_stamp = "2012-01-03T10:21:32.000000Z";
    google::protobuf::Timestamp gtime;
    EXPECT_TRUE(
        google::protobuf::util::TimeUtil::FromString(new_stamp, &gtime));
    auto new_time = TimeUtils::parse_timestamp(gtime);
    auto new_count = std::chrono::duration_cast<std::chrono::microseconds>(
                         new_time.time_since_epoch())
                         .count();
    EXPECT_EQ(exp_count, new_count);

    // try to set file mtime
    FileUtils::setFileMtime(path, new_time);
    // check the file mtime
    auto mtime = FileUtils::getFileMtime(path);
    auto count = std::chrono::duration_cast<std::chrono::microseconds>(
                     mtime.time_since_epoch())
                     .count();
    EXPECT_EQ(count, new_count);

    // and change it back
    const int fd = open(path, O_RDWR);
    FileUtils::setFileMtime(fd, orig_time);
    mtime = FileUtils::getFileMtime(fd);
    count = std::chrono::duration_cast<std::chrono::microseconds>(
                mtime.time_since_epoch())
                .count();
    EXPECT_EQ(count, orig_count);
}

TEST(FileUtilsTests, CopyFile)
{
    TemporaryDirectory output_directory;

    const std::string output_path =
        std::string(output_directory.name()) + "/executable.sh";

    EXPECT_FALSE(FileUtils::isRegularFile(output_path.c_str()));

    const std::string data = "#!/bin/bash";
    EXPECT_NO_THROW(FileUtils::writeFileAtomically(output_path, data, 0744,
                                                   output_directory.name()));
    EXPECT_TRUE(
        buildboxcommontest::TestUtils::pathExists(output_path.c_str()));

    // Try to copy this
    const std::string copy_path =
        std::string(output_directory.name()) + "/copy.sh";
    EXPECT_FALSE(buildboxcommontest::TestUtils::pathExists(copy_path.c_str()));
    FileUtils::copyFile(output_path.c_str(), copy_path.c_str());

    // Data is correct:
    std::ifstream file(copy_path, std::ifstream::binary);
    std::stringstream read_data;
    read_data << file.rdbuf();

    EXPECT_EQ(read_data.str(), data);

    // It is a regular file
    struct stat stat_buf = {};
    const int stat_status = stat(copy_path.c_str(), &stat_buf);
    EXPECT_EQ(stat_status, 0);

    EXPECT_TRUE(S_ISREG(stat_buf.st_mode));

    // It have the correct permissions:
    const auto file_permissions = stat_buf.st_mode & 0777;
    EXPECT_EQ(file_permissions, 0744);
}

TEST(FileUtilsTests, CopyFileMode)
{
    TemporaryDirectory output_directory;

    const std::string output_path =
        std::string(output_directory.name()) + "/executable.sh";

    EXPECT_FALSE(FileUtils::isRegularFile(output_path.c_str()));

    const std::string data = "#!/bin/bash";
    EXPECT_NO_THROW(FileUtils::writeFileAtomically(output_path, data, 0755,
                                                   output_directory.name()));
    EXPECT_TRUE(
        buildboxcommontest::TestUtils::pathExists(output_path.c_str()));

    // Try to copy this with a custom mode
    const std::string copy_path =
        std::string(output_directory.name()) + "/copy.sh";
    EXPECT_FALSE(buildboxcommontest::TestUtils::pathExists(copy_path.c_str()));
    FileUtils::copyFile(AT_FDCWD, output_path.c_str(), AT_FDCWD,
                        copy_path.c_str(), 0666);

    // Data is correct:
    std::ifstream file(copy_path, std::ifstream::binary);
    std::stringstream read_data;
    read_data << file.rdbuf();

    EXPECT_EQ(read_data.str(), data);

    // It is a regular file
    struct stat stat_buf = {};
    const int stat_status = stat(copy_path.c_str(), &stat_buf);
    EXPECT_EQ(stat_status, 0);

    EXPECT_TRUE(S_ISREG(stat_buf.st_mode));

    // It have the correct permissions:
    const auto file_permissions = stat_buf.st_mode & 0777;
    EXPECT_EQ(file_permissions, 0666);
}

TEST(FileUtilsTests, CopyFileStat)
{
    TemporaryDirectory output_directory;

    const std::string output_path =
        std::string(output_directory.name()) + "/executable.sh";

    EXPECT_FALSE(FileUtils::isRegularFile(output_path.c_str()));

    const std::string data = "#!/bin/bash";
    EXPECT_NO_THROW(FileUtils::writeFileAtomically(output_path, data, 0744,
                                                   output_directory.name()));
    EXPECT_TRUE(
        buildboxcommontest::TestUtils::pathExists(output_path.c_str()));

    // Try to copy this
    const std::string copy_path =
        std::string(output_directory.name()) + "/copy.sh";
    EXPECT_FALSE(buildboxcommontest::TestUtils::pathExists(copy_path.c_str()));
    struct stat destStat = {};
    FileUtils::copyFile(AT_FDCWD, output_path.c_str(), AT_FDCWD,
                        copy_path.c_str(), 0, nullptr, &destStat);

    // Data is correct:
    std::ifstream file(copy_path, std::ifstream::binary);
    std::stringstream read_data;
    read_data << file.rdbuf();

    EXPECT_EQ(read_data.str(), data);

    struct stat st = {};
    EXPECT_EQ(stat(copy_path.c_str(), &st), 0);

    // Verify that the destStat output parameter is filled correctly
    EXPECT_EQ(st.st_dev, destStat.st_dev);
    EXPECT_EQ(st.st_ino, destStat.st_ino);
    EXPECT_EQ(st.st_mode, destStat.st_mode);
}

TEST(FileUtilsTests, ExistsInDir)
{
    TemporaryDirectory outputDirectory;
    FileDescriptor dirfd(open(outputDirectory.name(), O_DIRECTORY | O_RDONLY));

    const std::string outputPath =
        std::string(outputDirectory.name()) + "/executable.sh";

    EXPECT_FALSE(FileUtils::existsInDir(
        dirfd.get(), FileUtils::pathBasename(outputPath).c_str()));

    const std::string data = "#!/bin/bash";
    EXPECT_NO_THROW(FileUtils::writeFileAtomically(outputPath, data, 0755,
                                                   outputDirectory.name()));
    EXPECT_TRUE(FileUtils::existsInDir(
        dirfd.get(), FileUtils::pathBasename(outputPath).c_str()));
}

TEST(FileUtilsTests, CopyRecursively)
{
    TemporaryDirectory outputDirectory;

    const std::string srcPath = outputDirectory.strname() + "/src";
    EXPECT_NO_THROW(FileUtils::createDirectory(srcPath.c_str()));

    const std::string nestedDir = srcPath + "/nested";
    const std::string nestedFile = nestedDir + "/test.sh";
    const std::string srcFile = srcPath + "/executable.sh";
    const std::string symlinkPath = srcPath + "/symlink";

    const std::string data = "#!/bin/bash";
    EXPECT_NO_THROW(
        FileUtils::writeFileAtomically(srcFile, data, 0755, srcPath.c_str()));
    EXPECT_NO_THROW(FileUtils::createDirectory(nestedDir.c_str()));
    EXPECT_NO_THROW(FileUtils::writeFileAtomically(nestedFile, data, 0755,
                                                   nestedDir.c_str()));
    EXPECT_NE(symlink(outputDirectory.name(), symlinkPath.c_str()), -1);

    const std::string destPath = outputDirectory.strname() + "/dest";
    EXPECT_NO_THROW(FileUtils::createDirectory(destPath.c_str()));

    FileDescriptor srcfd(open(srcPath.c_str(), O_DIRECTORY | O_RDONLY));
    EXPECT_TRUE(srcfd.get() != -1);
    FileDescriptor destfd(open(destPath.c_str(), O_DIRECTORY));
    EXPECT_TRUE(destfd.get() != -1);
    FileUtils::copyRecursively(srcfd.get(), destfd.get());

    const std::string destFilePath =
        outputDirectory.strname() + "/dest/executable.sh";
    const std::string destNestedFilePath =
        outputDirectory.strname() + "/dest/nested/test.sh";
    const std::string destSymlinkPath =
        outputDirectory.strname() + "/dest/symlink";
    EXPECT_TRUE(
        buildboxcommontest::TestUtils::pathExists(destFilePath.c_str()));
    EXPECT_TRUE(
        buildboxcommontest::TestUtils::pathExists(destNestedFilePath.c_str()));
    EXPECT_TRUE(FileUtils::isSymlink(destSymlinkPath.c_str()));
}

TEST(FileUtilsTests, ThrowOnPermissionsError)
{
    TemporaryDirectory top_level;
    TemporaryDirectory test_dir(top_level.name(),
                                "FileUtils_ThrowOnPermissionsError");
    TemporaryFile test_file(top_level.name(),
                            "FileUtils_ThrowOnPermissionsError");

    // Test will not work if run as root
    if (getuid() != 0) {
        EXPECT_EQ(chmod(top_level.name(), !S_IRWXU), 0);

        EXPECT_THROW(FileUtils::isDirectory(test_dir.name()),
                     std::system_error);
        EXPECT_THROW(FileUtils::isDirectoryNoFollow(test_dir.name()),
                     std::system_error);
        EXPECT_THROW(FileUtils::isRegularFile(test_file.name()),
                     std::system_error);
        EXPECT_THROW(FileUtils::isRegularFileNoFollow(test_file.name()),
                     std::system_error);

        chmod(top_level.name(), S_IRWXU);
    }
}

TEST(FileUtilsTests, DoNotThrowIfNotExist)
{
    std::string file_name;
    std::string dir_name;
    {
        TemporaryDirectory test_dir;
        TemporaryFile test_file;

        file_name = test_file.name();
        dir_name = test_dir.name();

        EXPECT_TRUE(FileUtils::isDirectory(dir_name.c_str()));
        EXPECT_TRUE(FileUtils::isDirectoryNoFollow(dir_name.c_str()));
        EXPECT_TRUE(FileUtils::isRegularFile(file_name.c_str()));
        EXPECT_TRUE(FileUtils::isRegularFileNoFollow(file_name.c_str()));
    }
    EXPECT_FALSE(FileUtils::isDirectory(dir_name.c_str()));
    EXPECT_FALSE(FileUtils::isDirectoryNoFollow(dir_name.c_str()));
    EXPECT_FALSE(FileUtils::isRegularFile(file_name.c_str()));
    EXPECT_FALSE(FileUtils::isRegularFileNoFollow(file_name.c_str()));
}

static bool fd_valid(int fd) { return fd >= 0 && fcntl(fd, F_GETFD) >= 0; }

TEST(FileDescriptorTest, DefaultConstructor)
{
    FileDescriptor fd;
    EXPECT_LT(fd.get(), 0);
}

TEST(FileDescriptorTest, Destructor)
{
    int fd = open(".", O_RDONLY);
    EXPECT_TRUE(fd_valid(fd));

    {
        FileDescriptor fd_no_close(fd, false);
        EXPECT_EQ(fd_no_close.get(), fd);
    }
    EXPECT_TRUE(fd_valid(fd));

    {
        FileDescriptor fd_close(fd);
        EXPECT_EQ(fd_close.get(), fd);
    }
    EXPECT_FALSE(fd_valid(fd));
}

TEST(FileDescriptorTest, MoveConstructor)
{
    // NOLINTBEGIN (clang-analyzer-cplusplus.Move) This test is explicitly
    // checking the move constructor is working correctly and must access the
    // file descriptors after moving.
    int fd = open(".", O_RDONLY);
    EXPECT_TRUE(fd_valid(fd));
    {
        FileDescriptor fd1(fd);
        EXPECT_EQ(fd1.get(), fd);

        FileDescriptor fd2(std::move(fd1));
        EXPECT_TRUE(fd_valid(fd));
        EXPECT_LT(fd1.get(), 0);
        EXPECT_EQ(fd2.get(), fd);
    }
    EXPECT_FALSE(fd_valid(fd));
    // NOLINTEND
}

TEST(FileDescriptorTest, MoveAssignment)
{
    // NOLINTBEGIN (clang-analyzer-cplusplus.Move) This test is explicitly
    // checking the move assignment is working correctly and must access the
    // file descriptors after moving.
    int fd1 = open(".", O_RDONLY);
    int fd2 = open(".", O_RDONLY);
    EXPECT_TRUE(fd_valid(fd1));
    EXPECT_TRUE(fd_valid(fd2));
    {
        FileDescriptor fd_move_1(fd1);
        FileDescriptor fd_move_2(fd2);
        EXPECT_TRUE(fd_valid(fd1));
        EXPECT_TRUE(fd_valid(fd2));
        EXPECT_EQ(fd_move_1.get(), fd1);
        EXPECT_EQ(fd_move_2.get(), fd2);

        fd_move_2 = std::move(fd_move_1);
        EXPECT_TRUE(fd_valid(fd1));
        EXPECT_FALSE(fd_valid(fd2));
        EXPECT_LT(fd_move_1.get(), 0);
        EXPECT_EQ(fd_move_2.get(), fd1);
    }
    EXPECT_FALSE(fd_valid(fd1));
    EXPECT_FALSE(fd_valid(fd2));
    // NOLINTEND
}

class OpenInRootFixture : public ::testing::Test {
  protected:
    OpenInRootFixture()
    {
        // Testing with the following directory structure:
        //
        // * root_directory/      symlink
        //      | subdir1/  <--------------------|
        //           | subdir2/                  |
        //               | file.txt              |
        //               | symlink/ -------------|

        const std::string root_directory_path = root_directory.name();

        const auto subdir1_path = root_directory_path + "/subdir1/";
        FileUtils::createDirectory(subdir1_path.c_str());

        const auto subdir2_path = root_directory_path + "/subdir1/subdir2/";
        FileUtils::createDirectory(subdir2_path.c_str());

        const auto symlink1_path =
            root_directory_path + "/subdir1/subdir2/symlink";
        const auto symlink2_path =
            root_directory_path + "/subdir1/subdir2/relative_symlink";

        if (symlink("/subdir1", symlink1_path.c_str()) == -1) {
            throw std::system_error(
                errno, std::system_category(),
                "Error creating symlink in the test directory structure.");
        }

        if (symlink("..", symlink2_path.c_str()) == -1) {
            throw std::system_error(
                errno, std::system_category(),
                "Error creating symlink in the test directory structure.");
        }

        FileUtils::writeFileAtomically(
            std::string(root_directory_path + "/subdir1/subdir2/file.txt")
                .c_str(),
            "Some data...");

        root_directory_fd =
            open(root_directory.name(), O_DIRECTORY | O_RDONLY);
    }

    TemporaryDirectory root_directory;
    int root_directory_fd = -1;

    OpenInRootFixture(const OpenInRootFixture &other) = delete;
    OpenInRootFixture(OpenInRootFixture &&other) = delete;
    OpenInRootFixture &operator=(const OpenInRootFixture &other) = delete;
    OpenInRootFixture &operator=(OpenInRootFixture &&other) = delete;

  public:
    ~OpenInRootFixture() override { close(root_directory_fd); }
};

void assertFileInDirectory(const int dir_fd, const std::string &filename)
{
    EXPECT_NE(dir_fd, -1);

    int file_fd = openat(dir_fd, filename.c_str(), O_RDONLY);
    EXPECT_NE(file_fd, -1);

    close(file_fd);
}

TEST_F(OpenInRootFixture, ValidPath)
{
    FileDescriptor directory_fd(FileUtils::openInRoot(
        root_directory_fd, "subdir1/subdir2", O_RDONLY | O_DIRECTORY));

    assertFileInDirectory(directory_fd.get(), "file.txt");
}

TEST_F(OpenInRootFixture, OpenInputRoot)
{
    FileDescriptor directory_fd(
        FileUtils::openInRoot(root_directory_fd, ".", O_RDONLY | O_DIRECTORY));

    EXPECT_NE(directory_fd.get(), -1);
}

TEST_F(OpenInRootFixture, ValidPaths)
{
    FileDescriptor subdir1_fd(FileUtils::openInRoot(
        root_directory_fd, "subdir1/", O_RDONLY | O_DIRECTORY));
    EXPECT_NE(subdir1_fd.get(), -1);

    FileDescriptor subdir2_fd(FileUtils::openInRoot(
        subdir1_fd.get(), "subdir2/", O_RDONLY | O_DIRECTORY));
    EXPECT_NE(subdir2_fd.get(), -1);

    assertFileInDirectory(subdir2_fd.get(), "file.txt");
}

TEST_F(OpenInRootFixture, RootFDArgumentIsNotClosed)
{
    FileDescriptor directory_fd(FileUtils::openInRoot(
        root_directory_fd, "subdir1/subdir2", O_RDONLY | O_DIRECTORY));

    EXPECT_NE(fcntl(root_directory_fd, F_GETFD), -1);
}

TEST_F(OpenInRootFixture, OpenFile)
{
    FileDescriptor fd(FileUtils::openInRoot(
        root_directory_fd, "subdir1/subdir2/file.txt", O_RDONLY));
    EXPECT_NE(fd.get(), -1);
}

TEST_F(OpenInRootFixture, DirectoryCreation)
{
    // By default opening a path with missing parts should be considered a
    // failure
    std::string rootMissingPath = "subdir1/root/missing/parts";
    FileDescriptor rootMissingFd(FileUtils::openInRoot(
        root_directory_fd, rootMissingPath, O_RDONLY | O_DIRECTORY));
    EXPECT_EQ(rootMissingFd.get(), -1);
    std::string beneathMissingPath = "subdir1/beneath/missing/parts";
    FileDescriptor beneathMissingFd(FileUtils::openBeneath(
        root_directory_fd, beneathMissingPath, O_RDONLY | O_DIRECTORY));
    EXPECT_EQ(beneathMissingFd.get(), -1);

    // When the `createDirectories` boolean is set, this should be successful
    // and directories should be created for the missing parts
    FileDescriptor rootCreatedFd(FileUtils::openInRoot(
        root_directory_fd, rootMissingPath, O_RDONLY | O_DIRECTORY, true));
    EXPECT_NE(rootCreatedFd.get(), -1);
    FileDescriptor beneathCreatedFd(FileUtils::openBeneath(
        root_directory_fd, beneathMissingPath, O_RDONLY | O_DIRECTORY, true));
    EXPECT_NE(beneathCreatedFd.get(), -1);
}

TEST_F(OpenInRootFixture, RootDotDot)
{
    // `..` right inside the root directory should be ignored as `/..` is
    // equivalent to `/` with `openInRoot()`.
    FileDescriptor subdir2_fd(FileUtils::openInRoot(
        root_directory_fd, "../subdir1/subdir2/file.txt", O_RDONLY));
    EXPECT_NE(subdir2_fd.get(), -1);

    // In `openBeneath()` this is considered a failure as it would escape the
    // directory.
    FileDescriptor subdir2_fd_beneath(FileUtils::openBeneath(
        root_directory_fd, "../subdir1/subdir2/file.txt", O_RDONLY));
    EXPECT_EQ(subdir2_fd_beneath.get(), -1);
    EXPECT_EQ(errno, EXDEV);
}

TEST_F(OpenInRootFixture, AbsoluteSymlinkInsideRoot)
{
    // openInRoot() resolves absolute symlinks relative to the root directory
    FileDescriptor fd(FileUtils::openInRoot(
        root_directory_fd, "subdir1/subdir2/symlink", O_RDONLY | O_DIRECTORY));
    EXPECT_NE(fd.get(), -1);

    // openBeneath() rejects absolute symlinks
    FileDescriptor fd_beneath(FileUtils::openBeneath(
        root_directory_fd, "subdir1/subdir2/symlink", O_RDONLY | O_DIRECTORY));
    EXPECT_EQ(fd_beneath.get(), -1);
}

TEST_F(OpenInRootFixture, InnerRelativeSymlink)
{
    // Access file indirectly via relative symlink
    FileDescriptor fd(FileUtils::openInRoot(
        root_directory_fd, "subdir1/subdir2/relative_symlink/subdir2/file.txt",
        O_RDONLY));
    EXPECT_NE(fd.get(), -1);

    FileDescriptor fd_beneath(FileUtils::openBeneath(
        root_directory_fd, "subdir1/subdir2/relative_symlink/subdir2/file.txt",
        O_RDONLY));
    EXPECT_NE(fd_beneath.get(), -1);

    // Attempt to open non-existing file via relative symlink
    FileDescriptor fd2(FileUtils::openInRoot(
        root_directory_fd, "subdir1/subdir2/relative_symlink/subdir2/no-entry",
        O_RDONLY));
    EXPECT_EQ(fd2.get(), -1);
    EXPECT_EQ(errno, ENOENT);

    FileDescriptor fd2_beneath(FileUtils::openBeneath(
        root_directory_fd, "subdir1/subdir2/relative_symlink/subdir2/no-entry",
        O_RDONLY));
    EXPECT_EQ(fd2_beneath.get(), -1);
    EXPECT_EQ(errno, ENOENT);
}

TEST_F(OpenInRootFixture, InnerAbsoluteSymlink)
{
    // Access file indirectly via absolute symlink
    FileDescriptor fd(FileUtils::openInRoot(
        root_directory_fd, "subdir1/subdir2/symlink/subdir2/file.txt",
        O_RDONLY));
    EXPECT_NE(fd.get(), -1);

    // openBeneath() rejects absolute symlinks
    FileDescriptor fd_beneath(FileUtils::openBeneath(
        root_directory_fd, "subdir1/subdir2/symlink/subdir2/file.txt",
        O_RDONLY));
    EXPECT_EQ(fd_beneath.get(), -1);
    EXPECT_EQ(errno, EXDEV);

    // Attempt to open non-existing file via absolute symlink
    FileDescriptor fd2(FileUtils::openInRoot(
        root_directory_fd, "subdir1/subdir2/symlink/subdir2/no-entry",
        O_RDONLY));
    EXPECT_EQ(fd2.get(), -1);
    EXPECT_EQ(errno, ENOENT);

    // openBeneath() rejects absolute symlinks
    FileDescriptor fd2_beneath(FileUtils::openBeneath(
        root_directory_fd, "subdir1/subdir2/symlink/subdir2/no-entry",
        O_RDONLY));
    EXPECT_EQ(fd2_beneath.get(), -1);
    EXPECT_EQ(errno, EXDEV);
}

TEST_F(OpenInRootFixture, SymlinkEscapingRoot)
{
    // Trying to open "subdir1/subdir2/relative_symlink" with `subdir2/` as
    // the root. `relative_symlink` points to the parent directory `subdir1/`.
    FileDescriptor subdir2_fd(
        openat(root_directory_fd, "subdir1/subdir2/", O_DIRECTORY | O_RDONLY));
    ASSERT_NE(subdir2_fd.get(), -1);

    // Sanity check that it works with unrestricted openat().
    FileDescriptor fd_openat(openat(
        subdir2_fd.get(), "relative_symlink/subdir2/file.txt", O_RDONLY));
    ASSERT_NE(fd_openat.get(), -1);

    // openBeneath() should not allow escaping the root and return an error.
    FileDescriptor dirfd_beneath(FileUtils::openBeneath(
        subdir2_fd.get(), "relative_symlink", O_RDONLY | O_DIRECTORY));
    EXPECT_EQ(dirfd_beneath.get(), -1);
    EXPECT_EQ(errno, EXDEV);

    // Check that it's also rejected as part of a longer path
    FileDescriptor fd_beneath(FileUtils::openBeneath(
        subdir2_fd.get(), "relative_symlink/subdir2/file.txt", O_RDONLY));
    EXPECT_EQ(fd_beneath.get(), -1);
    EXPECT_EQ(errno, EXDEV);

    // openInRoot() does not return an error for `..` symlinks but it will
    // point back to the root, see also RootDotDot test
    FileDescriptor dirfd_inroot(FileUtils::openInRoot(
        subdir2_fd.get(), "relative_symlink", O_RDONLY | O_DIRECTORY));
    EXPECT_NE(dirfd_inroot.get(), -1);

    // As it points back to the root (which is subdir2), it should not find
    // the file with the following path
    FileDescriptor fd_inroot(FileUtils::openInRoot(
        subdir2_fd.get(), "relative_symlink/subdir2/file.txt", O_RDONLY));
    EXPECT_EQ(fd_inroot.get(), -1);
    EXPECT_EQ(errno, ENOENT);

    // However, it should find the file with an adjusted path.
    FileDescriptor fd2_inroot(FileUtils::openInRoot(
        subdir2_fd.get(), "relative_symlink/file.txt", O_RDONLY));
    EXPECT_NE(fd2_inroot.get(), -1);
}

TEST(FileUtilsTests, WriteDigestToFile)
{
    TemporaryDirectory tmpdir;
    const std::string digestFilePath =
        std::string(tmpdir.name()) + "/digest.txt";

    // Create a test digest
    Digest testDigest;
    testDigest.set_hash(
        "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
    testDigest.set_size_bytes(0);

    // Write digest to file
    EXPECT_NO_THROW(FileUtils::writeDigestToFile(testDigest, digestFilePath));

    // Verify file exists and has correct content
    EXPECT_TRUE(FileUtils::isRegularFile(digestFilePath.c_str()));

    const std::string expectedContent =
        "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/0";
    const std::string actualContent =
        FileUtils::getFileContents(digestFilePath.c_str());

    EXPECT_EQ(actualContent, expectedContent);
}

TEST(FileUtilsTests, WriteDigestToFileError)
{
    const std::string invalidPath =
        "/invalid/path/that/does/not/exist/digest.txt";

    Digest testDigest;
    testDigest.set_hash("test_hash");
    testDigest.set_size_bytes(42);

    // Should throw exception for invalid path
    EXPECT_THROW(FileUtils::writeDigestToFile(testDigest, invalidPath),
                 std::runtime_error);
}
