BareGit
#include "thread_pool.h"

#include <atomic>
#include <gtest/gtest.h>
#include <latch>
#include <memory>
#include <limits>
#include <stdexcept>
#include <string>

namespace
{

void increment(std::atomic<int>& count)
{
    ++count;
}

void block(std::shared_future<void> release, std::latch& started,
           std::atomic<int>& completed)
{
    started.count_down();
    release.wait();
    ++completed;
}

void fail()
{
    throw std::runtime_error("Task failed");
}

void assignValue(const std::unique_ptr<int>& value, std::atomic<int>& result)
{
    result = *value;
}

TEST(ThreadPool, CapacityAndReuse)
{
    std::atomic<int> completed{0};
    std::latch started(2);
    auto pool = ThreadPool::create(2).value();
    // Promise destruction releases waiters after a failed assertion.
    std::promise<void> release;
    const auto ready = release.get_future().share();
    auto first = pool->trySubmit(
        std::bind_front(block, ready, std::ref(started), std::ref(completed)))
            .value();
    auto second = pool->trySubmit(
        std::bind_front(block, ready, std::ref(started), std::ref(completed)))
            .value();
    ASSERT_TRUE(first.has_value() && second.has_value())
        << "Accept up to capacity";
    started.wait();
    auto full = pool->trySubmit(
        std::bind_front(increment, std::ref(completed)));
    ASSERT_TRUE(full.has_value() && !*full) << "Full pool is not an error";
    release.set_value();
    first->get();
    second->get();
    pool->waitIdle();
    EXPECT_EQ(completed.load(), 2) << "Rejected task never executes";
    auto next = pool->trySubmit(
        std::bind_front(increment, std::ref(completed))).value();
    ASSERT_TRUE(next.has_value()) << "Reuse a worker";
    next->get();
    pool->waitIdle();
    EXPECT_EQ(completed.load(), 3) << "Execute reused worker task";
}

TEST(ThreadPool, MoveOnlyTasks)
{
    std::atomic<int> result{0};
    auto pool = ThreadPool::create(1).value();
    auto next = pool->trySubmit(std::bind_front(
        assignValue, std::make_unique<int>(42), std::ref(result))).value();
    ASSERT_TRUE(next.has_value()) << "Accept move-only task";
    next->get();
    pool->waitIdle();
    EXPECT_EQ(result.load(), 42) << "Execute move-only task";
}

void submitBlocked(ThreadPool& pool, std::shared_future<void> release,
                   std::latch& started, std::atomic<int>& completed,
                   std::atomic<int>& accepted)
{
    auto submitted = pool.trySubmit(std::bind_front(
        block, release, std::ref(started), std::ref(completed)));
    if(submitted && *submitted)
    {
        ++accepted;
    }
}

TEST(ThreadPool, ConcurrentSubmission)
{
    std::atomic<int> completed{0};
    std::atomic<int> accepted{0};
    std::latch started(4);
    auto pool = ThreadPool::create(4).value();
    std::promise<void> release;
    const auto ready = release.get_future().share();
    std::vector<std::jthread> producers;
    for(int i = 0; i < 16; ++i)
    {
        producers.emplace_back(submitBlocked, std::ref(*pool), ready,
                               std::ref(started), std::ref(completed),
                               std::ref(accepted));
    }
    producers.clear();
    EXPECT_EQ(accepted.load(), 4) << "Concurrent submissions respect capacity";
    started.wait();
    release.set_value();
    pool->waitIdle();
    EXPECT_EQ(completed.load(), 4) << "Execute each accepted task once";
}

TEST(ThreadPool, Destruction)
{
    std::atomic<int> completed{0};
    std::latch started(2);
    std::optional<std::future<void>> first;
    std::optional<std::future<void>> second;
    {
        auto pool = ThreadPool::create(2).value();
        std::promise<void> release;
        const auto ready = release.get_future().share();
        first = pool->trySubmit(std::bind_front(
            block, ready, std::ref(started), std::ref(completed))).value();
        second = pool->trySubmit(std::bind_front(
            block, ready, std::ref(started), std::ref(completed))).value();
        ASSERT_TRUE(first.has_value() && second.has_value()) << "Accept tasks";
        // Promise destruction releases work immediately before pool teardown.
    }
    first->get();
    second->get();
    EXPECT_EQ(completed.load(), 2) << "Destruction finishes all accepted work";
}

TEST(ThreadPool, InvalidArguments)
{
    auto invalid = ThreadPool::create(0);
    ASSERT_TRUE(!invalid) << "Reject zero capacity";
    ASSERT_TRUE(!invalid.error().msg().empty()) << "Explain invalid capacity";
    EXPECT_THROW(ThreadPool::create(
        std::numeric_limits<std::size_t>::max()), std::length_error);
    auto pool = ThreadPool::create(1).value();
    auto empty = pool->trySubmit({});
    ASSERT_TRUE(!empty) << "Reject empty task with an error";
    ASSERT_TRUE(!empty.error().msg().empty()) << "Explain invalid task";
    std::atomic<int> completed{0};
    auto valid = pool->trySubmit(
        std::bind_front(increment, std::ref(completed)));
    ASSERT_TRUE(valid && *valid)
        << "Invalid submission leaves capacity available";
    (*valid)->get();
    pool->waitIdle();
}

}

TEST(ThreadPoolDeathTest, UnexpectedTaskExceptionTerminates)
{
    ASSERT_DEATH({
        auto pool = ThreadPool::create(1).value();
        auto submitted = pool->trySubmit(fail).value();
        submitted->get();
    }, "");
}