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

#include <buildboxcommon_protos.h>

#include <gtest/gtest.h>

#include <mutex>
#include <thread>

#include <chrono>

class DigestOperationSerializerFixture : public testing::Test {
  protected:
    DigestOperationSerializerFixture() {}

    buildboxcasd::DigestOperationQueue d_Serializer;
};

TEST_F(DigestOperationSerializerFixture, TestSingleCall)
{
    unsigned int numberOfInvocations = 0;
    buildboxcommon::Digest digestGiven;
    auto f = [&numberOfInvocations,
              &digestGiven](const buildboxcommon::Digest &d) {
        numberOfInvocations++;
        digestGiven = d;
    };

    buildboxcommon::Digest digest;
    digest.set_hash("hash");
    digest.set_size_bytes(123);
    d_Serializer.runExclusively(digest, f);

    ASSERT_EQ(numberOfInvocations, 1);
    ASSERT_EQ(digestGiven, digest);
}

TEST_F(DigestOperationSerializerFixture, TestTwoNonOverlappingCalls)
{
    std::set<buildboxcommon::Digest> digestArguments;
    auto f = [&digestArguments](const buildboxcommon::Digest &digest) {
        digestArguments.insert(digest);
    };

    buildboxcommon::Digest digest1;
    digest1.set_hash("hash1");
    digest1.set_size_bytes(123);

    buildboxcommon::Digest digest2;
    digest2.set_hash("hash2");
    digest2.set_size_bytes(456);

    std::thread thread(
        [this, &f, &digest1]() { d_Serializer.runExclusively(digest1, f); });
    d_Serializer.runExclusively(digest2, f);

    thread.join();

    ASSERT_EQ(digestArguments.size(), 2);
    ASSERT_TRUE(digestArguments.count(digest1));
    ASSERT_TRUE(digestArguments.count(digest2));
}

TEST_F(DigestOperationSerializerFixture, TestTwoSimultaneousCallsComplete)
{
    std::mutex orderMutex;
    std::vector<buildboxcommon::Digest> executionOrder;
    const auto f = [&](const buildboxcommon::Digest &digest) {
        std::lock_guard<std::mutex> lock(orderMutex);
        executionOrder.push_back(digest);
    };

    buildboxcommon::Digest digest;
    digest.set_hash("hash");
    digest.set_size_bytes(1234);

    std::thread thread(
        [this, &f, &digest]() { d_Serializer.runExclusively(digest, f); });

    d_Serializer.runExclusively(digest, f);

    thread.join();
    ASSERT_EQ(executionOrder.size(), 2);
    ASSERT_EQ(executionOrder[0], digest);
    ASSERT_EQ(executionOrder[1], digest);
}

TEST_F(DigestOperationSerializerFixture, TestOperationsAreExecutedSequentially)
{
    std::chrono::steady_clock::time_point f1Start;
    std::chrono::steady_clock::time_point f1End;
    int f1NumberOfInvocations = 0;

    const buildboxcasd::DigestOperationQueue::DigestOperation f1 =
        [&f1Start, &f1End,
         &f1NumberOfInvocations](const buildboxcommon::Digest &) {
            f1Start = std::chrono::steady_clock::now();

            f1NumberOfInvocations++;
            std::this_thread::sleep_for(std::chrono::seconds(2));

            f1End = std::chrono::steady_clock::now();
        };

    std::chrono::steady_clock::time_point f2Start;
    std::chrono::steady_clock::time_point f2End;
    int f2NumberOfInvocations = 0;

    const buildboxcasd::DigestOperationQueue::DigestOperation f2 =
        [&f2Start, &f2End,
         &f2NumberOfInvocations](const buildboxcommon::Digest &) {
            f2Start = std::chrono::steady_clock::now();

            f2NumberOfInvocations++;
            std::this_thread::sleep_for(std::chrono::seconds(2));

            f2End = std::chrono::steady_clock::now();
        };

    buildboxcommon::Digest digest;
    digest.set_hash("hash");
    digest.set_size_bytes(1234);

    std::thread thread1([&]() { d_Serializer.runExclusively(digest, f1); });
    std::thread thread2([&]() { d_Serializer.runExclusively(digest, f2); });

    thread1.join();
    thread2.join();

    // `f1()` and `f2()` ran sequentially:
    ASSERT_EQ(f1NumberOfInvocations, 1);
    ASSERT_EQ(f2NumberOfInvocations, 1);
    ASSERT_TRUE(f1Start >= f2End || f2Start >= f1End);
}
