#include "gtest/gtest.h"
#include "gmock/gmock.h"
#include "async/uveventloop.h"
#include "async/uvsignal.h"
#include "async/uvpipe.h"
#include "utils/uniqueptrcast.h"
#include <algorithm>
#include <experimental/filesystem>
#include <vector>

using namespace ::testing;
using namespace myapp::async;
using namespace myapp::utils;

namespace fs = std::experimental::filesystem;

class UvAsyncTests : public Test
{
public:
    UvAsyncTests()
    {
        loop = std::static_pointer_cast<uv_event_loop>(event_loop::create());
    }

    std::shared_ptr<uv_event_loop> loop;
};

TEST_F(UvAsyncTests, UvEventLoop_Destructor_CloseAllHandles)
{
    using handle_ptr_t = std::shared_ptr<uvw::TimerHandle>;
    std::vector<handle_ptr_t> vec;
    std::generate_n(std::back_inserter(vec), 3, [this]{
        return loop->handle().resource<typename handle_ptr_t::element_type>();
    });

    loop = nullptr;

    auto all_closing = std::all_of(begin(vec), end(vec), [](handle_ptr_t const& handle) {
        return handle->closing();
    });
    EXPECT_TRUE(all_closing);

    auto all_unreferenced = std::none_of(begin(vec), end(vec), [](handle_ptr_t const& handle) {
        return handle->referenced();
    });
    EXPECT_TRUE(all_unreferenced);
}

TEST_F(UvAsyncTests, UvSignal_Destructor_CloseHandle)
{
    auto instance = signal::create(loop);
    auto handle = static_unique_pointer_cast<uv_signal>(instance)->handle().shared_from_this();

    instance = nullptr;

    ASSERT_TRUE(handle->closing());
}

TEST_F(UvAsyncTests, UvPipe_Destructor_CloseHandle)
{
    auto instance = pipe::create(loop);
    auto handle = static_unique_pointer_cast<uv_pipe>(instance)->handle().shared_from_this();

    instance = nullptr;

    ASSERT_TRUE(handle->closing());
}

TEST_F(UvAsyncTests, UvPipe_Bind_Socket)
{
    auto instance = pipe::create(loop);
    auto tmpsockpath = fs::u8path(u8"/tmp/myapptest_uvpipetests.sock");

    instance->on_error([&](auto async_result)
    {
        EXPECT_THROW({ async_result(); }, std::runtime_error);
        FAIL() << "Bind error";
    });

    instance->bind(tmpsockpath);

    EXPECT_TRUE(fs::is_socket(tmpsockpath));

    instance = nullptr;

    EXPECT_FALSE(fs::is_socket(tmpsockpath));
}

TEST_F(UvAsyncTests, UvPipe_Bind_Existent_Throws)
{
    auto instance = pipe::create(loop);
    auto called = false;

    instance->on_error([&](auto async_result)
    {
        called = true;
        EXPECT_THROW({ async_result(); }, std::runtime_error);
    });

    instance->bind("/dev/null");

    loop->run();
    ASSERT_TRUE(called);
}

TEST_F(UvAsyncTests, UvPipe_Bind_NoPermission_Throws)
{
    auto instance = pipe::create(loop);
    auto called = false;

    instance->on_error([&](auto async_result)
    {
        called = true;
        EXPECT_THROW({ async_result(); }, std::runtime_error);
    });

    instance->bind("/sys/nopermission");

    loop->run();
    ASSERT_TRUE(called);
}

TEST_F(UvAsyncTests, UvPipe_Connect_NonSocket_Throws)
{
    auto instance = pipe::create(loop);
    bool called = false;

    instance->on_error([&](auto async_result)
    {
        called = true;
        EXPECT_THROW({ async_result(); }, std::runtime_error);
    });

    instance->connect("/dev/null", [] {
        FAIL() << "Should not be connected";
    });

    loop->run();
    ASSERT_TRUE(called);
}

TEST_F(UvAsyncTests, UvPipe_Connect_NonExistent_Throws)
{
    auto instance = pipe::create(loop);
    bool called = false;

    instance->on_error([&](auto async_result)
    {
        called = true;
        EXPECT_THROW({ async_result(); }, std::runtime_error);
    });

    instance->connect("/dev/nonexistent", [] {
        FAIL() << "Should not be connected";
    });

    loop->run();
    ASSERT_TRUE(called);
}

TEST_F(UvAsyncTests, UvPipe_Accept_NonListening_Throws)
{
    auto instance = pipe::create(loop);
    bool called = false;

    instance->on_error([&](auto async_result)
    {
        called = true;
        EXPECT_THROW({ async_result(); }, std::runtime_error);
    });

    instance->accept();

    loop->run();
    ASSERT_TRUE(called);
}

TEST_F(UvAsyncTests, UvPipe_Listen_Unbound_Throws)
{
    auto instance = pipe::create(loop);
    bool called = false;

    instance->on_error([&](auto async_result)
    {
        called = true;
        EXPECT_THROW({ async_result(); }, std::runtime_error);
    });

    instance->listen(1, []
    {
        FAIL() << "Should not be in listen state";
    });

    loop->run();
    ASSERT_TRUE(called);
}

TEST_F(UvAsyncTests, UvPipe_Write_WithoutPeer_Throws)
{
    auto instance = pipe::create(loop);
    bool called = false;
    instance->on_error([&](auto async_result)
    {
        called = true;
        EXPECT_THROW({ async_result(); }, std::runtime_error);
    });

    instance->write("Test", []
    {
        FAIL() << "Should wrote nothing";
    });

    loop->run();
    ASSERT_TRUE(called);
}

TEST_F(UvAsyncTests, UvPipe_Shutdown_WithoutPeer_Throws)
{
    auto instance = pipe::create(loop);
    bool called = false;

    instance->on_error([&](auto async_result)
    {
        called = true;
        EXPECT_THROW({ async_result(); }, std::runtime_error);
    });

    instance->shutdown([]
    {
        FAIL() << "Should not be in shutdown state";
    });

    loop->run();
    ASSERT_TRUE(called);
}

// This test does not throw -> it ends on assert from libuv
//
// TEST_F(UvAsyncTests, UvPipe_Read_WithoutPeer_Throws)
// {
//     auto instance = pipe::create(loop);
//     bool called = false;

//     instance->on_error([&](auto async_result)
//     {
//         called = true;
//         EXPECT_THROW({ async_result(); }, std::runtime_error);
//     });

//     instance->read(
//     [](std::unique_ptr<char[]> buffer, unsigned size) {
//         FAIL() << "Should not get any data";
//     },
//     [] {
//         FAIL() << "Should not end";
//     });

//     loop->run();
//     ASSERT_TRUE(called);
// }

class UvPipeTests : public Test
{
public:
    UvPipeTests()
    {
        loop = std::static_pointer_cast<uv_event_loop>(event_loop::create());

        auto srv_ = pipe::create(loop);
        srv = std::move(static_unique_pointer_cast<uv_pipe>(srv_));

        auto cli_ = pipe::create(loop);
        cli = std::move(static_unique_pointer_cast<uv_pipe>(cli_));

        fail_on_error(srv);
        fail_on_error(cli);
    }

    void fail_on_error(std::unique_ptr<uv_pipe>& pipe)
    {
        pipe->on_error([](auto async_result)
        {
            try {
                async_result();
            }
            catch (std::runtime_error& e) {
                FAIL() << e.what();
                return;
            }

            FAIL() << "Error";
        });
    }

    std::shared_ptr<uv_event_loop> loop;
    std::unique_ptr<uv_pipe> srv;
    std::unique_ptr<uv_pipe> cli;
};

TEST_F(UvPipeTests, UvPipe_ClientConnect_RaiseServerOnListen_ServerAccept_RaiseClientOnConnected)
{
    auto tmpsockpath = fs::u8path(u8"/tmp/myapptest_uvpipetests.sock");
    auto srv_on_listen = false;
    auto cli_on_connected = false;
    srv->bind(tmpsockpath);

    srv->listen(1, [&] { srv_on_listen = true; srv->accept(); });
    cli->connect(tmpsockpath, [&] { cli_on_connected = true; });

    loop->run_once();
    EXPECT_TRUE(srv_on_listen);
    EXPECT_TRUE(cli_on_connected);
}

TEST_F(UvPipeTests, UvPipe_Write_RaiseOnWrote_RaiseOnData)
{
    using namespace std::string_literals;
    auto tmpsockpath = fs::u8path(u8"/tmp/myapptest_uvpipetests.sock");
    auto server_on_wrote = 0;
    auto client_on_data = 0;
    srv->bind(tmpsockpath);

    srv->listen(1, [&] {
        {
            auto cli_ = srv->accept();
            cli_->write("Hello, world!"s, [&]{ ++server_on_wrote; });
            cli_->write("Hello, world!"s, [&]{ ++server_on_wrote; });
        }

        srv->close();
    });
    cli->connect(tmpsockpath, [&] {
        cli->read(
            [&](std::string data) {
                ++client_on_data;
                EXPECT_THAT(data, StrEq("Hello, world!Hello, world!"s)); // contiguous stream
            }, [] {});
    });

    loop->run();
    EXPECT_EQ(2, server_on_wrote);
    EXPECT_EQ(1, client_on_data);
}

TEST_F(UvPipeTests, UvPipe_Close_PeerRaiseOnEnd)
{
    auto tmpsockpath = fs::u8path(u8"/tmp/myapptest_uvpipetests.sock");
    auto on_end = false;
    srv->bind(tmpsockpath);

    srv->listen(1, [&] {
        srv->accept(); // peer closes here
        srv->close();
    });
    cli->connect(tmpsockpath, [&] {
        cli->read([&](auto data) {},
            [&] { on_end = true; });
    });

    loop->run();
    EXPECT_TRUE(on_end);
}
