/**
 * @file tlsSocket_test.hpp
 * @author Konrad Zemek
 * @copyright (C) 2015 ACK CYFRONET AGH
 * @copyright This software is released under the MIT license cited in
 * 'LICENSE.md'
 */

#include "communication/etls/tlsApplication.h"
#include "communication/etls/tlsSocket.h"
#include "testServer.h"
#include "testUtils.h"

#include <gtest/gtest.h>

#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstring>
#include <string>
#include <thread>
#include <vector>

using namespace std::literals;
using namespace testing;

struct TLSSocketTest : public Test {
    std::string host{"127.0.0.1"};
    unsigned short port{randomPort()};
    TestServer server{port};

    one::communication::etls::TLSApplication app;
    one::communication::etls::TLSSocket::Ptr socket;

    TLSSocketTest()
        : socket{std::make_shared<one::communication::etls::TLSSocket>(app)}
    {
    }
};

struct TLSSocketTestC : public TLSSocketTest {
    TLSSocketTestC()
    {
        std::atomic<bool> connected{false};
        socket->connectAsync(
            socket, host, port, {[&](auto) { connected = true; }, [](auto) {}});
        waitFor(connected);
    }
};

TEST_F(TLSSocketTest, shouldConnectToTheServer)
{
    socket->connectAsync(socket, host, port, {[](auto) {}, [](auto) {}});
    ASSERT_TRUE(server.waitForConnection(5s));
}

TEST_F(TLSSocketTest, shouldNotifyOnConnectionSuccess)
{
    std::atomic<bool> called{false};

    socket->connectAsync(
        socket, host, port, {[&](auto) { called = true; }, [](auto) {}});

    ASSERT_TRUE(waitFor(called));
}

TEST_F(TLSSocketTest, shouldNotifyOnConnectionError)
{
    server.failConnection();
    std::atomic<bool> called{false};

    socket->connectAsync(
        socket, host, port, {[](auto) {}, [&](auto) { called = true; }});

    ASSERT_TRUE(waitFor(called));
}

TEST_F(TLSSocketTestC, shouldSendMessages)
{
    const auto data = randomData();
    socket->sendAsync(socket, asio::buffer(data), {[] {}, [](auto) {}});

    std::vector<char> received(data.size());
    server.receive(asio::buffer(received));

    ASSERT_EQ(data, received);
}

TEST_F(TLSSocketTestC, shouldNotifyOnSuccessfulSend)
{
    std::atomic<bool> called{false};
    const auto data = randomData();

    socket->sendAsync(
        socket, asio::buffer(data), {[&] { called = true; }, [](auto) {}});

    ASSERT_TRUE(waitFor(called));
}

TEST_F(TLSSocketTestC, shouldNotifyOnSendError)
{
    std::atomic<bool> closed{false};
    std::atomic<bool> called{false};
    const auto data = randomData();

    socket->closeAsync(socket, {[&] { closed = true; }, [](auto) {}});

    ASSERT_TRUE(waitFor(closed));

    socket->sendAsync(
        socket, asio::buffer(data), {[] {}, [&](auto) { called = true; }});

    ASSERT_TRUE(waitFor(called));
}

TEST_F(TLSSocketTestC, shouldReceiveMessages)
{
    std::atomic<bool> called{false};

    const auto data = randomData();
    server.send(asio::buffer(data));

    std::vector<char> received(data.size());
    socket->recvAsync(socket, asio::buffer(received),
        {[&](auto) { called = true; }, [](auto) {}});

    waitFor(called);

    ASSERT_EQ(data, received);
}

TEST_F(TLSSocketTestC, shouldNotifyOnRecvSuccess)
{
    std::atomic<bool> called{false};
    auto data = randomData();

    server.send(asio::buffer(data));

    socket->recvAsync(socket, asio::buffer(data),
        {[&](auto) { called = true; }, [](auto) {}});

    ASSERT_TRUE(waitFor(called));
}

TEST_F(TLSSocketTestC, shouldNotifyOnRecvError)
{
    std::atomic<bool> called{false};
    auto data = randomData();

    server.fail();

    socket->recvAsync(socket, asio::buffer(data),
        {[](auto) {}, [&](auto) { called = true; }});

    ASSERT_TRUE(waitFor(called));
}

TEST_F(TLSSocketTestC, shouldReceiveMessagesAsTheyCome)
{
    std::atomic<bool> called{false};
    asio::mutable_buffer buffer;

    const auto data = randomData();
    server.send(asio::buffer(data));

    std::vector<char> received(data.size() + 100);

    socket->recvAnyAsync(socket, asio::buffer(received),
        {[&](auto b) {
             buffer = b;
             called = true;
         },
            [](auto) {}});

    waitFor(called);

    ASSERT_NE(data, received);
    ASSERT_LT(0u, asio::buffer_size(buffer));
    ASSERT_EQ(0,
        memcmp(asio::buffer_cast<char *>(buffer), data.data(),
            asio::buffer_size(buffer)));
}

TEST_F(TLSSocketTestC, shouldNotifyOnRecvAnySuccess)
{
    std::atomic<bool> called{false};
    auto data = randomData();

    server.send(asio::buffer(data));

    socket->recvAnyAsync(socket, asio::buffer(data),
        {[&](auto) { called = true; }, [](auto) {}});

    ASSERT_TRUE(waitFor(called));
}

TEST_F(TLSSocketTest, shouldNotifyOnRecvAnyError)
{
    std::atomic<bool> called{false};
    auto data = randomData();

    socket->recvAnyAsync(socket, asio::buffer(data),
        {[](auto) {}, [&](auto) { called = true; }});

    ASSERT_TRUE(waitFor(called));
}

TEST_F(TLSSocketTestC, shouldReturnLocalEndpoint)
{
    asio::ip::tcp::endpoint endpoint;
    std::atomic<bool> called{false};

    socket->localEndpointAsync(socket,
        {[&](auto e) {
             endpoint = e;
             called = true;
         },
            [](auto) {}});

    ASSERT_TRUE(waitFor(called));
    ASSERT_EQ("127.0.0.1", endpoint.address().to_string());
    ASSERT_NE(port, endpoint.port());
}

TEST_F(TLSSocketTestC, shouldReturnRemoteEndpoint)
{
    asio::ip::tcp::endpoint endpoint;
    std::atomic<bool> called{false};

    socket->remoteEndpointAsync(socket,
        {[&](auto e) {
             endpoint = e;
             called = true;
         },
            [](auto) {}});

    ASSERT_TRUE(waitFor(called));
    ASSERT_EQ("127.0.0.1", endpoint.address().to_string());
    ASSERT_EQ(port, endpoint.port());
}

TEST_F(TLSSocketTestC, shouldBeShutdownable)
{
    std::atomic<bool> shutdownCalled{false};
    socket->shutdownAsync(socket, asio::socket_base::shutdown_send,
        {[&]() { shutdownCalled = true; }, [](auto) {}});

    ASSERT_TRUE(waitFor(shutdownCalled));

    std::atomic<bool> sendCalled{false};
    auto data = randomData();

    socket->sendAsync(socket, asio::buffer(data),
        {[] {},
            [&](auto ec) {
                ASSERT_EQ(
                    std::string{"Broken pipe"}, std::string{ec.message()});
                sendCalled = true;
            }});

    ASSERT_TRUE(waitFor(sendCalled));
}
