/*
 * 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 <build/bazel/remote/asset/v1/remote_asset.grpc.pb.h>
#include <buildboxcasd_fslocalassetstorage.h>
#include <buildboxcasd_localraproxyinstance.h>
#include <buildboxcasd_server.h>
#include <buildboxcommon_connectionoptions.h>
#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_grpcerror.h>
#include <buildboxcommon_temporarydirectory.h>

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

using namespace buildboxcasd;
using namespace buildboxcommon;
using namespace build::bazel::remote::asset::v1;

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

class RaProxyInstanceTest // NOLINT
                          // (cppcoreguidelines-special-member-functions)
    : public ::testing::Test {
  protected:
    RaProxyInstanceTest()
        : sockets_directory(),
          TEST_REMOTE_SERVER_ADDRESS("unix://" +
                                     std::string(sockets_directory.name()) +
                                     "/remote.sock"),
          local_storage_directory(), remote_storage_directory(),
          local_asset_storage(std::make_shared<FsLocalAssetStorage>(
              local_storage_directory.name())),
          remote_asset_storage(std::make_shared<FsLocalAssetStorage>(
              remote_storage_directory.name())),
          remote_server(
              std::make_shared<Server>(nullptr, remote_asset_storage, nullptr))
    {
        // Set up and start remote server
        remote_server->addLocalServerInstance("");
        remote_server->addListeningPort(TEST_REMOTE_SERVER_ADDRESS);
        remote_server->start();

        // Set up connection options pointing to real remote server
        connection_options.setUrl(TEST_REMOTE_SERVER_ADDRESS);
        connection_options.setInstanceName("");
        connection_options.setRetryDelay("1");
        connection_options.setRetryLimit("1");
        connection_options.setRequestTimeout("10");

        // Create the proxy instance with real remote connection
        proxy_instance = std::make_unique<LocalRaProxyInstance>(
            local_asset_storage, connection_options);
    }

    ~RaProxyInstanceTest() override
    {
        if (remote_server) {
            remote_server->shutdown(gpr_time_0(GPR_TIMESPAN));
            remote_server->wait();
        }
    }

    buildboxcommon::TemporaryDirectory sockets_directory;
    const std::string TEST_REMOTE_SERVER_ADDRESS;

    buildboxcommon::TemporaryDirectory local_storage_directory;
    buildboxcommon::TemporaryDirectory remote_storage_directory;

    std::shared_ptr<FsLocalAssetStorage> local_asset_storage;
    std::shared_ptr<FsLocalAssetStorage> remote_asset_storage;
    std::shared_ptr<Server> remote_server;

    buildboxcommon::ConnectionOptions connection_options;
    std::unique_ptr<LocalRaProxyInstance> proxy_instance;
};

TEST_F(RaProxyInstanceTest, PushAndFetchBlob)
{
    const std::string test_uri = "test://blob/uri";
    const std::string test_content = "test blob content";

    // Verify blob is not in local storage initially
    FetchBlobRequest fetchRequest;
    fetchRequest.add_uris(test_uri);
    FetchBlobResponse fetchResponse;

    EXPECT_FALSE(local_asset_storage->lookup(fetchRequest, &fetchResponse));

    // Verify fetch returns NOT_FOUND initially (nothing in local or remote)
    try {
        grpc::Status fetchStatus =
            proxy_instance->FetchBlob(fetchRequest, &fetchResponse);
        FAIL() << "Expected GrpcError to be thrown";
    }
    catch (const buildboxcommon::GrpcError &e) {
        // Expected when asset not found in remote
        EXPECT_EQ(e.status.error_code(), grpc::StatusCode::NOT_FOUND);
    }

    // Push the blob
    PushBlobRequest pushRequest;
    pushRequest.add_uris(test_uri);
    pushRequest.mutable_blob_digest()->CopyFrom(
        DigestGenerator::hash(test_content));
    PushBlobResponse pushResponse;

    grpc::Status pushStatus =
        proxy_instance->PushBlob(pushRequest, &pushResponse);
    EXPECT_TRUE(pushStatus.ok());

    // Verify blob is now in local storage
    EXPECT_TRUE(local_asset_storage->lookup(fetchRequest, &fetchResponse));

    // Verify fetch now succeeds (should be in local cache after push)
    grpc::Status fetchStatus2 =
        proxy_instance->FetchBlob(fetchRequest, &fetchResponse);
    EXPECT_TRUE(fetchStatus2.ok());
    EXPECT_EQ(fetchResponse.uri(), test_uri);
}

TEST_F(RaProxyInstanceTest, FetchBlobMissingReturnsNotFound)
{
    // Test fetching a blob that doesn't exist anywhere
    FetchBlobRequest request;
    request.add_uris("test://missing/blob");
    FetchBlobResponse response;

    try {
        grpc::Status status = proxy_instance->FetchBlob(request, &response);
        FAIL() << "Expected GrpcError to be thrown";
    }
    catch (const buildboxcommon::GrpcError &e) {
        // Expected when asset not found
        EXPECT_EQ(e.status.error_code(), grpc::StatusCode::NOT_FOUND);
    }
}

TEST_F(RaProxyInstanceTest, FetchDirectoryMissingReturnsNotFound)
{
    // Test fetching a directory that doesn't exist anywhere
    FetchDirectoryRequest request;
    request.add_uris("test://missing/directory");
    FetchDirectoryResponse response;

    try {
        grpc::Status status =
            proxy_instance->FetchDirectory(request, &response);
        FAIL() << "Expected GrpcError to be thrown";
    }
    catch (const buildboxcommon::GrpcError &e) {
        // Expected when asset not found
        EXPECT_EQ(e.status.error_code(), grpc::StatusCode::NOT_FOUND);
    }
}

TEST_F(RaProxyInstanceTest, PushAndFetchDirectory)
{
    const std::string test_uri = "test://directory/uri";
    const std::string test_digest_content = "test directory digest";

    // Verify directory is not in local storage initially
    FetchDirectoryRequest fetchRequest;
    fetchRequest.add_uris(test_uri);
    FetchDirectoryResponse fetchResponse;

    EXPECT_FALSE(local_asset_storage->lookup(fetchRequest, &fetchResponse));

    // Verify fetch returns NOT_FOUND initially
    try {
        grpc::Status fetchStatus =
            proxy_instance->FetchDirectory(fetchRequest, &fetchResponse);
        FAIL() << "Expected GrpcError to be thrown";
    }
    catch (const buildboxcommon::GrpcError &e) {
        // Expected when asset not found in remote
        EXPECT_EQ(e.status.error_code(), grpc::StatusCode::NOT_FOUND);
    }

    // Push the directory
    PushDirectoryRequest pushRequest;
    pushRequest.add_uris(test_uri);
    pushRequest.mutable_root_directory_digest()->CopyFrom(
        DigestGenerator::hash(test_digest_content));
    PushDirectoryResponse pushResponse;

    grpc::Status pushStatus =
        proxy_instance->PushDirectory(pushRequest, &pushResponse);
    EXPECT_TRUE(pushStatus.ok());

    // Verify directory is now in local storage
    EXPECT_TRUE(local_asset_storage->lookup(fetchRequest, &fetchResponse));

    // Verify fetch now succeeds (should be in local cache after push)
    grpc::Status fetchStatus2 =
        proxy_instance->FetchDirectory(fetchRequest, &fetchResponse);
    EXPECT_TRUE(fetchStatus2.ok());
    EXPECT_EQ(fetchResponse.uri(), test_uri);
}
