/*
 * Copyright 2025 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 <buildboxcasd_metricnames.h>
#include <buildboxcasd_server.h>
#include <cstdlib>
#include <map>
#include <memory>
#include <string>

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

#include <gtest/gtest.h>

#include <buildboxcommon_digestgenerator.h>

#include <buildboxcasd_fslocalactionstorage.h>
#include <buildboxcommon_fslocalcas.h>

using namespace buildboxcasd;
using namespace buildboxcommon;
using namespace buildboxcommon::buildboxcommonmetrics;

const std::string TEST_INSTANCE_NAME = "testInstances/instance1";

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

void generateAndStoreFiles(std::map<std::string, std::string> *fileContents,
                           std::map<std::string, Digest> *fileDigests,
                           const std::shared_ptr<LocalCas> &storage)
{
    (*fileContents)["file1.c"] = "file1: [...data...]";
    (*fileDigests)["file1.c"] =
        DigestGenerator::hash((*fileContents)["file1.c"]);

    storage->writeBlob((*fileDigests)["file1.c"], (*fileContents)["file1.c"]);
}

static Digest
createDirectoryTree(const std::map<std::string, Digest> &fileDigests,
                    const std::shared_ptr<LocalCas> &storage)
{

    Directory rootDirectory;

    FileNode *f1 = rootDirectory.add_files();
    f1->set_name("file1.c");
    f1->mutable_digest()->CopyFrom(fileDigests.at("file1.c"));

    const auto serializedRootDirectory = rootDirectory.SerializeAsString();
    auto rootDirectoryDigest = DigestGenerator::hash(serializedRootDirectory);

    storage->writeBlob(rootDirectoryDigest, serializedRootDirectory);

    return rootDirectoryDigest;
}

class NestedServerFixture : public ::testing::Test {
  protected:
    NestedServerFixture()
        : TEST_SERVER_ADDRESS("unix://" + std::string(socketDirectory.name()) +
                              "/casd.sock"),
          storage(std::make_shared<FsLocalCas>(storageRootDirectory.name())),
          assetStorage(std::make_shared<FsLocalAssetStorage>(
              storageRootDirectory.name())),
          actionStorage(std::make_shared<FsLocalActionStorage>(
              storageRootDirectory.name())),
          server(
              std::make_shared<Server>(storage, assetStorage, actionStorage))
    {
        generateAndStoreFiles(&fileContents, &fileDigests, storage);
        rootDirectoryDigest = createDirectoryTree(fileDigests, storage);

        // Building and starting a server:
        server->addLocalServerInstance(TEST_INSTANCE_NAME);
        server->addListeningPort(TEST_SERVER_ADDRESS);
        server->start();

        buildboxcommon::ConnectionOptions connectionOptions;
        connectionOptions.setUrl(TEST_SERVER_ADDRESS);
        connectionOptions.setInstanceName(TEST_INSTANCE_NAME);

        auto grpcClient = std::make_shared<GrpcClient>();
        grpcClient->init(connectionOptions);
        casClient = std::make_unique<CASClient>(grpcClient);
        casClient->init();

        buildboxcommon::RemoteApisSocketConfig remoteApisSocket;
        remoteApisSocket.set_instance_name(TEST_INSTANCE_NAME);
        remoteApisSocket.set_path("reapi.sock");

        stagedDirectory = casClient->stage(
            rootDirectoryDigest, StageTreeRequest_StagingMode_COPY_OR_LINK, "",
            SystemUtils::getProcessCredentials, {}, remoteApisSocket);

        buildboxcommon::ConnectionOptions nestedConnectionOptions;
        nestedConnectionOptions.setUrl("unix://" + stagedDirectory->path() +
                                       "/reapi.sock");

        auto nestedGrpcClient = std::make_shared<GrpcClient>();
        nestedGrpcClient->init(nestedConnectionOptions);
        nestedCasClient = std::make_unique<CASClient>(nestedGrpcClient);
        nestedCasClient->init();

        buildboxcommon::buildboxcommonmetrics::clearAllMetricCollection();
    }

    buildboxcommon::TemporaryDirectory socketDirectory;
    const std::string TEST_SERVER_ADDRESS;

    buildboxcommon::TemporaryDirectory storageRootDirectory;
    std::shared_ptr<FsLocalCas> storage;
    std::shared_ptr<FsLocalAssetStorage> assetStorage;
    std::shared_ptr<FsLocalActionStorage> actionStorage;
    std::shared_ptr<Server> server;

    Digest rootDirectoryDigest;

    std::map<std::string, Digest> fileDigests;
    std::map<std::string, std::string> fileContents;

    std::unique_ptr<CASClient::StagedDirectory> stagedDirectory;

    std::unique_ptr<CASClient> casClient;
    std::unique_ptr<CASClient> nestedCasClient;
};

TEST_F(NestedServerFixture, CaptureUnmodifiedInputFile)
{
    auto responses = nestedCasClient->captureFiles(
        {"/file1.c"}, {}, false /* bypass_local_cache */);
    EXPECT_EQ(responses.size(), 1);

    EXPECT_TRUE(
        buildboxcommon::buildboxcommonmetrics::collectedByNameWithValue<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
            MetricNames::COUNTER_NAME_LOCAL_CAS_INODE_CACHE_HITS,
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue(1)));
}

TEST_F(NestedServerFixture, CaptureModifiedFile)
{
    // Touch file to invalidate inode cache entry
    buildboxcommon::FileUtils::setFileTimes(
        (stagedDirectory->path() + "/file1.c").c_str(), std::nullopt,
        std::nullopt);

    auto responses = nestedCasClient->captureFiles(
        {"/file1.c"}, {}, false /* bypass_local_cache */);
    EXPECT_EQ(responses.size(), 1);

    EXPECT_TRUE(
        buildboxcommon::buildboxcommonmetrics::collectedByNameWithValue<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
            MetricNames::COUNTER_NAME_LOCAL_CAS_INODE_CACHE_MISSES,
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue(1)));
}

TEST_F(NestedServerFixture, CaptureModifiedFileTwice)
{
    // Touch file to invalidate inode cache entry
    buildboxcommon::FileUtils::setFileTimes(
        (stagedDirectory->path() + "/file1.c").c_str(), std::nullopt,
        std::nullopt);

    auto responses = nestedCasClient->captureFiles(
        {"/file1.c"}, {}, false /* bypass_local_cache */);
    EXPECT_EQ(responses.size(), 1);

    auto responses2 = nestedCasClient->captureFiles(
        {"/file1.c"}, {}, false /* bypass_local_cache */);
    EXPECT_EQ(responses2.size(), 1);

    // 1 miss and 1 hit
    EXPECT_TRUE(
        buildboxcommon::buildboxcommonmetrics::allCollectedByNameWithValues<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>({
            {MetricNames::COUNTER_NAME_LOCAL_CAS_INODE_CACHE_HITS,
             buildboxcommon::buildboxcommonmetrics::CountingMetricValue(1)},
            {MetricNames::COUNTER_NAME_LOCAL_CAS_INODE_CACHE_MISSES,
             buildboxcommon::buildboxcommonmetrics::CountingMetricValue(1)},
        }));
}

TEST_F(NestedServerFixture, HashUnmodifiedInputFile)
{
    auto responses = nestedCasClient->hashFiles({"/file1.c"}, {});
    EXPECT_EQ(responses.size(), 1);

    EXPECT_TRUE(
        buildboxcommon::buildboxcommonmetrics::collectedByNameWithValue<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
            MetricNames::COUNTER_NAME_LOCAL_CAS_INODE_CACHE_HITS,
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue(1)));
}

TEST_F(NestedServerFixture, HashModifiedFile)
{
    // Touch file to invalidate inode cache entry
    buildboxcommon::FileUtils::setFileTimes(
        (stagedDirectory->path() + "/file1.c").c_str(), std::nullopt,
        std::nullopt);

    auto responses = nestedCasClient->hashFiles({"/file1.c"}, {});
    EXPECT_EQ(responses.size(), 1);

    EXPECT_TRUE(
        buildboxcommon::buildboxcommonmetrics::collectedByNameWithValue<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
            MetricNames::COUNTER_NAME_LOCAL_CAS_INODE_CACHE_MISSES,
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue(1)));
}

TEST_F(NestedServerFixture, HashModifiedFileTwice)
{
    // Touch file to invalidate inode cache entry
    buildboxcommon::FileUtils::setFileTimes(
        (stagedDirectory->path() + "/file1.c").c_str(), std::nullopt,
        std::nullopt);

    auto responses = nestedCasClient->hashFiles({"/file1.c"}, {});
    EXPECT_EQ(responses.size(), 1);

    auto responses2 = nestedCasClient->hashFiles({"/file1.c"}, {});
    EXPECT_EQ(responses2.size(), 1);

    // 1 miss and 1 hit
    EXPECT_TRUE(
        buildboxcommon::buildboxcommonmetrics::allCollectedByNameWithValues<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>({
            {MetricNames::COUNTER_NAME_LOCAL_CAS_INODE_CACHE_HITS,
             buildboxcommon::buildboxcommonmetrics::CountingMetricValue(1)},
            {MetricNames::COUNTER_NAME_LOCAL_CAS_INODE_CACHE_MISSES,
             buildboxcommon::buildboxcommonmetrics::CountingMetricValue(1)},
        }));
}
