/*
 * Copyright 2019 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 <buildboxcommon_fslocalcas.h>
#include <buildboxcommon_metricnames.h>
#include <buildboxcommonmetrics_countingmetricvalue.h>
#include <buildboxcommonmetrics_metriccollectorfactory.h>
#include <buildboxcommonmetrics_testingutils.h>

#include <fstream>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_temporarydirectory.h>
#include <buildboxcommon_temporaryfile.h>

#include <gtest/gtest.h>

using namespace buildboxcommon;
using buildboxcommon::DigestGenerator;

#if __APPLE__
#define st_mtim st_mtimespec
#endif

namespace {
static void writeToFile(const std::string &data, const std::string &path)
{
    ASSERT_TRUE(buildboxcommon::FileUtils::isRegularFile(path.c_str()));
    std::ofstream file(path, std::ofstream::binary);
    file << data;
    file.close();
}

void assertFilePermissionsMatch(const std::string &path_in_cas,
                                const mode_t expected_permissions)
{
    struct stat statbuf = {};
    ASSERT_EQ(stat(path_in_cas.c_str(), &statbuf), 0);

    const mode_t file_permissions =
        (statbuf.st_mode & static_cast<mode_t>(~S_IFMT));
    ASSERT_EQ(file_permissions, expected_permissions);
}

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

// Other processess are allowed to access the cache directory directly
// but object files should be immutable, so we expect files to be
// read-only for everybody (0444).
const mode_t EXPECTED_CAS_FILE_PERMISSIONS = S_IRUSR | S_IRGRP | S_IROTH;

class LocalCasFixture : public ::testing::Test {
  protected:
    LocalCasFixture() : cas(cas_root_directory.name())
    {
        buildboxcommon::buildboxcommonmetrics::clearAllMetricCollection();
    }

    buildboxcommon::TemporaryDirectory cas_root_directory;
    FsLocalCas cas;

    void assertFilePermissionsInCasAreValid(const Digest &digest)
    {
        assertFilePermissionsMatch(cas.path(digest),
                                   EXPECTED_CAS_FILE_PERMISSIONS);
    }
};

TEST(FsLocalCasTest, RootDirectoryWithOrWithoutTrailingSlash)
{

    buildboxcommon::TemporaryDirectory dir;
    const std::string base_path = dir.name();
    {
        const auto root_directory = base_path + "/cas1";
        ASSERT_NO_THROW(FsLocalCas cas(root_directory));
    }

    {
        const auto root_directory = base_path + "/cas2/";
        ASSERT_NO_THROW(FsLocalCas cas(root_directory));
    }
}

TEST_F(LocalCasFixture, EmptyCasDirectoryStructure)
{
    const auto cas_objects_path =
        std::string(cas_root_directory.name()) + "/cas/objects/";
    const auto temp_objects_path =
        std::string(cas_root_directory.name()) + "/cas/tmp/";

    ASSERT_EQ(access(cas_objects_path.c_str(), F_OK), 0);
    ASSERT_EQ(access(temp_objects_path.c_str(), F_OK), 0);
}

TEST_F(LocalCasFixture, EmptyBlobPreStored)
{
    // A newly created `FsLocalCas` contains the empty blob.
    ASSERT_TRUE(cas.hasBlob(DigestGenerator::hash("")));

    // But not any arbitrary 0-byte blob:
    Digest d = DigestGenerator::hash("non-empty");
    d.set_size_bytes(0);
    ASSERT_FALSE(cas.hasBlob(d));
}

TEST_F(LocalCasFixture, TestEmpty)
{
    ASSERT_FALSE(cas.hasBlob(DigestGenerator::hash("data")));
}

TEST_F(LocalCasFixture, WriteAndReadEmptyBlob)
{
    Digest digest = DigestGenerator::hash("");
    cas.writeBlob(digest, "");

    ASSERT_TRUE(cas.hasBlob(digest));
    ASSERT_EQ(*cas.readBlob(digest), "");

    // Reading/writing an empty blob is reading 0 bytes, so no counter is
    // published
    ASSERT_FALSE(buildboxcommon::buildboxcommonmetrics::allCollectedByName<
                 buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
        {CommonMetricNames::COUNTER_NAME_BYTES_READ,
         CommonMetricNames::COUNTER_NAME_BYTES_WRITE}));
}

TEST_F(LocalCasFixture, WriteWithInvalidHash)
{
    const std::string data = "data";
    Digest digest = DigestGenerator::hash(data);

    digest.set_hash(digest.hash() + "a");

    ASSERT_THROW(cas.writeBlob(digest, data), std::invalid_argument);
}

TEST_F(LocalCasFixture, WriteWithInvalidSize)
{
    const std::string data = "data";
    Digest digest = DigestGenerator::hash(data);

    digest.set_size_bytes(digest.size_bytes() + 1);

    const auto invalid_data_size =
        static_cast<google::protobuf::int64>(data.size() + 1);
    digest.set_size_bytes(invalid_data_size);

    ASSERT_THROW(cas.writeBlob(digest, data), std::invalid_argument);
}

TEST_F(LocalCasFixture, TestWriteAndRead)
{
    const std::string data = "someData";
    const auto digest = DigestGenerator::hash(data);

    ASSERT_FALSE(cas.hasBlob(digest));

    cas.writeBlob(digest, data);

    // Data was written and is valid:
    ASSERT_TRUE(cas.hasBlob(digest));
    ASSERT_EQ(*cas.readBlob(digest), data);
    ASSERT_EQ(*cas.readBlob(digest, 0, static_cast<size_t>(data.size())),
              data);
    // One write and two full reads
    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::allCollectedByNameWithValues<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
            {{CommonMetricNames::COUNTER_NUM_BLOBS_WRITTEN_TO_LOCAL,
              buildboxcommon::buildboxcommonmetrics::CountingMetricValue(1)},
             {CommonMetricNames::COUNTER_NAME_BYTES_WRITE,
              buildboxcommon::buildboxcommonmetrics::CountingMetricValue(
                  digest.size_bytes())},
             {CommonMetricNames::COUNTER_NAME_BYTES_READ,
              buildboxcommon::buildboxcommonmetrics::CountingMetricValue(
                  digest.size_bytes() * 2)}}));
}

TEST_F(LocalCasFixture, TestWriteAndReadBinaryData)
{
    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());
    ASSERT_EQ(data_string.size(), raw_data.size());

    const auto digest = DigestGenerator::hash(data_string);
    ASSERT_FALSE(cas.hasBlob(digest));
    cas.writeBlob(digest, data_string);
    ASSERT_TRUE(cas.hasBlob(digest));

    const std::string read_data = *cas.readBlob(digest);
    ASSERT_TRUE(
        std::equal(read_data.cbegin(), read_data.cend(), raw_data.cbegin()));
    ASSERT_EQ(read_data, data_string);
    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::allCollectedByNameWithValues<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
            {{CommonMetricNames::COUNTER_NUM_BLOBS_WRITTEN_TO_LOCAL,
              buildboxcommon::buildboxcommonmetrics::CountingMetricValue(1)},
             {CommonMetricNames::COUNTER_NAME_BYTES_WRITE,
              buildboxcommon::buildboxcommonmetrics::CountingMetricValue(
                  digest.size_bytes())},
             {CommonMetricNames::COUNTER_NAME_BYTES_READ,
              buildboxcommon::buildboxcommonmetrics::CountingMetricValue(
                  digest.size_bytes())}}));
}

TEST_F(LocalCasFixture, WriteBlobFilePermissions)
{
    const std::string data = "Some data...";
    const Digest digest = DigestGenerator::hash(data);

    cas.writeBlob(digest, data);
    ASSERT_TRUE(cas.hasBlob(digest));

    assertFilePermissionsInCasAreValid(digest);
}

TEST_F(LocalCasFixture, ReadMissingBlobReturnsNullUniquePointer)
{
    const auto digest = DigestGenerator::hash("dataNotPresent");
    ASSERT_FALSE(cas.hasBlob(digest));
    ASSERT_NO_THROW(cas.readBlob(digest));
    ASSERT_EQ(cas.readBlob(digest), nullptr);
}

TEST_F(LocalCasFixture, ReadEmptyBlob)
{
    const auto digest = DigestGenerator::hash("");
    cas.writeBlob(digest, "");

    ASSERT_EQ(*cas.readBlob(digest, 0, LocalCas::npos), "");
}

TEST_F(LocalCasFixture, ReadMultipleEmptyBlob)
{
    const auto digest = DigestGenerator::hash("");
    cas.writeBlob(digest, "");

    ASSERT_EQ(*cas.readBlob(digest, 0, LocalCas::npos), "");
    ASSERT_EQ(*cas.readBlob(digest, 0, LocalCas::npos), "");
    ASSERT_EQ(*cas.readBlob(digest, 0, LocalCas::npos), "");
    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::collectedByNameWithValue<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
            CommonMetricNames::COUNTER_NUM_BLOBS_READ_FROM_LOCAL,
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue(3)));
}

TEST_F(LocalCasFixture, ReadEmptyBlobWithZeroLength)
{

    const auto digest = DigestGenerator::hash("");
    cas.writeBlob(digest, "");

    ASSERT_EQ(*cas.readBlob(digest, 0, 0), "");
    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::collectedByNameWithValue<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
            CommonMetricNames::COUNTER_NUM_BLOBS_READ_FROM_LOCAL,
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue(1)));
}

TEST_F(LocalCasFixture, ReadEmptyBlobWithPositiveOffsetAndZeroLength)
{
    const auto digest = DigestGenerator::hash("");
    cas.writeBlob(digest, "");

    ASSERT_THROW(cas.readBlob(digest, 1, 0), std::out_of_range);
    ASSERT_FALSE(buildboxcommon::buildboxcommonmetrics::allCollectedByName<
                 buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
        {CommonMetricNames::COUNTER_NUM_BLOBS_READ_FROM_LOCAL,
         CommonMetricNames::COUNTER_NAME_BYTES_READ}));
}

TEST_F(LocalCasFixture, ReadEmptyBlobWithPositiveOffsetAndNposLength)
{
    const auto digest = DigestGenerator::hash("");
    cas.writeBlob(digest, "");

    ASSERT_THROW(cas.readBlob(digest, 1, LocalCas::npos), std::out_of_range);
}

TEST_F(LocalCasFixture, EmptyRead)
{
    const auto data = "alpha";
    const auto digest = DigestGenerator::hash(data);
    cas.writeBlob(digest, data);

    ASSERT_EQ(*cas.readBlob(digest, 1, 0), "");
    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::collectedByNameWithValue<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
            CommonMetricNames::COUNTER_NUM_BLOBS_READ_FROM_LOCAL,
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue(1)));
}

TEST_F(LocalCasFixture, PartialRead)
{
    const auto data = "alpha bravo charlie";
    const auto digest = DigestGenerator::hash(data);
    cas.writeBlob(digest, data);

    ASSERT_EQ(*cas.readBlob(digest, 6, 5), "bravo");
    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::allCollectedByNameWithValues<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
            {{CommonMetricNames::COUNTER_NUM_BLOBS_READ_FROM_LOCAL,
              buildboxcommon::buildboxcommonmetrics::CountingMetricValue(1)},
             {CommonMetricNames::COUNTER_NAME_BYTES_READ,
              buildboxcommon::buildboxcommonmetrics::CountingMetricValue(
                  5)}}));
}

TEST_F(LocalCasFixture, InvalidOffsetThrows)
{
    const auto data = "123";
    const auto digest = DigestGenerator::hash(data);
    cas.writeBlob(digest, data);

    ASSERT_THROW(cas.readBlob(digest, 4, 1), std::out_of_range);
}

TEST_F(LocalCasFixture, ReadingUntilEndFromDataSize)
{
    const std::string data = "abcde";
    const auto digest = DigestGenerator::hash(data);
    cas.writeBlob(digest, data);

    ASSERT_EQ(*cas.readBlob(digest, data.size(), LocalCas::npos), "");
}

TEST_F(LocalCasFixture, ExcessiveLengthThrows)
{
    const std::string data = "abcde";
    const auto digest = DigestGenerator::hash(data);
    cas.writeBlob(digest, data);

    ASSERT_THROW(cas.readBlob(digest, 1, static_cast<size_t>(data.size())),
                 std::out_of_range);
    ASSERT_FALSE(buildboxcommon::buildboxcommonmetrics::allCollectedByName<
                 buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
        {CommonMetricNames::COUNTER_NUM_BLOBS_READ_FROM_LOCAL,
         CommonMetricNames::COUNTER_NAME_BYTES_READ}));
}

TEST_F(LocalCasFixture, NposBytesReadsUntilEnd)
{

    const auto data = "-abcdefghi";
    const auto digest = DigestGenerator::hash(data);
    cas.writeBlob(digest, data);

    // Do it twice to ensure the counter works
    ASSERT_EQ(*cas.readBlob(digest, 1, LocalCas::npos), "abcdefghi");
    ASSERT_EQ(*cas.readBlob(digest, 1, LocalCas::npos), "abcdefghi");
    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::allCollectedByNameWithValues<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
            {{CommonMetricNames::COUNTER_NUM_BLOBS_READ_FROM_LOCAL,
              buildboxcommon::buildboxcommonmetrics::CountingMetricValue(2)},
             {CommonMetricNames::COUNTER_NAME_BYTES_READ,
              buildboxcommon::buildboxcommonmetrics::CountingMetricValue(
                  18)}}));
}

TEST_F(LocalCasFixture, TestWriteOutputs)
{
    const auto data = "data123";
    const auto digest = DigestGenerator::hash(data);
    ASSERT_FALSE(cas.hasBlob(digest));

    cas.writeBlob(digest, data);

    // Checking that the physical file is where we expect it:
    const auto cas_objects_path =
        std::string(cas_root_directory.name()) + "/cas/objects";
    const auto parent_subdirectory_name = digest.hash().substr(0, 2);
    const auto filename = digest.hash().substr(2);

    const auto expected_file_path =
        cas_objects_path + "/" + parent_subdirectory_name + "/" + filename;

    // File exists:
    ASSERT_EQ(access(expected_file_path.c_str(), F_OK), 0);

    // And the data stored in it is what we wrote:
    std::ifstream file(expected_file_path);
    ASSERT_TRUE(file.good());

    std::stringstream file_contents;
    file_contents << file.rdbuf();
    ASSERT_TRUE(file_contents.good());

    ASSERT_EQ(file_contents.str(), data);

    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::allCollectedByNameWithValues<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>({
            {CommonMetricNames::COUNTER_NUM_BLOBS_WRITTEN_TO_LOCAL,
             buildboxcommon::buildboxcommonmetrics::CountingMetricValue(1)},
            {CommonMetricNames::COUNTER_NAME_BYTES_WRITE,
             buildboxcommon::buildboxcommonmetrics::CountingMetricValue(
                 digest.size_bytes())},
        }));
}

TEST_F(LocalCasFixture, GetStoragePath)
{
    const auto cas_objects_path =
        std::string(cas_root_directory.name()) + "/cas";
    ASSERT_EQ(cas.path(), cas_objects_path);
};

TEST_F(LocalCasFixture, GetPath)
{
    const auto data = "data";
    const auto digest = DigestGenerator::hash(data);
    cas.writeBlob(digest, data);

    const size_t hash_prefix_length =
        static_cast<size_t>(cas.pathHashPrefixLength());

    const std::string expected_path =
        std::string(cas_root_directory.name()) + "/cas/objects/" +
        digest.hash().substr(0, hash_prefix_length) + "/" +
        digest.hash().substr(hash_prefix_length, std::string::npos);

    ASSERT_EQ(cas.path(digest), expected_path);
    ASSERT_EQ(cas.pathUnchecked(digest), expected_path);
}

TEST_F(LocalCasFixture, GetPathForBlobNotPresentThrows)
{
    const auto digest =
        DigestGenerator::hash("Data not present in the LocalCAS.");
    ASSERT_FALSE(cas.hasBlob(digest));
    ASSERT_THROW(cas.path(digest), BlobNotFoundException);
    ASSERT_NO_THROW(cas.pathUnchecked(digest));
}

TEST_F(LocalCasFixture, TempDirectoryInsideStagingDir)
{
    const auto temp_dir = cas.createStagingDirectory();

    const auto temp_objects_path =
        std::string(cas_root_directory.name()) + "/cas/staging/cas-tmp";

    ASSERT_EQ(
        std::string(temp_dir.name()).substr(0, temp_objects_path.length()),
        temp_objects_path);
}

TEST_F(LocalCasFixture, TempFileInsideTmpDir)
{
    const auto temp_file = cas.createTemporaryFile();

    const auto temp_objects_path =
        std::string(cas_root_directory.name()) + "/cas/tmp/cas-tmp";

    ASSERT_EQ(
        std::string(temp_file.name()).substr(0, temp_objects_path.length()),
        temp_objects_path);
}

TEST_F(LocalCasFixture, MoveBlob)
{
    // Creating a temporary file inside the CAS temp directory and writing
    // some data to it:
    auto temp_file = cas.createTemporaryFile();
    const auto tempPath = temp_file.strname();
    const std::string data = "Hello. This is some data.";
    const Digest digest = DigestGenerator::hash(data);
    writeToFile(data, tempPath);

    ASSERT_FALSE(cas.hasBlob(digest));
    // We'll now move the file into the LocalCAS. (Since the file isn't
    // there, the move is performed and `moveBlobFromTemporaryFile()` returns
    // `true`.)
    ASSERT_TRUE(cas.moveBlobFromTemporaryFile(digest, std::move(temp_file)));

    // The data is correct:
    ASSERT_TRUE(cas.hasBlob(digest));
    ASSERT_EQ(*cas.readBlob(digest), data);

    // Being a move, we expect the the source file to be deleted:
    ASSERT_FALSE(buildboxcommon::FileUtils::isRegularFile(tempPath.c_str()));

    // The operation updates metrics about the write:
    const std::vector<
        std::pair<std::string,
                  buildboxcommon::buildboxcommonmetrics::CountingMetricValue>>
        expected_metrics = {
            {CommonMetricNames::COUNTER_NUM_BLOBS_WRITTEN_TO_LOCAL,
             buildboxcommon::buildboxcommonmetrics::CountingMetricValue(1)},
            {CommonMetricNames::COUNTER_NAME_BYTES_WRITE,
             buildboxcommon::buildboxcommonmetrics::CountingMetricValue(
                 digest.size_bytes())},
        };

    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::allCollectedByNameWithValues<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
            expected_metrics));
}

TEST_F(LocalCasFixture, MoveBlobFinalFilePermissions)
{
    // Creating a temporary file inside the CAS temp directory and writing
    // some data to it:
    auto temp_file = cas.createTemporaryFile();
    const std::string data = "Hello. This is some data.";
    const Digest digest = DigestGenerator::hash(data);
    writeToFile(data, temp_file.strname());

    ASSERT_FALSE(cas.hasBlob(digest));

    // Moving it into the LocalCAS:
    ASSERT_TRUE(cas.moveBlobFromTemporaryFile(digest, std::move(temp_file)));
    ASSERT_TRUE(cas.hasBlob(digest));

    assertFilePermissionsInCasAreValid(digest);
}

TEST_F(LocalCasFixture, MoveExistingBlob)
{
    // Creating a temporary file inside the CAS temp directory and writing
    // some data to it:
    auto temp_file = cas.createTemporaryFile();
    const auto tempPath = temp_file.strname();
    const std::string data = "Hello. This is some data.";
    const Digest digest = DigestGenerator::hash(data);
    writeToFile(data, tempPath);

    // We want the blob to be present before the move:
    cas.writeBlob(digest, data);
    ASSERT_TRUE(cas.hasBlob(digest));

    // Now we'll move the file into the LocalCAS. (Since the blob is
    // already in the CAS, `moveBlobFromTemporaryFile()` returns `false`.)
    ASSERT_FALSE(cas.moveBlobFromTemporaryFile(digest, std::move(temp_file)));

    // The move is still performed, and the source file deleted:
    ASSERT_FALSE(buildboxcommon::FileUtils::isRegularFile(tempPath.c_str()));

    // The data is correct:
    ASSERT_TRUE(cas.hasBlob(digest));
    ASSERT_EQ(*cas.readBlob(digest), data);
}

TEST_F(LocalCasFixture, MoveBlobWithPathOutsideCASTempDir)
{
    // Creating an arbitrary temporary file in a directory that is not the
    // CAS temp directory and writing some data to it:
    buildboxcommon::TemporaryFile temp_file;
    const auto tempPath = temp_file.strname();
    const std::string data = "Hello. This is some data.";
    const Digest digest = DigestGenerator::hash(data);
    writeToFile(data, tempPath);

    // This is not allowed by `moveBlobFromTemporaryFile()`, which expects a
    // file created with `cas.createTemporaryFile()`:
    ASSERT_THROW(cas.moveBlobFromTemporaryFile(digest, std::move(temp_file)),
                 std::invalid_argument);
}

TEST_F(LocalCasFixture, MoveExternalFileNotAllowedThrows)
{
    // Creating an arbitrary temporary file in a directory that is not the
    // CAS temp directory and writing some data to it:
    buildboxcommon::TemporaryFile temp_file;
    const std::string data = "Data.";
    const Digest digest = DigestGenerator::hash(data);
    writeToFile(data, temp_file.name());

    ASSERT_FALSE(cas.externalFileMovesAllowed());
    ASSERT_THROW(
        cas.moveBlobFromExternalFile(digest, AT_FDCWD, temp_file.name()),
        std::invalid_argument);

    // Since the move failed, the source file is still there:
    ASSERT_TRUE(buildboxcommon::FileUtils::isRegularFile(temp_file.name()));
}

TEST(LocalCasMoveFilesTest, MoveBlobFromExternalFile)
{
    buildboxcommon::TemporaryDirectory cas_root;
    FsLocalCas cas(cas_root.name(), true);
    ASSERT_TRUE(cas.externalFileMovesAllowed());

    // Creating a file in a directory that is not the CAS temp directory and
    // writing some data to it:
    buildboxcommon::TemporaryFile temp_file;
    const std::string data = "Hello. This is some data to add to CAS.";
    const Digest digest = DigestGenerator::hash(data);
    writeToFile(data, temp_file.name());

    ASSERT_TRUE(
        cas.moveBlobFromExternalFile(digest, AT_FDCWD, temp_file.name()));

    ASSERT_TRUE(cas.hasBlob(digest));
    ASSERT_EQ(*cas.readBlob(digest), data);

    assertFilePermissionsMatch(cas.path(digest),
                               EXPECTED_CAS_FILE_PERMISSIONS);

    ASSERT_FALSE(buildboxcommon::FileUtils::isRegularFile(temp_file.name()));
}

TEST(LocalCasMoveFilesTest, MoveExistingBlobFromExternalFile)
{
    buildboxcommon::TemporaryDirectory cas_root;
    FsLocalCas cas(cas_root.name(), true);
    ASSERT_TRUE(cas.externalFileMovesAllowed());

    // Pre-inserting a blob:
    const std::string data = "Hello. This is some data to add to CAS.";
    const Digest digest = DigestGenerator::hash(data);

    cas.writeBlob(digest, data);
    ASSERT_TRUE(cas.hasBlob(digest));

    // Now we insert the same blob from a file:
    buildboxcommon::TemporaryFile temp_file;
    writeToFile(data, temp_file.name());

    ASSERT_TRUE(
        cas.moveBlobFromExternalFile(digest, AT_FDCWD, temp_file.name()));

    // But still, the source file was deleted:
    ASSERT_FALSE(buildboxcommon::FileUtils::isRegularFile(temp_file.name()));
}

TEST_F(LocalCasFixture, DeleteBlob)
{
    const std::string data = "Blob's content.";
    const Digest digest = DigestGenerator::hash(data);

    cas.writeBlob(digest, data);
    const std::string path = cas.path(digest);

    ASSERT_TRUE(buildboxcommon::FileUtils::isRegularFile(path.c_str()));
    ASSERT_TRUE(cas.deleteBlob(digest));
    ASSERT_FALSE(buildboxcommon::FileUtils::isRegularFile(path.c_str()));
}

TEST_F(LocalCasFixture, DeleteBlobWithOlderMtime)
{
    const std::string data = "Blob's content.";
    const Digest digest = DigestGenerator::hash(data);

    cas.writeBlob(digest, data);
    const std::string path = cas.path(digest);
    ASSERT_TRUE(buildboxcommon::FileUtils::isRegularFile(path.c_str()));

    sleep(1); // Makes sure that we get a different timestamp.
    const blob_time_type time_now = std::chrono::system_clock::now();
    ASSERT_TRUE(cas.deleteBlob(digest, time_now));
    ASSERT_FALSE(buildboxcommon::FileUtils::isRegularFile(path.c_str()));
}

TEST_F(LocalCasFixture, DeleteBlobWithMatchingMtime)
{
    const std::string data = "Blob's content.";
    const Digest digest = DigestGenerator::hash(data);

    cas.writeBlob(digest, data);
    const std::string path = cas.path(digest);
    ASSERT_TRUE(buildboxcommon::FileUtils::isRegularFile(path.c_str()));

    struct stat st = {};
    ASSERT_EQ(stat(path.c_str(), &st), 0);
    const blob_time_type blob_mtime =
        std::chrono::system_clock::from_time_t(st.st_mtim.tv_sec) +
        std::chrono::microseconds{st.st_mtim.tv_nsec / 1000};

    ASSERT_TRUE(cas.deleteBlob(digest, blob_mtime));
    ASSERT_FALSE(buildboxcommon::FileUtils::isRegularFile(path.c_str()));
}

TEST_F(LocalCasFixture, DeleteBlobWithMoreRecentMtime)
{
    const std::string data = "Blob's content.";
    const Digest digest = DigestGenerator::hash(data);

    cas.writeBlob(digest, data);
    const std::string path = cas.path(digest);
    ASSERT_TRUE(buildboxcommon::FileUtils::isRegularFile(path.c_str()));

    // Reading the file's actual `mtime` and using a second earlier as the
    // cutting point:
    struct stat st = {};
    ASSERT_EQ(stat(path.c_str(), &st), 0);
    const blob_time_type time_before_last_modification =
        std::chrono::system_clock::from_time_t(st.st_mtim.tv_sec - 1);

    ASSERT_FALSE(cas.deleteBlob(digest, time_before_last_modification));
    ASSERT_TRUE(buildboxcommon::FileUtils::isRegularFile(path.c_str()));
}

TEST_F(LocalCasFixture, TruncatedCopy)
{
    const std::string data = "someData";
    const auto digest = DigestGenerator::hash(data);

    ASSERT_FALSE(cas.hasBlob(digest));

    cas.writeBlob(digest, data);

    // Data was written and is valid:
    ASSERT_TRUE(cas.hasBlob(digest));
    ASSERT_EQ(*cas.readBlob(digest, 0, data.size()), data);

    // Truncate object file to check error handling after system crash
    const std::string path = cas.path(digest);
    ASSERT_EQ(chmod(path.c_str(), 0755), 0);
    ASSERT_EQ(truncate(path.c_str(), 0), 0);

    // Verify that truncation is detected when attempting to copy/reflink
    buildboxcommon::TemporaryFile tempCopy;
    ASSERT_FALSE(cas.copyBlob(digest, AT_FDCWD, tempCopy.name()));
    ASSERT_FALSE(cas.hasBlob(digest));
}

TEST_F(LocalCasFixture, TruncatedRead)
{
    const std::string data = "someData";
    const auto digest = DigestGenerator::hash(data);

    ASSERT_FALSE(cas.hasBlob(digest));

    cas.writeBlob(digest, data);

    // Data was written and is valid:
    ASSERT_TRUE(cas.hasBlob(digest));
    ASSERT_EQ(*cas.readBlob(digest, 0, data.size()), data);

    // Truncate object file to check error handling after system crash
    const std::string path = cas.path(digest);
    ASSERT_EQ(chmod(path.c_str(), 0755), 0);
    ASSERT_EQ(truncate(path.c_str(), 0), 0);

    // Verify that truncation is detected when attempting to read
    ASSERT_EQ(cas.readBlob(digest, 0, data.size()).get(), nullptr);
    ASSERT_FALSE(cas.hasBlob(digest));
}

TEST_F(LocalCasFixture, CopyNonExisting)
{
    const std::string data = "someData";
    const auto digest = DigestGenerator::hash(data);

    ASSERT_FALSE(cas.hasBlob(digest));

    // Verify that copyBlob returns false if the blob is not found
    buildboxcommon::TemporaryFile tempCopy;
    ASSERT_FALSE(cas.copyBlob(digest, AT_FDCWD, tempCopy.name()));
}

TEST(LocalCasMetadataFileTests, DiskUsageMetadataFileIsCreated)
{
    buildboxcommon::TemporaryDirectory cas_root_directory;
    const std::string data = "Sample blob to take up some space.";

    {
        FsLocalCas cas(cas_root_directory.name());
        cas.writeBlob(DigestGenerator::hash(data), data);
    }

    // The file was created:
    const auto metadata_file_path =
        std::string(cas_root_directory.name()) + "/cas/disk_usage";

    ASSERT_TRUE(
        buildboxcommon::FileUtils::isRegularFile(metadata_file_path.c_str()));

    // And it contains the disk usage at the moment of destruction:
    const std::string disk_usage_string =
        buildboxcommon::FileUtils::getFileContents(metadata_file_path.c_str());
    const long disk_usage = std::stol(disk_usage_string);

    ASSERT_EQ(disk_usage, data.size());
}

TEST(LocalCasMetadataFileTests, DiskUsageMetadataFileIsRead)
{
    buildboxcommon::TemporaryDirectory cas_root_directory;
    const std::string cas_root_path = cas_root_directory.name();
    const std::string cas_subdirectory_path = cas_root_path + "/cas";

    const std::string disk_usage_metadata_file_path =
        cas_subdirectory_path + "/disk_usage";

    // Manually creating a valid disk-usage metadata file:
    ASSERT_NO_THROW(buildboxcommon::FileUtils::createDirectory(
        cas_subdirectory_path.c_str()));

    const int64_t phony_disk_usage = 2048;
    ASSERT_NO_THROW(buildboxcommon::FileUtils::writeFileAtomically(
        disk_usage_metadata_file_path, std::to_string(phony_disk_usage)));

    // The new CAS in reports the value that it read from the file:
    FsLocalCas cas(cas_root_directory.name());
    ASSERT_EQ(cas.getDiskUsage(), phony_disk_usage);
}

TEST(LocalCasMetadataFileTests, DiskUsageMetadataInvalidFileIsIgnored)
{
    buildboxcommon::TemporaryDirectory cas_root_directory;
    const std::string cas_root_path = cas_root_directory.name();

    const std::string blob = "Sample blob to take up some space.";

    {
        FsLocalCas cas(cas_root_directory.name());
        cas.writeBlob(DigestGenerator::hash(blob), blob);
    }

    // Overwriting the disk-usage metadata file with a valid value:
    const auto disk_usage_metadata_file_path =
        cas_root_path + "/cas/disk_usage";

    const std::string contents = "This is not a valid disk usage value!";
    ASSERT_NO_THROW(buildboxcommon::FileUtils::writeFileAtomically(
        disk_usage_metadata_file_path, contents));

    // The new CAS instance ignores the file and computes the disk usage:
    {
        FsLocalCas cas(cas_root_directory.name());
        ASSERT_EQ(cas.getDiskUsage(), blob.size());
    }
}

TEST(LocalCasMetadataFileTests, DiskUsageMetadataFileIsRemovedOnFirstWrite)
{
    buildboxcommon::TemporaryDirectory cas_root_directory;
    const std::string cas_root_path = cas_root_directory.name();

    {
        FsLocalCas cas(cas_root_directory.name());
    }

    const std::string disk_usage_metadata_file_path =
        cas_root_path + "/cas/disk_usage";

    {
        FsLocalCas cas(cas_root_directory.name());

        ASSERT_TRUE(buildboxcommon::FileUtils::isRegularFile(
            disk_usage_metadata_file_path.c_str()));

        ASSERT_TRUE(cas.writeBlob(DigestGenerator::hash("data"), "data"));

        ASSERT_FALSE(buildboxcommon::FileUtils::isRegularFile(
            disk_usage_metadata_file_path.c_str()));
    }
}

TEST(LocalCasMetadataFileTests, DiskUsageMetadataFileIsRemovedOnFirstMove)
{
    buildboxcommon::TemporaryDirectory cas_root_directory;
    const std::string cas_root_path = cas_root_directory.name();

    const std::string data = "Blob content";
    const Digest digest = DigestGenerator::hash(data);

    {
        FsLocalCas cas(cas_root_directory.name());
    }

    const std::string disk_usage_metadata_file_path =
        cas_root_path + "/cas/disk_usage";

    {
        FsLocalCas cas(cas_root_directory.name());

        auto blob_file = cas.createTemporaryFile();
        std::ofstream blob_stream(blob_file.name(), std::ofstream::binary);
        blob_stream << data;
        blob_stream.close();

        ASSERT_TRUE(buildboxcommon::FileUtils::isRegularFile(
            disk_usage_metadata_file_path.c_str()));

        ASSERT_TRUE(
            cas.moveBlobFromTemporaryFile(digest, std::move(blob_file)));

        ASSERT_FALSE(buildboxcommon::FileUtils::isRegularFile(
            disk_usage_metadata_file_path.c_str()));
    }
}

TEST(LocalCasMetadataFileTests, DiskUsageMetadataFileIsRemovedOnFirstDelete)
{
    buildboxcommon::TemporaryDirectory cas_root_directory;
    const std::string cas_root_path = cas_root_directory.name();

    const std::string data = "Blob content";
    const Digest digest = DigestGenerator::hash(data);

    {
        FsLocalCas cas(cas_root_directory.name());
        ASSERT_TRUE(cas.writeBlob(digest, data));
    }

    const std::string disk_usage_metadata_file_path =
        cas_root_path + "/cas/disk_usage";

    {
        FsLocalCas cas(cas_root_directory.name());

        ASSERT_TRUE(buildboxcommon::FileUtils::isRegularFile(
            disk_usage_metadata_file_path.c_str()));

        ASSERT_TRUE(cas.deleteBlob(digest));

        ASSERT_FALSE(buildboxcommon::FileUtils::isRegularFile(
            disk_usage_metadata_file_path.c_str()));
    }
}
