/****************************************************************************
**
** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
** Contact: http://www.qt-project.org/legal
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL21$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Digia. For licensing terms and
** conditions see http://qt.digia.com/licensing. For further information
** use the contact form at http://qt.digia.com/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** In addition, as a special exception, Digia gives you certain additional
** rights. These rights are described in the Digia Qt LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include <QtTest/QtTest>
#include <QtCore/QThread>
#include <QtCore/QSemaphore>
#include <QtCore/QElapsedTimer>
#include <QtCore/QSharedPointer>
#include <QtCore/QVector>
#include <QtNetwork/QTcpSocket>
#include <QtNetwork/QSslSocket>
#include <QtNetwork/QNetworkReply>
#include <QtNetwork/QNetworkAccessManager>

#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;
    QVector<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<QVector<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") == 0)
        QSKIP("QNetworkAccessManager tests disabled");
#endif
}

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

bool nativeLookup(const char *hostname, int port, QByteArray &buf)
{
#if !defined(QT_NO_GETADDRINFO) && 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(QVector<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));
        QVERIFY2(socket->waitForConnected(10000), "Timeout connecting to " + url.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 " + url.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: " + url.encodedHost() + "\r\n"
                      "\r\n");
        while (socket->bytesToWrite())
            QVERIFY2(socket->waitForBytesWritten(10000), "Timeout writing to " + url.encodedHost());

        while (socket->state() == QAbstractSocket::ConnectedState && !timeout.hasExpired(10000)) {
            socket->waitForReadyRead(10000);
            byteCounter += socket->readAll().size(); // discard
        }
        QVERIFY2(!timeout.hasExpired(10000), "Timeout reading from " + url.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(QVector<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));
        }

        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: " + url.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 " + url.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(QVector<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();

    QVector<QUrl>::ConstIterator it = urlList.constBegin();
    while (it != urlList.constEnd()) {
        QElapsedTimer timeout;
        byteCounter = 0;
        timeout.start();

        QVector<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));
            }

            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: " + url.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(QVector<QUrl>, urlList);

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

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

    QVector<QUrl>::ConstIterator it = urlList.constBegin();
    while (it != urlList.constEnd()) {
        QElapsedTimer timeout;
        byteCounter = 0;
        timeout.start();

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

        QVector<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"
