/*
 * Copyright 2022 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.
 */

#ifndef INCLUDED_BUILDBOXCASD_NOUPDATESERVER_H
#define INCLUDED_BUILDBOXCASD_NOUPDATESERVER_H

#include <buildboxcasd_server.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>

#include <buildboxcasd_fslocalactionstorage.h>
#include <buildboxcommon_casclient.h>
#include <buildboxcommon_fslocalcas.h>
#include <buildboxcommon_temporarydirectory.h>
#include <grpcpp/create_channel.h>
#include <grpcpp/impl/codegen/call.h>
#include <grpcpp/support/sync_stream.h>

#include <tuple>

using namespace buildboxcasd;
using namespace buildboxcommon;

// Runs the test code both with and without an ActionCache instance
enum Cache { ENABLED, DISABLED };

const std::vector<std::string> EMPTY_INSTANCES = {""};
const std::unordered_map<std::string, std::string> EMPTY_INSTANCE_MAPPINGS;

class ProxyNoUpdateFixture : public ::testing::TestWithParam<Cache> {
  protected:
    ProxyNoUpdateFixture()
        : TEST_PROXY_SERVER_ADDRESS("unix://" +
                                    std::string(sockets_directory.name()) +
                                    "/proxy.sock"),
          TEST_REMOTE_SERVER_ADDRESS("unix://" +
                                     std::string(sockets_directory.name()) +
                                     "/remote.sock"),
          local_storage(std::make_shared<FsLocalCas>(
              local_storage_root_directory.name(), true)),
          local_asset_storage(std::make_shared<FsLocalAssetStorage>(
              local_storage_root_directory.name())),
          local_action_storage(std::make_shared<FsLocalActionStorage>(
              local_storage_root_directory.name())),
          grpc_client(std::make_shared<buildboxcommon::GrpcClient>()),
          cas_client(std::make_shared<buildboxcommon::CASClient>(grpc_client)),
          remote_storage(std::make_shared<FsLocalCas>(
              remote_storage_root_directory.name())),
          remote_asset_storage(std::make_shared<FsLocalAssetStorage>(
              remote_storage_root_directory.name())),
          remote_action_storage(std::make_shared<FsLocalActionStorage>(
              remote_storage_root_directory.name())),
          remote_server(std::make_shared<Server>(
              remote_storage, remote_asset_storage, remote_action_storage)),
          proxy_cas_channel(grpc::CreateChannel(
              TEST_PROXY_SERVER_ADDRESS, grpc::InsecureChannelCredentials())),
          cas_stub(ContentAddressableStorage::NewStub(proxy_cas_channel)),
          cas_bytestream_stub(ByteStream::NewStub(proxy_cas_channel)),
          localcas_stub(
              LocalContentAddressableStorage::NewStub(proxy_cas_channel))
    {
        const int proxy_findmissingblobs_cache_ttl_seconds = 5 * 60;

        // Building and starting the remote server:
        remote_server->addLocalServerInstance("");
        remote_server->addListeningPort(TEST_REMOTE_SERVER_ADDRESS);
        remote_server->start();

        // Setting up ConnectionOptions used by the proxy to connect with the
        // remote server:
        buildboxcommon::ConnectionOptions connection_options;
        connection_options.setUrl(TEST_REMOTE_SERVER_ADDRESS);
        connection_options.setInstanceName("");
        connection_options.setRetryDelay("10");
        connection_options.setRetryLimit("1");
        connection_options.setRequestTimeout("10");

        resource_name_prefix = "";
        if (GetParam() == ENABLED) {
            local_server = std::make_shared<Server>(
                local_storage, local_asset_storage, local_action_storage,
                nullptr, nullptr, nullptr, connection_options,
                std::optional(connection_options),
                std::optional(connection_options),
                std::optional<ConnectionOptions>(), EMPTY_INSTANCES,
                EMPTY_INSTANCE_MAPPINGS, true,
                proxy_findmissingblobs_cache_ttl_seconds);
        }
        else {
            local_server = std::make_shared<Server>(
                local_storage, local_asset_storage, local_action_storage,
                nullptr, nullptr, nullptr, connection_options,
                std::optional(connection_options),
                std::optional<ConnectionOptions>(),
                std::optional<ConnectionOptions>(), EMPTY_INSTANCES,
                EMPTY_INSTANCE_MAPPINGS, true,
                proxy_findmissingblobs_cache_ttl_seconds);
        }

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

        local_cas_servicer = std::dynamic_pointer_cast<LocalCasServicer>(
            local_server->localCasServicer());

        ac_servicer = std::dynamic_pointer_cast<ActionCacheServicer>(
            local_server->localAcServicer());

        remote_ac_servicer = std::dynamic_pointer_cast<ActionCacheServicer>(
            remote_server->localAcServicer());

        // Building and starting the proxy server:
        local_server->addListeningPort(TEST_PROXY_SERVER_ADDRESS);
        local_server->start();

        // Initialize the CAS Client for direct access to the remote server
        grpc_client->init(connection_options);
        cas_client->init();
    }

    ~ProxyNoUpdateFixture() {}

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

        return *blob_ptr;
    }

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

        return *blob_ptr;
    }

    void stopRemoteServer()
    {
        remote_server->shutdown();
        remote_server->wait();
    }

    // Directory to contain the sockets where the services will listen:
    buildboxcommon::TemporaryDirectory sockets_directory;
    const std::string TEST_PROXY_SERVER_ADDRESS, TEST_REMOTE_SERVER_ADDRESS;

    // Local proxy:
    buildboxcommon::TemporaryDirectory local_storage_root_directory;
    std::shared_ptr<FsLocalCas> local_storage;
    std::shared_ptr<FsLocalAssetStorage> local_asset_storage;
    std::shared_ptr<FsLocalActionStorage> local_action_storage;
    std::shared_ptr<buildboxcommon::GrpcClient> grpc_client;
    std::shared_ptr<buildboxcommon::CASClient> cas_client;
    std::shared_ptr<Server> local_server;
    std::shared_ptr<CasRemoteExecutionServicer> cas_servicer;
    std::shared_ptr<ActionCacheServicer> ac_servicer;
    std::shared_ptr<LocalCasServicer> local_cas_servicer;
    std::vector<std::string> instance_names = {""};
    std::string resource_name_prefix;

    // Remote CAS server:
    buildboxcommon::TemporaryDirectory remote_storage_root_directory;
    std::shared_ptr<FsLocalCas> remote_storage;
    std::shared_ptr<FsLocalAssetStorage> remote_asset_storage;
    std::shared_ptr<FsLocalActionStorage> remote_action_storage;
    std::shared_ptr<Server> remote_server;
    std::shared_ptr<ActionCacheServicer> remote_ac_servicer;

    // Contexts:
    grpc::ClientContext client_context;
    grpc::ServerContext server_context;

    // gRPC stubs (allow to invoke gRPC calls):
    std::shared_ptr<grpc::Channel> proxy_cas_channel;
    std::unique_ptr<ContentAddressableStorage::Stub> cas_stub;
    std::unique_ptr<ByteStream::Stub> cas_bytestream_stub;
    std::unique_ptr<LocalContentAddressableStorage::Stub> localcas_stub;
};

#endif
