/*
 * Copyright 2020 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.
 */

#ifndef INCLUDED_BUILDBOXCOMMONTEST_UTILS
#define INCLUDED_BUILDBOXCOMMONTEST_UTILS

#include <dirent.h>
#include <fstream>
#include <iostream>
#include <memory>
#include <string>
#include <sys/stat.h>

#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_mergeutil.h>
#include <buildboxcommon_protos.h>

using namespace buildboxcommon;

namespace buildboxcommontest {

struct TestUtils {
    static bool pathExists(const char *path)
    {
        struct stat statResult;
        return stat(path, &statResult) == 0;
    }

    static void touchFile(const char *path)
    {
        std::fstream fs;
        fs.open(path, std::ios::out);
        fs.close();
    }

    static std::string createSubDirectory(const std::string &root_path,
                                          const std::string &subdir_name)
    {
        auto subdir = root_path + "/" + subdir_name;
        FileUtils::createDirectory(subdir.c_str());
        return subdir;
    }

    static std::string createFileInDirectory(const std::string &file_name,
                                             const std::string &dir_name)
    {
        const auto file_in_dir = dir_name + "/" + file_name;
        touchFile(file_in_dir.c_str());
        assert(pathExists(file_in_dir.c_str()));
        return file_in_dir;
    }
};

class MockWriter final
    : public grpc::ServerWriterInterface<google::longrunning::Operation> {
  public:
    MockWriter() : write_calls(0) {};

    bool Write(const Operation &msg, grpc::WriteOptions options) override
    {
        write_calls += 1;
        operations.push_back(msg);
        return true;
    };

    void SendInitialMetadata() override {};

    int write_calls;
    std::vector<Operation> operations;
};

// Helper factory classes to declaratively create test nodes
struct FileNodeFactory {
    FileNodeFactory(const std::string &name, bool executable = false,
                    const std::string &content = "",
                    const std::map<std::string, std::string> &properties = {})
        : d_name(name), d_executable(executable), d_content(content),
          d_properties(properties)
    {
        if (content.empty()) {
            // give a reasonable default binary content
            d_content = name + "_content";
        }
    }

    FileNode create() const
    {
        FileNode node;
        node.set_name(d_name);
        node.set_is_executable(d_executable);
        node.mutable_digest()->CopyFrom(DigestGenerator::hash(d_content));
        for (const auto &it : d_properties) {
            auto nodeProperty =
                node.mutable_node_properties()->add_properties();
            nodeProperty->set_name(it.first);
            nodeProperty->set_name(it.second);
        }
        return node;
    }

    std::string d_name;
    bool d_executable;
    std::string d_content;
    std::map<std::string, std::string> d_properties;
};

struct SymlinkNodeFactory {
    SymlinkNodeFactory(
        const std::string &name, const std::string &target,
        const std::map<std::string, std::string> &properties = {})
        : d_name(name), d_target(target), d_properties(properties)
    {
    }

    SymlinkNode create() const
    {
        SymlinkNode node;
        node.set_name(d_name);
        node.set_target(d_target);
        for (const auto &it : d_properties) {
            auto nodeProperty =
                node.mutable_node_properties()->add_properties();
            nodeProperty->set_name(it.first);
            nodeProperty->set_name(it.second);
        }
        return node;
    }

    std::string d_name;
    std::string d_target;
    std::map<std::string, std::string> d_properties;
};

struct DirectoryFactory;

struct DirectoryNodeFactory {
    DirectoryNodeFactory(const std::string &name,
                         const DirectoryFactory &directory);

    DirectoryNode create(DirectoryTree &outputTree) const;

    std::string d_name;
    // Indirection due to mutual recursive definition
    std::shared_ptr<DirectoryFactory> d_directory;
};

struct DirectoryFactory {
    DirectoryFactory(const std::vector<FileNodeFactory> &files,
                     const std::vector<DirectoryNodeFactory> &directories,
                     const std::vector<SymlinkNodeFactory> &symlinks,
                     const std::map<std::string, std::string> &properties = {},
                     std::optional<mode_t> mode = {});

    static DirectoryFactory
    withFiles(const std::vector<FileNodeFactory> &files)
    {
        return {files, {}, {}};
    }

    static DirectoryFactory
    withDirectories(const std::vector<DirectoryNodeFactory> &directories)
    {
        return {{}, directories, {}};
    }

    Directory create(DirectoryTree &outputTree) const;

    std::vector<FileNodeFactory> d_files;
    std::vector<DirectoryNodeFactory> d_directories;
    std::vector<SymlinkNodeFactory> d_symlinks;
    std::map<std::string, std::string> d_properties;
    std::optional<mode_t> d_mode;
};

} // namespace buildboxcommontest

#endif
