// SPDX-FileCopyrightText: 2020 Contributors to Chatterino <https://chatterino.com>
//
// SPDX-License-Identifier: MIT

#include "common/network/NetworkRequest.hpp"

#include "common/network/NetworkManager.hpp"
#include "common/network/NetworkResult.hpp"
#include "NetworkHelpers.hpp"
#include "Test.hpp"
#include "util/QMagicEnum.hpp"

#include <QCoreApplication>

using namespace chatterino;

namespace {

QString getStatusURL(int code)
{
    return QString("%1/status/%2").arg(HTTPBIN_BASE_URL).arg(code);
}

QString getDelayURL(int delay)
{
    return QString("%1/delay/%2").arg(HTTPBIN_BASE_URL).arg(delay);
}

QString getHttpbinUrl(QStringView path)
{
    return QString("%1/%2").arg(HTTPBIN_BASE_URL, path);
}

QHttpMultiPart *makeMultipart()
{
    auto *multipart = new QHttpMultiPart(QHttpMultiPart::FormDataType);

    QHttpPart part;
    part.setHeader(QNetworkRequest::ContentDispositionHeader,
                   QVariant("form-data; name=\"text\""));
    part.setBody("my text");

    multipart->append(part);
    return multipart;
}

}  // namespace

TEST(NetworkRequest, Success)
{
    const std::vector<int> codes{200, 201, 202, 203, 204, 205, 206};

    EXPECT_TRUE(NetworkManager::workerThread->isRunning());

    for (const auto code : codes)
    {
        auto url = getStatusURL(code);
        RequestWaiter waiter;

        NetworkRequest(url)
            .onSuccess([code, &waiter, url](const NetworkResult &result) {
                EXPECT_EQ(result.status(), code);
                waiter.requestDone();
            })
            .onError([&](const NetworkResult & /*result*/) {
                // The codes should *not* throw an error
                EXPECT_TRUE(false);
                waiter.requestDone();
            })
            .execute();

        waiter.waitForRequest();
    }

    EXPECT_TRUE(NetworkManager::workerThread->isRunning());
}

TEST(NetworkRequest, FinallyCallbackOnSuccess)
{
    const std::vector<int> codes{200, 201, 202, 203, 204, 205, 206};

    EXPECT_TRUE(NetworkManager::workerThread->isRunning());

    for (const auto code : codes)
    {
        auto url = getStatusURL(code);
        RequestWaiter waiter;

        bool finallyCalled = false;

        NetworkRequest(url)
            .finally([&waiter, &finallyCalled] {
                finallyCalled = true;
                waiter.requestDone();
            })
            .execute();

        waiter.waitForRequest();

        EXPECT_TRUE(finallyCalled);
    }
}

TEST(NetworkRequest, Error)
{
    const std::vector<int> codes{
        400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410,
        411, 412, 413, 414, 418, 500, 501, 502, 503, 504,
    };

    EXPECT_TRUE(NetworkManager::workerThread->isRunning());

    for (const auto code : codes)
    {
        auto url = getStatusURL(code);
        RequestWaiter waiter;

        NetworkRequest(url)
            .onSuccess([&waiter, url](const NetworkResult & /*result*/) {
                // The codes should throw an error
                EXPECT_TRUE(false);
                waiter.requestDone();
            })
            .onError([code, &waiter, url](const NetworkResult &result) {
                EXPECT_EQ(result.status(), code);

                waiter.requestDone();
            })
            .execute();

        waiter.waitForRequest();
    }

    EXPECT_TRUE(NetworkManager::workerThread->isRunning());
}

TEST(NetworkRequest, FinallyCallbackOnError)
{
    const std::vector<int> codes{
        400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410,
        411, 412, 413, 414, 418, 500, 501, 502, 503, 504,
    };

    EXPECT_TRUE(NetworkManager::workerThread->isRunning());

    for (const auto code : codes)
    {
        auto url = getStatusURL(code);
        RequestWaiter waiter;

        bool finallyCalled = false;

        NetworkRequest(url)
            .finally([&waiter, &finallyCalled] {
                finallyCalled = true;
                waiter.requestDone();
            })
            .execute();

        waiter.waitForRequest();

        EXPECT_TRUE(finallyCalled);
    }
}

TEST(NetworkRequest, TimeoutTimingOut)
{
    EXPECT_TRUE(NetworkManager::workerThread->isRunning());

    auto url = getDelayURL(5);
    RequestWaiter waiter;

    NetworkRequest(url)
        .timeout(1000)
        .onSuccess([&waiter](const NetworkResult & /*result*/) {
            // The timeout should throw an error
            EXPECT_TRUE(false);
            waiter.requestDone();
        })
        .onError([&waiter, url](const NetworkResult &result) {
            qDebug() << QTime::currentTime().toString()
                     << "timeout request finish error";
            EXPECT_EQ(result.error(),
                      NetworkResult::NetworkError::TimeoutError);
            EXPECT_EQ(result.status(), std::nullopt);

            waiter.requestDone();
        })
        .execute();

    waiter.waitForRequest();

    EXPECT_TRUE(NetworkManager::workerThread->isRunning());
}

TEST(NetworkRequest, TimeoutNotTimingOut)
{
    EXPECT_TRUE(NetworkManager::workerThread->isRunning());

    auto url = getDelayURL(1);
    RequestWaiter waiter;

    NetworkRequest(url)
        .timeout(3000)
        .onSuccess([&waiter, url](const NetworkResult &result) {
            EXPECT_EQ(result.status(), 200);

            waiter.requestDone();
        })
        .onError([&waiter, url](const NetworkResult & /*result*/) {
            // The timeout should *not* throw an error
            EXPECT_TRUE(false);
            waiter.requestDone();
        })
        .execute();

    waiter.waitForRequest();

    EXPECT_TRUE(NetworkManager::workerThread->isRunning());
}

TEST(NetworkRequest, FinallyCallbackOnTimeout)
{
    EXPECT_TRUE(NetworkManager::workerThread->isRunning());

    auto url = getDelayURL(5);

    RequestWaiter waiter;
    bool finallyCalled = false;
    bool onSuccessCalled = false;
    bool onErrorCalled = false;

    NetworkRequest(url)
        .timeout(1000)
        .onSuccess([&](const NetworkResult & /*result*/) {
            onSuccessCalled = true;
        })
        .onError([&](const NetworkResult &result) {
            onErrorCalled = true;
            EXPECT_EQ(result.error(),
                      NetworkResult::NetworkError::TimeoutError);
            EXPECT_EQ(result.status(), std::nullopt);
        })
        .finally([&] {
            finallyCalled = true;
            waiter.requestDone();
        })
        .execute();

    waiter.waitForRequest();

    EXPECT_TRUE(finallyCalled);
    EXPECT_TRUE(onErrorCalled);
    EXPECT_FALSE(onSuccessCalled);
    EXPECT_TRUE(NetworkManager::workerThread->isRunning());
}

/// Ensure timeouts don't expire early just because their request took a bit longer to actually fire
///
/// We need to ensure all requests are "executed" before we start waiting for them
TEST(NetworkRequest, BatchedTimeouts)
{
#if QT_VERSION >= QT_VERSION_CHECK(6, 3, 0)
    // roughly num network manager worker threads * 2
    static const auto numRequests = 10;

    struct RequestState {
        RequestWaiter waiter;
        bool errored = false;
    };

    EXPECT_TRUE(NetworkManager::workerThread->isRunning());

    std::vector<std::shared_ptr<RequestState>> states;

    for (auto i = 1; i <= numRequests; ++i)
    {
        auto state = std::make_shared<RequestState>();

        auto url = getDelayURL(1);

        NetworkRequest(url)
            .timeout(1500)
            .onError([=](const NetworkResult &result) {
                (void)result;
                state->errored = true;
            })
            .finally([=] {
                state->waiter.requestDone();
            })
            .execute();

        states.emplace_back(state);
    }

    for (const auto &state : states)
    {
        state->waiter.waitForRequest();
        EXPECT_FALSE(state->errored);
    }
#endif
}

TEST(NetworkRequest, HttpGetHeaders)
{
    EXPECT_TRUE(NetworkManager::workerThread->isRunning());

    RequestWaiter waiter;
    NetworkRequest(getHttpbinUrl(u"headers"), NetworkRequestType::Get)
        .timeout(1000)
        .onSuccess([&](const NetworkResult &result) {
            // make sure we don't attempt to send a body
            auto body = QString::fromUtf8(result.getData());
            EXPECT_FALSE(body.contains("content-length", Qt::CaseInsensitive));
            waiter.requestDone();
        })
        .execute();

    waiter.waitForRequest();
}

TEST(NetworkRequest, HttpBody)
{
    EXPECT_TRUE(NetworkManager::workerThread->isRunning());

    for (auto ty : std::array{
             NetworkRequestType::Post,
             NetworkRequestType::Put,
             NetworkRequestType::Patch,
         })
    {
        auto path = qmagicenum::enumNameString(ty).toLower();
        RequestWaiter waiter;
        bool success = false;
        NetworkRequest(getHttpbinUrl(path), ty)
            .timeout(1000)
            .payload("foobar")
            .header("Content-Type", "text/plain")
            .onSuccess([&](const NetworkResult &result) {
                EXPECT_EQ(result.getData(), "foobar") << path;
                success = true;
            })
            .finally([&] {
                waiter.requestDone();
            })
            .execute();

        waiter.waitForRequest();
        ASSERT_TRUE(success) << path;
    }
}

TEST(NetworkRequest, HttpBodyMultipart)
{
    EXPECT_TRUE(NetworkManager::workerThread->isRunning());

    for (auto ty : std::array{
             NetworkRequestType::Post,
             NetworkRequestType::Put,
             NetworkRequestType::Patch,
         })
    {
        auto path = qmagicenum::enumNameString(ty).toLower();
        RequestWaiter waiter;
        bool success = false;
        NetworkRequest(getHttpbinUrl(path), ty)
            .timeout(1000)
            .multiPart(makeMultipart())
            .onSuccess([&](const NetworkResult &result) {
                auto body = QString::fromUtf8(result.getData());
                EXPECT_TRUE(body.contains("Content-Disposition: form-data; "
                                          "name=\"text\"\r\n\r\nmy text",
                                          Qt::CaseInsensitive))
                    << "Path:" << path << "Body:" << body;
                success = true;
            })
            .finally([&] {
                waiter.requestDone();
            })
            .execute();

        waiter.waitForRequest();
        ASSERT_TRUE(success) << path;
    }
}
