// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QTest>
#include <QtTest/qtesteventloop.h>
#include <QtCore/QElapsedTimer>
#include <QtCore/QList>
#include <QtCore/QSemaphore>
#include <QtCore/QSharedPointer>
#include <QtCore/QThread>
#include <QtNetwork/QNetworkAccessManager>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QSslSocket>
#include <QtNetwork/QTcpSocket>

#ifdef QT_BUILD_INTERNAL
# include <private/qnetworkaccessmanager_p.h>
#endif

#include <qplatformdefs.h>
#ifdef Q_OS_UNIX
# include <sys/types.h>
# include <sys/socket.h>
# include <sys/select.h>
# include <netinet/in.h>
# include <errno.h>
# include <netdb.h>
# include <signal.h>
# include <unistd.h>
# include <fcntl.h>

typedef int SOCKET;
# define INVALID_SOCKET -1
# define SOCKET_ERROR -1

#elif defined(Q_OS_WIN)
# include <winsock2.h>
#endif


class tst_NetworkRemoteStressTest : public QObject
{
    Q_OBJECT
public:
    enum { AttemptCount = 100 };
    tst_NetworkRemoteStressTest();

    qint64 byteCounter;
    QNetworkAccessManager manager;
    QList<QUrl> httpUrls, httpsUrls, mixedUrls;
    bool intermediateDebug;

private:
    void clearManager();

public slots:
    void initTestCase_data();
    void init();

    void slotReadAll() { byteCounter += static_cast<QIODevice *>(sender())->readAll().size(); }

private Q_SLOTS:
    void blockingSequentialRemoteHosts();
    void sequentialRemoteHosts();
    void parallelRemoteHosts_data();
    void parallelRemoteHosts();
    void namRemoteGet_data();
    void namRemoteGet();
};

tst_NetworkRemoteStressTest::tst_NetworkRemoteStressTest()
    : intermediateDebug(qgetenv("STRESSDEBUG").toInt() > 0)
{
#ifdef Q_OS_WIN
    WSAData wsadata;

    // IPv6 requires Winsock v2.0 or better.
    WSAStartup(MAKEWORD(2,0), &wsadata);
#elif defined(Q_OS_UNIX)
    ::signal(SIGALRM, SIG_IGN);
#endif

    QFile urlList(":/url-list.txt");
    if (urlList.open(QIODevice::ReadOnly)) {
        while (!urlList.atEnd()) {
            QByteArray line = urlList.readLine().trimmed();
            QUrl url = QUrl::fromEncoded(line);
            if (url.scheme() == "http") {
                httpUrls << url;
                mixedUrls << url;
            } else if (url.scheme() == "https") {
                httpsUrls << url;
                mixedUrls << url;
            }
        }
    }

    httpUrls << httpUrls;
    httpsUrls << httpsUrls;
}

void tst_NetworkRemoteStressTest::initTestCase_data()
{
    QTest::addColumn<QList<QUrl>>("urlList");
    QTest::addColumn<bool>("useSslSocket");

    QTest::newRow("no-ssl") << httpUrls << false;
//    QTest::newRow("no-ssl-in-sslsocket") << httpUrls << true;
    QTest::newRow("ssl") << httpsUrls << true;
    QTest::newRow("mixed") << mixedUrls << false;
//    QTest::newRow("mixed-in-sslsocket") << mixedUrls << true;
}

void tst_NetworkRemoteStressTest::init()
{
    // clear the internal cache
#ifndef QT_BUILD_INTERNAL
    if (strncmp(QTest::currentTestFunction(), "nam", 3) == 0)
        QSKIP("QNetworkAccessManager tests disabled");
#endif
}

void tst_NetworkRemoteStressTest::clearManager()
{
#ifdef QT_BUILD_INTERNAL
    QNetworkAccessManagerPrivate::clearAuthenticationCache(&manager);
    QNetworkAccessManagerPrivate::clearConnectionCache(&manager);
    manager.setProxy(QNetworkProxy());
    manager.setCache(0);
#endif
}

bool nativeLookup(const char *hostname, int port, QByteArray &buf)
{
#if 0
    addrinfo *res = 0;
    struct addrinfo hints;
    memset(&hints, 0, sizeof(hints));
    hints.ai_family = PF_UNSPEC;

    int result = getaddrinfo(QUrl::toAce(hostname).constData(), QByteArray::number(port).constData(), &hints, &res);
    if (!result)
        return false;
    for (addrinfo *node = res; node; node = node->ai_next) {
        if (node->ai_family == AF_INET) {
            buf = QByteArray((char *)node->ai_addr, node->ai_addrlen);
            break;
        }
    }
    freeaddrinfo(res);
#else
    hostent *result = gethostbyname(hostname);
    if (!result || result->h_addrtype != AF_INET)
        return false;

    struct sockaddr_in s;
    s.sin_family = AF_INET;
    s.sin_port = htons(port);
    s.sin_addr =  *(struct in_addr *) result->h_addr_list[0];

    buf = QByteArray((char *)&s, sizeof s);
#endif

    return !buf.isEmpty();
}

bool nativeSelect(int fd, int timeout, bool selectForWrite)
{
    if (timeout < 0)
        return false;

    // wait for connected
    fd_set fds, fde;
    FD_ZERO(&fds);
    FD_ZERO(&fde);
    FD_SET(fd, &fds);
    FD_SET(fd, &fde);

    int ret;
    do {
        struct timeval tv;
        tv.tv_sec = timeout / 1000;
        tv.tv_usec = timeout % 1000;
        if (selectForWrite)
            ret = ::select(fd + 1, 0, &fds, &fde, &tv);
        else
            ret = ::select(fd + 1, &fds, 0, &fde, &tv);
    } while (ret == -1 && errno == EINTR);
    return ret != 0;
}

void tst_NetworkRemoteStressTest::blockingSequentialRemoteHosts()
{
    QFETCH_GLOBAL(QList<QUrl>, urlList);
    QFETCH_GLOBAL(bool, useSslSocket);

    qint64 totalBytes = 0;
    QElapsedTimer outerTimer;
    outerTimer.start();

#ifdef QT_NO_SSL
    QVERIFY(!useSslSocket);
#endif // QT_NO_SSL

    for (int i = 0; i < urlList.size(); ++i) {
        const QUrl &url = urlList.at(i);
        bool isHttps = url.scheme() == "https";
        QElapsedTimer timeout;
        byteCounter = 0;
        timeout.start();

        QSharedPointer<QTcpSocket> socket;
#ifndef QT_NO_SSL
        if (useSslSocket || isHttps)
            socket = QSharedPointer<QTcpSocket>(new QSslSocket);
#endif // QT_NO_SSL
        if (socket.isNull())
            socket = QSharedPointer<QTcpSocket>(new QTcpSocket);

        socket->connectToHost(url.host(), url.port(isHttps ? 443 : 80));
        const QByteArray encodedHost = url.host(QUrl::FullyEncoded).toLatin1();
        QVERIFY2(socket->waitForConnected(10000), "Timeout connecting to " + encodedHost);

#ifndef QT_NO_SSL
        if (isHttps) {
            static_cast<QSslSocket *>(socket.data())->setProtocol(QSsl::TlsV1_0);
            static_cast<QSslSocket *>(socket.data())->startClientEncryption();
            static_cast<QSslSocket *>(socket.data())->ignoreSslErrors();
            QVERIFY2(static_cast<QSslSocket *>(socket.data())->waitForEncrypted(10000), "Timeout starting TLS with " + encodedHost);
        }
#endif // QT_NO_SSL

        socket->write("GET " + url.toEncoded(QUrl::RemoveScheme | QUrl::RemoveAuthority | QUrl::RemoveFragment) + " HTTP/1.0\r\n"
                      "Connection: close\r\n"
                      "User-Agent: tst_QTcpSocket_stresstest/1.0\r\n"
                      "Host: " + encodedHost + "\r\n"
                      "\r\n");
        while (socket->bytesToWrite())
            QVERIFY2(socket->waitForBytesWritten(10000), "Timeout writing to " + encodedHost);

        while (socket->state() == QAbstractSocket::ConnectedState && !timeout.hasExpired(10000)) {
            socket->waitForReadyRead(10000);
            byteCounter += socket->readAll().size(); // discard
        }
        QVERIFY2(!timeout.hasExpired(10000), "Timeout reading from " + encodedHost);

        totalBytes += byteCounter;
        if (intermediateDebug) {
            double rate = (byteCounter * 1.0 / timeout.elapsed());
            qDebug() << i << url << byteCounter << "bytes in" << timeout.elapsed() << "ms:"
                    << (rate / 1024.0 * 1000) << "kB/s";
        }
    }
    qDebug() << "Average transfer rate was" << (totalBytes / 1024.0 * 1000 / outerTimer.elapsed()) << "kB/s";
}

void tst_NetworkRemoteStressTest::sequentialRemoteHosts()
{
    QFETCH_GLOBAL(QList<QUrl>, urlList);
    QFETCH_GLOBAL(bool, useSslSocket);

#ifdef QT_NO_SSL
    QVERIFY(!useSslSocket);
#endif // QT_NO_SSL

    qint64 totalBytes = 0;
    QElapsedTimer outerTimer;
    outerTimer.start();

    for (int i = 0; i < urlList.size(); ++i) {
        const QUrl &url = urlList.at(i);
        bool isHttps = url.scheme() == "https";
        QElapsedTimer timeout;
        byteCounter = 0;
        timeout.start();

        QSharedPointer<QTcpSocket> socket;
#ifndef QT_NO_SSL
        if (useSslSocket || isHttps)
            socket = QSharedPointer<QTcpSocket>(new QSslSocket);
#endif // QT_NO_SSL
        if (socket.isNull())
            socket = QSharedPointer<QTcpSocket>(new QTcpSocket);
        if (isHttps) {
#ifndef QT_NO_SSL
            static_cast<QSslSocket *>(socket.data())->setProtocol(QSsl::TlsV1_0);
            static_cast<QSslSocket *>(socket.data())->connectToHostEncrypted(url.host(), url.port(443));
            static_cast<QSslSocket *>(socket.data())->ignoreSslErrors();
#endif // QT_NO_SSL
        } else {
            socket->connectToHost(url.host(), url.port(80));
        }

        const QByteArray encodedHost = url.host(QUrl::FullyEncoded).toLatin1();
        socket->write("GET " + url.toEncoded(QUrl::RemoveScheme | QUrl::RemoveAuthority | QUrl::RemoveFragment) + " HTTP/1.0\r\n"
                      "Connection: close\r\n"
                      "User-Agent: tst_QTcpSocket_stresstest/1.0\r\n"
                      "Host: " + encodedHost + "\r\n"
                      "\r\n");
        connect(socket.data(), SIGNAL(readyRead()), SLOT(slotReadAll()));

        QTestEventLoop::instance().connect(socket.data(), SIGNAL(disconnected()), SLOT(exitLoop()));
        QTestEventLoop::instance().enterLoop(30);
        QVERIFY2(!QTestEventLoop::instance().timeout(), "Timeout with " + encodedHost + "; "
                 + QByteArray::number(socket->bytesToWrite()) + " bytes to write");

        totalBytes += byteCounter;
        if (intermediateDebug) {
            double rate = (byteCounter * 1.0 / timeout.elapsed());
            qDebug() << i << url << byteCounter << "bytes in" << timeout.elapsed() << "ms:"
                    << (rate / 1024.0 * 1000) << "kB/s";
        }
    }
    qDebug() << "Average transfer rate was" << (totalBytes / 1024.0 * 1000 / outerTimer.elapsed()) << "kB/s";
}

void tst_NetworkRemoteStressTest::parallelRemoteHosts_data()
{
    QTest::addColumn<int>("parallelAttempts");
    QTest::newRow("1") << 1;
    QTest::newRow("2") << 2;
    QTest::newRow("4") << 4;
    QTest::newRow("5") << 5;
    QTest::newRow("6") << 6;
    QTest::newRow("8") << 8;
    QTest::newRow("10") << 10;
    QTest::newRow("25") << 25;
    QTest::newRow("500") << 500;
}

void tst_NetworkRemoteStressTest::parallelRemoteHosts()
{
    QFETCH_GLOBAL(QList<QUrl>, urlList);
    QFETCH_GLOBAL(bool, useSslSocket);

    QFETCH(int, parallelAttempts);

#ifdef QT_NO_SSL
    QVERIFY(!useSslSocket);
#endif // QT_NO_SSL

    qint64 totalBytes = 0;
    QElapsedTimer outerTimer;
    outerTimer.start();

    auto it = urlList.constBegin();
    while (it != urlList.constEnd()) {
        QElapsedTimer timeout;
        byteCounter = 0;
        timeout.start();

        QList<QSharedPointer<QTcpSocket> > sockets;
        sockets.reserve(parallelAttempts);
        for (int j = 0; j < parallelAttempts && it != urlList.constEnd(); ++j, ++it) {
            const QUrl &url = *it;
            bool isHttps = url.scheme() == "https";
            QTcpSocket *socket = 0;
#ifndef QT_NO_SSL
            if (useSslSocket || isHttps)
                socket = new QSslSocket;
#endif // QT_NO_SSL
            if (!socket)
                socket = new QTcpSocket;
            if (isHttps) {
#ifndef QT_NO_SSL
                static_cast<QSslSocket *>(socket)->setProtocol(QSsl::TlsV1_0);
                static_cast<QSslSocket *>(socket)->connectToHostEncrypted(url.host(), url.port(443));
                static_cast<QSslSocket *>(socket)->ignoreSslErrors();
#endif // QT_NO_SSL
            } else {
                socket->connectToHost(url.host(), url.port(isHttps ? 443 : 80));
            }

            const QByteArray encodedHost = url.host(QUrl::FullyEncoded).toLatin1();
            socket->write("GET " + url.toEncoded(QUrl::RemoveScheme | QUrl::RemoveAuthority | QUrl::RemoveFragment) + " HTTP/1.0\r\n"
                          "Connection: close\r\n"
                          "User-Agent: tst_QTcpSocket_stresstest/1.0\r\n"
                          "Host: " + encodedHost + "\r\n"
                          "\r\n");
            connect(socket, SIGNAL(readyRead()), SLOT(slotReadAll()));
            QTestEventLoop::instance().connect(socket, SIGNAL(disconnected()), SLOT(exitLoop()));
            socket->setProperty("remoteUrl", url);

            sockets.append(QSharedPointer<QTcpSocket>(socket));
        }

        while (!timeout.hasExpired(10000)) {
            QTestEventLoop::instance().enterLoop(10);
            int done = 0;
            for (int j = 0; j < sockets.size(); ++j)
                done += sockets[j]->state() == QAbstractSocket::UnconnectedState ? 1 : 0;
            if (done == sockets.size())
                break;
        }
        for (int j = 0; j < sockets.size(); ++j)
            if (sockets[j]->state() != QAbstractSocket::UnconnectedState) {
                qDebug() << "Socket to" << sockets[j]->property("remoteUrl").toUrl() << "still open with"
                        << sockets[j]->bytesToWrite() << "bytes to write";
                QFAIL("Timed out");
            }

        totalBytes += byteCounter;
        if (intermediateDebug) {
            double rate = (byteCounter * 1.0 / timeout.elapsed());
            qDebug() << byteCounter << "bytes in" << timeout.elapsed() << "ms:"
                    << (rate / 1024.0 * 1000) << "kB/s";
        }
    }
    qDebug() << "Average transfer rate was" << (totalBytes / 1024.0 * 1000 / outerTimer.elapsed()) << "kB/s";
}

void tst_NetworkRemoteStressTest::namRemoteGet_data()
{
    QTest::addColumn<int>("parallelAttempts");
    QTest::newRow("1") << 1;
    QTest::newRow("2") << 2;
    QTest::newRow("4") << 4;
    QTest::newRow("5") << 5;
    QTest::newRow("6") << 6;
    QTest::newRow("8") << 8;
    QTest::newRow("10") << 10;
    QTest::newRow("25") << 25;
    QTest::newRow("500") << 500;
}

void tst_NetworkRemoteStressTest::namRemoteGet()
{
    QFETCH_GLOBAL(QList<QUrl>, urlList);

    QFETCH(int, parallelAttempts);
    bool pipelineAllowed = false;// QFETCH(bool, pipelineAllowed);

    qint64 totalBytes = 0;
    QElapsedTimer outerTimer;
    outerTimer.start();

    auto it = urlList.constBegin();
    while (it != urlList.constEnd()) {
        QElapsedTimer timeout;
        byteCounter = 0;
        timeout.start();

        QNetworkRequest req;
        req.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, pipelineAllowed);

        QList<QSharedPointer<QNetworkReply> > replies;
        replies.reserve(parallelAttempts);
        for (int j = 0; j < parallelAttempts && it != urlList.constEnd(); ++j) {
            req.setUrl(*it++);
            QNetworkReply *r = manager.get(req);
            r->ignoreSslErrors();

            connect(r, SIGNAL(readyRead()), SLOT(slotReadAll()));
            QTestEventLoop::instance().connect(r, SIGNAL(finished()), SLOT(exitLoop()));

            replies.append(QSharedPointer<QNetworkReply>(r));
        }

        while (!timeout.hasExpired(30000)) {
            QTestEventLoop::instance().enterLoop(30 - timeout.elapsed() / 1000);
            int done = 0;
            for (int j = 0; j < replies.size(); ++j)
                done += replies[j]->isFinished() ? 1 : 0;
            if (done == replies.size())
                break;
        }
        if (timeout.hasExpired(30000)) {
            for (int j = 0; j < replies.size(); ++j)
                if (!replies[j]->isFinished())
                    qDebug() << "Request" << replies[j]->url() << "not finished";
            QFAIL("Timed out");
        }
        replies.clear();

        totalBytes += byteCounter;
        if (intermediateDebug) {
            double rate = (byteCounter * 1.0 / timeout.elapsed());
            qDebug() << byteCounter << "bytes in" << timeout.elapsed() << "ms:"
                    << (rate / 1024.0 * 1000) << "kB/s";
        }
    }
    qDebug() << "Average transfer rate was" << (totalBytes / 1024.0 * 1000 / outerTimer.elapsed()) << "kB/s";
}

QTEST_MAIN(tst_NetworkRemoteStressTest);

#include "tst_network_remote_stresstest.moc"
