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


#include <QTest>
#include <QTestEventLoop>
#include <QAuthenticator>
#include <QTcpServer>

#include "private/qhttpnetworkconnection_p.h"
#include "private/qnoncontiguousbytedevice_p.h"

#include "../../../network-settings.h"

class tst_QHttpNetworkConnection: public QObject
{
    Q_OBJECT

public Q_SLOTS:
    void finishedReply();
    void finishedWithError(QNetworkReply::NetworkError errorCode, const QString &detail);
    void challenge401(const QHttpNetworkRequest &request, QAuthenticator *authenticator);
#ifndef QT_NO_SSL
    void sslErrors(const QList<QSslError> &errors);
#endif
private:
    bool finishedCalled;
    bool finishedWithErrorCalled;
    QNetworkReply::NetworkError netErrorCode;
    QString (*httpServerName)() = QtNetworkSettings::httpServerName;

private Q_SLOTS:
    void initTestCase();
    void options_data();
    void options();
    void get_data();
    void get();
    void head_data();
    void head();
    void post_data();
    void post();
    void put_data();
    void put();
    void _delete_data();
    void _delete();
    void trace_data();
    void trace();
    void _connect_data();
    void _connect();
#ifndef QT_NO_COMPRESS
    void compression_data();
    void compression();
#endif
#ifndef QT_NO_SSL
    void ignoresslerror_data();
    void ignoresslerror();
#endif
#ifdef QT_NO_SSL
    void nossl_data();
    void nossl();
#endif
    void get401_data();
    void get401();

    void getMultiple_data();
    void getMultiple();
    void getMultipleWithPipeliningAndMultiplePriorities();
    void getMultipleWithPriorities();

    void getEmptyWithPipelining();

    void getAndEverythingShouldBePipelined();

    void getAndThenDeleteObject();
    void getAndThenDeleteObject_data();

    void overlappingCloseAndWrite();
};

void tst_QHttpNetworkConnection::initTestCase()
{
#if defined(QT_TEST_SERVER)
    QVERIFY(QtNetworkSettings::verifyConnection(httpServerName(), 80));
#else
    if (!QtNetworkSettings::verifyTestNetworkSettings())
        QSKIP("No network test server available");
#endif
}

void tst_QHttpNetworkConnection::options_data()
{
    // not tested yet
}

void tst_QHttpNetworkConnection::options()
{
    QEXPECT_FAIL("", "not tested yet", Continue);
    QVERIFY(false);
}

void tst_QHttpNetworkConnection::head_data()
{
    QTest::addColumn<QString>("protocol");
    QTest::addColumn<QString>("host");
    QTest::addColumn<QString>("path");
    QTest::addColumn<ushort>("port");
    QTest::addColumn<bool>("encrypt");
    QTest::addColumn<int>("statusCode");
    QTest::addColumn<QString>("statusString");
    QTest::addColumn<int>("contentLength");

    QTest::newRow("success-internal") << "http://" << httpServerName() << "/qtest/rfc3252.txt" << ushort(80) << false << 200 << "OK" << 25962;
    QTest::newRow("failure-path") << "http://" << httpServerName() << "/t" << ushort(80) << false << 404 << "Not Found" << -1;
    QTest::newRow("failure-protocol") << "" << httpServerName() << "/qtest/rfc3252.txt" << ushort(80) << false << 400 << "Bad Request" << -1;
}

void tst_QHttpNetworkConnection::head()
{
    QFETCH(QString, protocol);
    QFETCH(QString, host);
    QFETCH(QString, path);
    QFETCH(ushort, port);
    QFETCH(bool, encrypt);
    QFETCH(int, statusCode);
    QFETCH(QString, statusString);
    QFETCH(int, contentLength);

    QHttpNetworkConnection connection(QHttpNetworkConnectionPrivate::defaultHttpChannelCount, host, port, encrypt);
    QCOMPARE(connection.port(), port);
    QCOMPARE(connection.hostName(), host);
    QCOMPARE(connection.isSsl(), encrypt);

    QHttpNetworkRequest request(protocol + host + path, QHttpNetworkRequest::Head);
    QHttpNetworkReply *reply = connection.sendRequest(request);

    QTRY_VERIFY_WITH_TIMEOUT(reply->isFinished(), 30000);
    QCOMPARE(reply->statusCode(), statusCode);
    QCOMPARE(reply->reasonPhrase(), statusString);
    // only check it if it is set and expected
    if (reply->contentLength() != -1 && contentLength != -1)
        QCOMPARE(reply->contentLength(), qint64(contentLength));

    QVERIFY(reply->isFinished());

    delete reply;
}

void tst_QHttpNetworkConnection::get_data()
{
    QTest::addColumn<QString>("protocol");
    QTest::addColumn<QString>("host");
    QTest::addColumn<QString>("path");
    QTest::addColumn<ushort>("port");
    QTest::addColumn<bool>("encrypt");
    QTest::addColumn<int>("statusCode");
    QTest::addColumn<QString>("statusString");
    QTest::addColumn<int>("contentLength");
    QTest::addColumn<int>("downloadSize");

    QTest::newRow("success-internal") << "http://" << httpServerName() << "/qtest/rfc3252.txt" << ushort(80) << false << 200 << "OK" << 25962 << 25962;

    QTest::newRow("failure-path") << "http://" << httpServerName() << "/t" << ushort(80) << false << 404 << "Not Found" << -1 << -1;
    QTest::newRow("failure-protocol") << "" << httpServerName() << "/qtest/rfc3252.txt" << ushort(80) << false << 400 << "Bad Request" << -1 << -1;
}

void tst_QHttpNetworkConnection::get()
{
    QFETCH(QString, protocol);
    QFETCH(QString, host);
    QFETCH(QString, path);
    QFETCH(ushort, port);
    QFETCH(bool, encrypt);
    QFETCH(int, statusCode);
    QFETCH(QString, statusString);
    QFETCH(int, contentLength);
    QFETCH(int, downloadSize);

    QHttpNetworkConnection connection(QHttpNetworkConnectionPrivate::defaultHttpChannelCount, host, port, encrypt);
    QCOMPARE(connection.port(), port);
    QCOMPARE(connection.hostName(), host);
    QCOMPARE(connection.isSsl(), encrypt);

    QHttpNetworkRequest request(protocol + host + path);
    QHttpNetworkReply *reply = connection.sendRequest(request);

    QTRY_VERIFY_WITH_TIMEOUT(reply->bytesAvailable(), 30000);

    QCOMPARE(reply->statusCode(), statusCode);
    QCOMPARE(reply->reasonPhrase(), statusString);
    // only check it if it is set and expected
    if (reply->contentLength() != -1 && contentLength != -1)
        QCOMPARE(reply->contentLength(), qint64(contentLength));

    QTRY_VERIFY_WITH_TIMEOUT(reply->isFinished(), 30000);
    QByteArray ba = reply->readAll();
    //do not require server generated error pages to be a fixed size
    if (downloadSize != -1)
        QCOMPARE(ba.size(), downloadSize);
    //but check against content length if it was sent
    if (reply->contentLength() != -1)
        QCOMPARE(ba.size(), (int)reply->contentLength());

    delete reply;
}

void tst_QHttpNetworkConnection::finishedReply()
{
    finishedCalled = true;
}

void tst_QHttpNetworkConnection::finishedWithError(QNetworkReply::NetworkError errorCode, const QString &detail)
{
    Q_UNUSED(detail);
    finishedWithErrorCalled = true;
    netErrorCode = errorCode;
}

void tst_QHttpNetworkConnection::put_data()
{

    QTest::addColumn<QString>("protocol");
    QTest::addColumn<QString>("host");
    QTest::addColumn<QString>("path");
    QTest::addColumn<ushort>("port");
    QTest::addColumn<bool>("encrypt");
    QTest::addColumn<QString>("data");
    QTest::addColumn<bool>("succeed");

    QTest::newRow("success-internal") << "http://" << httpServerName() << "/dav/file1.txt" << ushort(80) << false << "Hello World\nEnd of file\n"<<true;
    QTest::newRow("fail-internal") << "http://" << httpServerName() << "/dav2/file1.txt" << ushort(80) << false << "Hello World\nEnd of file\n"<<false;
    QTest::newRow("fail-host") << "http://" << "invalid.test.qt-project.org" << "/dav2/file1.txt" << ushort(80) << false << "Hello World\nEnd of file\n"<<false;
}

void tst_QHttpNetworkConnection::put()
{
    QFETCH(QString, protocol);
    QFETCH(QString, host);
    QFETCH(QString, path);
    QFETCH(ushort, port);
    QFETCH(bool, encrypt);
    QFETCH(QString, data);
    QFETCH(bool, succeed);

    QHttpNetworkConnection connection(QHttpNetworkConnectionPrivate::defaultHttpChannelCount, host, port, encrypt);
    QCOMPARE(connection.port(), port);
    QCOMPARE(connection.hostName(), host);
    QCOMPARE(connection.isSsl(), encrypt);

    QHttpNetworkRequest request(protocol + host + path, QHttpNetworkRequest::Put);

    QByteArray array = data.toLatin1();
    QNonContiguousByteDevice *bd = QNonContiguousByteDeviceFactory::create(array);
    bd->setParent(this);
    request.setUploadByteDevice(bd);

    finishedCalled = false;
    finishedWithErrorCalled = false;

    QHttpNetworkReply *reply = connection.sendRequest(request);
    connect(reply, SIGNAL(finished()), SLOT(finishedReply()));
    connect(reply, SIGNAL(finishedWithError(QNetworkReply::NetworkError,QString)),
        SLOT(finishedWithError(QNetworkReply::NetworkError,QString)));

    QTRY_VERIFY_WITH_TIMEOUT(reply->isFinished() || finishedCalled || finishedWithErrorCalled, 30000);

    if (reply->isFinished()) {
        QByteArray ba;
        while (reply->bytesAvailable())
            ba += reply->readAny();
    } else if(finishedWithErrorCalled) {
        if(!succeed) {
            delete reply;
            return;
        } else {
            QFAIL("Error in PUT");
        }
    } else {
        QFAIL("PUT timed out");
    }

    int status = reply->statusCode();
    if (status != 200 && status != 201 && status != 204) {
        if (succeed) {
            qDebug()<<"PUT failed, Status Code:" <<status;
            QFAIL("Error in PUT");
        }
    } else {
        if (!succeed) {
            qDebug()<<"PUT Should fail, Status Code:" <<status;
            QFAIL("Error in PUT");
        }
    }
    delete reply;
}

void tst_QHttpNetworkConnection::post_data()
{
    QTest::addColumn<QString>("protocol");
    QTest::addColumn<QString>("host");
    QTest::addColumn<QString>("path");
    QTest::addColumn<ushort>("port");
    QTest::addColumn<bool>("encrypt");
    QTest::addColumn<QString>("data");
    QTest::addColumn<int>("statusCode");
    QTest::addColumn<QString>("statusString");
    QTest::addColumn<int>("contentLength");
    QTest::addColumn<int>("downloadSize");

    QTest::newRow("success-internal") << "http://" << httpServerName() << "/qtest/cgi-bin/echo.cgi" << ushort(80) << false << "7 bytes" << 200 << "OK" << 7 << 7;
    QTest::newRow("failure-internal") << "http://" << httpServerName() << "/t" << ushort(80) << false << "Hello World" << 404 << "Not Found" << -1 << -1;
}

void tst_QHttpNetworkConnection::post()
{
    QFETCH(QString, protocol);
    QFETCH(QString, host);
    QFETCH(QString, path);
    QFETCH(ushort, port);
    QFETCH(bool, encrypt);
    QFETCH(QString, data);
    QFETCH(int, statusCode);
    QFETCH(QString, statusString);
    QFETCH(int, contentLength);
    QFETCH(int, downloadSize);

    QHttpNetworkConnection connection(QHttpNetworkConnectionPrivate::defaultHttpChannelCount, host, port, encrypt);
    QCOMPARE(connection.port(), port);
    QCOMPARE(connection.hostName(), host);
    QCOMPARE(connection.isSsl(), encrypt);

    QHttpNetworkRequest request(protocol + host + path, QHttpNetworkRequest::Post);

    QByteArray array = data.toLatin1();
    QNonContiguousByteDevice *bd = QNonContiguousByteDeviceFactory::create(array);
    bd->setParent(this);
    request.setUploadByteDevice(bd);

    QHttpNetworkReply *reply = connection.sendRequest(request);

    QTRY_VERIFY_WITH_TIMEOUT(reply->bytesAvailable(), 30000);
    QCOMPARE(reply->statusCode(), statusCode);
    QCOMPARE(reply->reasonPhrase(), statusString);

    qint64 cLen = reply->contentLength();
    if (contentLength != -1) {
        // only check the content length if test expected it to be set
        if (cLen==-1) {
            // HTTP 1.1 server may respond with chunked encoding and in that
            // case contentLength is not present in reply -> verify that it is the case
            QByteArray transferEnc = reply->headerField("Transfer-Encoding");
            QCOMPARE(transferEnc, QByteArray("chunked"));
        } else {
            QCOMPARE(cLen, qint64(contentLength));
        }
    }

    QTRY_VERIFY_WITH_TIMEOUT(reply->isFinished(), 30000);
    QByteArray ba = reply->readAll();
    //don't require fixed size for generated error pages
    if (downloadSize != -1)
        QCOMPARE(ba.size(), downloadSize);
    //but do compare with content length if possible
    if (cLen != -1)
        QCOMPARE(ba.size(), (int)cLen);

    delete reply;
}

void tst_QHttpNetworkConnection::_delete_data()
{
    // not tested yet
}

void tst_QHttpNetworkConnection::_delete()
{
    QEXPECT_FAIL("", "not tested yet", Continue);
    QVERIFY(false);
}

void tst_QHttpNetworkConnection::trace_data()
{
    // not tested yet
}

void tst_QHttpNetworkConnection::trace()
{
    QEXPECT_FAIL("", "not tested yet", Continue);
    QVERIFY(false);
}

void tst_QHttpNetworkConnection::_connect_data()
{
    // not tested yet
}

void tst_QHttpNetworkConnection::_connect()
{
    QEXPECT_FAIL("", "not tested yet", Continue);
    QVERIFY(false);
}

void tst_QHttpNetworkConnection::challenge401(const QHttpNetworkRequest &request,
                                                        QAuthenticator *authenticator)
{
    Q_UNUSED(request);

    QHttpNetworkReply *reply = qobject_cast<QHttpNetworkReply*>(sender());
    if (reply) {
        QHttpNetworkConnection *c = reply->connection();

        QVariant val = c->property("setCredentials");
        if (val.toBool()) {
            QVariant user = c->property("username");
            QVariant password = c->property("password");
            authenticator->setUser(user.toString());
            authenticator->setPassword(password.toString());
            c->setProperty("setCredentials", false);
        }
    }
}

void tst_QHttpNetworkConnection::get401_data()
{
    QTest::addColumn<QString>("protocol");
    QTest::addColumn<QString>("host");
    QTest::addColumn<QString>("path");
    QTest::addColumn<ushort>("port");
    QTest::addColumn<bool>("encrypt");
    QTest::addColumn<bool>("setCredentials");
    QTest::addColumn<QString>("username");
    QTest::addColumn<QString>("password");
    QTest::addColumn<int>("statusCode");

    QTest::newRow("no-credentials") << "http://" << httpServerName() << "/qtest/rfcs-auth/index.html" << ushort(80) << false << false << "" << ""<<401;
    QTest::newRow("invalid-credentials") << "http://" << httpServerName() << "/qtest/rfcs-auth/index.html" << ushort(80) << false << true << "test" << "test"<<401;
    QTest::newRow("valid-credentials") << "http://" << httpServerName() << "/qtest/rfcs-auth/index.html" << ushort(80) << false << true << "httptest" << "httptest"<<200;
    QTest::newRow("digest-authentication-invalid") << "http://" << httpServerName() << "/qtest/auth-digest/index.html" << ushort(80) << false << true << "wrong" << "wrong"<<401;
    QTest::newRow("digest-authentication-valid") << "http://" << httpServerName() << "/qtest/auth-digest/index.html" << ushort(80) << false << true << "httptest" << "httptest"<<200;
}

void tst_QHttpNetworkConnection::get401()
{
    QFETCH(QString, protocol);
    QFETCH(QString, host);
    QFETCH(QString, path);
    QFETCH(ushort, port);
    QFETCH(bool, encrypt);
    QFETCH(bool, setCredentials);
    QFETCH(QString, username);
    QFETCH(QString, password);
    QFETCH(int, statusCode);

    QHttpNetworkConnection connection(QHttpNetworkConnectionPrivate::defaultHttpChannelCount, host, port, encrypt);
    QCOMPARE(connection.port(), port);
    QCOMPARE(connection.hostName(), host);
    QCOMPARE(connection.isSsl(), encrypt);
    connection.setProperty("setCredentials", setCredentials);
    connection.setProperty("username", username);
    connection.setProperty("password", password);

    QHttpNetworkRequest request(protocol + host + path);
    QHttpNetworkReply *reply = connection.sendRequest(request);
    connect(reply, SIGNAL(authenticationRequired(QHttpNetworkRequest,QAuthenticator*)),
                           SLOT(challenge401(QHttpNetworkRequest,QAuthenticator*)));

    finishedCalled = false;
    finishedWithErrorCalled = false;

    connect(reply, SIGNAL(finished()), SLOT(finishedReply()));
    connect(reply, SIGNAL(finishedWithError(QNetworkReply::NetworkError,QString)),
        SLOT(finishedWithError(QNetworkReply::NetworkError,QString)));

    QTRY_VERIFY_WITH_TIMEOUT(finishedCalled || finishedWithErrorCalled, 30000);
    QCOMPARE(reply->statusCode(), statusCode);
    delete reply;
}

#ifndef QT_NO_COMPRESS
void tst_QHttpNetworkConnection::compression_data()
{
    QTest::addColumn<QString>("protocol");
    QTest::addColumn<QString>("host");
    QTest::addColumn<QString>("path");
    QTest::addColumn<ushort>("port");
    QTest::addColumn<bool>("encrypt");
    QTest::addColumn<int>("statusCode");
    QTest::addColumn<QString>("statusString");
    QTest::addColumn<int>("contentLength");
    QTest::addColumn<int>("downloadSize");
    QTest::addColumn<bool>("autoCompress");
    QTest::addColumn<QString>("contentCoding");

    QTest::newRow("success-autogzip-temp") << "http://" << httpServerName() << "/qtest/rfcs/rfc2616.html" << ushort(80) << false << 200 << "OK" << -1 << 418321 << true << "";
    QTest::newRow("success-nogzip-temp") << "http://" << httpServerName() << "/qtest/rfcs/rfc2616.html" << ushort(80) << false << 200 << "OK" << 418321 << 418321 << false << "identity";
    QTest::newRow("success-manualgzip-temp") << "http://" << httpServerName() << "/qtest/deflate/rfc2616.html" << ushort(80) << false << 200 << "OK" << 119124 << 119124 << false << "gzip";

}

void tst_QHttpNetworkConnection::compression()
{
    QFETCH(QString, protocol);
    QFETCH(QString, host);
    QFETCH(QString, path);
    QFETCH(ushort, port);
    QFETCH(bool, encrypt);
    QFETCH(int, statusCode);
    QFETCH(QString, statusString);
    QFETCH(int, contentLength);
    QFETCH(int, downloadSize);
    QFETCH(bool, autoCompress);
    QFETCH(QString, contentCoding);

    QHttpNetworkConnection connection(QHttpNetworkConnectionPrivate::defaultHttpChannelCount, host, port, encrypt);
    QCOMPARE(connection.port(), port);
    QCOMPARE(connection.hostName(), host);
    QCOMPARE(connection.isSsl(), encrypt);

    QHttpNetworkRequest request(protocol + host + path);
    if (!autoCompress)
        request.setHeaderField("Accept-Encoding", contentCoding.toLatin1());
    QHttpNetworkReply *reply = connection.sendRequest(request);

    QTRY_VERIFY_WITH_TIMEOUT(reply->bytesAvailable(), 30000);
    QCOMPARE(reply->statusCode(), statusCode);
    QCOMPARE(reply->reasonPhrase(), statusString);
    bool isLengthOk = (reply->contentLength() == qint64(contentLength)
                      || reply->contentLength() == qint64(downloadSize)
                      || reply->contentLength() == -1); //apache2 does not send content-length for compressed pages

    QVERIFY(isLengthOk);

    QTRY_VERIFY_WITH_TIMEOUT(reply->isFinished(), 30000);
    QByteArray ba = reply->readAll();
    QCOMPARE(ba.size(), downloadSize);

    delete reply;
}
#endif

#ifndef QT_NO_SSL
void tst_QHttpNetworkConnection::sslErrors(const QList<QSslError> &errors)
{
    Q_UNUSED(errors);

    QHttpNetworkReply *reply = qobject_cast<QHttpNetworkReply*>(sender());
    if (reply) {
        QHttpNetworkConnection *connection = reply->connection();

        QVariant val = connection->property("ignoreFromSignal");
        if (val.toBool())
            connection->ignoreSslErrors();
        finishedWithErrorCalled = true;
    }
}

void tst_QHttpNetworkConnection::ignoresslerror_data()
{
    QTest::addColumn<QString>("protocol");
    QTest::addColumn<QString>("host");
    QTest::addColumn<QString>("path");
    QTest::addColumn<ushort>("port");
    QTest::addColumn<bool>("encrypt");
    QTest::addColumn<bool>("ignoreInit");
    QTest::addColumn<bool>("ignoreFromSignal");
    QTest::addColumn<int>("statusCode");

    // This test will work only if the website has ssl errors.
    // fluke's certificate is signed by a non-standard authority.
    // Since we don't introduce that CA into the SSL verification chain,
    // connecting should fail.
    QTest::newRow("success-init") << "https://" << httpServerName() << "/" << ushort(443) << true << true << false << 200;
    QTest::newRow("success-fromSignal") << "https://" << httpServerName() << "/" << ushort(443) << true << false << true << 200;
    QTest::newRow("failure") << "https://" << httpServerName() << "/" << ushort(443) << true << false << false << 100;
}

void tst_QHttpNetworkConnection::ignoresslerror()
{
    QFETCH(QString, protocol);
    QFETCH(QString, host);
    QFETCH(QString, path);
    QFETCH(ushort, port);
    QFETCH(bool, encrypt);
    QFETCH(bool, ignoreInit);
    QFETCH(bool, ignoreFromSignal);
    QFETCH(int, statusCode);

    QHttpNetworkConnection connection(QHttpNetworkConnectionPrivate::defaultHttpChannelCount, host, port, encrypt);
    QCOMPARE(connection.port(), port);
    QCOMPARE(connection.hostName(), host);
    if (ignoreInit)
        connection.ignoreSslErrors();
    QCOMPARE(connection.isSsl(), encrypt);
    connection.setProperty("ignoreFromSignal", ignoreFromSignal);

    QHttpNetworkRequest request(protocol + host + path);
    QHttpNetworkReply *reply = connection.sendRequest(request);
    connect(reply, SIGNAL(sslErrors(QList<QSslError>)),
        SLOT(sslErrors(QList<QSslError>)));

    finishedWithErrorCalled = false;

    connect(reply, SIGNAL(finished()), SLOT(finishedReply()));

    QTRY_VERIFY_WITH_TIMEOUT(reply->bytesAvailable() || (statusCode == 100 && finishedWithErrorCalled), 30000);
    QCOMPARE(reply->statusCode(), statusCode);
    delete reply;
}
#endif

#ifdef QT_NO_SSL
void tst_QHttpNetworkConnection::nossl_data()
{
    QTest::addColumn<QString>("protocol");
    QTest::addColumn<QString>("host");
    QTest::addColumn<QString>("path");
    QTest::addColumn<ushort>("port");
    QTest::addColumn<bool>("encrypt");
    QTest::addColumn<QNetworkReply::NetworkError>("networkError");

    QTest::newRow("protocol-error") << "https://" << httpServerName() << "/" << ushort(443) << true <<QNetworkReply::ProtocolUnknownError;
}

void tst_QHttpNetworkConnection::nossl()
{
    QFETCH(QString, protocol);
    QFETCH(QString, host);
    QFETCH(QString, path);
    QFETCH(ushort, port);
    QFETCH(bool, encrypt);
    QFETCH(QNetworkReply::NetworkError, networkError);

    QHttpNetworkConnection connection(QHttpNetworkConnectionPrivate::defaultHttpChannelCount, host, port, encrypt);
    QCOMPARE(connection.port(), port);
    QCOMPARE(connection.hostName(), host);

    QHttpNetworkRequest request(protocol + host + path);
    QHttpNetworkReply *reply = connection.sendRequest(request);

    finishedWithErrorCalled = false;
    netErrorCode = QNetworkReply::NoError;

    connect(reply, SIGNAL(finished()), SLOT(finishedReply()));
    connect(reply, SIGNAL(finishedWithError(QNetworkReply::NetworkError,QString)),
        SLOT(finishedWithError(QNetworkReply::NetworkError,QString)));

    QTRY_VERIFY_WITH_TIMEOUT(finishedWithErrorCalled, 30000);
    QCOMPARE(netErrorCode, networkError);
    delete reply;
}
#endif


void tst_QHttpNetworkConnection::getMultiple_data()
{
    QTest::addColumn<quint16>("connectionCount");
    QTest::addColumn<bool>("pipeliningAllowed");
    // send 100 requests. apache will usually force-close after 100 requests in a single tcp connection
    QTest::addColumn<int>("requestCount");

    QTest::newRow("6 connections, no pipelining, 100 requests")  << quint16(6) << false << 100;
    QTest::newRow("1 connection, no pipelining, 100 requests")  << quint16(1) << false << 100;
    QTest::newRow("6 connections, pipelining allowed, 100 requests")  << quint16(6) << true << 100;
    QTest::newRow("1 connection, pipelining allowed, 100 requests")  << quint16(1) << true << 100;
}

static bool allRepliesFinished(const QList<QHttpNetworkReply*> *_replies)
{
    const QList<QHttpNetworkReply*> &replies = *_replies;
    for (int i = 0; i < replies.size(); i++)
        if (!replies.at(i)->isFinished())
            return false;
    return true;
}

void tst_QHttpNetworkConnection::getMultiple()
{
    QFETCH(quint16, connectionCount);
    QFETCH(bool, pipeliningAllowed);
    QFETCH(int, requestCount);

    QHttpNetworkConnection connection(connectionCount, httpServerName());

    QList<QHttpNetworkRequest*> requests;
    QList<QHttpNetworkReply*> replies;

    for (int i = 0; i < requestCount; i++) {
        // depending on what you use the results will vary.
        // for the "real" results, use a URL that has "internet latency" for you. Then (6 connections, pipelining) will win.
        // for LAN latency, you will possibly get that (1 connection, no pipelining) is the fastest
        QHttpNetworkRequest *request = new QHttpNetworkRequest("http://" + httpServerName() + "/qtest/rfc3252.txt");
        if (pipeliningAllowed)
            request->setPipeliningAllowed(true);
        requests.append(request);
        QHttpNetworkReply *reply = connection.sendRequest(*request);
        replies.append(reply);
    }

    QTRY_VERIFY_WITH_TIMEOUT(allRepliesFinished(&replies), 60000);
    qDeleteAll(requests);
    qDeleteAll(replies);
}

void tst_QHttpNetworkConnection::getMultipleWithPipeliningAndMultiplePriorities()
{
    quint16 requestCount = 100;

    // use 2 connections.
    QHttpNetworkConnection connection(2, httpServerName());

    QList<QHttpNetworkRequest*> requests;
    QList<QHttpNetworkReply*> replies;

    for (int i = 0; i < requestCount; i++) {
        QHttpNetworkRequest *request = nullptr;
        if (i % 3)
            request = new QHttpNetworkRequest("http://" + httpServerName() + "/qtest/rfc3252.txt", QHttpNetworkRequest::Get);
        else
            request = new QHttpNetworkRequest("http://" + httpServerName() + "/qtest/rfc3252.txt", QHttpNetworkRequest::Head);

        if (i % 2 || i % 3)
            request->setPipeliningAllowed(true);

        if (i % 3)
            request->setPriority(QHttpNetworkRequest::HighPriority);
        else if (i % 5)
            request->setPriority(QHttpNetworkRequest::NormalPriority);
        else if (i % 7)
            request->setPriority(QHttpNetworkRequest::LowPriority);

        requests.append(request);
        QHttpNetworkReply *reply = connection.sendRequest(*request);
        replies.append(reply);
    }

    QTRY_VERIFY_WITH_TIMEOUT(allRepliesFinished(&replies), 60000);

    int pipelinedCount = 0;
    for (int i = 0; i < replies.size(); i++) {
        QVERIFY (!(replies.at(i)->request().isPipeliningAllowed() == false
            && replies.at(i)->isPipeliningUsed()));

        if (replies.at(i)->isPipeliningUsed())
            pipelinedCount++;
    }

    // We allow pipelining for every 2nd,3rd,4th,6th,8th,9th,10th etc request.
    // Assume that half of the requests had been pipelined.
    // (this is a very relaxed condition, when last measured 79 of 100
    // requests had been pipelined)
    QVERIFY(pipelinedCount >= requestCount / 2);

    qDeleteAll(requests);
    qDeleteAll(replies);
}

class GetMultipleWithPrioritiesReceiver : public QObject
{
    Q_OBJECT
public:
    int highPrioReceived;
    int lowPrioReceived;
    int requestCount;
    GetMultipleWithPrioritiesReceiver(int rq) : highPrioReceived(0), lowPrioReceived(0), requestCount(rq) { }
public Q_SLOTS:
    void finishedSlot() {
        QHttpNetworkReply *reply = (QHttpNetworkReply*) sender();
        if (reply->request().priority() == QHttpNetworkRequest::HighPriority)
            highPrioReceived++;
        else if (reply->request().priority() == QHttpNetworkRequest::LowPriority)
            lowPrioReceived++;
        else
            QFAIL("Wrong priority!?");

        QVERIFY(highPrioReceived + 7 >= lowPrioReceived);

        if (highPrioReceived + lowPrioReceived == requestCount)
            QTestEventLoop::instance().exitLoop();
    }
};

void tst_QHttpNetworkConnection::getMultipleWithPriorities()
{
    quint16 requestCount = 100;
    // use 2 connections.
    QHttpNetworkConnection connection(2, httpServerName());
    GetMultipleWithPrioritiesReceiver receiver(requestCount);
    QUrl url("http://" + httpServerName() + "/qtest/rfc3252.txt");
    QList<QHttpNetworkRequest*> requests;
    QList<QHttpNetworkReply*> replies;

    for (int i = 0; i < requestCount; i++) {
        QHttpNetworkRequest *request = nullptr;
        if (i % 3)
            request = new QHttpNetworkRequest(url, QHttpNetworkRequest::Get);
        else
            request = new QHttpNetworkRequest(url, QHttpNetworkRequest::Head);

        if (i % 2)
            request->setPriority(QHttpNetworkRequest::HighPriority);
        else
            request->setPriority(QHttpNetworkRequest::LowPriority);

        requests.append(request);
        QHttpNetworkReply *reply = connection.sendRequest(*request);
        connect(reply, SIGNAL(finished()), &receiver, SLOT(finishedSlot()));
        replies.append(reply);
    }

    QTestEventLoop::instance().enterLoop(40);
    QVERIFY(!QTestEventLoop::instance().timeout());

    qDeleteAll(requests);
    qDeleteAll(replies);
}


class GetEmptyWithPipeliningReceiver : public QObject
{
    Q_OBJECT
public:
    int receivedCount;
    int requestCount;
    GetEmptyWithPipeliningReceiver(int rq) : receivedCount(0),requestCount(rq) { }
public Q_SLOTS:
    void finishedSlot() {
        QHttpNetworkReply *reply = (QHttpNetworkReply*) sender();
        Q_UNUSED(reply);
        receivedCount++;

        if (receivedCount == requestCount)
            QTestEventLoop::instance().exitLoop();
    }
};

void tst_QHttpNetworkConnection::getEmptyWithPipelining()
{
    quint16 requestCount = 50;
    // use 2 connections.
    QHttpNetworkConnection connection(2, httpServerName());
    GetEmptyWithPipeliningReceiver receiver(requestCount);

    QUrl url("http://" + httpServerName() + "/cgi-bin/echo.cgi"); // a get on this = getting an empty file
    QList<QHttpNetworkRequest*> requests;
    QList<QHttpNetworkReply*> replies;

    for (int i = 0; i < requestCount; i++) {
        QHttpNetworkRequest *request = nullptr;
        request = new QHttpNetworkRequest(url, QHttpNetworkRequest::Get);
        request->setPipeliningAllowed(true);

        requests.append(request);
        QHttpNetworkReply *reply = connection.sendRequest(*request);
        connect(reply, SIGNAL(finished()), &receiver, SLOT(finishedSlot()));
        replies.append(reply);
    }

    QTestEventLoop::instance().enterLoop(20);
    QVERIFY(!QTestEventLoop::instance().timeout());

    qDeleteAll(requests);
    qDeleteAll(replies);
}

class GetAndEverythingShouldBePipelinedReceiver : public QObject
{
    Q_OBJECT
public:
    int receivedCount;
    int requestCount;
    GetAndEverythingShouldBePipelinedReceiver(int rq) : receivedCount(0),requestCount(rq) { }
public Q_SLOTS:
    void finishedSlot() {
        QHttpNetworkReply *reply = (QHttpNetworkReply*) sender();
        Q_UNUSED(reply);
        receivedCount++;

        if (receivedCount == requestCount)
            QTestEventLoop::instance().exitLoop();
    }
};

void tst_QHttpNetworkConnection::getAndEverythingShouldBePipelined()
{
    quint16 requestCount = 100;
    // use 1 connection.
    QHttpNetworkConnection connection(1, httpServerName());
    QUrl url("http://" + httpServerName() + "/qtest/rfc3252.txt");
    QList<QHttpNetworkRequest*> requests;
    QList<QHttpNetworkReply*> replies;

    GetAndEverythingShouldBePipelinedReceiver receiver(requestCount);

    for (int i = 0; i < requestCount; i++) {
        QHttpNetworkRequest *request = nullptr;
        request = new QHttpNetworkRequest(url, QHttpNetworkRequest::Get);
        request->setPipeliningAllowed(true);
        requests.append(request);
        QHttpNetworkReply *reply = connection.sendRequest(*request);
        connect(reply, SIGNAL(finished()), &receiver, SLOT(finishedSlot()));
        replies.append(reply);
    }
    QTestEventLoop::instance().enterLoop(40);
    QVERIFY(!QTestEventLoop::instance().timeout());

    qDeleteAll(requests);
    qDeleteAll(replies);

}


void tst_QHttpNetworkConnection::getAndThenDeleteObject_data()
{
    QTest::addColumn<bool>("replyFirst");

    QTest::newRow("delete-reply-first") << true;
    QTest::newRow("delete-connection-first") << false;
}

void tst_QHttpNetworkConnection::getAndThenDeleteObject()
{
    // yes, this will leak if the testcase fails. I don't care. It must not fail then :P
    QHttpNetworkConnection *connection = new QHttpNetworkConnection(QHttpNetworkConnectionPrivate::defaultHttpChannelCount, httpServerName());
    QHttpNetworkRequest request("http://" + httpServerName() + "/qtest/bigfile");
    QHttpNetworkReply *reply = connection->sendRequest(request);
    reply->setDownstreamLimited(true);

    QTRY_VERIFY_WITH_TIMEOUT(reply->bytesAvailable(), 30000);
    QCOMPARE(reply->statusCode() ,200);
    QVERIFY(!reply->isFinished()); // must not be finished

    QFETCH(bool, replyFirst);

    if (replyFirst) {
        delete reply;
        delete connection;
    } else {
        delete connection;
        delete reply;
    }
}

class TestTcpServer : public QTcpServer
{
    Q_OBJECT
public:
    TestTcpServer() : errorCodeReports(0)
    {
        connect(this, &QTcpServer::newConnection, this, &TestTcpServer::onNewConnection);
        QVERIFY(listen(QHostAddress::LocalHost));
    }

    int errorCodeReports;

public slots:
    void onNewConnection()
    {
        QTcpSocket *socket = nextPendingConnection();
        if (!socket)
            return;
        // close socket instantly!
        connect(socket, &QTcpSocket::readyRead, socket, &QTcpSocket::close);
    }

    void onReply(QNetworkReply::NetworkError code)
    {
        QCOMPARE(code, QNetworkReply::RemoteHostClosedError);
        ++errorCodeReports;
    }
};

void tst_QHttpNetworkConnection::overlappingCloseAndWrite()
{
    // server accepts connections, but closes the socket instantly
    TestTcpServer server;
    QNetworkAccessManager accessManager;

    // ten requests are scheduled. All should result in an RemoteHostClosed...
    QUrl url;
    url.setScheme(QStringLiteral("http"));
    url.setHost(server.serverAddress().toString());
    url.setPort(server.serverPort());
    for (int i = 0; i < 10; ++i) {
        QNetworkRequest request(url);
        QNetworkReply *reply = accessManager.get(request);
        QObject::connect(reply, &QNetworkReply::errorOccurred,
                         &server, &TestTcpServer::onReply);
    }

    QTRY_COMPARE(server.errorCodeReports, 10);
}


QTEST_MAIN(tst_QHttpNetworkConnection)
#include "tst_qhttpnetworkconnection.moc"
