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

#include <fcntl.h>
#include <gmock/gmock.h>

#include <buildboxcommonmetrics_countingmetricvalue.h>
#include <buildboxcommonmetrics_distributionmetricvalue.h>
#include <buildboxcommonmetrics_durationmetricvalue.h>
#include <buildboxcommonmetrics_testingutils.h>
#include <grpcpp/client_context.h>
#include <grpcpp/create_channel.h>
#include <grpcpp/impl/codegen/call.h>
#include <grpcpp/server_context.h>
#include <grpcpp/support/sync_stream.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;
}();

class CasServerFixture : public ::testing::Test {
  protected:
    CasServerFixture()
        : TEST_SERVER_ADDRESS("unix://" +
                              std::string(socket_directory.name()) +
                              "/proxy.sock"),
          storage(std::make_shared<FsLocalCas>(storage_root_directory.name())),
          asset_storage(std::make_shared<FsLocalAssetStorage>(
              storage_root_directory.name())),
          action_storage(std::make_shared<FsLocalActionStorage>(
              storage_root_directory.name())),
          server(std::make_shared<Server>(storage, asset_storage,
                                          action_storage)),
          cas_channel(grpc::CreateChannel(TEST_SERVER_ADDRESS,
                                          grpc::InsecureChannelCredentials())),
          cas_stub(ContentAddressableStorage::NewStub(cas_channel)),
          cas_bytestream_stub(ByteStream::NewStub(cas_channel)),
          capabilities_stub(Capabilities::NewStub(cas_channel))
    {
        // Pre-loading some content in the storage:
        storage->writeBlob(DigestGenerator::hash("data1"), "data1");
        storage->writeBlob(DigestGenerator::hash("data2"), "data2");

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

        cas_servicer = std::dynamic_pointer_cast<CasRemoteExecutionServicer>(
            server->remoteExecutionCasServicer());
    }

    inline std::string readBlob(const Digest &digest) const
    {
        const auto blob_ptr = storage->readBlob(digest);
        if (blob_ptr == nullptr) {
            throw std::invalid_argument("Digest " + toString(digest) +
                                        " is not present in storage.");
        }

        return *blob_ptr;
    }

    void prepareTree(Digest *root_digest_out,
                     Digest *subdirectory_digest_out = nullptr)
    {
        /* Creates the following directory structure:
         *
         * root/                <-- `root_directory_digest_out`
         *  |-- file1.sh*
         *  |-- subdir1/        <-- `subdirectory_digest_out` (optional)
         *           |-- file2.c
         *
         * And returns pointers to the root directory and to the `subdir1/`
         * digests.
         *
         * If `subdirectory_digest_out` is a null pointer, it does not store
         * that subdirectory in the CAS.
         */

        Directory root_directory;

        // Adding a file to it:
        FileNode *f1 = root_directory.add_files();
        f1->set_name("file1.sh");
        f1->set_is_executable(true);
        f1->mutable_digest()->CopyFrom(DigestGenerator::hash("file1Contents"));

        // Creating `subdir1/`:
        Directory subdirectory;

        // Adding `subdir1/file2.c`:
        FileNode *f3 = subdirectory.add_files();
        f3->set_name("file2.c");
        f3->mutable_digest()->CopyFrom(DigestGenerator::hash("file3Contents"));

        // Adding `subdir1/` under `dirA/`:
        DirectoryNode *d1 = root_directory.add_directories();
        d1->set_name("subdir1");

        const auto serialized_subdirectory = subdirectory.SerializeAsString();

        const auto subdirectory_digest =
            DigestGenerator::hash(serialized_subdirectory);
        d1->mutable_digest()->CopyFrom(subdirectory_digest);

        const auto serialized_root_directory =
            root_directory.SerializeAsString();
        const auto root_directory_digest =
            DigestGenerator::hash(serialized_root_directory);

        // Storing the two Directory protos...
        storage->writeBlob(root_directory_digest, serialized_root_directory);

        if (subdirectory_digest_out != nullptr) {
            storage->writeBlob(subdirectory_digest, serialized_subdirectory);
            *subdirectory_digest_out = subdirectory_digest;
        }

        *root_digest_out = root_directory_digest;
    }

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

    buildboxcommon::TemporaryDirectory storage_root_directory;
    std::shared_ptr<FsLocalCas> storage;
    std::shared_ptr<FsLocalAssetStorage> asset_storage;
    std::shared_ptr<FsLocalActionStorage> action_storage;
    std::shared_ptr<Server> server;

    std::shared_ptr<CasRemoteExecutionServicer> cas_servicer;

    grpc::ClientContext client_context;
    grpc::ServerContext server_context;

    std::shared_ptr<grpc::Channel> cas_channel;
    std::unique_ptr<ContentAddressableStorage::Stub> cas_stub;
    std::unique_ptr<ByteStream::Stub> cas_bytestream_stub;

    std::unique_ptr<Capabilities::Stub> capabilities_stub;
};

TEST_F(CasServerFixture, FindMissingBlobs)
{
    FindMissingBlobsRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);

    const auto digest1 = DigestGenerator::hash("data1");
    ASSERT_TRUE(storage->hasBlob(digest1));

    const auto digest3 = DigestGenerator::hash("data3");
    ASSERT_FALSE(storage->hasBlob(digest3));

    const auto r1 = request.add_blob_digests();
    r1->CopyFrom(digest1);
    const auto r2 = request.add_blob_digests();
    r2->CopyFrom(digest3);

    FindMissingBlobsResponse response;
    cas_servicer->FindMissingBlobs(&server_context, &request, &response);

    ASSERT_EQ(response.missing_blob_digests().size(), 1);
    ASSERT_EQ(response.missing_blob_digests(0).size_bytes(),
              digest3.size_bytes());
    ASSERT_EQ(response.missing_blob_digests(0).hash(), digest3.hash());

    ASSERT_TRUE(collectedByName<DurationMetricValue>(
        buildboxcasd::MetricNames::TIMER_NAME_CAS_FIND_MISSING_BLOBS));

    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::collectedByNameWithValue<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
            MetricNames::COUNTER_NUM_BLOBS_FIND_MISSING,
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue(2)));

    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::allCollectedByNameWithValues<
            buildboxcommon::buildboxcommonmetrics::DistributionMetricValue>({
            {MetricNames::DISTRIBUTION_NAME_CAS_FIND_MISSING_BLOBS_SIZES,
             buildboxcommon::buildboxcommonmetrics::DistributionMetricValue(
                 digest1.size_bytes())},
            {MetricNames::DISTRIBUTION_NAME_CAS_FIND_MISSING_BLOBS_SIZES,
             buildboxcommon::buildboxcommonmetrics::DistributionMetricValue(
                 digest3.size_bytes())},
        }));
}

TEST_F(CasServerFixture, BatchUpdateBlobs)
{
    BatchUpdateBlobsRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);

    const auto data4 = "data4";
    const auto data5 = "data5";

    const Digest d4 = DigestGenerator::hash(data4);
    const Digest d5 = DigestGenerator::hash(data5);

    auto entry1 = request.add_requests();
    entry1->mutable_digest()->CopyFrom(d4);
    entry1->set_data(data4);

    auto entry2 = request.add_requests();
    entry2->mutable_digest()->CopyFrom(d5);
    entry2->set_data(data5);

    BatchUpdateBlobsResponse response;
    cas_servicer->BatchUpdateBlobs(&server_context, &request, &response);

    // Response is valid:
    ASSERT_EQ(response.responses().size(), 2);

    std::set<std::string> digests;
    for (const auto &r : response.responses()) {
        ASSERT_EQ(r.status().code(), grpc::StatusCode::OK);
        digests.insert(r.digest().hash());
    }

    ASSERT_EQ(digests.count(d4.hash()), 1);
    ASSERT_EQ(digests.count(d5.hash()), 1);

    // And data was stored:
    ASSERT_TRUE(storage->hasBlob(d4));
    ASSERT_TRUE(storage->hasBlob(d5));

    ASSERT_TRUE(collectedByName<DurationMetricValue>(
        buildboxcasd::MetricNames::TIMER_NAME_CAS_BATCH_UPDATE_BLOBS));

    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::collectedByNameWithValue<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
            MetricNames::COUNTER_NUM_BLOBS_BATCH_UPDATE,
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue(2)));

    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::allCollectedByNameWithValues<
            buildboxcommon::buildboxcommonmetrics::DistributionMetricValue>({
            {MetricNames::DISTRIBUTION_NAME_CAS_BATCH_UPDATE_BLOB_SIZES,
             buildboxcommon::buildboxcommonmetrics::DistributionMetricValue(
                 d4.size_bytes())},
            {MetricNames::DISTRIBUTION_NAME_CAS_BATCH_UPDATE_BLOB_SIZES,
             buildboxcommon::buildboxcommonmetrics::DistributionMetricValue(
                 d5.size_bytes())},
        }));
}

TEST_F(CasServerFixture, BatchReadBlobs)
{
    // We'll request two blobs that we know are in the CAS:
    const auto digest1 = DigestGenerator::hash("data1");
    const auto digest2 = DigestGenerator::hash("data2");

    ASSERT_TRUE(storage->hasBlob(digest1));
    ASSERT_TRUE(storage->hasBlob(digest2));

    std::set<std::string> request_hashes = {digest1.hash(), digest2.hash()};

    BatchReadBlobsRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);
    request.add_digests()->CopyFrom(digest1);
    request.add_digests()->CopyFrom(digest2);
    ASSERT_EQ(request.digests_size(), 2);

    BatchReadBlobsResponse response;
    cas_servicer->BatchReadBlobs(&server_context, &request, &response);
    ASSERT_EQ(response.responses().size(), 2);

    for (const auto &r : response.responses()) {
        ASSERT_EQ(r.status().code(), grpc::StatusCode::OK);
        // The hash is what we asked for:
        ASSERT_EQ(request_hashes.count(r.digest().hash()), 1);
        // The data corresponds to that blob in storage:
        ASSERT_EQ(r.data(), readBlob(r.digest()));
    }

    ASSERT_TRUE(collectedByName<DurationMetricValue>(
        buildboxcasd::MetricNames::TIMER_NAME_CAS_BATCH_READ_BLOBS));

    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::collectedByNameWithValue<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
            MetricNames::COUNTER_NUM_BLOBS_BATCH_READ,
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue(2)));

    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::allCollectedByNameWithValues<
            buildboxcommon::buildboxcommonmetrics::DistributionMetricValue>({
            {MetricNames::DISTRIBUTION_NAME_CAS_BATCH_READ_BLOBS_SIZES,
             buildboxcommon::buildboxcommonmetrics::DistributionMetricValue(
                 digest1.size_bytes())},
            {MetricNames::DISTRIBUTION_NAME_CAS_BATCH_READ_BLOBS_SIZES,
             buildboxcommon::buildboxcommonmetrics::DistributionMetricValue(
                 digest2.size_bytes())},
        }));
}

TEST_F(CasServerFixture, BatchReadBlobsWithMissing)
{
    const auto digest1 = DigestGenerator::hash("data1");
    const auto digest3 = DigestGenerator::hash("data3");

    ASSERT_TRUE(storage->hasBlob(digest1));
    ASSERT_FALSE(storage->hasBlob(digest3));

    BatchReadBlobsRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);
    request.add_digests()->CopyFrom(digest1);
    request.add_digests()->CopyFrom(digest3);

    BatchReadBlobsResponse response;
    cas_servicer->BatchReadBlobs(&server_context, &request, &response);

    ASSERT_EQ(response.responses().size(), request.digests_size());

    for (const auto &r : response.responses()) {
        if (r.digest().hash() == digest1.hash()) {
            ASSERT_EQ(r.status().code(), grpc::StatusCode::OK);
            ASSERT_EQ(r.data(), readBlob(r.digest()));
        }
        else if (r.digest().hash() == digest3.hash()) {
            ASSERT_EQ(r.status().code(), grpc::StatusCode::NOT_FOUND);
        }
        else {
            FAIL() << "Digest in response was not requested.";
        }
    }
    ASSERT_TRUE(collectedByName<DurationMetricValue>(
        buildboxcasd::MetricNames::TIMER_NAME_CAS_BATCH_READ_BLOBS));

    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::collectedByNameWithValue<
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue>(
            MetricNames::COUNTER_NUM_BLOBS_BATCH_READ,
            buildboxcommon::buildboxcommonmetrics::CountingMetricValue(2)));
}

TEST_F(CasServerFixture, BatchReadBlobsSizedExceeded)
{
    // Trying to fetch a blob that exceeds the gRPC message limit returns an
    // invalid-argument error:
    const std::string big_blob(GRPC_DEFAULT_MAX_RECV_MESSAGE_LENGTH, 'X');
    auto digest = DigestGenerator::hash(big_blob);

    BatchReadBlobsRequest request;
    request.add_digests()->CopyFrom(digest);

    BatchReadBlobsResponse response;
    const grpc::Status return_status =
        cas_servicer->BatchReadBlobs(&server_context, &request, &response);
    ASSERT_EQ(return_status.error_code(), grpc::StatusCode::INVALID_ARGUMENT);
    ASSERT_FALSE(collectedByName<DurationMetricValue>(
        buildboxcasd::MetricNames::TIMER_NAME_CAS_BATCH_READ_BLOBS));

    ASSERT_FALSE(collectedByName<CountingMetricValue>(
        MetricNames::COUNTER_NUM_BLOBS_BATCH_READ));
}

TEST_F(CasServerFixture, GetTreeWithMissingRoot)
{
    Digest root_digest;
    root_digest.set_hash("this-digest-is-not-in-the-CAS");

    GetTreeRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);
    request.mutable_root_digest()->CopyFrom(root_digest);

    auto reader = cas_stub->GetTree(&client_context, request);

    GetTreeResponse response;
    ASSERT_FALSE(reader->Read(&response));
    ASSERT_EQ(reader->Finish().error_code(), grpc::StatusCode::NOT_FOUND);
    ASSERT_TRUE(collectedByName<DurationMetricValue>(
        buildboxcasd::MetricNames::TIMER_NAME_CAS_GET_TREE));
}

void assertResponsesContainWholeTree(
    const std::vector<GetTreeResponse> &responses,
    const Digest &subdirectory_digest)
{
    /* Helper that asserts that the union `GetTreeResponse`s in the list
     * contain the whole tree without regard for the ordering.
     */
    bool found_root = false, found_subdir = false;
    for (const GetTreeResponse &response : responses) {
        for (const auto &node : response.directories()) {
            if (node.directories_size() > 0) {
                // This node is `root/`:
                found_root = true;
                ASSERT_EQ(node.directories_size(), 1);
                ASSERT_EQ(node.files_size(), 1);
                ASSERT_EQ(node.files(0).name(), "file1.sh");
                ASSERT_TRUE(node.files(0).is_executable());

                // So `root/subdir1` is there:
                const auto &subdir1 = node.directories(0);
                ASSERT_EQ(subdir1.name(), "subdir1");
                ASSERT_EQ(subdir1.digest().hash(), subdirectory_digest.hash());
            }
            else {
                // This node represents `subdir1/`:
                found_subdir = true;
                ASSERT_EQ(node.directories_size(), 0);
                ASSERT_EQ(node.files_size(), 1);
                ASSERT_EQ(node.files(0).name(), "file2.c");
                ASSERT_FALSE(node.files(0).is_executable());
            }
        }
    }
    ASSERT_TRUE(found_root);
    ASSERT_TRUE(found_subdir);
}

TEST_F(CasServerFixture, GetTree)
{
    /* We'll test this directory structure:
     *  root/
     *  |-- file1.sh*
     *  |-- subdir1/
     *           |-- file2.c
     */

    // Creating `root/`:
    Digest root_directory_digest, subdirectory_digest;
    prepareTree(&root_directory_digest, &subdirectory_digest);

    // Requesting the tree:
    GetTreeRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);
    request.mutable_root_digest()->CopyFrom(root_directory_digest);

    auto reader = cas_stub->GetTree(&client_context, request);

    // We received a valid reply:
    GetTreeResponse response;
    ASSERT_TRUE(reader->Read(&response));
    ASSERT_EQ(reader->Finish().error_code(), grpc::StatusCode::OK);

    // And it contains what we want:
    ASSERT_EQ(response.directories_size(), 2); // `root/` and `root/subdir1`

    // The tree contains a single page, there are no more:
    ASSERT_EQ(response.next_page_token(), "");

    assertResponsesContainWholeTree({response}, subdirectory_digest);
    ASSERT_TRUE(collectedByName<DurationMetricValue>(
        buildboxcasd::MetricNames::TIMER_NAME_CAS_GET_TREE));
}

TEST_F(CasServerFixture, GetPartialTree)
{
    /* We'll test this directory structure:
     *   root/
     *  |-- file1.sh*
     *  |-- subdir1/
     *           |-- file2.c
     *
     * But the server won't have `subdir1/` in its CAS.
     *
     * According to the Remote Execution specification, this is not a problem:
     * the server must return the parts of the tree that are present and
     * ignore the rest.
     */

    Digest root_directory_digest;
    prepareTree(&root_directory_digest);

    // Request the tree:
    GetTreeRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);

    request.mutable_root_digest()->CopyFrom(root_directory_digest);

    auto reader = cas_stub->GetTree(&client_context, request);

    // We received a valid reply:
    GetTreeResponse response;
    ASSERT_TRUE(reader->Read(&response));
    ASSERT_EQ(reader->Finish().error_code(), grpc::StatusCode::OK);

    // And it contains what we want:
    ASSERT_EQ(response.directories_size(), 1);

    const auto node = response.directories(0);
    ASSERT_EQ(node.directories_size(), 1);
    ASSERT_EQ(node.files_size(), 1);
    ASSERT_EQ(node.files(0).name(), "file1.sh");
    ASSERT_TRUE(node.files(0).is_executable());

    // The tree contains a single page, there are no more:
    ASSERT_EQ(response.next_page_token(), "");
    ASSERT_TRUE(collectedByName<DurationMetricValue>(
        buildboxcasd::MetricNames::TIMER_NAME_CAS_GET_TREE));
}

TEST_F(CasServerFixture, GetTreeWithTwoPages)
{
    /* We'll test this directory structure:
     *  root/
     *  |-- file1.sh*
     *  |-- subdir1/
     *           |-- file2.c
     *
     * forcing the server to split the reply into two `GetTreeResponse`s.
     *
     * The server is going to generate the two pages and return them in the
     * stream.
     */

    // Creating `root/`:
    Digest root_directory_digest, subdirectory_digest;
    prepareTree(&root_directory_digest, &subdirectory_digest);

    // Requesting the tree:
    GetTreeRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);
    request.mutable_root_digest()->CopyFrom(root_directory_digest);
    request.set_page_size(1);

    auto reader = cas_stub->GetTree(&client_context, request);

    // We received two responses:
    GetTreeResponse response1, response2;
    ASSERT_TRUE(reader->Read(&response1));
    ASSERT_TRUE(reader->Read(&response2));

    ASSERT_EQ(response1.directories_size(), 1);
    ASSERT_EQ(response2.directories_size(), 1);

    ASSERT_EQ(response1.next_page_token(), "1");
    ASSERT_EQ(response2.next_page_token(), "");

    assertResponsesContainWholeTree({response1, response2},
                                    subdirectory_digest);
}

TEST_F(CasServerFixture, GetTreeSkippingPages)
{
    /* We'll test this directory structure:
     *  root/
     *  |-- file1.sh*
     *  |-- subdir1/
     *           |-- file2.c
     *
     * forcing the server to split the reply into two `GetTreeResponse`s.
     *
     * The server is going to generate the two pages and return them in the
     * stream.
     */

    // Creating `root/`:
    Digest root_directory_digest, subdirectory_digest;
    prepareTree(&root_directory_digest, &subdirectory_digest);

    // We'll first request the whole tree and simulate an error fetching the
    // second page:
    GetTreeResponse page1;
    {
        grpc::ClientContext context;
        GetTreeRequest request;
        request.set_instance_name(TEST_INSTANCE_NAME);
        request.mutable_root_digest()->CopyFrom(root_directory_digest);
        request.set_page_size(1);

        auto reader = cas_stub->GetTree(&context, request);

        ASSERT_TRUE(reader->Read(&page1));
    }

    ASSERT_EQ(page1.next_page_token(), "1");

    // Retrying the request but skipping page 1, which we already have:
    GetTreeResponse page2;
    {
        grpc::ClientContext context;
        GetTreeRequest request;
        request.set_instance_name(TEST_INSTANCE_NAME);
        request.mutable_root_digest()->CopyFrom(root_directory_digest);
        request.set_page_size(1);
        request.set_page_token(page1.next_page_token());

        auto reader = cas_stub->GetTree(&context, request);

        ASSERT_TRUE(reader->Read(&page2));
        ASSERT_EQ(reader->Finish().error_code(), grpc::StatusCode::OK);
    }

    ASSERT_EQ(page2.next_page_token(), "");

    // We effectively fetched the complete tree:
    assertResponsesContainWholeTree({page1, page2}, subdirectory_digest);
    ASSERT_TRUE(collectedByName<DurationMetricValue>(
        buildboxcasd::MetricNames::TIMER_NAME_CAS_GET_TREE));
}

TEST_F(CasServerFixture, GetTreeWithPageTokenZero)
{
    // Creating `root/`:
    Digest root_directory_digest, subdirectory_digest;
    prepareTree(&root_directory_digest, &subdirectory_digest);

    // Requesting the tree:
    GetTreeRequest request;
    request.mutable_root_digest()->CopyFrom(root_directory_digest);
    request.set_page_size(1);
    request.set_page_token("0");

    // While page 0 exists for a tree, the server never returns it as a
    // `next_page_token`, and according to the REAPI spec:
    // "A page token, [...] must be a value received in a previous
    // [GetTreeResponse]".
    // So the following request should return `INVALID_ARGUMENT`.

    auto reader = cas_stub->GetTree(&client_context, request);

    GetTreeResponse response;
    ASSERT_FALSE(reader->Read(&response));
    ASSERT_EQ(reader->Finish().error_code(),
              grpc::StatusCode::INVALID_ARGUMENT);
    ASSERT_TRUE(collectedByName<DurationMetricValue>(
        buildboxcasd::MetricNames::TIMER_NAME_CAS_GET_TREE));
}

TEST_F(CasServerFixture, GetTreeWithInvalidPageToken)
{
    // Creating `root/`:
    Digest root_directory_digest, subdirectory_digest;
    prepareTree(&root_directory_digest, &subdirectory_digest);

    // Requesting the tree:
    GetTreeRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);
    request.mutable_root_digest()->CopyFrom(root_directory_digest);
    request.set_page_size(1);
    request.set_page_token("-1");

    auto reader = cas_stub->GetTree(&client_context, request);

    GetTreeResponse response;
    ASSERT_FALSE(reader->Read(&response));
    ASSERT_EQ(reader->Finish().error_code(),
              grpc::StatusCode::INVALID_ARGUMENT);
    ASSERT_TRUE(collectedByName<DurationMetricValue>(
        buildboxcasd::MetricNames::TIMER_NAME_CAS_GET_TREE));
}

TEST_F(CasServerFixture, GetTreeWithNonExistentPageToken)
{
    // Creating `root/`:
    Digest root_directory_digest, subdirectory_digest;
    prepareTree(&root_directory_digest, &subdirectory_digest);

    // Requesting the tree:
    GetTreeRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);
    request.mutable_root_digest()->CopyFrom(root_directory_digest);
    request.set_page_size(1);
    request.set_page_token("3"); // This page does not exist.

    auto reader = cas_stub->GetTree(&client_context, request);

    GetTreeResponse response;
    ASSERT_FALSE(reader->Read(&response));
    ASSERT_EQ(reader->Finish().error_code(),
              grpc::StatusCode::INVALID_ARGUMENT);
    ASSERT_TRUE(collectedByName<DurationMetricValue>(
        buildboxcasd::MetricNames::TIMER_NAME_CAS_GET_TREE));
}

TEST_F(CasServerFixture, BytestreamReadWithInvalidResourceName)
{
    ReadRequest request;
    request.set_resource_name("/root/path/to/something");

    auto reader = cas_bytestream_stub->Read(&client_context, request);

    ReadResponse response;
    ASSERT_FALSE(reader->Read(&response));
    ASSERT_EQ(reader->Finish().error_code(),
              grpc::StatusCode::INVALID_ARGUMENT);
}

TEST_F(CasServerFixture, BytestreamReadWithNegativeReadOffset)
{
    const auto digest = DigestGenerator::hash("data1");
    ASSERT_TRUE(storage->hasBlob(digest));

    ReadRequest request;
    request.set_resource_name(TEST_INSTANCE_NAME + "/blobs/" + digest.hash() +
                              "/" + std::to_string(digest.size_bytes()));

    request.set_read_offset(-1);

    auto reader = cas_bytestream_stub->Read(&client_context, request);

    ReadResponse response;
    ASSERT_FALSE(reader->Read(&response));
    ASSERT_EQ(reader->Finish().error_code(), grpc::StatusCode::OUT_OF_RANGE);
}

TEST_F(CasServerFixture, BytestreamReadWithNegativeReadLimit)
{
    const std::string data = "data1";
    const Digest digest = DigestGenerator::hash(data);
    ASSERT_TRUE(storage->hasBlob(digest));

    ReadRequest request;

    request.set_resource_name(TEST_INSTANCE_NAME + "/blobs/" + digest.hash() +
                              "/" + std::to_string(digest.size_bytes()));

    request.set_read_limit(-3);

    auto reader = cas_bytestream_stub->Read(&client_context, request);

    ReadResponse response;
    ASSERT_FALSE(reader->Read(&response));
    ASSERT_EQ(reader->Finish().error_code(),
              grpc::StatusCode::INVALID_ARGUMENT);
}

TEST_F(CasServerFixture, BytestreamReadWithReadOffsetExceedingRealSize)
{
    const std::string data = "data1";
    const auto digest = DigestGenerator::hash(data);
    ASSERT_TRUE(storage->hasBlob(digest));

    ReadRequest request;
    request.set_resource_name(TEST_INSTANCE_NAME + "/blobs/" + digest.hash() +
                              "/" + std::to_string(digest.size_bytes()));

    request.set_read_offset(
        static_cast<google::protobuf::int64>(data.size() + 1));

    auto reader = cas_bytestream_stub->Read(&client_context, request);

    ReadResponse response;
    ASSERT_FALSE(reader->Read(&response));
    ASSERT_EQ(reader->Finish().error_code(), grpc::StatusCode::OUT_OF_RANGE);
}

TEST_F(CasServerFixture, BytestreamWriteWithInvalidResourceName)
{
    WriteRequest request;
    request.set_resource_name("/root/path/to/something");
    request.set_data("data");
    request.set_finish_write(true);

    WriteResponse response;
    auto writer = cas_bytestream_stub->Write(&client_context, &response);

    ASSERT_TRUE(writer->Write(request));
    ASSERT_TRUE(writer->WritesDone());
    ASSERT_EQ(response.committed_size(), 0);
    ASSERT_EQ(writer->Finish().error_code(),
              grpc::StatusCode::INVALID_ARGUMENT);
}

TEST_F(CasServerFixture, BytestreamReadMissingBlob)
{
    const Digest digest = DigestGenerator::hash("data25");
    ASSERT_FALSE(storage->hasBlob(digest));

    ReadRequest request;
    request.set_resource_name(TEST_INSTANCE_NAME + "/blobs/" + digest.hash() +
                              "/" + std::to_string(digest.size_bytes()));

    auto reader = cas_bytestream_stub->Read(&client_context, request);

    ReadResponse response;
    ASSERT_FALSE(reader->Read(&response));
    ASSERT_EQ(reader->Finish().error_code(), grpc::StatusCode::NOT_FOUND);
}

TEST_F(CasServerFixture, BytestreamReadEmptyBlob)
{
    const Digest digest = DigestGenerator::hash("");

    storage->writeBlob(digest, "");
    ASSERT_TRUE(storage->hasBlob(digest));

    ReadRequest request;

    request.set_resource_name(TEST_INSTANCE_NAME + "/blobs/" + digest.hash() +
                              "/" + std::to_string(digest.size_bytes()));

    auto reader = cas_bytestream_stub->Read(&client_context, request);

    ReadResponse response;
    ASSERT_TRUE(reader->Read(&response));
    ASSERT_EQ(response.data(), "");
    ASSERT_EQ(reader->Finish().error_code(), grpc::StatusCode::OK);
}

TEST_F(CasServerFixture, BytestreamRead)
{
    const std::string data = "data1";
    const Digest digest = DigestGenerator::hash(data);
    ASSERT_TRUE(storage->hasBlob(digest));

    ReadRequest request;

    request.set_resource_name(TEST_INSTANCE_NAME + "/blobs/" + digest.hash() +
                              "/" + std::to_string(digest.size_bytes()));

    auto reader = cas_bytestream_stub->Read(&client_context, request);

    ReadResponse response;
    ASSERT_TRUE(reader->Read(&response));
    ASSERT_EQ(response.data(), data);
    ASSERT_EQ(reader->Finish().error_code(), grpc::StatusCode::OK);

    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::collectedByNameWithValue<
            buildboxcommon::buildboxcommonmetrics::DistributionMetricValue>(
            MetricNames::DISTRIBUTION_NAME_CAS_BYTESTREAM_READ_BLOB_SIZES,
            buildboxcommon::buildboxcommonmetrics::DistributionMetricValue(
                digest.size_bytes())));
}

TEST_F(CasServerFixture, PreserveEmbeddedNULs)
{
    // Create an empty data array
    constexpr unsigned long one_megabytes = 1024UL * 1024UL;
    std::string data = std::string("");
    data.reserve(one_megabytes);

    // Fill data with a repeating NUL terminated string
    const int somedataLength = 9;
    const std::string somedata = std::string("SomeData\0", somedataLength);
    while (data.size() + somedataLength <= one_megabytes) {
        data += somedata;
    }

    // Upload the file to CAS
    Digest digest = DigestGenerator::hash(data);
    storage->writeBlob(digest, data);
    ASSERT_TRUE(storage->hasBlob(digest));

    // Read the file back
    auto result = storage->readBlob(digest);

    // Compare equality with input value
    ASSERT_EQ(*result, data);
}

TEST_F(CasServerFixture, BytestreamLargeRead)
{
    // This blob will have to be split into multiple chunks due to the gRPC's
    // message size limit:

    std::string data;
    while (data.size() <= 16UL * 1024UL * 1024UL) {
        data += "SomeData";
    }

    const Digest digest = DigestGenerator::hash(data);
    storage->writeBlob(digest, data);
    ASSERT_TRUE(storage->hasBlob(digest));

    ReadRequest request;

    request.set_resource_name(TEST_INSTANCE_NAME + "/blobs/" + digest.hash() +
                              "/" + std::to_string(digest.size_bytes()));

    auto reader = cas_bytestream_stub->Read(&client_context, request);

    ReadResponse response;
    std::string fetched_data;
    while (reader->Read(&response)) {
        fetched_data += response.data();
    }
    ASSERT_EQ(DigestGenerator::hash(fetched_data), digest);

    ASSERT_EQ(reader->Finish().error_code(), grpc::StatusCode::OK);
}

TEST_F(CasServerFixture, BytestreamReadWithLimit)
{
    const std::string data = "data1";
    const Digest digest = DigestGenerator::hash(data);
    ASSERT_TRUE(storage->hasBlob(digest));

    ReadRequest request;

    request.set_resource_name(TEST_INSTANCE_NAME + "/blobs/" + digest.hash() +
                              "/" + std::to_string(digest.size_bytes()));

    request.set_read_limit(2);

    auto reader = cas_bytestream_stub->Read(&client_context, request);

    ReadResponse response;
    ASSERT_TRUE(reader->Read(&response));
    ASSERT_EQ(response.data(), "da");
    ASSERT_EQ(reader->Finish().error_code(), grpc::StatusCode::OK);
}

TEST_F(CasServerFixture, BytestreamReadWithOffset)
{
    const std::string data = "data1";
    const Digest digest = DigestGenerator::hash(data);
    ASSERT_TRUE(storage->hasBlob(digest));

    ReadRequest request;

    request.set_resource_name(TEST_INSTANCE_NAME + "/blobs/" + digest.hash() +
                              "/" + std::to_string(digest.size_bytes()));

    request.set_read_offset(3);

    auto reader = cas_bytestream_stub->Read(&client_context, request);

    ReadResponse response;
    ASSERT_TRUE(reader->Read(&response));
    ASSERT_EQ(response.data(), "a1");
    ASSERT_EQ(reader->Finish().error_code(), grpc::StatusCode::OK);
}

TEST_F(CasServerFixture, BytestreamReadWithOffsetAndLimit)
{
    const std::string data = "data1";
    const Digest digest = DigestGenerator::hash(data);
    ASSERT_TRUE(storage->hasBlob(digest));

    ReadRequest request;

    request.set_resource_name(TEST_INSTANCE_NAME + "/blobs/" + digest.hash() +
                              "/" + std::to_string(digest.size_bytes()));

    request.set_read_limit(2);
    request.set_read_offset(1);

    auto reader = cas_bytestream_stub->Read(&client_context, request);

    ReadResponse response;
    ASSERT_TRUE(reader->Read(&response));
    ASSERT_EQ(response.data(), "at");
    ASSERT_EQ(reader->Finish().error_code(), grpc::StatusCode::OK);
}

TEST_F(CasServerFixture, BytestreamReadWithNoInstanceName)
{
    const auto digest = DigestGenerator::hash("data1");

    ReadRequest request;
    request.set_resource_name("blobs/" + digest.hash() + "/" +
                              std::to_string(digest.size_bytes()));

    auto reader = cas_bytestream_stub->Read(&client_context, request);

    ReadResponse response;
    ASSERT_FALSE(reader->Read(&response));
    ASSERT_EQ(reader->Finish().error_code(),
              grpc::StatusCode::INVALID_ARGUMENT);
}

TEST_F(CasServerFixture, BytestreamReadWithInvalidInstanceName)
{
    const auto digest = DigestGenerator::hash("data1");

    ReadRequest request;
    request.set_resource_name("this-instance-does-not-exist/blobs/" +
                              digest.hash() + "/" +
                              std::to_string(digest.size_bytes()));

    auto reader = cas_bytestream_stub->Read(&client_context, request);

    ReadResponse response;
    ASSERT_FALSE(reader->Read(&response));
    ASSERT_EQ(reader->Finish().error_code(),
              grpc::StatusCode::INVALID_ARGUMENT);
}

TEST_F(CasServerFixture, BytestreamResourceNameIgnoreValuesAfterSize)
{
    const Digest digest = DigestGenerator::hash("data1");
    ASSERT_TRUE(storage->hasBlob(digest));

    ReadRequest request;

    request.set_resource_name(TEST_INSTANCE_NAME + "/blobs/" + digest.hash() +
                              "/" + std::to_string(digest.size_bytes()) +
                              "/extra/path/to/file.c");

    auto reader = cas_bytestream_stub->Read(&client_context, request);

    ReadResponse response;
    ASSERT_TRUE(reader->Read(&response));
    ASSERT_EQ(response.data(), readBlob(digest));
    ASSERT_EQ(reader->Finish().error_code(), grpc::StatusCode::OK);
}

TEST_F(CasServerFixture, BytestreamWriteEmptyBlob)
{
    const Digest digest = DigestGenerator::hash("");

    ASSERT_TRUE(storage->hasBlob(digest));
    // (The empty blob is available in an empty CAS.)

    WriteRequest request;
    request.set_resource_name(
        TEST_INSTANCE_NAME + "/uploads/uuid-goes-here/blobs/" + digest.hash() +
        "/" + std::to_string(digest.size_bytes()));
    request.set_data("");
    request.set_finish_write(true);

    WriteResponse response;
    auto writer = cas_bytestream_stub->Write(&client_context, &response);

    ASSERT_TRUE(writer->Write(request));
    ASSERT_TRUE(writer->WritesDone());
    ASSERT_EQ(writer->Finish().error_code(), grpc::StatusCode::OK);
    ASSERT_EQ(response.committed_size(), readBlob(digest).size());

    ASSERT_TRUE(storage->hasBlob(digest));
    ASSERT_EQ(readBlob(digest), "");
}

TEST_F(CasServerFixture, BytestreamWrite)
{
    const std::string data = "data10";
    const Digest digest = DigestGenerator::hash(data);

    ASSERT_FALSE(storage->hasBlob(digest));

    WriteRequest request;
    request.set_resource_name(
        TEST_INSTANCE_NAME + "/uploads/uuid-goes-here/blobs/" + digest.hash() +
        "/" + std::to_string(digest.size_bytes()));
    request.set_data(data);
    request.set_finish_write(true);

    WriteResponse response;
    auto writer = cas_bytestream_stub->Write(&client_context, &response);

    ASSERT_TRUE(writer->Write(request));
    ASSERT_TRUE(writer->WritesDone());
    ASSERT_EQ(writer->Finish().error_code(), grpc::StatusCode::OK);
    ASSERT_EQ(response.committed_size(), readBlob(digest).size());

    ASSERT_TRUE(storage->hasBlob(digest));
    ASSERT_EQ(readBlob(digest), data);

    ASSERT_TRUE(
        buildboxcommon::buildboxcommonmetrics::collectedByNameWithValue<
            buildboxcommon::buildboxcommonmetrics::DistributionMetricValue>(
            MetricNames::DISTRIBUTION_NAME_CAS_BYTESTREAM_WRITE_BLOB_SIZES,
            buildboxcommon::buildboxcommonmetrics::DistributionMetricValue(
                digest.size_bytes())));
}

TEST_F(CasServerFixture, BytestreamWriteMultiPart)
{
    const std::string data = "Part1|Part2";
    const Digest digest = DigestGenerator::hash(data);

    ASSERT_FALSE(storage->hasBlob(digest));

    const std::string resource_name =
        TEST_INSTANCE_NAME + "/uploads/uuid-goes-here/blobs/" + digest.hash() +
        "/" + std::to_string(digest.size_bytes());

    WriteRequest request1;
    request1.set_resource_name(resource_name);
    request1.set_data("Part1|");
    request1.set_finish_write(false);

    WriteRequest request2;
    request2.set_resource_name(resource_name);
    request2.set_data("Part2");
    request2.set_finish_write(true);

    WriteResponse response;
    auto writer = cas_bytestream_stub->Write(&client_context, &response);

    ASSERT_TRUE(writer->Write(request1));
    ASSERT_TRUE(writer->Write(request2));
    ASSERT_TRUE(writer->WritesDone());

    ASSERT_EQ(writer->Finish().error_code(), grpc::StatusCode::OK);
    ASSERT_EQ(response.committed_size(), readBlob(digest).size());

    ASSERT_TRUE(storage->hasBlob(digest));
    ASSERT_EQ(readBlob(digest), data);
}

TEST_F(CasServerFixture, BytestreamWriteExistingBlob)
{
    // According to the spec:
    // "When attempting an upload, if another client has already completed the
    // upload (which may occur in the middle of a single upload if another
    // client uploads the same blob concurrently), the request will terminate
    // immediately with a response whose `committed_size` is the full size of
    // the uploaded file (regardless of how much data was transmitted by the
    // client)"
    const std::string data = "data1";
    const Digest digest = DigestGenerator::hash(data);
    ASSERT_TRUE(storage->hasBlob(digest));

    const auto resource_name =
        TEST_INSTANCE_NAME + "/uploads/uuid-goes-here/blobs/" + digest.hash() +
        "/" + std::to_string(digest.size_bytes());

    // Sending a request containing only a part of the blob:
    WriteRequest request1;
    request1.set_resource_name(resource_name);
    request1.set_data(data.substr(0, 1));
    request1.set_finish_write(false);

    WriteResponse response;
    auto writer = cas_bytestream_stub->Write(&client_context, &response);
    ASSERT_TRUE(writer->Write(request1));

    // The server reports that the whole blob is successfully uploaded, even
    // though we sent just a portion of it:
    ASSERT_TRUE(writer->WritesDone());
    ASSERT_EQ(writer->Finish().error_code(), grpc::StatusCode::OK);

    ASSERT_TRUE(storage->hasBlob(digest));
    ASSERT_EQ(readBlob(digest), data);
}

TEST_F(CasServerFixture, BytestreamWriteMultiPartWithoutFinishWriteFails)
{
    const std::string data = "Part1|Part2";
    const Digest digest = DigestGenerator::hash(data);

    ASSERT_FALSE(storage->hasBlob(digest));

    const std::string resource_name =
        TEST_INSTANCE_NAME + "/uploads/uuid-goes-here/blobs/" + digest.hash() +
        "/" + std::to_string(digest.size_bytes());

    WriteRequest request1;
    request1.set_resource_name(resource_name);
    request1.set_data("Part1|");
    request1.set_finish_write(false);

    WriteRequest request2;
    request2.set_resource_name(resource_name);
    request2.set_data("Part2");
    request2.set_finish_write(false);
    // ^ The last request should set `finish_write`. The write will fail.

    WriteResponse response;
    auto writer = cas_bytestream_stub->Write(&client_context, &response);

    ASSERT_TRUE(writer->Write(request1));
    ASSERT_TRUE(writer->Write(request2));
    ASSERT_TRUE(writer->WritesDone());

    ASSERT_EQ(writer->Finish().error_code(),
              grpc::StatusCode::INVALID_ARGUMENT);

    // The write was not committed:
    ASSERT_EQ(response.committed_size(), 0);
    ASSERT_FALSE(storage->hasBlob(digest));
}

TEST_F(CasServerFixture, BytestreamWriteWithInvalidDigestFails)
{
    const std::string data = "data11";
    Digest digest;
    digest.set_hash("hash");

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

    ASSERT_FALSE(storage->hasBlob(digest));

    WriteRequest request;
    request.set_resource_name(
        TEST_INSTANCE_NAME + "/uploads/uuid-goes-here/blobs/" + digest.hash() +
        "/" + std::to_string(digest.size_bytes()));
    request.set_data(data);
    request.set_finish_write(true);

    WriteResponse response;
    auto writer = cas_bytestream_stub->Write(&client_context, &response);

    ASSERT_TRUE(writer->Write(request));
    ASSERT_TRUE(writer->WritesDone());
    ASSERT_EQ(writer->Finish().error_code(),
              grpc::StatusCode::INVALID_ARGUMENT);
    ASSERT_EQ(response.committed_size(), 0);
}

TEST_F(CasServerFixture, BytestreamWriteWithInvalidInstanceName)
{
    const Digest digest = DigestGenerator::hash("data");

    WriteRequest request;
    request.set_resource_name(
        "non-existent-instance/uploads/uuid-goes-here/blobs/" + digest.hash() +
        "/" + std::to_string(digest.size_bytes()));
    request.set_finish_write(true);

    WriteResponse response;
    auto writer = cas_bytestream_stub->Write(&client_context, &response);

    ASSERT_TRUE(writer->Write(request));
    ASSERT_TRUE(writer->WritesDone());
    ASSERT_EQ(writer->Finish().error_code(),
              grpc::StatusCode::INVALID_ARGUMENT);
    ASSERT_EQ(response.committed_size(), 0);
}

TEST_F(CasServerFixture, BytestreamWriteExtraRequestFails)
{
    const std::string data = "data10";
    const Digest digest = DigestGenerator::hash(data);

    const std::string resource_name = "uploads/uuid-goes-here/blobs/" +
                                      digest.hash() + "/" +
                                      std::to_string(digest.size_bytes());
    WriteRequest request1;
    request1.set_resource_name(resource_name);
    request1.set_data(data);
    request1.set_finish_write(true);
    // (After setting `finish_write` it is illegal to send more requests.)

    WriteRequest request2 = request1;

    WriteResponse response;
    auto writer = cas_bytestream_stub->Write(&client_context, &response);

    ASSERT_TRUE(writer->Write(request1));
    ASSERT_TRUE(writer->Write(request2));
    ASSERT_TRUE(writer->WritesDone());

    ASSERT_EQ(writer->Finish().error_code(),
              grpc::StatusCode::INVALID_ARGUMENT);
    ASSERT_EQ(response.committed_size(), 0);
}

TEST_F(CasServerFixture, BytestreamWriteMismatchedRequests)
{
    const std::string data = "Part1|Part2";
    const Digest digest = DigestGenerator::hash(data);

    const std::string resource_name = "uploads/uuid-goes-here/blobs/" +
                                      digest.hash() + "/" +
                                      std::to_string(digest.size_bytes());
    WriteRequest request1;
    request1.set_resource_name(resource_name);
    request1.set_data("Part1|");
    request1.set_finish_write(false);

    WriteRequest request2;
    request2.set_resource_name(resource_name + "1");
    // (This changes the Digest, which must match in all requests.)

    request2.set_data("Part2");
    request2.set_finish_write(true);

    WriteResponse response;
    auto writer = cas_bytestream_stub->Write(&client_context, &response);

    ASSERT_TRUE(writer->Write(request1));
    ASSERT_TRUE(writer->Write(request2));
    ASSERT_TRUE(writer->WritesDone());

    ASSERT_EQ(writer->Finish().error_code(),
              grpc::StatusCode::INVALID_ARGUMENT);
    ASSERT_EQ(response.committed_size(), 0);
}

TEST_F(CasServerFixture, BytestreamWriteMultiPartInvalidDigestFails)
{
    const std::string data = "Part1|Part2";
    const Digest digest = DigestGenerator::hash(data);

    const std::string resource_name = "uploads/uuid-goes-here/blobs/" +
                                      digest.hash() + "/" +
                                      std::to_string(digest.size_bytes());
    WriteRequest request1;
    request1.set_resource_name(resource_name);
    request1.set_data("Part1|");
    request1.set_finish_write(false);

    WriteRequest request2;
    request2.set_resource_name(resource_name);
    request2.set_data("SomeOtherPart");
    request2.set_finish_write(true);

    WriteResponse response;
    auto writer = cas_bytestream_stub->Write(&client_context, &response);

    ASSERT_TRUE(writer->Write(request1));
    ASSERT_TRUE(writer->Write(request2));
    ASSERT_TRUE(writer->WritesDone());

    ASSERT_EQ(writer->Finish().error_code(),
              grpc::StatusCode::INVALID_ARGUMENT);
    ASSERT_EQ(response.committed_size(), 0);
}

TEST_F(CasServerFixture, FindMissingBlobsInInvalidInstance)
{
    FindMissingBlobsRequest request;
    request.set_instance_name("non-existent");

    FindMissingBlobsResponse response;

    const auto status =
        cas_servicer->FindMissingBlobs(&server_context, &request, &response);
    ASSERT_EQ(status.error_code(), grpc::INVALID_ARGUMENT);
    ASSERT_TRUE(collectedByName<DurationMetricValue>(
        buildboxcasd::MetricNames::TIMER_NAME_CAS_FIND_MISSING_BLOBS));

    ASSERT_FALSE(collectedByName<CountingMetricValue>(
        MetricNames::COUNTER_NUM_BLOBS_FIND_MISSING));
}

TEST_F(CasServerFixture, UpdateBlobsInInvalidInstance)
{
    BatchUpdateBlobsRequest request;
    request.set_instance_name("");

    BatchUpdateBlobsResponse response;

    const auto status =
        cas_servicer->BatchUpdateBlobs(&server_context, &request, &response);
    ASSERT_EQ(status.error_code(), grpc::INVALID_ARGUMENT);

    ASSERT_FALSE(collectedByName<CountingMetricValue>(
        MetricNames::COUNTER_NUM_BLOBS_BATCH_UPDATE));
}

TEST_F(CasServerFixture, BatchReadBlobsInInvalidInstance)
{
    BatchReadBlobsRequest request;
    request.set_instance_name("instance123");

    BatchReadBlobsResponse response;

    const auto status =
        cas_servicer->BatchReadBlobs(&server_context, &request, &response);
    ASSERT_EQ(status.error_code(), grpc::INVALID_ARGUMENT);

    ASSERT_FALSE(collectedByName<CountingMetricValue>(
        MetricNames::COUNTER_NUM_BLOBS_BATCH_READ));
}

TEST_F(CasServerFixture, GetTreeInInvalidInstance)
{
    GetTreeRequest request;
    request.set_instance_name("instance123");

    auto reader = cas_stub->GetTree(&client_context, request);

    GetTreeResponse response;
    ASSERT_FALSE(reader->Read(&response));

    const auto status = reader->Finish();
    ASSERT_EQ(status.error_code(), grpc::INVALID_ARGUMENT);
}

TEST_F(CasServerFixture, TestCapabilities)
{
    GetCapabilitiesRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);

    ServerCapabilities response;

    const grpc::Status response_status = capabilities_stub->GetCapabilities(
        &client_context, request, &response);
    ASSERT_EQ(response_status.error_code(), grpc::StatusCode::OK);

    ASSERT_EQ(response.cache_capabilities().max_batch_total_size_bytes(), 0);

    ASSERT_EQ(response.cache_capabilities().digest_functions_size(), 1);
#ifndef __SANITIZE_ADDRESS__
    ASSERT_EQ(response.cache_capabilities().digest_functions(0),
              DigestGenerator::digestFunction());
#endif
    ASSERT_EQ(response.high_api_version().major(), 2);
    ASSERT_EQ(response.high_api_version().minor(), 12);
    ASSERT_EQ(response.high_api_version().patch(), 0);
    ASSERT_EQ(response.low_api_version().major(), 2);
    ASSERT_EQ(response.low_api_version().minor(), 1);
    ASSERT_EQ(response.low_api_version().patch(), 0);
    ASSERT_TRUE(collectedByName<DurationMetricValue>(
        buildboxcasd::MetricNames::TIMER_NAME_CAS_GET_CAPABILITIES));
}

TEST_F(CasServerFixture, TestCapabilitiesWithInvalidInstanceName)
{
    GetCapabilitiesRequest request;

    ServerCapabilities response;
    const grpc::Status response_status = capabilities_stub->GetCapabilities(
        &client_context, request, &response);

    ASSERT_EQ(response_status.error_code(),
              grpc::StatusCode::INVALID_ARGUMENT);
}

TEST_F(CasServerFixture, FindMissingBlobsWithUnsupportedDigestFunction)
{
    FindMissingBlobsRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);
    request.set_digest_function(DigestFunction_Value_SHA384);

    FindMissingBlobsResponse response;
    const auto status =
        cas_servicer->FindMissingBlobs(&server_context, &request, &response);
    ASSERT_EQ(status.error_code(), grpc::StatusCode::INVALID_ARGUMENT);
}

TEST_F(CasServerFixture, FindMissingBlobsWithMatchingDigestFunction)
{
    FindMissingBlobsRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);
    request.set_digest_function(DigestGenerator::digestFunction());

    const auto digest1 = DigestGenerator::hash("data1");
    request.add_blob_digests()->CopyFrom(digest1);

    FindMissingBlobsResponse response;
    const auto status =
        cas_servicer->FindMissingBlobs(&server_context, &request, &response);
    ASSERT_EQ(status.error_code(), grpc::StatusCode::OK);
}

TEST_F(CasServerFixture, FindMissingBlobsWithUnsetDigestFunction)
{
    FindMissingBlobsRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);

    const auto digest1 = DigestGenerator::hash("data1");
    request.add_blob_digests()->CopyFrom(digest1);

    FindMissingBlobsResponse response;
    const auto status =
        cas_servicer->FindMissingBlobs(&server_context, &request, &response);
    ASSERT_EQ(status.error_code(), grpc::StatusCode::OK);
}

TEST_F(CasServerFixture, BatchUpdateBlobsWithUnsupportedDigestFunction)
{
    BatchUpdateBlobsRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);
    request.set_digest_function(DigestFunction_Value_SHA384);

    BatchUpdateBlobsResponse response;
    const auto status =
        cas_servicer->BatchUpdateBlobs(&server_context, &request, &response);
    ASSERT_EQ(status.error_code(), grpc::StatusCode::INVALID_ARGUMENT);
}

TEST_F(CasServerFixture, BatchReadBlobsWithUnsupportedDigestFunction)
{
    BatchReadBlobsRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);
    request.set_digest_function(DigestFunction_Value_SHA384);

    BatchReadBlobsResponse response;
    const auto status =
        cas_servicer->BatchReadBlobs(&server_context, &request, &response);
    ASSERT_EQ(status.error_code(), grpc::StatusCode::INVALID_ARGUMENT);
}

TEST_F(CasServerFixture, GetTreeWithUnsupportedDigestFunction)
{
    GetTreeRequest request;
    request.set_instance_name(TEST_INSTANCE_NAME);
    request.set_digest_function(DigestFunction_Value_SHA384);

    auto reader = cas_stub->GetTree(&client_context, request);

    GetTreeResponse response;
    ASSERT_FALSE(reader->Read(&response));
    ASSERT_EQ(reader->Finish().error_code(),
              grpc::StatusCode::INVALID_ARGUMENT);
}
