/*
 * Copyright 2020 Bloomberg Finance LP
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

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

#include <buildboxcommon_assetclient.h>
#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_grpcclient.h>
#include <buildboxcommon_grpcerror.h>
#include <buildboxcommon_merklize.h>
#include <buildboxcommon_temporarydirectory.h>

#include <gtest/gtest.h>

using namespace buildboxcasd;
using namespace buildboxcommon;

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

const std::string TEST_INSTANCE_NAME = "testInstances/instance1";
const std::string TEST_URI = "urn:fdc:example.com:2020:1";

enum ModeParam : std::uint8_t {
    DIRECT_MODE,
    PROXY_MODE,
};

class RaServerFixture : public ::testing::TestWithParam<ModeParam> {
  protected:
    RaServerFixture()
        : TEST_SERVER_ADDRESS("unix://" +
                              std::string(socket_directory.name()) +
                              "/remote.sock"),
          TEST_PROXY_SERVER_ADDRESS("unix://" +
                                    std::string(socket_directory.name()) +
                                    "/proxy.sock"),
          cas_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>(cas_storage, asset_storage,
                                          action_storage))
    {
        // Build and start server
        server->addLocalServerInstance(TEST_INSTANCE_NAME);
        server->addListeningPort(TEST_SERVER_ADDRESS);
        server->start();

        buildboxcommon::ConnectionOptions connection_options,
            client_connection_options;
        connection_options.setUrl(TEST_SERVER_ADDRESS);
        connection_options.setInstanceName(TEST_INSTANCE_NAME);
        connection_options.setRetryDelay("10");
        connection_options.setRetryLimit("1");
        connection_options.setRequestTimeout("10");

        std::vector<std::string> instances = {""};
        std::unordered_map<std::string, std::string> instanceMappings;
        if (GetParam() == PROXY_MODE) {
            // Build and start proxy server
            proxy_cas_storage = std::make_shared<FsLocalCas>(
                proxy_storage_root_directory.name());
            proxy_asset_storage = std::make_shared<FsLocalAssetStorage>(
                proxy_storage_root_directory.name());
            proxy_action_storage = std::make_shared<FsLocalActionStorage>(
                proxy_storage_root_directory.name());
            proxy_server = std::make_shared<Server>(
                proxy_cas_storage, proxy_asset_storage, proxy_action_storage,
                nullptr, nullptr, nullptr, connection_options,
                std::optional(connection_options),
                std::optional(connection_options),
                std::optional(connection_options), instances,
                instanceMappings);

            proxy_server->addListeningPort(TEST_PROXY_SERVER_ADDRESS);
            proxy_server->start();

            // Client access via proxy
            client_connection_options.setUrl(TEST_PROXY_SERVER_ADDRESS);
        }
        else {
            // Direct access without proxy
            client_connection_options = connection_options;
        }

        grpc_client = std::make_shared<GrpcClient>();
        grpc_client->init(client_connection_options);
        client = std::make_shared<AssetClient>(grpc_client);
        client->init();
    }

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

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

    buildboxcommon::TemporaryDirectory proxy_storage_root_directory;
    std::shared_ptr<FsLocalCas> proxy_cas_storage;
    std::shared_ptr<FsLocalAssetStorage> proxy_asset_storage;
    std::shared_ptr<FsLocalActionStorage> proxy_action_storage;
    std::shared_ptr<Server> proxy_server;

    std::shared_ptr<GrpcClient> grpc_client;
    std::shared_ptr<AssetClient> client;
};

TEST_P(RaServerFixture, Blob)
{
    const auto digest1 = DigestGenerator::hash("data1");
    cas_storage->writeBlob(digest1, "data1");

    PushBlobRequest push_request;
    push_request.set_instance_name(grpc_client->instanceName());
    push_request.add_uris(TEST_URI);
    push_request.mutable_blob_digest()->CopyFrom(digest1);
    client->pushBlob(push_request);

    FetchBlobRequest fetch_request;
    FetchBlobResponse fetch_response;
    fetch_request.set_instance_name(grpc_client->instanceName());
    fetch_request.add_uris(TEST_URI);
    fetch_response = client->fetchBlob(fetch_request);

    ASSERT_EQ(fetch_response.blob_digest(), digest1);
}

TEST_P(RaServerFixture, BlobNotFound)
{
    try {
        FetchBlobRequest fetch_request;
        FetchBlobResponse fetch_response;
        fetch_request.set_instance_name(grpc_client->instanceName());
        fetch_request.add_uris(TEST_URI);
        fetch_response = client->fetchBlob(fetch_request);

        FAIL() << "Expected NOT_FOUND GrpcError";
    }
    catch (const GrpcError &e) {
        ASSERT_EQ(e.status.error_code(), grpc::StatusCode::NOT_FOUND);
    }
}

TEST_P(RaServerFixture, BlobReplace)
{
    const auto digest1 = DigestGenerator::hash("data1");
    const auto digest2 = DigestGenerator::hash("data2");
    cas_storage->writeBlob(digest1, "data1");
    cas_storage->writeBlob(digest2, "data2");

    PushBlobRequest push_request;
    push_request.set_instance_name(grpc_client->instanceName());
    push_request.add_uris(TEST_URI);
    push_request.mutable_blob_digest()->CopyFrom(digest1);
    client->pushBlob(push_request);

    // Replace blob for TEST_URI
    push_request.mutable_blob_digest()->CopyFrom(digest2);
    client->pushBlob(push_request);

    FetchBlobRequest fetch_request;
    FetchBlobResponse fetch_response;
    fetch_request.set_instance_name(grpc_client->instanceName());
    fetch_request.add_uris(TEST_URI);
    fetch_response = client->fetchBlob(fetch_request);

    ASSERT_EQ(fetch_response.blob_digest(), digest2);
}

TEST_P(RaServerFixture, BlobDelete)
{
    const auto digest1 = DigestGenerator::hash("data1");
    cas_storage->writeBlob(digest1, "data1");

    PushBlobRequest push_request;
    push_request.set_instance_name(grpc_client->instanceName());
    push_request.add_uris(TEST_URI);
    push_request.mutable_blob_digest()->CopyFrom(digest1);
    client->pushBlob(push_request);

    // Delete blob for TEST_URI
    push_request.clear_blob_digest();
    client->pushBlob(push_request);

    try {
        FetchBlobRequest fetch_request;
        FetchBlobResponse fetch_response;
        fetch_request.set_instance_name(grpc_client->instanceName());
        fetch_request.add_uris(TEST_URI);
        fetch_response = client->fetchBlob(fetch_request);

        FAIL() << "Expected NOT_FOUND GrpcError";
    }
    catch (const GrpcError &e) {
        ASSERT_EQ(e.status.error_code(), grpc::StatusCode::NOT_FOUND);
    }
}

TEST_P(RaServerFixture, Directory)
{
    const auto digest1 = DigestGenerator::hash("data1");
    cas_storage->writeBlob(digest1, "data1");

    Directory directory;
    FileNode *file_node = directory.add_files();
    file_node->set_name("data1");
    file_node->mutable_digest()->CopyFrom(digest1);
    const auto serialized_directory = directory.SerializeAsString();
    const auto directory_digest = DigestGenerator::hash(serialized_directory);
    cas_storage->writeBlob(directory_digest, serialized_directory);

    PushDirectoryRequest push_request;
    push_request.set_instance_name(grpc_client->instanceName());
    push_request.add_uris(TEST_URI);
    push_request.mutable_root_directory_digest()->CopyFrom(directory_digest);
    client->pushDirectory(push_request);

    FetchDirectoryRequest fetch_request;
    FetchDirectoryResponse fetch_response;
    fetch_request.set_instance_name(grpc_client->instanceName());
    fetch_request.add_uris(TEST_URI);
    fetch_response = client->fetchDirectory(fetch_request);

    ASSERT_EQ(fetch_response.root_directory_digest(), directory_digest);
}

TEST_P(RaServerFixture, DirectoryNotFound)
{
    try {
        FetchDirectoryRequest fetch_request;
        FetchDirectoryResponse fetch_response;
        fetch_request.set_instance_name(grpc_client->instanceName());
        fetch_request.add_uris(TEST_URI);
        fetch_response = client->fetchDirectory(fetch_request);

        FAIL() << "Expected NOT_FOUND GrpcError";
    }
    catch (const GrpcError &e) {
        ASSERT_EQ(e.status.error_code(), grpc::StatusCode::NOT_FOUND);
    }
}

INSTANTIATE_TEST_SUITE_P(Direct, RaServerFixture,
                         testing::Values(DIRECT_MODE));
INSTANTIATE_TEST_SUITE_P(Proxy, RaServerFixture, testing::Values(PROXY_MODE));
