// Copyright 2019 Bloomberg Finance L.P
//
// 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 "build/bazel/remote/execution/v2/remote_execution.pb.h"
#include <array>
#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_merklize.h>
#include <buildboxcommon_temporaryfile.h>
#include <filesystem>
#include <fnmatch.h>
#include <gtest/gtest.h>
#include <stdexcept>
#include <string>
#include <sys/stat.h>
#include <ThreadPool.h>
#include <unistd.h>
#include <unordered_set>

using namespace buildboxcommon;

namespace {
const auto digestFunctionInitializer = []() {
    buildboxcommon::DigestGenerator::init();
    return 0;
}();

Directory getDirectoryFromPath(const MerklizeResult &result,
                               const std::filesystem::path &path)
{

    Directory current = result.d_digestToDirectory.at(result.d_rootDigest);
    for (const auto &component : path) {
        bool found = false;
        for (const auto &child : current.directories()) {
            if (child.name() == component.string()) {
                found = true;
                current = result.d_digestToDirectory.at(child.digest());
                break;
            }
        }
        if (!found) {
            throw std::runtime_error("Directory not found in path: " +
                                     component.string());
        }
    }
    return current;
}

FileNode getFileFromPath(const MerklizeResult &result,
                         const std::filesystem::path &path)
{
    const Directory dir = getDirectoryFromPath(result, path.parent_path());
    for (const auto &file : dir.files()) {
        if (file.name() == path.filename().string()) {
            return file;
        }
    }
    throw std::runtime_error("File not found in directory");
}

std::string getSymlinkFromPath(const MerklizeResult &result,
                               const std::filesystem::path &path)
{
    const Directory dir = getDirectoryFromPath(result, path.parent_path());
    for (const auto &symlink : dir.symlinks()) {
        if (symlink.name() == path.filename().string()) {
            return symlink.target();
        }
    }
    throw std::runtime_error("Symlink not found in directory");
}
} // namespace

TEST(FileTest, ToFilenode)
{
    File file;
    file.d_digest.set_hash("HASH HERE");
    file.d_digest.set_size_bytes(123);
    file.d_executable = true;

    auto fileNode = file.to_filenode(std::string("file.name"));

    EXPECT_EQ(fileNode.name(), "file.name");
    EXPECT_EQ(fileNode.digest().hash(), "HASH HERE");
    EXPECT_EQ(fileNode.digest().size_bytes(), 123);
    EXPECT_TRUE(fileNode.is_executable());
    EXPECT_FALSE(fileNode.has_node_properties());
}

TEST(FileTest, ToFilenodeUnixMode)
{
    File file;
    file.d_digest.set_hash("HASH HERE");
    file.d_digest.set_size_bytes(123);
    file.d_nodeProperties.mutable_unix_mode()->set_value(0644);

    auto fileNode = file.to_filenode(std::string("file.name"));

    EXPECT_EQ(fileNode.name(), "file.name");
    EXPECT_EQ(fileNode.digest().hash(), "HASH HERE");
    EXPECT_EQ(fileNode.digest().size_bytes(), 123);
    EXPECT_FALSE(fileNode.is_executable());
    EXPECT_TRUE(fileNode.has_node_properties());
    EXPECT_FALSE(fileNode.node_properties().has_mtime());
    EXPECT_EQ(fileNode.node_properties().unix_mode().value(), 0644);
}

TEST(FileTest, AllowChmodToReadPermissions)
{
    // Skip this test when running as root since root can read files regardless
    // of permissions
    if (geteuid() == 0) {
        GTEST_SKIP() << "Skipping permission test when running as root";
    }

    TemporaryFile tempFile;
    const std::string testContent = "test content for chmod test";

    // Write content to the file
    FileUtils::writeFileAtomically(tempFile.name(), testContent);

    // Remove all read permissions (make unreadable: write-only for owner)
    ASSERT_EQ(0, chmod(tempFile.name(), 0200));

    // Verify file is unreadable by checking permissions
    struct stat st{};
    ASSERT_EQ(0, stat(tempFile.name(), &st));
    EXPECT_EQ(st.st_mode & 0777, 0200);

    // Test without allowChmodToRead - should throw an exception
    EXPECT_THROW(
        { File file(tempFile.name(), {}, {}, {}, false); }, std::exception);

    // Test with allowChmodToRead - should succeed
    File file(tempFile.name(), {}, {}, {}, true);
    EXPECT_EQ(file.d_digest, DigestGenerator::hash(testContent));
    EXPECT_FALSE(file.d_executable);

    // Verify original permissions are restored after successful capture
    ASSERT_EQ(0, stat(tempFile.name(), &st));
    EXPECT_EQ(st.st_mode & 0777, 0200);
}

TEST(NestedDirectoryTest, EmptyNestedDirectory)
{
    std::unordered_map<buildboxcommon::Digest, std::string> digestMap;
    auto digest = NestedDirectory().to_digest(&digestMap);
    EXPECT_EQ(1, digestMap.size());
    ASSERT_EQ(1, digestMap.count(digest));

    buildboxcommon::Directory message;
    message.ParseFromString(digestMap[digest]);
    EXPECT_EQ(0, message.files_size());
    EXPECT_EQ(0, message.directories_size());
}

TEST(NestedDirectoryTest, TrivialNestedDirectory)
{
    File file;
    file.d_digest.set_hash("DIGESTHERE");

    NestedDirectory directory;
    directory.add(file, "sample");

    std::unordered_map<buildboxcommon::Digest, std::string> digestMap;
    auto digest = directory.to_digest(&digestMap);
    EXPECT_EQ(1, digestMap.size());
    ASSERT_EQ(1, digestMap.count(digest));

    buildboxcommon::Directory message;
    message.ParseFromString(digestMap[digest]);
    EXPECT_EQ(0, message.directories_size());
    ASSERT_EQ(1, message.files_size());
    EXPECT_EQ("sample", message.files(0).name());
    EXPECT_EQ("DIGESTHERE", message.files(0).digest().hash());
}

TEST(NestedDirectoryTest, Subdirectories)
{
    File file;
    file.d_digest.set_hash("HASH1");

    File file2;
    file2.d_digest.set_hash("HASH2");

    NestedDirectory directory;
    directory.add(file, "sample");
    directory.add(file2, "subdir/anothersubdir/sample2");

    std::unordered_map<buildboxcommon::Digest, std::string> digestMap;
    auto digest = directory.to_digest(&digestMap);
    EXPECT_EQ(3, digestMap.size());
    ASSERT_EQ(1, digestMap.count(digest));

    buildboxcommon::Directory message;
    message.ParseFromString(digestMap[digest]);

    EXPECT_EQ(1, message.files_size());
    EXPECT_EQ("sample", message.files(0).name());
    EXPECT_EQ("HASH1", message.files(0).digest().hash());
    ASSERT_EQ(1, message.directories_size());
    EXPECT_EQ("subdir", message.directories(0).name());

    ASSERT_EQ(1, digestMap.count(message.directories(0).digest()));
    buildboxcommon::Directory subdir1;
    subdir1.ParseFromString(digestMap[message.directories(0).digest()]);
    EXPECT_EQ(0, subdir1.files_size());
    ASSERT_EQ(1, subdir1.directories_size());
    EXPECT_EQ("anothersubdir", subdir1.directories(0).name());

    ASSERT_EQ(1, digestMap.count(subdir1.directories(0).digest()));
    buildboxcommon::Directory subdir2;
    subdir2.ParseFromString(digestMap[subdir1.directories(0).digest()]);
    EXPECT_EQ(0, subdir2.directories_size());
    ASSERT_EQ(1, subdir2.files_size());
    EXPECT_EQ("sample2", subdir2.files(0).name());
    EXPECT_EQ("HASH2", subdir2.files(0).digest().hash());
}

TEST(NestedDirectoryTest, AddSingleDirectory)
{
    NestedDirectory directory;
    directory.addDirectory("foo");

    std::unordered_map<buildboxcommon::Digest, std::string> digestMap;
    auto digest = directory.to_digest(&digestMap);

    buildboxcommon::Directory message;
    message.ParseFromString(digestMap[digest]);
    EXPECT_EQ(0, message.files_size());
    ASSERT_EQ(1, message.directories_size());
    EXPECT_EQ("foo", message.directories(0).name());
}

TEST(NestedDirectoryTest, AddSlashDirectory)
{
    NestedDirectory directory;
    directory.addDirectory("/");

    std::unordered_map<buildboxcommon::Digest, std::string> digestMap;
    auto digest = directory.to_digest(&digestMap);

    buildboxcommon::Directory message;
    message.ParseFromString(digestMap[digest]);
    EXPECT_EQ(0, message.files_size());
    ASSERT_EQ(0, message.directories_size());
}

TEST(NestedDirectoryTest, AddAbsoluteDirectory)
{
    NestedDirectory directory;
    directory.addDirectory("/root");

    std::unordered_map<buildboxcommon::Digest, std::string> digestMap;
    auto digest = directory.to_digest(&digestMap);

    buildboxcommon::Directory message;
    message.ParseFromString(digestMap[digest]);
    EXPECT_EQ(0, message.files_size());
    ASSERT_EQ(1, message.directories_size());
    EXPECT_EQ("root", message.directories(0).name());
}

TEST(NestedDirectoryTest, EmptySubdirectories)
{

    NestedDirectory directory;
    directory.addDirectory("foo/bar/baz");

    std::unordered_map<buildboxcommon::Digest, std::string> digestMap;
    auto digest = directory.to_digest(&digestMap);

    buildboxcommon::Directory message;
    message.ParseFromString(digestMap[digest]);
    EXPECT_EQ(0, message.files_size());
    ASSERT_EQ(1, message.directories_size());
    EXPECT_EQ("foo", message.directories(0).name());

    buildboxcommon::Directory subdir;
    subdir.ParseFromString(digestMap[message.directories(0).digest()]);
    EXPECT_EQ(0, message.files_size());
    EXPECT_EQ(1, subdir.directories_size());
    EXPECT_EQ("bar", subdir.directories(0).name());

    buildboxcommon::Directory subdir2;
    subdir.ParseFromString(digestMap[subdir.directories(0).digest()]);
    EXPECT_EQ(0, message.files_size());
    EXPECT_EQ(1, subdir.directories_size());
    EXPECT_EQ("baz", subdir.directories(0).name());
}

TEST(NestedDirectoryTest, AddDirsToExistingNestedDirectory)
{
    File file;
    file.d_digest.set_hash("DIGESTHERE");

    NestedDirectory directory;
    directory.add(file, "directory/file");
    directory.addDirectory("directory/foo");
    directory.addDirectory("otherdir");

    std::unordered_map<buildboxcommon::Digest, std::string> digestMap;
    auto digest = directory.to_digest(&digestMap);

    buildboxcommon::Directory message;
    message.ParseFromString(digestMap[digest]);
    EXPECT_EQ(0, message.files_size());
    ASSERT_EQ(2, message.directories_size());
    EXPECT_EQ("directory", message.directories(0).name());
    EXPECT_EQ("otherdir", message.directories(1).name());

    buildboxcommon::Directory subdir;
    subdir.ParseFromString(digestMap[message.directories(0).digest()]);
    EXPECT_EQ(1, subdir.files_size());
    EXPECT_EQ(1, subdir.directories_size());
    EXPECT_EQ("file", subdir.files(0).name());
    EXPECT_EQ("foo", subdir.directories(0).name());
}

TEST(NestedDirectoryTest, AddNonCanonicalPaths)
{
    File file;
    file.d_digest.set_hash("DIGESTHERE");

    NestedDirectory directory;
    EXPECT_THROW(directory.add(file, "./foo"), std::invalid_argument);
    EXPECT_THROW(directory.add(file, "../foo"), std::invalid_argument);
    EXPECT_THROW(directory.addSymlink("target", "./link"),
                 std::invalid_argument);
    EXPECT_THROW(directory.addSymlink("target", "../link"),
                 std::invalid_argument);
    EXPECT_THROW(directory.addDirectory("./bar"), std::invalid_argument);
    EXPECT_THROW(directory.addDirectory("../bar"), std::invalid_argument);
    EXPECT_THROW(directory.addDirectory("bar/."), std::invalid_argument);
    EXPECT_THROW(directory.addDirectory("bar/.."), std::invalid_argument);
}

TEST(NestedDirectoryTest, SubdirectoriesToTree)
{
    File file;
    file.d_digest.set_hash("HASH1");

    File file2;
    file2.d_digest.set_hash("HASH2");

    NestedDirectory directory;
    directory.add(file, "sample");
    directory.add(file2, "subdir/anothersubdir/sample2");

    auto tree = directory.to_tree();
    EXPECT_EQ(2, tree.children_size());

    std::unordered_map<buildboxcommon::Digest, buildboxcommon::Directory>
        digestMap;
    for (auto &child : tree.children()) {
        digestMap[DigestGenerator::hash(child)] = child;
    }

    const auto &root = tree.root();

    EXPECT_EQ(1, root.files_size());
    EXPECT_EQ("sample", root.files(0).name());
    EXPECT_EQ("HASH1", root.files(0).digest().hash());
    ASSERT_EQ(1, root.directories_size());
    EXPECT_EQ("subdir", root.directories(0).name());

    ASSERT_EQ(1, digestMap.count(root.directories(0).digest()));
    buildboxcommon::Directory subdir1 =
        digestMap[root.directories(0).digest()];
    EXPECT_EQ(0, subdir1.files_size());
    ASSERT_EQ(1, subdir1.directories_size());
    EXPECT_EQ("anothersubdir", subdir1.directories(0).name());

    ASSERT_EQ(1, digestMap.count(subdir1.directories(0).digest()));
    buildboxcommon::Directory subdir2 =
        digestMap[subdir1.directories(0).digest()];
    EXPECT_EQ(0, subdir2.directories_size());
    ASSERT_EQ(1, subdir2.files_size());
    EXPECT_EQ("sample2", subdir2.files(0).name());
    EXPECT_EQ("HASH2", subdir2.files(0).digest().hash());
}

TEST(NestedDirectoryTest, MakeNestedDirectory)
{
    FileDescriptor dirfd(open(".", O_RDONLY | O_DIRECTORY));
    auto result = Merklizer(false).merklize(dirfd.get());

    const Directory dir = getDirectoryFromPath(result, "");
    EXPECT_EQ(2, dir.directories_size());
    EXPECT_EQ(2, dir.files_size());
    EXPECT_EQ(1, dir.symlinks_size());

    EXPECT_EQ(
        "abc",
        FileUtils::getFileContents(
            result.d_digestToPath[getFileFromPath(result, "abc.txt").digest()]
                .c_str()));

    EXPECT_EQ("abc.txt", getSymlinkFromPath(result, "symlink"));

    auto subdirectory = getDirectoryFromPath(result, "subdir");
    EXPECT_EQ(0, subdirectory.directories_size());
    EXPECT_EQ(1, subdirectory.files_size());
    EXPECT_EQ(0, subdirectory.symlinks_size());
    EXPECT_EQ("abc",
              FileUtils::getFileContents(
                  result
                      .d_digestToPath[getFileFromPath(result, "subdir/abc.txt")
                                          .digest()]
                      .c_str()));

    auto ignoreDirectory = getDirectoryFromPath(result, "ignore");
    EXPECT_EQ(1, ignoreDirectory.directories_size());
    EXPECT_EQ(3, ignoreDirectory.files_size());
    EXPECT_EQ(0, ignoreDirectory.symlinks_size());
    EXPECT_EQ("abc\n",
              FileUtils::getFileContents(
                  result
                      .d_digestToPath[getFileFromPath(result, "ignore/abc.txt")
                                          .digest()]
                      .c_str()));
}

TEST(NestedDirectoryTest, MakeNestedDirectoryThreaded)
{
    ThreadPool pool(4, "");
    FileDescriptor dirfd(open(".", O_RDONLY | O_DIRECTORY));
    auto result = Merklizer(false, {}, {}, &pool).merklize(dirfd.get());

    const Directory dir = getDirectoryFromPath(result, "");
    EXPECT_EQ(2, dir.directories_size());
    EXPECT_EQ(2, dir.files_size());
    EXPECT_EQ(1, dir.symlinks_size());

    EXPECT_EQ(
        "abc",
        FileUtils::getFileContents(
            result.d_digestToPath[getFileFromPath(result, "abc.txt").digest()]
                .c_str()));

    EXPECT_EQ("abc.txt", getSymlinkFromPath(result, "symlink"));

    auto subdirectory = getDirectoryFromPath(result, "subdir");
    EXPECT_EQ(0, subdirectory.directories_size());
    EXPECT_EQ(1, subdirectory.files_size());
    EXPECT_EQ(0, subdirectory.symlinks_size());
    EXPECT_EQ("abc",
              FileUtils::getFileContents(
                  result
                      .d_digestToPath[getFileFromPath(result, "subdir/abc.txt")
                                          .digest()]
                      .c_str()));

    auto ignoreDirectory = getDirectoryFromPath(result, "ignore");
    EXPECT_EQ(1, ignoreDirectory.directories_size());
    EXPECT_EQ(3, ignoreDirectory.files_size());
    EXPECT_EQ(0, ignoreDirectory.symlinks_size());
    EXPECT_EQ("abc\n",
              FileUtils::getFileContents(
                  result
                      .d_digestToPath[getFileFromPath(result, "ignore/abc.txt")
                                          .digest()]
                      .c_str()));
}

TEST(NestedDirectoryTest, MakeNestedDirectoryThreadedException)
{
    ThreadPool pool(4, "");
    FileDescriptor dirfd(open(".", O_RDONLY | O_DIRECTORY));
    auto merklizer = Merklizer(false, {}, {}, &pool);

    // Exception is properly thrown, no segfault
    EXPECT_THROW(merklizer.merklize(
                     dirfd.get(), "",
                     [](int) -> Digest { throw std::runtime_error("error"); }),
                 std::runtime_error);
}

TEST(NestedDirectoryTest, MakeNestedDirectoryWithPermissionOverride)
{
    // g+w and u-w
    FileDescriptor dirfd(open(".", O_RDONLY | O_DIRECTORY));
    auto result =
        Merklizer(false, {"unix_mode"})
            .merklize(dirfd.get(), "", Merklizer::hashFile,
                      [](mode_t mode) { return (mode | S_IWGRP) & ~S_IWUSR; });

    const Directory dir = getDirectoryFromPath(result, "");
    EXPECT_EQ(2, dir.directories_size());
    EXPECT_EQ(2, dir.files_size());
    EXPECT_EQ(1, dir.symlinks_size());

    const auto textFileUnixMode = getFileFromPath(result, "abc.txt")
                                      .node_properties()
                                      .unix_mode()
                                      .value();
    EXPECT_TRUE(textFileUnixMode & S_IWGRP);
    EXPECT_FALSE(textFileUnixMode & S_IWUSR);

    const auto shFileUnixMode = getFileFromPath(result, "script.sh")
                                    .node_properties()
                                    .unix_mode()
                                    .value();
    EXPECT_TRUE(shFileUnixMode & S_IWGRP);
    EXPECT_FALSE(shFileUnixMode & S_IWUSR);

    auto subdirectory = getDirectoryFromPath(result, "subdir");
    EXPECT_EQ(0, subdirectory.directories_size());
    EXPECT_EQ(1, subdirectory.files_size());
    EXPECT_EQ(0, subdirectory.symlinks_size());
    const auto subdirTextUnixMode = getFileFromPath(result, "subdir/abc.txt")
                                        .node_properties()
                                        .unix_mode()
                                        .value();
    EXPECT_TRUE(subdirTextUnixMode & S_IWGRP);
    EXPECT_FALSE(subdirTextUnixMode & S_IWUSR);
}

TEST(NestedDirectoryTest, MakeNestedDirectoryCaptureMtimeMode)
{
    // g+w and u-w
    FileDescriptor dirfd(open(".", O_RDONLY | O_DIRECTORY));
    auto result =
        Merklizer(false, {"unix_mode", "mtime"}).merklize(dirfd.get());
    const Directory dir = getDirectoryFromPath(result, "");
    EXPECT_EQ(2, dir.directories_size());
    EXPECT_EQ(2, dir.files_size());
    EXPECT_EQ(1, dir.symlinks_size());

    auto subdirectory = getDirectoryFromPath(result, "subdir");
    EXPECT_EQ(0, subdirectory.directories_size());
    EXPECT_EQ(1, subdirectory.files_size());
    EXPECT_EQ(0, subdirectory.symlinks_size());
    EXPECT_TRUE(subdirectory.node_properties().has_mtime());
    EXPECT_TRUE(subdirectory.node_properties().has_unix_mode());
}

TEST(NestedDirectoryTest, MakeNestedDirectoryFollowingSymlinks)
{
    const bool followSymlinks = true;
    FileDescriptor dirfd(open(".", O_RDONLY | O_DIRECTORY));
    auto result = Merklizer(followSymlinks).merklize(dirfd.get());

    const Directory dir = getDirectoryFromPath(result, "");
    EXPECT_EQ(2, dir.directories_size());
    EXPECT_EQ(2 + 1, dir.files_size());
    EXPECT_TRUE(dir.symlinks().empty());

    EXPECT_EQ(
        "abc",
        FileUtils::getFileContents(
            result.d_digestToPath[getFileFromPath(result, "abc.txt").digest()]
                .c_str()));
    EXPECT_EQ(
        "abc",
        FileUtils::getFileContents(
            result.d_digestToPath[getFileFromPath(result, "symlink").digest()]
                .c_str()));

    auto subdirectory = getDirectoryFromPath(result, "subdir");
    EXPECT_EQ(0, subdirectory.directories_size());
    EXPECT_EQ(1, subdirectory.files_size());
    EXPECT_EQ(0, subdirectory.symlinks_size());
    EXPECT_EQ("abc",
              FileUtils::getFileContents(
                  result
                      .d_digestToPath[getFileFromPath(result, "subdir/abc.txt")
                                          .digest()]
                      .c_str()));

    auto ignoreDirectory = getDirectoryFromPath(result, "ignore");
    EXPECT_EQ(1, ignoreDirectory.directories_size());
    EXPECT_EQ(3, ignoreDirectory.files_size());
    EXPECT_EQ(0, ignoreDirectory.symlinks_size());
    EXPECT_EQ("abc\n",
              FileUtils::getFileContents(
                  result
                      .d_digestToPath[getFileFromPath(result, "ignore/abc.txt")
                                          .digest()]
                      .c_str()));
}

TEST(NestedDirectoryTest, MakeNestedDirectoryIgnoreFiles)
{
    auto ignorePatterns = std::istringstream("*.oo");
    const auto ignoreMatcher = std::make_shared<IgnoreMatcher>(
        "./ignore", IgnoreMatcher::parseIgnorePatterns(ignorePatterns));
    FileDescriptor dirfd(open("./ignore", O_RDONLY | O_DIRECTORY));
    auto result = Merklizer(false, {}, ignoreMatcher).merklize(dirfd.get());

    const Directory dir = getDirectoryFromPath(result, "");
    EXPECT_EQ(1, dir.directories_size());
    EXPECT_EQ(1, dir.files_size());

    EXPECT_NE(dir.files(0).name(), "foo.oo");
    EXPECT_NE(dir.files(0).name(), "bar.oo");

    // *.oo in subdir is also ignored since the pattern basename only match
    const Directory subdir = getDirectoryFromPath(result, "subdir");
    EXPECT_TRUE(subdir.files().empty());
}

TEST(NestedDirectoryTest,
     MakeNestedDirectoryNothingIgnoredWhenNoPatternMatched)
{
    auto ignorePatterns = std::istringstream("*.pyc\n*.hs\nnode_moduels/\n");
    const auto ignoreMatcher = std::make_shared<IgnoreMatcher>(
        "./ignore", IgnoreMatcher::parseIgnorePatterns(ignorePatterns));
    FileDescriptor dirfd(open("./ignore", O_RDONLY | O_DIRECTORY));
    auto result = Merklizer(false, {}, ignoreMatcher).merklize(dirfd.get());

    const Directory dir = getDirectoryFromPath(result, "");
    EXPECT_EQ(1, dir.directories_size());
    EXPECT_EQ(3, dir.files_size());

    EXPECT_NO_THROW(getFileFromPath(result, "foo.oo"));
    EXPECT_NO_THROW(getFileFromPath(result, "bar.oo"));
    EXPECT_NO_THROW(getFileFromPath(result, "abc.txt"));

    EXPECT_NO_THROW(getFileFromPath(result, "subdir/foo.oo"));
}

TEST(NestedDirectoryTest, MakeNestedDirectoryIgnoreToplevelFiles)
{
    auto ignorePatterns = std::istringstream("/*.oo");
    const auto ignoreMatcher = std::make_shared<IgnoreMatcher>(
        "./ignore", IgnoreMatcher::parseIgnorePatterns(ignorePatterns));
    FileDescriptor dirfd(open("./ignore", O_RDONLY | O_DIRECTORY));
    auto result = Merklizer(false, {}, ignoreMatcher).merklize(dirfd.get());

    const Directory dir = getDirectoryFromPath(result, "");
    EXPECT_EQ(1, dir.directories_size());
    EXPECT_EQ(1, dir.files_size());

    EXPECT_NE(dir.files(0).name(), "foo.oo");
    EXPECT_NE(dir.files(0).name(), "bar.oo");

    EXPECT_NO_THROW(getFileFromPath(result, "subdir/foo.oo"));
}

TEST(NestedDirectoryTest, MakeNestedDirectoryIgnoreDirectory)
{
    auto ignorePatterns = std::istringstream("subdir/");
    const auto ignoreMatcher = std::make_shared<IgnoreMatcher>(
        "./ignore", IgnoreMatcher::parseIgnorePatterns(ignorePatterns));
    FileDescriptor dirfd(open("./ignore", O_RDONLY | O_DIRECTORY));
    auto result = Merklizer(false, {}, ignoreMatcher).merklize(dirfd.get());

    const Directory dir = getDirectoryFromPath(result, "");
    EXPECT_EQ(0, dir.directories_size());
    EXPECT_EQ(3, dir.files_size());

    EXPECT_NO_THROW(getFileFromPath(result, "foo.oo"));
    EXPECT_NO_THROW(getFileFromPath(result, "bar.oo"));
    EXPECT_NO_THROW(getFileFromPath(result, "abc.txt"));
}

TEST(NestedDirectoryTest, MakeNestedDirectoryIgnoreEverything)
{
    auto ignorePatterns = std::istringstream("/*");
    const auto ignoreMatcher = std::make_shared<IgnoreMatcher>(
        "./ignore", IgnoreMatcher::parseIgnorePatterns(ignorePatterns));
    FileDescriptor dirfd(open("./ignore", O_RDONLY | O_DIRECTORY));
    auto result = Merklizer(false, {}, ignoreMatcher).merklize(dirfd.get());

    const Directory dir = getDirectoryFromPath(result, "");
    EXPECT_EQ(0, dir.directories_size());
    EXPECT_EQ(0, dir.files_size());
    EXPECT_EQ(0, dir.symlinks_size());
}

// Make sure the digest is calculated correctly regardless of the order in
// which the files are added. Important for caching.
TEST(NestedDirectoryTest, ConsistentDigestRegardlessOfFileOrder)
{
    constexpr int N = 5;
    // Get us some mock files
    std::array<File, N> files{};
    for (int i = 0; i < N; i++) {
        files.at(i).d_digest.set_hash("HASH_" + std::to_string(i));
    }

    // Create Nested Directory and add everything in-order
    NestedDirectory directory1;
    for (int i = 0; i < N; i++) {
        std::string fn =
            "subdir_" + std::to_string(i) + "/file_" + std::to_string(i);
        directory1.add(files.at(i), fn.c_str());
    }

    // Create another Nested Directory and add everything in reverse order
    NestedDirectory directory2;
    for (int i = N - 1; i >= 0; i--) {
        std::string fn =
            "subdir_" + std::to_string(i) + "/file_" + std::to_string(i);
        directory2.add(files.at(i), fn.c_str());
    }

    // Make sure the actual digests of those two directories are identical
    EXPECT_EQ(directory1.to_digest().hash(), directory2.to_digest().hash());
}

// Make sure digests of directories containing different files are different
TEST(NestedDirectoryTest, NestedDirectoryDigestsReallyBasedOnFiles)
{
    constexpr int N = 5;
    // Get us some mock files
    std::array<File, N> files_dir1{}; // Files to add in the first directory
    std::array<File, N> files_dir2{}; // Files to add in the second directory
    for (int i = 0; i < N; i++) {
        files_dir1.at(i).d_digest.set_hash("HASH_DIR1_" + std::to_string(i));
        files_dir2.at(i).d_digest.set_hash("HASH_DIR2_" + std::to_string(i));
    }

    // Create Nested Directories and add everything in-order
    NestedDirectory directory1, directory2;
    for (int i = 0; i < N; i++) {
        std::string fn =
            "subdir_" + std::to_string(i) + "/file_" + std::to_string(i);
        directory1.add(files_dir1.at(i), fn.c_str());
        directory2.add(files_dir2.at(i), fn.c_str());
    }

    // Make sure the digests are different
    EXPECT_NE(directory1.to_digest().hash(), directory2.to_digest().hash());
}

TEST(NestedDirectoryTest, AddConflictingEntries)
{
    File file;
    file.d_digest.set_hash("DIGESTHERE");

    NestedDirectory directory;

    directory.addDirectory("foo");
    EXPECT_THROW(directory.add(file, "foo"), std::runtime_error);
    EXPECT_THROW(directory.addSymlink("target", "foo"), std::runtime_error);

    directory.add(file, "bar");
    EXPECT_THROW(directory.addDirectory("bar"), std::runtime_error);
    EXPECT_THROW(directory.addSymlink("target", "bar"), std::runtime_error);

    directory.addSymlink("target", "link");
    EXPECT_THROW(directory.addDirectory("link"), std::runtime_error);
    EXPECT_THROW(directory.add(file, "link"), std::runtime_error);
}

TEST(IgnoreMatcherTests, IgnoreFileInBaseDir)
{
    // GIVEN
    const auto baseDir = "./workspace";
    const std::vector<IgnorePattern> patterns{{"tmpfile", false, false}};
    const IgnoreMatcher matcher{
        baseDir, std::make_shared<std::vector<IgnorePattern>>(patterns)};
    const auto path = "./workspace/tmpfile";

    // WHEN
    auto ignored = matcher.match(path);

    // THEN
    ASSERT_TRUE(ignored);
}

TEST(IgnoreMatcherTests, IgnoreDirInBaseDir)
{
    // GIVEN
    const auto baseDir = "./workspace";
    const std::vector<IgnorePattern> patterns{{".git/", false, false}};
    const IgnoreMatcher matcher{
        baseDir, std::make_shared<std::vector<IgnorePattern>>(patterns)};
    const std::string path = "./workspace/.git";

    // WHEN
    auto ignored = matcher.match(path + "/");

    // THEN
    ASSERT_TRUE(ignored);
}

TEST(IgnoreMatcherTests, IgnoreFileInBaseDirWithTrailingSlash)
{
    // GIVEN
    const auto baseDir = "./workspace/";
    const std::vector<IgnorePattern> patterns{{"tmp*", true, false}};
    const IgnoreMatcher matcher{
        baseDir, std::make_shared<std::vector<IgnorePattern>>(patterns)};
    const auto path = "./workspace//tmpfile";

    // WHEN
    auto ignored = matcher.match(path);

    // THEN
    ASSERT_TRUE(ignored);
}

TEST(IgnoreMatcherTests, IgnoreWildcard)
{
    // GIVEN
    const auto baseDir = "./workspace";
    const std::vector<IgnorePattern> patterns{{"*.o", true, false}};
    const IgnoreMatcher matcher{
        baseDir, std::make_shared<std::vector<IgnorePattern>>(patterns)};
    const auto path1 = "./workspace/foo.o";
    const auto path2 = "./workspace/bar.o";

    // WHEN
    auto ignored1 = matcher.match(path1);
    auto ignored2 = matcher.match(path2);

    // THEN
    ASSERT_TRUE(ignored1);
    ASSERT_TRUE(ignored2);
}

TEST(IgnoreMatcherTests, NotIgnoredIfNotMatched)
{
    // GIVEN
    const auto baseDir = "./workspace";
    const std::vector<IgnorePattern> patterns{{"foo*", true, false}};
    const IgnoreMatcher matcher{
        baseDir, std::make_shared<std::vector<IgnorePattern>>(patterns)};
    const auto path = "./workspace/bar";

    // WHEN
    auto ignored = matcher.match(path);

    // THEN
    ASSERT_FALSE(ignored);
}

TEST(IgnoreMatcherTests, ParsePatterns)
{
    // GIVEN
    const auto input = "*.o\n.git/\nfoo/bar\ndir/build/";
    std::istringstream iss(input);

    // WHEN
    const auto patterns = IgnoreMatcher::parseIgnorePatterns(iss);

    // THEN
    ASSERT_EQ(patterns->size(), 4);
    ASSERT_EQ(patterns->at(0), IgnorePattern("*.o", true, false));
    ASSERT_EQ(patterns->at(1), IgnorePattern(".git", true, true));
    ASSERT_EQ(patterns->at(2), IgnorePattern("foo/bar", false, false));
    ASSERT_EQ(patterns->at(3), IgnorePattern("dir/build", false, true));
}

TEST(IgnoreMatcherTests, ParsePatternsTrimSpaces)
{
    // GIVEN
    const auto input = "\n\n*.o  \n*.pyc ";
    std::istringstream iss(input);

    // WHEN
    const auto patterns = IgnoreMatcher::parseIgnorePatterns(iss);

    // THEN
    ASSERT_EQ(patterns->size(), 2);
    ASSERT_EQ(patterns->at(0), IgnorePattern("*.o", true, false));
    ASSERT_EQ(patterns->at(1), IgnorePattern("*.pyc", true, false));
}

TEST(IgnoreMatcherTests, ParsePatternsTrimBeginningSlashSoPatternIsRelative)
{
    // GIVEN
    const auto input = "/build";
    std::istringstream iss(input);

    // WHEN
    const auto patterns = IgnoreMatcher::parseIgnorePatterns(iss);

    // THEN
    ASSERT_EQ(patterns->size(), 1);
    ASSERT_EQ(patterns->front(), IgnorePattern("build", false, false));
}

TEST(IgnoreMatcherTests, ParsePatternsPreservesTrailingSlash)
{
    // GIVEN
    const auto input = "build/";
    std::istringstream iss(input);

    // WHEN
    const auto patterns = IgnoreMatcher::parseIgnorePatterns(iss);

    // THEN
    ASSERT_EQ(patterns->size(), 1);
    ASSERT_EQ(patterns->front(), IgnorePattern("build", true, true));
}

TEST(IgnoreMatcherTests, ParseAndMatch)
{
    // GIVEN
    const auto prefix = "/workspace";
    const auto input = "*.o\n"
                       ".git/\n"
                       "foo/bar\n"
                       "mydir/*\n"
                       "/ignore_only_dir/\n"
                       "/build\n"
                       "tmp/\n"
                       "/dev/*\n"
                       "/.gitignore";
    std::istringstream iss(input);
    const auto patterns = IgnoreMatcher::parseIgnorePatterns(iss);
    const IgnoreMatcher ignoreMatcher(prefix, patterns);

    // THEN
    ASSERT_TRUE(ignoreMatcher.match("/workspace/foo.o"));
    ASSERT_TRUE(ignoreMatcher.match("/workspace/bar.o"));
    ASSERT_TRUE(ignoreMatcher.match("/workspace/dir1/foo.o"));
    ASSERT_TRUE(ignoreMatcher.match("/workspace/dir1/bar.o"));
    ASSERT_TRUE(ignoreMatcher.match("/workspace/dir1/dir2/foo.o"));
    ASSERT_TRUE(ignoreMatcher.match("/workspace/dir1/dir2/bar.o"));
    ASSERT_TRUE(ignoreMatcher.match("/workspace/foo/bar"));
    ASSERT_TRUE(ignoreMatcher.match("/workspace/mydir/tmpfile"));
    ASSERT_TRUE(ignoreMatcher.match("/workspace/mydir/build"));
    ASSERT_TRUE(ignoreMatcher.match("/workspace/tmp", 0, true));
    ASSERT_TRUE(ignoreMatcher.match("/workspace/seriousproject/tmp", 0, true));
    ASSERT_TRUE(ignoreMatcher.match("/workspace/.gitignore"));
    ASSERT_FALSE(ignoreMatcher.match("/workspace/important"));
    ASSERT_FALSE(ignoreMatcher.match("/workspace/ignore_only_dir"));
    ASSERT_FALSE(ignoreMatcher.match("/workspace/dir/.gitignore"));
    ASSERT_TRUE(ignoreMatcher.match("/workspace/dev/foo"));
    ASSERT_FALSE(ignoreMatcher.match("/workspace/dev"));
    ASSERT_FALSE(ignoreMatcher.match("/workspace/somedir/dev/foo"));
}

TEST(MerklizerTest, CompatibleWithMakeNestedDirectory)
{
    auto ignorePatterns = std::istringstream("*.oo\nsubdir/");
    const auto ignoreMatcher = std::make_shared<IgnoreMatcher>(
        "./ignore", IgnoreMatcher::parseIgnorePatterns(ignorePatterns));

    ThreadPool pool(2, "");
    Merklizer merklizer(false, {"unix_mode", "mtime"}, ignoreMatcher, &pool);
    FileDescriptor dirfd(open(".", O_RDONLY | O_DIRECTORY));
    ASSERT_TRUE(dirfd.get() > 0);
    const auto merklizeResult = merklizer.merklize(
        dirfd.get(), ".", Merklizer::hashFile, {}, {{"foo", "bar"}});
    const auto tree1 = merklizeResult.tree();
    const auto [nestedDir, fileMap] = merklizer.makeNestedDirectory(
        dirfd.get(), ".", Merklizer::hashFile, {}, {{"foo", "bar"}});
    const auto tree2 = nestedDir.to_tree();

    EXPECT_EQ(merklizeResult.d_rootDigest, nestedDir.to_digest());
    EXPECT_EQ(tree1.root().SerializeAsString(),
              tree2.root().SerializeAsString());
    EXPECT_EQ(tree1.root().node_properties().mtime(),
              tree2.root().node_properties().mtime());
    EXPECT_EQ(fileMap, merklizeResult.d_digestToPath);
    std::unordered_set<std::string> childrenBlobs1, childrenBlobs2;
    for (const auto &child : tree1.children()) {
        childrenBlobs1.insert(child.SerializeAsString());
    }
    for (const auto &child : tree2.children()) {
        childrenBlobs2.insert(child.SerializeAsString());
    }
    EXPECT_EQ(childrenBlobs1, childrenBlobs2);
    EXPECT_EQ(merklizeResult.d_digestToPath, fileMap);
}
