#include <sdbusplus/async.hpp>

#include <gtest/gtest.h>

struct Context : public testing::Test
{
    ~Context() noexcept override = default;

    void TearDown() override
    {
        // Destructing the context can throw, so we have to do it in
        // the TearDown in order to make our destructor noexcept.
        ctx.reset();
    }

    void spawnStop()
    {
        ctx->spawn(stdexec::just() |
                   stdexec::then([this]() { ctx->request_stop(); }));
    }

    void runToStop()
    {
        spawnStop();
        ctx->run();
    }

    std::optional<sdbusplus::async::context> ctx{std::in_place};
};

TEST_F(Context, RunSimple)
{
    runToStop();
}

TEST_F(Context, SpawnedTask)
{
    ctx->spawn(stdexec::just());
    runToStop();
}

TEST_F(Context, ReentrantRun)
{
    runToStop();
    for (int i = 0; i < 100; ++i)
    {
        ctx->run();
    }
}

TEST_F(Context, SpawnThrowingTask)
{
    ctx->spawn(stdexec::just() |
               stdexec::then([]() { throw std::logic_error("Oops"); }));

    EXPECT_THROW(runToStop(), std::logic_error);
    ctx->run();
}

TEST_F(Context, SpawnManyThrowingTasks)
{
    static constexpr size_t count = 100;
    for (size_t i = 0; i < count; ++i)
    {
        ctx->spawn(stdexec::just() |
                   stdexec::then([]() { throw std::logic_error("Oops"); }));
    }
    spawnStop();

    for (size_t i = 0; i < count; ++i)
    {
        EXPECT_THROW(ctx->run(), std::logic_error);
    }
    ctx->run();
}

TEST_F(Context, SpawnDelayedTask)
{
    using namespace std::literals;
    static constexpr auto timeout = 500ms;

    auto start = std::chrono::steady_clock::now();

    bool ran = false;
    ctx->spawn(sdbusplus::async::sleep_for(*ctx, timeout) |
               stdexec::then([&ran]() { ran = true; }));

    runToStop();

    auto stop = std::chrono::steady_clock::now();

    EXPECT_TRUE(ran);
    EXPECT_GT(stop - start, timeout);
    EXPECT_LT(stop - start, timeout * 3);
}

TEST_F(Context, SpawnRecursiveTask)
{
    struct _
    {
        static auto one(size_t count, size_t& executed)
            -> sdbusplus::async::task<size_t>
        {
            if (count)
            {
                ++executed;
                co_return (co_await one(count - 1, executed)) + 1;
            }
            co_return co_await stdexec::just(0);
        }
    };

    static constexpr size_t count = 100;
    size_t executed = 0;

    ctx->spawn(_::one(count, executed) |
               stdexec::then([=](auto result) { EXPECT_EQ(result, count); }));

    runToStop();

    EXPECT_EQ(executed, count);
}

TEST_F(Context, DestructMatcherWithPendingAwait)
{
    using namespace std::literals;

    bool ran = false;
    auto m = std::make_optional<sdbusplus::async::match>(
        *ctx, sdbusplus::bus::match::rules::interfacesAdded(
                  "/this/is/a/bogus/path/for/SpawnMatcher"));

    // Await the match completion (which will never happen).
    ctx->spawn(m->next() | stdexec::then([&ran](...) { ran = true; }));

    // Destruct the match.
    ctx->spawn(sdbusplus::async::sleep_for(*ctx, 1ms) |
               stdexec::then([&m](...) { m.reset(); }));

    EXPECT_THROW(runToStop(), sdbusplus::exception::UnhandledStop);
    EXPECT_NO_THROW(ctx->run());
    EXPECT_FALSE(ran);
}

TEST_F(Context, DestructMatcherWithPendingAwaitAsTask)
{
    using namespace std::literals;

    auto m = std::make_optional<sdbusplus::async::match>(
        *ctx, sdbusplus::bus::match::rules::interfacesAdded(
                  "/this/is/a/bogus/path/for/SpawnMatcher"));

    struct _
    {
        static auto fn(decltype(m->next()) snd, bool& ran)
            -> sdbusplus::async::task<>
        {
            co_await std::move(snd);
            ran = true;
            co_return;
        }
    };

    bool ran = false;
    ctx->spawn(_::fn(m->next(), ran));
    ctx->spawn(sdbusplus::async::sleep_for(*ctx, 1ms) |
               stdexec::then([&]() { m.reset(); }));

    EXPECT_THROW(runToStop(), sdbusplus::exception::UnhandledStop);
    EXPECT_NO_THROW(ctx->run());
    EXPECT_FALSE(ran);
}