#include "tile/base/future.h"

#include "tile/base/thread/latch.h"

#include "gtest/gtest.h"

namespace tile {
using MoveOnlyType = std::unique_ptr<int>;

static_assert(!std::is_copy_constructible<Future<>>::value, "");
static_assert(!std::is_copy_assignable<Future<>>::value, "");
static_assert(!std::is_copy_constructible<Future<int, char>>::value, "");
static_assert(!std::is_copy_assignable<Future<int, char>>::value, "");

static_assert(std::is_move_constructible<Future<>>::value, "");
static_assert(std::is_move_assignable<Future<>>::value, "");
static_assert(std::is_move_constructible<Future<int, char>>::value, "");
static_assert(std::is_move_assignable<Future<int, char>>::value, "");

static_assert(!std::is_copy_constructible<Promise<>>::value, "");
static_assert(!std::is_copy_assignable<Promise<>>::value, "");
static_assert(!std::is_copy_constructible<Promise<int, double>>::value, "");
static_assert(!std::is_copy_assignable<Promise<int, double>>::value, "");

static_assert(std::is_move_constructible<Promise<>>::value, "");
static_assert(std::is_move_assignable<Promise<>>::value, "");
static_assert(std::is_move_constructible<Promise<int, double>>::value, "");
static_assert(std::is_move_assignable<Promise<int, double>>::value, "");

static_assert(!std::is_copy_constructible<future::Boxed<MoveOnlyType>>::value, "");
static_assert(!std::is_copy_assignable<future::Boxed<MoveOnlyType>>::value, "");
static_assert(std::is_move_constructible<future::Boxed<MoveOnlyType>>::value, "");
static_assert(std::is_move_assignable<future::Boxed<MoveOnlyType>>::value, "");

template<typename T>
using resource_ptr = std::unique_ptr<T, void (*)(T *)>;

struct NoCopy {
    NoCopy()                          = default;
    NoCopy(const NoCopy &)            = delete;
    NoCopy &operator=(const NoCopy &) = delete;
    NoCopy(NoCopy &&)                 = default;
    NoCopy &operator=(NoCopy &&)      = default;
};

Future<resource_ptr<void>, int>
AcquireXxxAsync()
{
    Promise<resource_ptr<void>, int> p;
    auto rc = p.GetFuture();

    NoCopy nc;
    auto moved_nc      = MakeMoveOnCopy(std::move(nc));
    auto moved_promise = MakeMoveOnCopy(p);
    std::thread([moved_promise]() mutable {
        std::this_thread::sleep_for(std::chrono::milliseconds(10));
        auto v1 = resource_ptr<void>(reinterpret_cast<void *>(1), [](void *) {});
        // moved_promise->SetValue(
        //     resource_ptr<void>(reinterpret_cast<void *>(1), [](void *) {}), 0);
        moved_promise->SetValue(std::move(v1), 0);
    }).detach();

    return rc;
}

TEST(Future, Initialization)
{
    // Uninitialized future
    Future<> uf1;
    Future<int, double> uf2;

    // Ready future
    Future<> vf1(futurize_values);
    Future<> tf1(futurize_tuple, std::make_tuple());
    Future<int> fi(10);
    Future<int, double> fid(futurize_values, 1, 2.0);
    Future<int, float> f2(std::move(fid));

    auto vf = MakeReadyFuture();
    auto mf = MakeReadyFuture(1, 2.0);

    ASSERT_EQ(10, BlockingGet(std::move(fi)));
    ASSERT_EQ(2.0, std::get<1>(BlockingGet(std::move(f2))));
    ASSERT_EQ(2.0, std::get<1>(BlockingGet(std::move(mf))));
}

TEST(Future, WhenAll)
{
    static FILE *const kDummyFile = reinterpret_cast<FILE *>(0x10000123);

    Future<FILE *, int> async_file(futurize_values, kDummyFile, 0);
    Future<FILE *, int> failure_file(futurize_values, nullptr, -1);
    Future<resource_ptr<FILE>, int> move_only_file{
        futurize_values, resource_ptr<FILE>{nullptr, nullptr},
         -2
    };

    Future<> void_op(futurize_values);

    bool called = false;

    WhenAll(std::move(async_file), std::move(failure_file), std::move(void_op), std::move(move_only_file))
        .Then([&](std::tuple<FILE *, int> af, std::tuple<FILE *, int> ff, std::tuple<resource_ptr<FILE>, int> mof) {
            ASSERT_EQ(kDummyFile, std::get<0>(af));
            ASSERT_EQ(0, std::get<1>(af));
            ASSERT_EQ(nullptr, std::get<0>(ff));
            ASSERT_EQ(-1, std::get<1>(ff));
            ASSERT_EQ(nullptr, std::get<0>(mof));
            ASSERT_EQ(-2, std::get<1>(mof));

            called = true;
        });

    ASSERT_TRUE(called);
}

TEST(Future, WhenAllForOtherThread)
{
    bool called = false;
    BlockingGet(WhenAll(AcquireXxxAsync(), AcquireXxxAsync())
                    .Then([&](std::tuple<resource_ptr<void>, int> a, std::tuple<resource_ptr<void>, int> b) {
                        ASSERT_NE(nullptr, std::get<0>(a));
                        ASSERT_EQ(0, std::get<1>(a));
                        ASSERT_NE(nullptr, std::get<0>(b));
                        ASSERT_EQ(0, std::get<1>(b));
                        called = true;
                    }));
    ASSERT_TRUE(called);
}

TEST(Future, WhenAllMultiThreaded)
{
    for (int i = 0; i != 100; ++i) {
        bool called = false;
        Promise<std::unique_ptr<int>, std::unique_ptr<char>> p1;
        Promise<> p2;
        Latch latch(2 + 1);

        auto all = WhenAll(p1.GetFuture(), p2.GetFuture())
                       .Then([&](std::tuple<std::unique_ptr<int>, std::unique_ptr<char>> &&p) {
                           ASSERT_NE(nullptr, std::get<0>(p));
                           ASSERT_EQ(nullptr, std::get<1>(p));

                           called = true;
                       });

        auto t1 = std::thread([&] {
            latch.ArriveAndWait();
            // set <ptr, null>
            p1.SetValue(make_unique<int>(), std::unique_ptr<char>());
        });
        auto t2 = std::thread([&] {
            latch.ArriveAndWait();

            p2.SetValue();
        });

        ASSERT_FALSE(called);
        latch.CountDown();
        BlockingGet(std::move(all));
        ASSERT_TRUE(called);

        t1.join();
        t2.join();
    }
}

TEST(Future, WhenAllcollectionMultiThreaded)
{
    for (int i = 0; i != 100; ++i) {
        constexpr auto kCount = 100;

        std::vector<Promise<bool>> vps(kCount);
        std::vector<Future<bool>> vfs;
        std::vector<std::thread> ts(kCount);

        Latch latch(kCount + 1);
        std::atomic<bool> called{false};
        for (auto &p : vps) { vfs.emplace_back(p.GetFuture()); }

        WhenAll(&vfs).Then([&](std::vector<bool> v) {
            ASSERT_TRUE(std::all_of(v.begin(), v.end(), [&](bool v) { return v; }));
            called = true;
        });

        for (int i = 0; i != kCount; ++i) {
            ts[i] = std::thread([&latch, &vps, i] {
                latch.ArriveAndWait();
                vps[i].SetValue(true);
            });
        }
        ASSERT_FALSE(called);
        latch.CountDown();
        for (auto &&e : ts) { e.join(); }
        ASSERT_TRUE(called);
    }
}

TEST(Future, WhenAllCollectionEmpty)
{
    {
        std::vector<Future<>> vfs;
        int x{};
        WhenAll(std::move(vfs)).Then([&] { x = 10; });
        ASSERT_EQ(10, x);
    }

    {
        std::vector<Future<int>> vfs;
        int x{};
        WhenAll(std::move(vfs)).Then([&](std::vector<int> &&) { x = 10; });
        ASSERT_EQ(10, x);
    }
}

TEST(Future, WhenAllForCollectionOfEmptyFuture)
{
    std::vector<Future<>> vfs;
    bool cont_called = false;

    for (int i = 0; i != 1000; ++i) { vfs.emplace_back(futurize_values); }

    // std::vector<Future<>> is special, the continuation is called with
    // no argument.
    WhenAll(std::move(vfs)).Then([&] { cont_called = true; });
    ASSERT_TRUE(cont_called);
}

TEST(Future, WhenAnyCollectionOfEmptyFuture)
{
    std::vector<Future<>> vfs;
    bool called = false;
    for (int i = 0; i != 1000; ++i) { vfs.emplace_back(futurize_values); }

    WhenAny(std::move(vfs)).Then([&](std::size_t index) { called = true; });
    ASSERT_TRUE(called);
}

TEST(Future, WhenAnyCollectionMultiThreaded)
{
    for (int i = 0; i != 100; ++i) {
        constexpr auto kCount = 100;
        std::vector<Promise<std::unique_ptr<int>, char>> vps(kCount);
        std::vector<Future<int>> vfs;
        std::vector<std::thread> ts;
        Latch latch(kCount + 1);
        std::atomic<int> x{};

        for (auto &&e : vps) {
            vfs.emplace_back(e.GetFuture().Then([&](std::unique_ptr<int> &&setter_index, char &&) {
                ++x;
                return *setter_index;
            }));
        }

        auto all = WhenAny(std::move(vfs));
        ASSERT_EQ(0, x);

        for (int i = 0; i != vps.size(); ++i) {
            auto ep = &vps[i];
            ts.push_back(std::thread([&, ep, i] {
                latch.ArriveAndWait();
                ep->SetValue(make_unique<int>(i), 'a');
            }));
        }
        ASSERT_EQ(0, x);

        std::thread starter([&] {
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
            latch.CountDown();
        });

        auto &&res   = BlockingGet(&all);
        auto &&index = std::get<0>(res);
        auto &&value = std::get<1>(res);

        ASSERT_GE(kCount, index);
        ASSERT_LE(0, index);
        ASSERT_EQ(index, value);

        for (auto &&e : ts) { e.join(); }
        starter.join();
        ASSERT_EQ(kCount, x);
    }
}

TEST(Future, DurationTimeout)
{
    auto one_sec = std::chrono::seconds(1);
    {
        Promise<int> p;
        auto rc = BlockingTryGet(p.GetFuture(), one_sec);
        ASSERT_FALSE(rc);
        p.SetValue(10);
    }
    {
        Promise<int> p;
        auto f  = p.GetFuture();
        auto rc = BlockingTryGet(&f, one_sec);
        ASSERT_FALSE(rc);
    }
    {
        Promise<> p;
        auto f  = p.GetFuture();
        auto rc = BlockingTryGet(&f, one_sec);
        ASSERT_FALSE(rc);
    }
}

}// namespace tile
