/*
 * 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_lrulocalcas.h>

#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_metricnames.h>
#include <buildboxcommon_temporarydirectory.h>

#include <buildboxcommonmetrics_countingmetricvalue.h>
#include <buildboxcommonmetrics_durationmetricvalue.h>
#include <buildboxcommonmetrics_gaugemetricvalue.h>
#include <buildboxcommonmetrics_testingutils.h>

#include <fstream>
#include <unistd.h>

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

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

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

using namespace buildboxcommon;
using namespace buildboxcommon::buildboxcommonmetrics;
using buildboxcommon::DigestGenerator;

static const int LRU_QUOTA_HIGH = 4096;
static const double LRU_QUOTA_LOW_RATIO = 0.5;

class LruLocalCasFixture : public ::testing::Test {
  protected:
    LruLocalCasFixture()
        : cas(std::make_unique<FsLocalCas>(cas_root_directory.name()),
              LRU_QUOTA_LOW_RATIO, LRU_QUOTA_HIGH, 0)
    {
    }

    buildboxcommon::TemporaryDirectory cas_root_directory;
    LruLocalCas cas;

    typedef std::vector<std::pair<std::string, GaugeMetricValue>>
        MetricValPairs;
    MetricValPairs getLruMetricPairs()
    {
        return {{CommonMetricNames::GAUGE_NAME_DISK_USAGE,
                 GaugeMetricValue(cas.getDiskUsage())},
                {CommonMetricNames::GAUGE_NAME_LRU_EFFECTIVE_HIGH_WATERMARK,
                 GaugeMetricValue(cas.getDiskQuota().value_or(0))},
                {CommonMetricNames::GAUGE_NAME_LRU_EFFECTIVE_LOW_WATERMARK,
                 GaugeMetricValue(
                     static_cast<GaugeMetricValue::GaugeMetricNumericType>(
                         LRU_QUOTA_HIGH * LRU_QUOTA_LOW_RATIO))},
                {CommonMetricNames::GAUGE_NAME_LRU_CONFIGURED_HIGH_WATERMARK,
                 GaugeMetricValue(LRU_QUOTA_HIGH)},
                {CommonMetricNames::GAUGE_NAME_LRU_LOW_WATERMARK_PERCENTAGE,
                 GaugeMetricValue(
                     static_cast<GaugeMetricValue::GaugeMetricNumericType>(
                         LRU_QUOTA_LOW_RATIO * 100))}};
    }
};

TEST_F(LruLocalCasFixture, InitialDiskUsagePublished)
{
    ASSERT_TRUE(
        allCollectedByNameWithValues<GaugeMetricValue>(getLruMetricPairs()));
}

// Test identical to FsLocalCas test
TEST_F(LruLocalCasFixture, TestWriteAndRead)
{
    const std::string data = "someData";
    const auto digest = DigestGenerator::hash(data);

    ASSERT_FALSE(cas.hasBlob(digest));

    const auto previous_disk_usage = cas.getDiskUsage();
    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);

    const auto current_disk_usage = cas.getDiskUsage();
    ASSERT_EQ(current_disk_usage, previous_disk_usage + digest.size_bytes());

    // Metric issued:
    ASSERT_TRUE(
        allCollectedByNameWithValues<GaugeMetricValue>(getLruMetricPairs()));
}

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

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

    ASSERT_EQ(cas.path(digest), cas.getBase()->path(digest));
}

TEST_F(LruLocalCasFixture, ExternalFileMovesDisabledByDefault)
{
    ASSERT_FALSE(cas.externalFileMovesAllowed());
}

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

    ASSERT_FALSE(cas.hasBlob(digest));

    // Write blob
    cas.writeBlob(digest, data);
    ASSERT_TRUE(cas.hasBlob(digest));

    // Delete blob and verify it has been deleted
    ASSERT_TRUE(cas.deleteBlob(digest));
    ASSERT_FALSE(cas.hasBlob(digest));

    // Try deleting it again
    ASSERT_FALSE(cas.deleteBlob(digest));

    // Metric issued:
    ASSERT_TRUE(
        allCollectedByNameWithValues<GaugeMetricValue>(getLruMetricPairs()));
}

TEST_F(LruLocalCasFixture, EmptyBlobIsNotDeleted)
{
    const Digest empty_blob_digest = DigestGenerator::hash("");

    ASSERT_TRUE(cas.hasBlob(empty_blob_digest));

    ASSERT_FALSE(cas.deleteBlob(empty_blob_digest));

    ASSERT_TRUE(cas.hasBlob(empty_blob_digest));
}

TEST_F(LruLocalCasFixture, Expire)
{
    auto digests = std::vector<Digest>(6);

    for (size_t i = 0; i < digests.size(); i++) {
        const auto data = std::string(1000, static_cast<char>('0' + i));
        digests[i] = DigestGenerator::hash(data);
        cas.writeBlob(digests[i], data);

        // Wait 1 second between writes to get different timestamps.
        sleep(1);
    }

    for (size_t i = 0; i < digests.size(); i++) {
        // Writing the fifth 1000 Byte blob should
        // expire the first three 1000 Byte blobs.
        if (i < 3) {
            ASSERT_FALSE(cas.hasBlob(digests[i]));
        }
        else {
            ASSERT_TRUE(cas.hasBlob(digests[i]));
        }
    }

    // Metrics issued:
    ASSERT_TRUE(collectedByName<DurationMetricValue>(
        CommonMetricNames::TIMER_NAME_LRU_CLEANUP));

    ASSERT_TRUE(
        allCollectedByNameWithValues<GaugeMetricValue>(getLruMetricPairs()));
}

TEST_F(LruLocalCasFixture, DiskUsage)
{
    cas.verifyDiskUsage();

    for (int i = 0; i < 10; i++) {
        const auto data = std::string(1000, static_cast<char>('0' + i));
        const auto digest = DigestGenerator::hash(data);
        cas.writeBlob(digest, data);
        cas.verifyDiskUsage();
    }
}

TEST_F(LruLocalCasFixture, DiskUsageDuplicates)
{
    cas.verifyDiskUsage();

    for (int i = 0; i < 20; i++) {
        const auto data = std::string(1000, static_cast<char>('0' + i / 2));
        const auto digest = DigestGenerator::hash(data);
        cas.writeBlob(digest, data);
        cas.verifyDiskUsage();
    }
}

TEST_F(LruLocalCasFixture, MoveBlobFromTmpFileUpdatesDiskUsage)
{
    // 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.name());

    ASSERT_FALSE(cas.hasBlob(digest));
    // We'll now move that file to the CAS.
    // (Since the file isn't there, the move is performed and
    // `moveBlobFromTemporaryFile()` returns `true`.)
    const auto disk_usage_before_move = cas.getDiskUsage();
    ASSERT_TRUE(cas.moveBlobFromTemporaryFile(digest, std::move(temp_file)));

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

    // And the disk usage increased accordingly:
    ASSERT_EQ(cas.getDiskUsage(),
              disk_usage_before_move + static_cast<int64_t>(data.size()));

    // Metric issued:
    ASSERT_TRUE(
        allCollectedByNameWithValues<GaugeMetricValue>(getLruMetricPairs()));
}

TEST_F(LruLocalCasFixture, MoveExistingBlobFromTmpFileDoesNotChangeDiskUsage)
{
    // Metric issued:
    ASSERT_TRUE(
        collectedByNameWithValue(CommonMetricNames::GAUGE_NAME_DISK_USAGE,
                                 GaugeMetricValue(cas.getDiskUsage())));

    // 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.name());

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

    const auto disk_usage_before_move = cas.getDiskUsage();

    // We'll now move that file to the CAS.
    // (Since the file is already stored, `moveBlobFromTemporaryFile()` returns
    // `false`.)
    ASSERT_FALSE(cas.moveBlobFromTemporaryFile(digest, std::move(temp_file)));

    // The disk usage did not change:
    ASSERT_EQ(cas.getDiskUsage(), disk_usage_before_move);
}

TEST(LruLocalCasEnablingMovesTest, MoveBlobFromExternalFileUpdatesDiskUsage)
{
    buildboxcommon::TemporaryDirectory cas_root;
    auto base_cas = std::make_unique<FsLocalCas>(cas_root.name(), true);
    LruLocalCas cas(std::move(base_cas), LRU_QUOTA_LOW_RATIO, LRU_QUOTA_HIGH,
                    0);

    ASSERT_TRUE(cas.externalFileMovesAllowed());

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

    ASSERT_FALSE(cas.hasBlob(digest));
    // We'll now move that file to the CAS.
    // (Since the file isn't there, the move is performed and
    // `moveBlobFromExternalFile()` returns `true`.)
    const auto disk_usage_before_move = cas.getDiskUsage();
    ASSERT_TRUE(
        cas.moveBlobFromExternalFile(digest, AT_FDCWD, temp_file.name()));

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

    // And the disk usage increased accordingly:
    ASSERT_EQ(cas.getDiskUsage(),
              disk_usage_before_move + static_cast<int64_t>(data.size()));
}
