/****************************************************************************
**
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** 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 The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/


#include <QtTest/QTest>
#include <QtTest/QTestEventLoop>

#include <QtCore/QQueue>
#include <QtCore/QString>
#include <QtCore/QCoreApplication>
#include <QtCore/QMetaType>
#include <QtCore/QTimer>

#include <private/qsocks5socketengine_p.h>
#include <qhostinfo.h>
#include <qhostaddress.h>
#include <qtcpsocket.h>
#include <qauthenticator.h>
#include <qdebug.h>
#include <qtcpserver.h>
#include <qmetatype.h>
#include <qdebug.h>

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

class tst_QSocks5SocketEngine : public QObject, public QAbstractSocketEngineReceiver
{
    Q_OBJECT

private slots:
    void initTestCase();
    void construction();
    void errorTest_data();
    void errorTest();
    void simpleConnectToIMAP();
    void simpleErrorsAndStates();
    void udpTest();
    void serverTest();
    void tcpSocketBlockingTest();
    void tcpSocketNonBlockingTest();
    void downloadBigFile();
   // void tcpLoopbackPerformance();
    void passwordAuth();
    void passwordAuth2();
    void fragmentation_data();
    void fragmentation();
    void incomplete_data();
    void incomplete();

protected slots:
    void proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *auth);

private:
    void readNotification() { }
    void writeNotification() { }
    void closeNotification() { }
    void exceptionNotification() { }
    void connectionNotification() { }
};

class MiniSocks5ResponseHandler : public QObject
{
    Q_OBJECT
public:
    QQueue<QByteArray> responses;
    QTcpSocket *client;

    MiniSocks5ResponseHandler(QQueue<QByteArray> r, QTcpSocket *c, int autoResponseTime)
        : responses(r), client(c)
    {
        client->setParent(this);
        connect(client, SIGNAL(disconnected()), SLOT(deleteLater()));
        connect(client, SIGNAL(readyRead()), SLOT(sendNextResponse()));
        if (autoResponseTime)
            QTimer::singleShot(autoResponseTime, this, SLOT(sendNextResponse()));
    }

private slots:
    void sendNextResponse()
    {
        // WARNING
        // this assumes that the client command is received in its entirety
        // should be ok, since SOCKSv5 commands are rather small
        if (responses.isEmpty())
            client->disconnectFromHost();
        else
            client->write(responses.dequeue());
    }
};

class MiniSocks5Server: public QTcpServer
{
    Q_OBJECT
public:
    QQueue<QByteArray> responses;
    int autoResponseTime;

    MiniSocks5Server(const QQueue<QByteArray> r, int t = 0)
        : responses(r), autoResponseTime(t)
    {
        listen();
        connect(this, SIGNAL(newConnection()), SLOT(handleNewConnection()));
    }

private slots:
    void handleNewConnection()
    {
        QTcpSocket *client = nextPendingConnection();
        new MiniSocks5ResponseHandler(responses, client, autoResponseTime);
    }
};

void tst_QSocks5SocketEngine::initTestCase()
{
#ifdef QT_TEST_SERVER
     QVERIFY(QtNetworkSettings::verifyConnection(QtNetworkSettings::socksProxyServerName(), 1080));
     QVERIFY(QtNetworkSettings::verifyConnection(QtNetworkSettings::httpServerName(), 80));
     QVERIFY(QtNetworkSettings::verifyConnection(QtNetworkSettings::imapServerName(), 143));
#else
    if (!QtNetworkSettings::verifyTestNetworkSettings())
        QSKIP("No network test server available");
#endif
}

//---------------------------------------------------------------------------
void tst_QSocks5SocketEngine::construction()
{
    QSocks5SocketEngine socketDevice;

    QVERIFY(!socketDevice.isValid());

    // Initialize device
    QVERIFY(socketDevice.initialize(QAbstractSocket::TcpSocket, QAbstractSocket::IPv4Protocol));
    QVERIFY(socketDevice.isValid());
    QCOMPARE(socketDevice.protocol(), QAbstractSocket::IPv4Protocol);
    QCOMPARE(socketDevice.socketType(), QAbstractSocket::TcpSocket);
    QCOMPARE(socketDevice.state(), QAbstractSocket::UnconnectedState);
   // QVERIFY(socketDevice.socketDescriptor() != -1);
    QCOMPARE(socketDevice.localAddress(), QHostAddress());
    QCOMPARE(socketDevice.localPort(), quint16(0));
    QCOMPARE(socketDevice.peerAddress(), QHostAddress());
    QCOMPARE(socketDevice.peerPort(), quint16(0));
    QCOMPARE(socketDevice.error(), QAbstractSocket::UnknownSocketError);

    //QTest::ignoreMessage(QtWarningMsg, "QSocketLayer::bytesAvailable() was called in QAbstractSocket::UnconnectedState");
    QCOMPARE(socketDevice.bytesAvailable(), 0);

    //QTest::ignoreMessage(QtWarningMsg, "QSocketLayer::hasPendingDatagrams() was called in QAbstractSocket::UnconnectedState");
    QVERIFY(!socketDevice.hasPendingDatagrams());
}

//---------------------------------------------------------------------------
void tst_QSocks5SocketEngine::errorTest_data()
{
    QTest::addColumn<QString>("hostname");
    QTest::addColumn<int>("port");
    QTest::addColumn<QString>("username");
    QTest::addColumn<QQueue<QByteArray> >("responses");
    QTest::addColumn<int>("expectedError");

    QQueue<QByteArray> responses;
    QTest::newRow("proxy-host-not-found") << "this-host-does-not-exist." << 1080 << QString()
                                          << responses
                                          << int(QAbstractSocket::ProxyNotFoundError);
    QTest::newRow("proxy-connection-refused") << "127.0.0.1" << 2 << QString()
                                              << responses
                                              << int(QAbstractSocket::ProxyConnectionRefusedError);

#define REPLY(name, contents)                                           \
    static const char raw_ ## name [] = contents;                       \
    const QByteArray name = QByteArray::fromRawData(raw_ ## name, sizeof raw_ ## name - 1)

    REPLY(garbage, "\4\4\4\4");
    // authentication method replies
    REPLY(noAuthentication, "\5\0");
    REPLY(passwordAuthentication, "\5\2");
    REPLY(garbageAuthentication, "\5\177");
    REPLY(noAcceptableAuthentication, "\5\377");
    // authentication replies
    REPLY(authenticationAccepted, "\5\0");
    REPLY(authenticationNotAccepted, "\5\1");
    // connection replies
    REPLY(connectionAccepted, "\5\0\0\4\177\0\0\1\0\100");
    REPLY(connectionNotAllowed, "\5\2\0");
    REPLY(networkUnreachable, "\5\3\0");
    REPLY(hostUnreachable, "\5\4\0");
    REPLY(connectionRefused, "\5\5\0");

#undef REPLY

    responses << garbage;
    QTest::newRow("garbage1") << QString() << 0 << QString() << responses
                              << int(QAbstractSocket::ProxyProtocolError);

    responses.clear();
    responses << noAuthentication << garbage;
    QTest::newRow("garbage2") << QString() << 0 << QString() << responses
                              << int(QAbstractSocket::ProxyProtocolError);

    responses.clear();
    responses << garbageAuthentication;
    QTest::newRow("unknown-auth-method") << QString() << 0 << QString()
                                         << responses
                                         << int(QAbstractSocket::SocketAccessError);

    responses.clear();
    responses << noAcceptableAuthentication;
    QTest::newRow("no-acceptable-authentication") << QString() << 0 << QString()
                                                  << responses
                                                  << int(QAbstractSocket::ProxyAuthenticationRequiredError);

    responses.clear();
    responses << passwordAuthentication << authenticationNotAccepted;
    QTest::newRow("authentication-required") << QString() << 0 << "foo"
                                             << responses
                                             << int(QAbstractSocket::ProxyAuthenticationRequiredError);

    responses.clear();
    responses << noAuthentication << connectionNotAllowed;
    QTest::newRow("connection-not-allowed") << QString() << 0 << QString()
                                            << responses
                                            << int(QAbstractSocket::SocketAccessError);

    responses.clear();
    responses << noAuthentication << networkUnreachable;
    QTest::newRow("network-unreachable") << QString() << 0 << QString()
                                         << responses
                                         << int(QAbstractSocket::NetworkError);

    responses.clear();
    responses << noAuthentication << hostUnreachable;
    QTest::newRow("host-unreachable") << QString() << 0 << QString()
                                      << responses
                                      << int(QAbstractSocket::HostNotFoundError);

    responses.clear();
    responses << noAuthentication << connectionRefused;
    QTest::newRow("connection-refused") << QString() << 0 << QString()
                                        << responses
                                        << int(QAbstractSocket::ConnectionRefusedError);
}

void tst_QSocks5SocketEngine::errorTest()
{
    QFETCH(QString, hostname);
    QFETCH(int, port);
    QFETCH(QString, username);
    QFETCH(QQueue<QByteArray>, responses);
    QFETCH(int, expectedError);

    MiniSocks5Server server(responses);

    if (hostname.isEmpty()) {
        hostname = "127.0.0.1";
        port = server.serverPort();
    }
    QTcpSocket socket;
    socket.setProxy(QNetworkProxy(QNetworkProxy::Socks5Proxy, hostname, port, username, username));
    socket.connectToHost("0.1.2.3", 12345);

    connect(&socket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)),
            &QTestEventLoop::instance(), SLOT(exitLoop()));
    QTestEventLoop::instance().enterLoop(10);
    QVERIFY(!QTestEventLoop::instance().timeout());

    QCOMPARE(int(socket.error()), expectedError);
}

//---------------------------------------------------------------------------
void tst_QSocks5SocketEngine::simpleConnectToIMAP()
{
    QSocks5SocketEngine socketDevice;

    // Initialize device
    QVERIFY(socketDevice.initialize(QAbstractSocket::TcpSocket, QAbstractSocket::IPv4Protocol));
    QCOMPARE(socketDevice.state(), QAbstractSocket::UnconnectedState);

    socketDevice.setProxy(QNetworkProxy(QNetworkProxy::Socks5Proxy, QtNetworkSettings::socksProxyServerName(), 1080));

    QVERIFY(!socketDevice.connectToHost(QtNetworkSettings::imapServerIp(), 143));
    QCOMPARE(socketDevice.state(), QAbstractSocket::ConnectingState);
    QVERIFY(socketDevice.waitForWrite());
    QCOMPARE(socketDevice.state(), QAbstractSocket::ConnectedState);
    QCOMPARE(socketDevice.peerAddress(), QtNetworkSettings::imapServerIp());

    // Wait for the greeting
    QVERIFY2(socketDevice.waitForRead(), qPrintable("Socket error:" + socketDevice.errorString()));

    // Read the greeting
    qint64 available = socketDevice.bytesAvailable();
    QVERIFY(available > 0);
    QByteArray array;
    array.resize(available);
    QVERIFY(socketDevice.read(array.data(), array.size()) == available);

    // Check that the greeting is what we expect it to be
    QVERIFY2(QtNetworkSettings::compareReplyIMAP(array), array.constData());

    // Write a logout message
    QByteArray array2 = "XXXX LOGOUT\r\n";
    QVERIFY(socketDevice.write(array2.data(),
                              array2.size()) == array2.size());

    // Wait for the response
    QVERIFY(socketDevice.waitForRead());

    available = socketDevice.bytesAvailable();
    QVERIFY(available > 0);
    array.resize(available);
    QVERIFY(socketDevice.read(array.data(), array.size()) == available);

    // Check that the greeting is what we expect it to be
    QCOMPARE(array.constData(), "* BYE LOGOUT received\r\nXXXX OK Completed\r\n");

    // Wait for the response
    QVERIFY(socketDevice.waitForRead());
    char c;
    QVERIFY(socketDevice.read(&c, sizeof(c)) == -1);
    QCOMPARE(socketDevice.error(), QAbstractSocket::RemoteHostClosedError);
    QCOMPARE(socketDevice.state(), QAbstractSocket::UnconnectedState);
}

//---------------------------------------------------------------------------
void tst_QSocks5SocketEngine::simpleErrorsAndStates()
{
    {
        QSocks5SocketEngine socketDevice;

        // Initialize device
        QVERIFY(socketDevice.initialize(QAbstractSocket::TcpSocket, QAbstractSocket::IPv4Protocol));

        socketDevice.setProxy(QNetworkProxy(QNetworkProxy::Socks5Proxy, QtNetworkSettings::socksProxyServerName(), 1080));

        QCOMPARE(socketDevice.state(), QAbstractSocket::UnconnectedState);
        QVERIFY(!socketDevice.connectToHost(QHostInfo::fromName(QtNetworkSettings::socksProxyServerName()).addresses().first(), 8088));
        QCOMPARE(socketDevice.state(), QAbstractSocket::ConnectingState);
        if (socketDevice.waitForWrite(15000)) {
            QVERIFY(socketDevice.state() == QAbstractSocket::UnconnectedState ||
                    socketDevice.state() == QAbstractSocket::ConnectedState);
        } else {
            QCOMPARE(socketDevice.error(), QAbstractSocket::SocketTimeoutError);
        }
    }

}

/*
//---------------------------------------------------------------------------
void tst_QSocks5SocketEngine::tcpLoopbackPerformance()
{
    QTcpServer server;

    // Bind to any port on all interfaces
    QVERIFY(server.bind(QHostAddress("0.0.0.0"), 0));
    QCOMPARE(server.state(), QAbstractSocket::BoundState);
    quint16 port = server.localPort();

    // Listen for incoming connections
    QVERIFY(server.listen());
    QCOMPARE(server.state(), QAbstractSocket::ListeningState);

    // Initialize a Tcp socket
    QSocks5SocketEngine client;
    QVERIFY(client.initialize(QAbstractSocket::TcpSocket));

    client.setProxy(QHostAddress("80.232.37.158"), 1081);

    // Connect to our server
    if (!client.connectToHost(QHostAddress("127.0.0.1"), port)) {
        QVERIFY(client.waitForWrite());
        QVERIFY(client.connectToHost(QHostAddress("127.0.0.1"), port));
    }

    // The server accepts the connectio
    int socketDescriptor = server.accept();
    QVERIFY(socketDescriptor > 0);

    // A socket device is initialized on the server side, passing the
    // socket descriptor from accept(). It's pre-connected.
    QSocketLayer serverSocket;
    QVERIFY(serverSocket.initialize(socketDescriptor));
    QCOMPARE(serverSocket.state(), QAbstractSocket::ConnectedState);

    const int messageSize = 1024 * 256;
    QByteArray message1(messageSize, '@');
    QByteArray answer(messageSize, '@');

    QTime timer;
    timer.start();
    qlonglong readBytes = 0;
    while (timer.elapsed() < 5000) {
        qlonglong written = serverSocket.write(message1.data(), message1.size());
        while (written > 0) {
            client.waitForRead();
            if (client.bytesAvailable() > 0) {
                qlonglong readNow = client.read(answer.data(), answer.size());
                written -= readNow;
                readBytes += readNow;
            }
        }
    }

    qDebug("\t\t%.1fMB/%.1fs: %.1fMB/s",
           readBytes / (1024.0 * 1024.0),
           timer.elapsed() / 1024.0,
           (readBytes / (timer.elapsed() / 1000.0)) / (1024 * 1024));
}
*/

//---------------------------------------------------------------------------
void tst_QSocks5SocketEngine::serverTest()
{
    QSocks5SocketEngine server;

    // Initialize a Tcp socket
    QVERIFY(server.initialize(QAbstractSocket::TcpSocket));

    QNetworkProxy proxy(QNetworkProxy::Socks5Proxy, QtNetworkSettings::socksProxyServerName(), 1080);

    server.setProxy(proxy);

    // Bind to any port on all interfaces
    QVERIFY(server.bind(QHostAddress("0.0.0.0"), 0));
    QCOMPARE(server.state(), QAbstractSocket::BoundState);

    // Listen for incoming connections
    QVERIFY(server.listen());
    QCOMPARE(server.state(), QAbstractSocket::ListeningState);

    // Initialize a Tcp socket
    QSocks5SocketEngine client;
    QVERIFY(client.initialize(QAbstractSocket::TcpSocket));

    client.setProxy(proxy);

   // QTest::wait(100000); // ### timing problem on win32


    // Connect to our server
    if (!client.connectToHost(server.localAddress(), server.localPort())) {
        QVERIFY(client.waitForWrite());
       // QTest::wait(100); // ### timing problem on win32
        QCOMPARE(client.state(), QAbstractSocket::ConnectedState);
        //QTest::wait(100);
    }

    QVERIFY(server.waitForRead());

    // The server accepts the connection
    int socketDescriptor = server.accept();
    QVERIFY(socketDescriptor > 0);

    // A socket device is initialized on the server side, passing the
    // socket descriptor from accept(). It's pre-connected.

    QSocks5SocketEngine serverSocket;
    QVERIFY(serverSocket.initialize(socketDescriptor));
    QCOMPARE(serverSocket.state(), QAbstractSocket::ConnectedState);

    QCOMPARE(serverSocket.localAddress(), client.peerAddress());
    QCOMPARE(serverSocket.localPort(), client.peerPort());
    // this seems depends on the socks server implementation, especially
    // when connecting /to/ the socks server /through/ the same socks server
    //QCOMPARE(serverSocket.peerAddress(), client.localAddress());
    //QCOMPARE(serverSocket.peerPort(), client.localPort());

    // The server socket sends a greeting to the client
    QByteArray greeting = "Greetings!";
    QVERIFY(serverSocket.write(greeting.data(),
                              greeting.size()) == greeting.size());

    // The client waits for the greeting to arrive
    QVERIFY(client.waitForRead());
    qint64 available = client.bytesAvailable();
    QVERIFY(available > 0);

    // The client reads the greeting and checks that it's correct
    QByteArray response;
    response.resize(available);
    QVERIFY(client.read(response.data(),
                       response.size()) == response.size());
    QCOMPARE(response, greeting);
}


//---------------------------------------------------------------------------
void tst_QSocks5SocketEngine::udpTest()
{
    QSocks5SocketEngine udpSocket;

    // Initialize device #1
    QVERIFY(udpSocket.initialize(QAbstractSocket::UdpSocket));
    QVERIFY(udpSocket.isValid());

    QNetworkProxy proxy(QNetworkProxy::Socks5Proxy, QtNetworkSettings::socksProxyServerName(), 1080);

    udpSocket.setProxy(proxy);

    QCOMPARE(udpSocket.protocol(), QAbstractSocket::IPv4Protocol);
    QCOMPARE(udpSocket.socketType(), QAbstractSocket::UdpSocket);
    QCOMPARE(udpSocket.state(), QAbstractSocket::UnconnectedState);

    // Bind #1
    bool bindSuccessful = udpSocket.bind(QHostAddress("0.0.0.0"), 0);
    if (!bindSuccessful)
        QEXPECT_FAIL("", "QTBUG-23380 / QTBUG-35490: Fails on some Ubuntu 11.10 x64 configurations and on new network test server", Abort);
    QVERIFY(bindSuccessful);
    QCOMPARE(udpSocket.state(), QAbstractSocket::BoundState);
    QVERIFY(udpSocket.localPort() != 0);

    // Initialize device #2
    QSocks5SocketEngine udpSocket2;
    QVERIFY(udpSocket2.initialize(QAbstractSocket::UdpSocket));

    udpSocket2.setProxy(proxy);

    // Connect device #2 to #1
    QVERIFY(udpSocket2.connectToHost(udpSocket.localAddress(), udpSocket.localPort()));
    QCOMPARE(udpSocket2.state(), QAbstractSocket::ConnectedState);

    // Write a message to #1
    QByteArray message1 = "hei der";
    QVERIFY(udpSocket2.write(message1.data(),
                            message1.size()) == message1.size());

    // Read the message from #2
    QVERIFY(udpSocket.waitForRead());
    QVERIFY(udpSocket.hasPendingDatagrams());
    qint64 available = udpSocket.pendingDatagramSize();
    QVERIFY(available > 0);
    QByteArray answer;
    answer.resize(available);
    QIpPacketHeader header;
    QCOMPARE(udpSocket.readDatagram(answer.data(), answer.size(),
                                    &header, QAbstractSocketEngine::WantDatagramSender),
             qint64(message1.size()));
    QVERIFY(header.senderAddress == udpSocket2.localAddress());
    QCOMPARE(header.senderAddress, udpSocket2.localAddress());
    QCOMPARE(header.senderPort, udpSocket2.localPort());
}

void tst_QSocks5SocketEngine::tcpSocketBlockingTest()
{
    QSocks5SocketEngineHandler socks5;

    QTcpSocket socket;

    // Connect
    socket.connectToHost(QtNetworkSettings::imapServerName(), 143);
    QVERIFY(socket.waitForConnected());
    QCOMPARE(socket.state(), QTcpSocket::ConnectedState);

    // Read greeting
    QVERIFY(socket.waitForReadyRead(5000));
    QByteArray s = socket.readLine();
    QVERIFY2(QtNetworkSettings::compareReplyIMAP(s), s.constData());

    // Write NOOP
    QCOMPARE((int) socket.write("1 NOOP\r\n", 8), 8);

    if (!socket.canReadLine())
        QVERIFY(socket.waitForReadyRead(5000));

    // Read response
    s = socket.readLine();
    QCOMPARE(s, QByteArrayLiteral("1 OK Completed\r\n"));

    // Write LOGOUT
    QCOMPARE((int) socket.write("2 LOGOUT\r\n", 10), 10);

    if (!socket.canReadLine())
        QVERIFY(socket.waitForReadyRead(5000));

    // Read two lines of respose
    s = socket.readLine();
    QCOMPARE(s, QByteArrayLiteral("* BYE LOGOUT received\r\n"));

    if (!socket.canReadLine())
        QVERIFY(socket.waitForReadyRead(5000));

    s = socket.readLine();
    QCOMPARE(s, QByteArrayLiteral("2 OK Completed\r\n"));

    // Close the socket
    socket.close();

    // Check that it's closed
    QCOMPARE(socket.state(), QTcpSocket::UnconnectedState);
}

//----------------------------------------------------------------------------------

void tst_QSocks5SocketEngine::tcpSocketNonBlockingTest()
{
    QSocks5SocketEngineHandler socks5;

    qint64 tcpSocketNonBlocking_totalWritten  = 0;
    QStringList tcpSocketNonBlocking_data;
    QTcpSocket socket;
    connect(&socket, &QAbstractSocket::hostFound,
            &QTestEventLoop::instance(), &QTestEventLoop::exitLoop);
    connect(&socket, &QAbstractSocket::connected,
            &QTestEventLoop::instance(), &QTestEventLoop::exitLoop);
    connect(&socket, &QIODevice::bytesWritten,
            [&tcpSocketNonBlocking_totalWritten] (qint64 written)
            {
                tcpSocketNonBlocking_totalWritten += written;
                QTestEventLoop::instance().exitLoop();
            });

    connect(&socket, &QIODevice::readyRead,
            [&tcpSocketNonBlocking_data, &socket] ()
            {
                while (socket.canReadLine())
                    tcpSocketNonBlocking_data.append(socket.readLine());
                QTestEventLoop::instance().exitLoop();
            });

    // Connect
    socket.connectToHost(QtNetworkSettings::imapServerName(), 143);
    QVERIFY(socket.state() == QTcpSocket::HostLookupState ||
            socket.state() == QTcpSocket::ConnectingState);

    QTestEventLoop::instance().enterLoop(30);
    if (QTestEventLoop::instance().timeout()) {
        QFAIL("Timed out");
    }

    if (socket.state() == QTcpSocket::ConnectingState) {
        QTestEventLoop::instance().enterLoop(30);
        if (QTestEventLoop::instance().timeout()) {
            QFAIL("Timed out");
        }
    }

    QCOMPARE(socket.state(), QTcpSocket::ConnectedState);

    QTestEventLoop::instance().enterLoop(30);
    if (QTestEventLoop::instance().timeout()) {
        QFAIL("Timed out");
    }

    // Read greeting
    QVERIFY(!tcpSocketNonBlocking_data.isEmpty());
    QByteArray data = tcpSocketNonBlocking_data.at(0).toLatin1();
    QVERIFY2(QtNetworkSettings::compareReplyIMAP(data), data.constData());

    tcpSocketNonBlocking_data.clear();

    tcpSocketNonBlocking_totalWritten = 0;

    // Write NOOP
    QCOMPARE((int) socket.write("1 NOOP\r\n", 8), 8);


    QTestEventLoop::instance().enterLoop(30);
    if (QTestEventLoop::instance().timeout()) {
        QFAIL("Timed out");
    }

    QCOMPARE(tcpSocketNonBlocking_totalWritten, 8);


    QTestEventLoop::instance().enterLoop(30);
    if (QTestEventLoop::instance().timeout()) {
        QFAIL("Timed out");
    }

    // Read response
    QVERIFY(!tcpSocketNonBlocking_data.isEmpty());
    QCOMPARE(tcpSocketNonBlocking_data.at(0), QLatin1String("1 OK Completed\r\n"));
    tcpSocketNonBlocking_data.clear();


    tcpSocketNonBlocking_totalWritten = 0;

    // Write LOGOUT
    QCOMPARE((int) socket.write("2 LOGOUT\r\n", 10), 10);

    QTestEventLoop::instance().enterLoop(30);
    if (QTestEventLoop::instance().timeout()) {
        QFAIL("Timed out");
    }

    QCOMPARE(tcpSocketNonBlocking_totalWritten, 10);

    // Wait for greeting
    QTestEventLoop::instance().enterLoop(30);
    if (QTestEventLoop::instance().timeout()) {
        QFAIL("Timed out");
    }

    // Read two lines of respose
    QCOMPARE(tcpSocketNonBlocking_data.at(0).toLatin1().constData(), "* BYE LOGOUT received\r\n");
    QCOMPARE(tcpSocketNonBlocking_data.at(1).toLatin1().constData(), "2 OK Completed\r\n");
    tcpSocketNonBlocking_data.clear();

    // Close the socket
    socket.close();

    // Check that it's closed
    QCOMPARE(socket.state(), QTcpSocket::UnconnectedState);
}

//----------------------------------------------------------------------------------

void tst_QSocks5SocketEngine::downloadBigFile()
{
    QSocks5SocketEngineHandler socks5;

    QTcpSocket socket;
    qint64 bytesAvailable = 0;

    QElapsedTimer stopWatch;
    stopWatch.start();

    connect(&socket, &QAbstractSocket::connected,
            &QTestEventLoop::instance(), &QTestEventLoop::exitLoop);
    connect(&socket, &QIODevice::readyRead,
            [&socket, &bytesAvailable] ()
            {
                const QByteArray tmp = socket.readAll();
                int correction = tmp.indexOf(char(0), 0); //skip header
                if (correction == -1)
                    correction = 0;
                bytesAvailable += (tmp.size() - correction);
                if (bytesAvailable >= 10000000)
                    QTestEventLoop::instance().exitLoop();
            });

    connect(&socket, &QAbstractSocket::errorOccurred,
            [&socket, &stopWatch] (QAbstractSocket::SocketError errorCode)
            {
                qWarning().noquote().nospace() << QTest::currentTestFunction()
                    << ": error " << errorCode << ": " << socket.errorString()
                    << " (" << stopWatch.elapsed() << "ms)";
            });

    socket.connectToHost(QtNetworkSettings::httpServerName(), 80);

    QTestEventLoop::instance().enterLoop(30);
    if (QTestEventLoop::instance().timeout())
        QFAIL("Network operation timed out");

    QByteArray hostName = QtNetworkSettings::httpServerName().toLatin1();
    QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
    QVERIFY(socket.write("GET /qtest/mediumfile HTTP/1.0\r\n") > 0);
    QVERIFY(socket.write("HOST: ") > 0);
    QVERIFY(socket.write(hostName.data()) > 0);
    QVERIFY(socket.write("\r\n") > 0);
    QVERIFY(socket.write("\r\n") > 0);

    stopWatch.restart();
    QTestEventLoop::instance().enterLoop(60);
    if (QTestEventLoop::instance().timeout())
        QFAIL("Network operation timed out");

    QCOMPARE(bytesAvailable, qint64(10000000));

    QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);

    /*qDebug("\t\t%.1fMB/%.1fs: %.1fMB/s",
           bytesAvailable / (1024.0 * 1024.0),
           stopWatch.elapsed() / 1024.0,
           (bytesAvailable / (stopWatch.elapsed() / 1000.0)) / (1024 * 1024));*/
}

void tst_QSocks5SocketEngine::passwordAuth()
{
    QSocks5SocketEngine socketDevice;

    // Initialize device
    QVERIFY(socketDevice.initialize(QAbstractSocket::TcpSocket, QAbstractSocket::IPv4Protocol));
    QCOMPARE(socketDevice.state(), QAbstractSocket::UnconnectedState);

    socketDevice.setProxy(QNetworkProxy(QNetworkProxy::Socks5Proxy, QtNetworkSettings::socksProxyServerName(), 1080, "qsockstest", "password"));

    // Connect to imap.trolltech.com's IP
    QVERIFY(!socketDevice.connectToHost(QtNetworkSettings::imapServerIp(), 143));
    QCOMPARE(socketDevice.state(), QAbstractSocket::ConnectingState);
    QVERIFY(socketDevice.waitForWrite());
    if (!socketDevice.connectToHost(QtNetworkSettings::imapServerIp(), 143)) {
        qDebug("%d, %s", socketDevice.error(), socketDevice.errorString().toLatin1().constData());
    }
    QCOMPARE(socketDevice.state(), QAbstractSocket::ConnectedState);
    QCOMPARE(socketDevice.peerAddress(), QtNetworkSettings::serverIP());

    // Wait for the greeting
    QVERIFY(socketDevice.waitForRead());

    // Read the greeting
    qint64 available = socketDevice.bytesAvailable();
    QVERIFY(available > 0);
    QByteArray array;
    array.resize(available);
    QVERIFY(socketDevice.read(array.data(), array.size()) == available);

    // Check that the greeting is what we expect it to be
    QVERIFY2(QtNetworkSettings::compareReplyIMAP(array), array.constData());

    // Write a logout message
    QByteArray array2 = "XXXX LOGOUT\r\n";
    QVERIFY(socketDevice.write(array2.data(),
                              array2.size()) == array2.size());

    // Wait for the response
    QVERIFY(socketDevice.waitForRead());

    available = socketDevice.bytesAvailable();
    QVERIFY(available > 0);
    array.resize(available);
    QVERIFY(socketDevice.read(array.data(), array.size()) == available);

    // Check that the greeting is what we expect it to be
    QCOMPARE(array.constData(), "* BYE LOGOUT received\r\nXXXX OK Completed\r\n");

    // Wait for the response
    QVERIFY(socketDevice.waitForRead());
    char c;
    QVERIFY(socketDevice.read(&c, sizeof(c)) == -1);
    QCOMPARE(socketDevice.error(), QAbstractSocket::RemoteHostClosedError);
    QCOMPARE(socketDevice.state(), QAbstractSocket::UnconnectedState);
}

//----------------------------------------------------------------------------------

void tst_QSocks5SocketEngine::proxyAuthenticationRequired(const QNetworkProxy &,
                                                          QAuthenticator *auth)
{
    auth->setUser("qsockstest");
    auth->setPassword("password");
}

void tst_QSocks5SocketEngine::passwordAuth2()
{
    QSocks5SocketEngine socketDevice;

    // Initialize device
    QVERIFY(socketDevice.initialize(QAbstractSocket::TcpSocket, QAbstractSocket::IPv4Protocol));
    QCOMPARE(socketDevice.state(), QAbstractSocket::UnconnectedState);

    socketDevice.setProxy(QNetworkProxy(QNetworkProxy::Socks5Proxy, QtNetworkSettings::socksProxyServerName(), 1081));
    socketDevice.setReceiver(this);

    QVERIFY(!socketDevice.connectToHost(QtNetworkSettings::imapServerIp(), 143));
    QCOMPARE(socketDevice.state(), QAbstractSocket::ConnectingState);
    while (socketDevice.state() == QAbstractSocket::ConnectingState) {
        QVERIFY(socketDevice.waitForWrite());
        socketDevice.connectToHost(QtNetworkSettings::imapServerIp(), 143);
    }
    if (socketDevice.state() != QAbstractSocket::ConnectedState)
        qDebug("%d, %s", socketDevice.error(), socketDevice.errorString().toLatin1().constData());
    QCOMPARE(socketDevice.state(), QAbstractSocket::ConnectedState);
    QCOMPARE(socketDevice.peerAddress(), QtNetworkSettings::imapServerIp());

    // Wait for the greeting
    QVERIFY(socketDevice.waitForRead());

    // Read the greeting
    qint64 available = socketDevice.bytesAvailable();
    QVERIFY(available > 0);
    QByteArray array;
    array.resize(available);
    QVERIFY(socketDevice.read(array.data(), array.size()) == available);

    // Check that the greeting is what we expect it to be
    QVERIFY2(QtNetworkSettings::compareReplyIMAP(array), array.constData());

    // Write a logout message
    QByteArray array2 = "XXXX LOGOUT\r\n";
    QVERIFY(socketDevice.write(array2.data(),
                              array2.size()) == array2.size());

    // Wait for the response
    QVERIFY(socketDevice.waitForRead());

    available = socketDevice.bytesAvailable();
    QVERIFY(available > 0);
    array.resize(available);
    QVERIFY(socketDevice.read(array.data(), array.size()) == available);

    // Check that the greeting is what we expect it to be
    QCOMPARE(array.constData(), "* BYE LOGOUT received\r\nXXXX OK Completed\r\n");

    // Wait for the response
    QVERIFY(socketDevice.waitForRead());
    char c;
    QVERIFY(socketDevice.read(&c, sizeof(c)) == -1);
    QCOMPARE(socketDevice.error(), QAbstractSocket::RemoteHostClosedError);
    QCOMPARE(socketDevice.state(), QAbstractSocket::UnconnectedState);
}

void tst_QSocks5SocketEngine::fragmentation_data()
{
    QTest::addColumn<QQueue<QByteArray> >("responses");

    QByteArray authMethodNone = QByteArray::fromRawData("\5\0", 2);
    QByteArray authMethodBasic = QByteArray::fromRawData("\5\2", 2);
    QByteArray authSuccess = QByteArray::fromRawData("\1\0", 2);
    QByteArray connectResponseIPv4 = QByteArray::fromRawData("\5\0\0\1\1\2\3\4\5\6", 10);
    QByteArray connectResponseIPv6 = QByteArray::fromRawData("\5\0\0\4\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef\5\6", 22);

    QQueue<QByteArray> responses;
    responses << authMethodNone.left(1) << authMethodNone.mid(1) << connectResponseIPv4;
    QTest::newRow("auth-method") << responses;

    responses.clear();
    responses << authMethodBasic << authSuccess.left(1) << authSuccess.mid(1) << connectResponseIPv4;
    QTest::newRow("auth-response") << responses;

    for (int i = 1; i < connectResponseIPv4.length() - 1; i++) {
        responses.clear();
        responses << authMethodNone << connectResponseIPv4.left(i) << connectResponseIPv4.mid(i);
        QTest::newRow(qPrintable(QString("connect-response-ipv4-") + QString::number(i))) << responses;
    }

    for (int i = 1; i < connectResponseIPv6.length() - 1; i++) {
        responses.clear();
        responses << authMethodNone << connectResponseIPv6.left(i) << connectResponseIPv6.mid(i);
        QTest::newRow(qPrintable(QString("connect-response-ipv6-") + QString::number(i))) << responses;
    }
}

void tst_QSocks5SocketEngine::fragmentation()
{
    QFETCH(QQueue<QByteArray>, responses);
    MiniSocks5Server server(responses, 500);

    QTcpSocket socket;
    socket.setProxy(QNetworkProxy(QNetworkProxy::Socks5Proxy, "localhost", server.serverPort(), "user", "password"));
    socket.connectToHost("0.1.2.3", 12345);

    connect(&socket, SIGNAL(connected()),
            &QTestEventLoop::instance(), SLOT(exitLoop()));
    QTestEventLoop::instance().enterLoop(10);
    QVERIFY(!QTestEventLoop::instance().timeout());

    QVERIFY(socket.localAddress() == QHostAddress("1.2.3.4") || socket.localAddress() == QHostAddress("0123:4567:89ab:cdef:0123:4567:89ab:cdef"));
    QCOMPARE(socket.localPort(), quint16(0x0506));
}

void tst_QSocks5SocketEngine::incomplete_data()
{
    QTest::addColumn<QQueue<QByteArray> >("responses");

    QByteArray authMethodNone = QByteArray::fromRawData("\5\0", 2);
    QByteArray authMethodBasic = QByteArray::fromRawData("\5\2", 2);
    QByteArray authSuccess = QByteArray::fromRawData("\1\0", 2);
    QByteArray connectResponseIPv4 = QByteArray::fromRawData("\5\0\0\1\1\2\3\4\5\6", 10);
    QByteArray connectResponseIPv6 = QByteArray::fromRawData("\5\0\0\4\x01\x23\x45\x67\x89\xab\xcd\xef\x01\x23\x45\x67\x89\xab\xcd\xef\5\6", 22);

    QQueue<QByteArray> responses;
    responses << authMethodNone.left(1);
    QTest::newRow("auth-method") << responses;

    responses.clear();
    responses << authMethodBasic << authSuccess.left(1);
    QTest::newRow("auth-response") << responses;

    for (int i = 1; i < connectResponseIPv4.length() - 1; i++) {
        responses.clear();
        responses << authMethodNone << connectResponseIPv4.left(i);
        QTest::newRow(qPrintable(QString("connect-response-ipv4-") + QString::number(i))) << responses;
    }

    for (int i = 1; i < connectResponseIPv6.length() - 1; i++) {
        responses.clear();
        responses << authMethodNone << connectResponseIPv6.left(i);
        QTest::newRow(qPrintable(QString("connect-response-ipv6-") + QString::number(i))) << responses;
    }
}

void tst_QSocks5SocketEngine::incomplete()
{
    QFETCH(QQueue<QByteArray>, responses);
    MiniSocks5Server server(responses, 500);

    QTcpSocket socket;
    socket.setProxy(QNetworkProxy(QNetworkProxy::Socks5Proxy, "127.0.0.1", server.serverPort(), "user", "password"));
    socket.connectToHost("0.1.2.3", 12345);

    connect(&socket, SIGNAL(connected()),
            &QTestEventLoop::instance(), SLOT(exitLoop()));
    connect(&socket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)),
            &QTestEventLoop::instance(), SLOT(exitLoop()));
    QTestEventLoop::instance().enterLoop(70);
    QVERIFY(!QTestEventLoop::instance().timeout());

    QCOMPARE(socket.error(), QAbstractSocket::ProxyConnectionClosedError);
}

//----------------------------------------------------------------------------------

QTEST_MAIN(tst_QSocks5SocketEngine)
#include "tst_qsocks5socketengine.moc"
