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

#include <buildboxcasd_fslocalactionstorage.h>
#include <buildboxcasd_localacproxyinstance.h>
#include <buildboxcasd_localcasinstance.h>
#include <buildboxcasd_proxymodefixture.h>
#include <buildboxcommon_casclient.h>
#include <buildboxcommon_filestager.h>
#include <buildboxcommon_fileutils.h>
#include <buildboxcommon_fslocalcas.h>
#include <buildboxcommon_grpcclient.h>
#include <buildboxcommon_logging.h>
#include <buildboxcommon_lrulocalcas.h>
#include <buildboxcommon_temporarydirectory.h>
#include <cstdlib>
#include <grpcpp/server_context.h>
#include <gtest/gtest.h>
#include <memory>

using namespace buildboxcasd;

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

class CasProxyFixture : public CasProxyModeFixture {
  protected:
    CasProxyFixture() {}

  public:
    ActionResult action_result_a;
    Digest action_digest_a;
    Digest outfile_digest_a;
    Digest stdout_digest_a;
    Digest stderr_digest_a;
    UpdateActionResultRequest update_request_a;
    GetActionResultRequest get_request_a;

    void SetUp() override
    {
        // Test ActionResult "A" to be used by test
        outfile_digest_a = DigestGenerator::hash("testa");
        stdout_digest_a = DigestGenerator::hash("stdouta");
        stderr_digest_a = DigestGenerator::hash("stderra");

        action_result_a.set_exit_code(static_cast<google::protobuf::int32>(8));
        action_result_a.mutable_stdout_digest()->CopyFrom(stdout_digest_a);
        action_result_a.mutable_stderr_digest()->CopyFrom(stderr_digest_a);
        OutputFile outfile_a;
        outfile_a.mutable_digest()->CopyFrom(outfile_digest_a);
        *action_result_a.add_output_files() = outfile_a;

        action_digest_a = DigestGenerator::hash("testa");
        update_request_a.mutable_action_digest()->CopyFrom(action_digest_a);
        update_request_a.mutable_action_result()->CopyFrom(action_result_a);
        get_request_a.mutable_action_digest()->CopyFrom(action_digest_a);
    }

    CasProxyFixture(const CasProxyFixture &other) = delete;
    CasProxyFixture(CasProxyFixture &&other) = delete;
    CasProxyFixture &operator=(const CasProxyFixture &other) = delete;
    CasProxyFixture &operator=(CasProxyFixture &&other) = delete;

  public:
    ~CasProxyFixture() override {}
};

TEST_P(CasProxyFixture, GetInCacheActionResult)
{
    ActionResult result_buffer;

    // Given: Data in local storage
    local_storage->writeBlob(outfile_digest_a, "testa");
    local_storage->writeBlob(stdout_digest_a, "stdouta");
    local_storage->writeBlob(stderr_digest_a, "stderra");

    // When: Update ActionResult to local server
    ASSERT_TRUE(ac_servicer
                    ->UpdateActionResult(&server_context, &update_request_a,
                                         &result_buffer)
                    .ok());
    EXPECT_EQ(result_buffer.exit_code(), action_result_a.exit_code());
    EXPECT_EQ(result_buffer.stdout_digest().hash(),
              action_result_a.stdout_digest().hash());

    // Then: Fetching it back to confirm it's there
    ASSERT_TRUE(
        ac_servicer
            ->GetActionResult(&server_context, &get_request_a, &result_buffer)
            .ok());
    EXPECT_EQ(result_buffer.exit_code(), action_result_a.exit_code());
    EXPECT_EQ(result_buffer.stdout_digest().hash(),
              action_result_a.stdout_digest().hash());
    // Then: Fetch it again is successful
    ASSERT_TRUE(
        ac_servicer
            ->GetActionResult(&server_context, &get_request_a, &result_buffer)
            .ok());
}

TEST_P(CasProxyFixture, GetNotInCacheActionResult)
{
    ActionResult result_buffer;

    // Given: Data in remote storage
    remote_storage->writeBlob(outfile_digest_a, "testa");
    remote_storage->writeBlob(stdout_digest_a, "stdouta");
    remote_storage->writeBlob(stderr_digest_a, "stderra");

    // When: Update ActionResult to remote server
    ASSERT_TRUE(remote_ac_servicer
                    ->UpdateActionResult(&server_context, &update_request_a,
                                         &result_buffer)
                    .ok());
    EXPECT_EQ(result_buffer.exit_code(), action_result_a.exit_code());
    EXPECT_EQ(result_buffer.stdout_digest().hash(),
              action_result_a.stdout_digest().hash());

    // Then: Fetching it from local will cause it to fetch from remote.
    ASSERT_TRUE(
        ac_servicer
            ->GetActionResult(&server_context, &get_request_a, &result_buffer)
            .ok());
    EXPECT_EQ(result_buffer.exit_code(), action_result_a.exit_code());
    EXPECT_EQ(result_buffer.stdout_digest().hash(),
              action_result_a.stdout_digest().hash());
    // Then: Fetch it again is successful
    ASSERT_TRUE(
        ac_servicer
            ->GetActionResult(&server_context, &get_request_a, &result_buffer)
            .ok());
}

TEST_P(CasProxyFixture, PropagateActionResultToRemote)
{
    auto result_buffer = std::make_shared<ActionResult>();

    // Given: Data in local and remote storage
    local_storage->writeBlob(outfile_digest_a, "testa");
    local_storage->writeBlob(stdout_digest_a, "stdouta");
    local_storage->writeBlob(stderr_digest_a, "stderra");
    remote_storage->writeBlob(outfile_digest_a, "testa");
    remote_storage->writeBlob(stdout_digest_a, "stdouta");
    remote_storage->writeBlob(stderr_digest_a, "stderra");

    // When: Update ActionResult to local server
    ASSERT_TRUE(ac_servicer
                    ->UpdateActionResult(&server_context, &update_request_a,
                                         result_buffer.get())
                    .ok());
    EXPECT_EQ(result_buffer->exit_code(), action_result_a.exit_code());
    EXPECT_EQ(result_buffer->stdout_digest().hash(),
              action_result_a.stdout_digest().hash());

    // Then: ActionResult will have propagated to remote server
    ASSERT_TRUE(remote_ac_servicer
                    ->GetActionResult(&server_context, &get_request_a,
                                      result_buffer.get())
                    .ok());
    EXPECT_EQ(result_buffer->exit_code(), action_result_a.exit_code());
    EXPECT_EQ(result_buffer->stdout_digest().hash(),
              action_result_a.stdout_digest().hash());
}

TEST_P(CasProxyFixture, BlobsMissingFromRemoteCas)
{
    auto result_buffer = std::make_shared<ActionResult>();

    // Given: Data in local storage
    local_storage->writeBlob(outfile_digest_a, "testa");
    local_storage->writeBlob(stdout_digest_a, "stdouta");
    local_storage->writeBlob(stderr_digest_a, "stderra");

    // When: Update ActionResult to local server
    ASSERT_TRUE(ac_servicer
                    ->UpdateActionResult(&server_context, &update_request_a,
                                         result_buffer.get())
                    .ok());
    EXPECT_EQ(result_buffer->exit_code(), action_result_a.exit_code());
    EXPECT_EQ(result_buffer->stdout_digest().hash(),
              action_result_a.stdout_digest().hash());

    // Then: ActionResult will have propagated to remote server but it's NOT
    // obtainable due to blobs missing from remote CAS
    ASSERT_FALSE(remote_ac_servicer
                     ->GetActionResult(&server_context, &get_request_a,
                                       result_buffer.get())
                     .ok());
}

INSTANTIATE_TEST_SUITE_P(
    StaticInstance, CasProxyFixture,
    testing::Combine(testing::Values(STATIC_INSTANCE),
                     testing::Values(FINDMISSINGBLOBS_CACHE_ON),
                     testing::ValuesIn(TEST_INSTANCE_NAMES),
                     testing::Values(NON_THREADED, THREADED),
                     testing::Values(AC_VALIDATION_CACHE_OFF,
                                     AC_VALIDATION_CACHE_ON)));
