/****************************************************************************
**
** Copyright (C) 2016 Kurt Pattyn <pattyn.kurt@gmail.com>.
** 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 <QString>
#include <QtTest>
#include <QtWebSockets/QWebSocket>
#include <QtWebSockets/QWebSocketServer>
#include <QtWebSockets/qwebsocketprotocol.h>

QT_USE_NAMESPACE

Q_DECLARE_METATYPE(QWebSocketProtocol::Version)

class EchoServer : public QObject
{
    Q_OBJECT
public:
    explicit EchoServer(QObject *parent = nullptr,
        quint64 maxAllowedIncomingMessageSize = QWebSocket::maxIncomingMessageSize(),
        quint64 maxAllowedIncomingFrameSize = QWebSocket::maxIncomingFrameSize());
    ~EchoServer();

    QHostAddress hostAddress() const { return m_pWebSocketServer->serverAddress(); }
    quint16 port() const { return m_pWebSocketServer->serverPort(); }

Q_SIGNALS:
    void newConnection(QUrl requestUrl);
    void newConnection(QNetworkRequest request);

private Q_SLOTS:
    void onNewConnection();
    void processTextMessage(QString message);
    void processBinaryMessage(QByteArray message);
    void socketDisconnected();

private:
    QWebSocketServer *m_pWebSocketServer;
    quint64 m_maxAllowedIncomingMessageSize;
    quint64 m_maxAllowedIncomingFrameSize;
    QList<QWebSocket *> m_clients;
};

EchoServer::EchoServer(QObject *parent, quint64 maxAllowedIncomingMessageSize, quint64 maxAllowedIncomingFrameSize) :
    QObject(parent),
    m_pWebSocketServer(new QWebSocketServer(QStringLiteral("Echo Server"),
                                            QWebSocketServer::NonSecureMode, this)),
    m_maxAllowedIncomingMessageSize(maxAllowedIncomingMessageSize),
    m_maxAllowedIncomingFrameSize(maxAllowedIncomingFrameSize),
    m_clients()
{
    if (m_pWebSocketServer->listen(QHostAddress(QStringLiteral("127.0.0.1")))) {
        connect(m_pWebSocketServer, SIGNAL(newConnection()),
                this, SLOT(onNewConnection()));
    }
}

EchoServer::~EchoServer()
{
    m_pWebSocketServer->close();
    qDeleteAll(m_clients.begin(), m_clients.end());
}

void EchoServer::onNewConnection()
{
    QWebSocket *pSocket = m_pWebSocketServer->nextPendingConnection();

    pSocket->setMaxAllowedIncomingFrameSize(m_maxAllowedIncomingFrameSize);
    pSocket->setMaxAllowedIncomingMessageSize(m_maxAllowedIncomingMessageSize);

    Q_EMIT newConnection(pSocket->requestUrl());
    Q_EMIT newConnection(pSocket->request());

    connect(pSocket, SIGNAL(textMessageReceived(QString)), this, SLOT(processTextMessage(QString)));
    connect(pSocket, SIGNAL(binaryMessageReceived(QByteArray)), this, SLOT(processBinaryMessage(QByteArray)));
    connect(pSocket, SIGNAL(disconnected()), this, SLOT(socketDisconnected()));

    m_clients << pSocket;
}

void EchoServer::processTextMessage(QString message)
{
    QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
    if (pClient) {
        pClient->sendTextMessage(message);
    }
}

void EchoServer::processBinaryMessage(QByteArray message)
{
    QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
    if (pClient) {
        pClient->sendBinaryMessage(message);
    }
}

void EchoServer::socketDisconnected()
{
    QWebSocket *pClient = qobject_cast<QWebSocket *>(sender());
    if (pClient) {
        m_clients.removeAll(pClient);
        pClient->deleteLater();
    }
}

class tst_QWebSocket : public QObject
{
    Q_OBJECT

public:
    tst_QWebSocket();

private Q_SLOTS:
    void init();
    void initTestCase();
    void cleanupTestCase();
    void tst_initialisation_data();
    void tst_initialisation();
    void tst_settersAndGetters();
    void tst_invalidOpen_data();
    void tst_invalidOpen();
    void tst_invalidOrigin();
    void tst_sendTextMessage();
    void tst_sendBinaryMessage();
    void tst_errorString();
    void tst_openRequest();
    void tst_moveToThread();
    void tst_moveToThreadNoWarning();
#ifndef QT_NO_NETWORKPROXY
    void tst_setProxy();
#endif
    void overlongCloseReason();
    void incomingMessageTooLong();
    void incomingFrameTooLong();
    void testingFrameAndMessageSizeApi();
};

tst_QWebSocket::tst_QWebSocket()
{
}

void tst_QWebSocket::init()
{
    qRegisterMetaType<QWebSocketProtocol::Version>("QWebSocketProtocol::Version");
}

void tst_QWebSocket::initTestCase()
{
}

void tst_QWebSocket::cleanupTestCase()
{
}

void tst_QWebSocket::tst_initialisation_data()
{
    QTest::addColumn<QString>("origin");
    QTest::addColumn<QString>("expectedOrigin");
    QTest::addColumn<QWebSocketProtocol::Version>("version");
    QTest::addColumn<QWebSocketProtocol::Version>("expectedVersion");

    QTest::newRow("Default origin and version")
            << QString() << QString()
            << QWebSocketProtocol::VersionUnknown << QWebSocketProtocol::VersionLatest;
    QTest::newRow("Specific origin and default version")
            << QStringLiteral("qt-project.org") << QStringLiteral("qt-project.org")
            << QWebSocketProtocol::VersionUnknown << QWebSocketProtocol::VersionLatest;
    QTest::newRow("Specific origin and specific version")
            << QStringLiteral("qt-project.org") << QStringLiteral("qt-project.org")
            << QWebSocketProtocol::Version7 << QWebSocketProtocol::Version7;
}

void tst_QWebSocket::tst_initialisation()
{
    QFETCH(QString, origin);
    QFETCH(QString, expectedOrigin);
    QFETCH(QWebSocketProtocol::Version, version);
    QFETCH(QWebSocketProtocol::Version, expectedVersion);

    QScopedPointer<QWebSocket> socket;

    if (origin.isEmpty() && (version == QWebSocketProtocol::VersionUnknown))
        socket.reset(new QWebSocket);
    else if (!origin.isEmpty() && (version == QWebSocketProtocol::VersionUnknown))
        socket.reset(new QWebSocket(origin));
    else
        socket.reset(new QWebSocket(origin, version));

    QCOMPARE(socket->origin(), expectedOrigin);
    QCOMPARE(socket->version(), expectedVersion);
    QCOMPARE(socket->error(), QAbstractSocket::UnknownSocketError);
    QVERIFY(socket->errorString().isEmpty());
    QVERIFY(!socket->isValid());
    QVERIFY(socket->localAddress().isNull());
    QCOMPARE(socket->localPort(), quint16(0));
    QCOMPARE(socket->pauseMode(), QAbstractSocket::PauseNever);
    QVERIFY(socket->peerAddress().isNull());
    QCOMPARE(socket->peerPort(), quint16(0));
    QVERIFY(socket->peerName().isEmpty());
    QCOMPARE(socket->state(), QAbstractSocket::UnconnectedState);
    QCOMPARE(socket->readBufferSize(), 0);
    QVERIFY(socket->resourceName().isEmpty());
    QVERIFY(!socket->requestUrl().isValid());
    QCOMPARE(socket->closeCode(), QWebSocketProtocol::CloseCodeNormal);
    QVERIFY(socket->closeReason().isEmpty());
    QVERIFY(socket->flush());
    QCOMPARE(socket->sendTextMessage(QStringLiteral("A text message")), 0);
    QCOMPARE(socket->sendBinaryMessage(QByteArrayLiteral("A binary message")), 0);
}

void tst_QWebSocket::tst_settersAndGetters()
{
    QWebSocket socket;

    socket.setPauseMode(QAbstractSocket::PauseNever);
    QCOMPARE(socket.pauseMode(), QAbstractSocket::PauseNever);
    socket.setPauseMode(QAbstractSocket::PauseOnSslErrors);
    QCOMPARE(socket.pauseMode(), QAbstractSocket::PauseOnSslErrors);

    socket.setReadBufferSize(0);
    QCOMPARE(socket.readBufferSize(), 0);
    socket.setReadBufferSize(128);
    QCOMPARE(socket.readBufferSize(), 128);
    socket.setReadBufferSize(-1);
    QCOMPARE(socket.readBufferSize(), -1);
}

void tst_QWebSocket::tst_invalidOpen_data()
{
    QTest::addColumn<QString>("url");
    QTest::addColumn<QString>("expectedUrl");
    QTest::addColumn<QString>("expectedPeerName");
    QTest::addColumn<QString>("expectedResourceName");
    QTest::addColumn<QAbstractSocket::SocketState>("stateAfterOpenCall");
    QTest::addColumn<int>("disconnectedCount");
    QTest::addColumn<int>("stateChangedCount");

    QTest::newRow("Illegal local address")
            << QStringLiteral("ws://127.0.0.1:1/") << QStringLiteral("ws://127.0.0.1:1/")
            << QStringLiteral("127.0.0.1")
            << QStringLiteral("/") << QAbstractSocket::ConnectingState
            << 1
            << 2;  //going from connecting to disconnected
    QTest::newRow("URL containing new line in the hostname")
            << QStringLiteral("ws://myhacky\r\nserver/") << QString()
            << QString()
            << QString() << QAbstractSocket::UnconnectedState
            << 0 << 0;
    QTest::newRow("URL containing new line in the resource name")
            << QStringLiteral("ws://127.0.0.1:1/tricky\r\npath") << QString()
            << QString()
            << QString()
            << QAbstractSocket::UnconnectedState
            << 0 << 0;
}

void tst_QWebSocket::tst_invalidOpen()
{
    QFETCH(QString, url);
    QFETCH(QString, expectedUrl);
    QFETCH(QString, expectedPeerName);
    QFETCH(QString, expectedResourceName);
    QFETCH(QAbstractSocket::SocketState, stateAfterOpenCall);
    QFETCH(int, disconnectedCount);
    QFETCH(int, stateChangedCount);
    QWebSocket socket;
    QSignalSpy errorSpy(&socket, SIGNAL(error(QAbstractSocket::SocketError)));
    QSignalSpy aboutToCloseSpy(&socket, SIGNAL(aboutToClose()));
    QSignalSpy connectedSpy(&socket, SIGNAL(connected()));
    QSignalSpy disconnectedSpy(&socket, SIGNAL(disconnected()));
    QSignalSpy stateChangedSpy(&socket, SIGNAL(stateChanged(QAbstractSocket::SocketState)));
    QSignalSpy readChannelFinishedSpy(&socket, SIGNAL(readChannelFinished()));
    QSignalSpy textFrameReceivedSpy(&socket, SIGNAL(textFrameReceived(QString,bool)));
    QSignalSpy binaryFrameReceivedSpy(&socket, SIGNAL(binaryFrameReceived(QByteArray,bool)));
    QSignalSpy textMessageReceivedSpy(&socket, SIGNAL(textMessageReceived(QString)));
    QSignalSpy binaryMessageReceivedSpy(&socket, SIGNAL(binaryMessageReceived(QByteArray)));
    QSignalSpy pongSpy(&socket, SIGNAL(pong(quint64,QByteArray)));
    QSignalSpy bytesWrittenSpy(&socket, SIGNAL(bytesWritten(qint64)));

    socket.open(QUrl(url));

    QVERIFY(socket.origin().isEmpty());
    QCOMPARE(socket.version(), QWebSocketProtocol::VersionLatest);
    //at this point the socket is in a connecting state
    //so, there should no error at this point
    QCOMPARE(socket.error(), QAbstractSocket::UnknownSocketError);
    QVERIFY(!socket.errorString().isEmpty());
    QVERIFY(!socket.isValid());
    QVERIFY(socket.localAddress().isNull());
    QCOMPARE(socket.localPort(), quint16(0));
    QCOMPARE(socket.pauseMode(), QAbstractSocket::PauseNever);
    QVERIFY(socket.peerAddress().isNull());
    QCOMPARE(socket.peerPort(), quint16(0));
    QCOMPARE(socket.peerName(), expectedPeerName);
    QCOMPARE(socket.state(), stateAfterOpenCall);
    QCOMPARE(socket.readBufferSize(), 0);
    QCOMPARE(socket.resourceName(), expectedResourceName);
    QCOMPARE(socket.requestUrl().toString(), expectedUrl);
    QCOMPARE(socket.closeCode(), QWebSocketProtocol::CloseCodeNormal);
    QVERIFY(socket.closeReason().isEmpty());
    QCOMPARE(socket.sendTextMessage(QStringLiteral("A text message")), 0);
    QCOMPARE(socket.sendBinaryMessage(QByteArrayLiteral("A text message")), 0);

    if (errorSpy.count() == 0)
        QVERIFY(errorSpy.wait());
    QCOMPARE(errorSpy.count(), 1);
    QList<QVariant> arguments = errorSpy.takeFirst();
    QAbstractSocket::SocketError socketError =
            qvariant_cast<QAbstractSocket::SocketError>(arguments.at(0));
    QCOMPARE(socketError, QAbstractSocket::ConnectionRefusedError);
    QCOMPARE(aboutToCloseSpy.count(), 0);
    QCOMPARE(connectedSpy.count(), 0);
    QCOMPARE(disconnectedSpy.count(), disconnectedCount);
    QCOMPARE(stateChangedSpy.count(), stateChangedCount);
    if (stateChangedCount == 2) {
        arguments = stateChangedSpy.takeFirst();
        QAbstractSocket::SocketState socketState =
                qvariant_cast<QAbstractSocket::SocketState>(arguments.at(0));
        arguments = stateChangedSpy.takeFirst();
        socketState = qvariant_cast<QAbstractSocket::SocketState>(arguments.at(0));
        QCOMPARE(socketState, QAbstractSocket::UnconnectedState);
    }
    QCOMPARE(readChannelFinishedSpy.count(), 0);
    QCOMPARE(textFrameReceivedSpy.count(), 0);
    QCOMPARE(binaryFrameReceivedSpy.count(), 0);
    QCOMPARE(textMessageReceivedSpy.count(), 0);
    QCOMPARE(binaryMessageReceivedSpy.count(), 0);
    QCOMPARE(pongSpy.count(), 0);
    QCOMPARE(bytesWrittenSpy.count(), 0);
}

void tst_QWebSocket::tst_invalidOrigin()
{
    QWebSocket socket(QStringLiteral("My server\r\nin the wild."));

    QSignalSpy errorSpy(&socket, SIGNAL(error(QAbstractSocket::SocketError)));
    QSignalSpy aboutToCloseSpy(&socket, SIGNAL(aboutToClose()));
    QSignalSpy connectedSpy(&socket, SIGNAL(connected()));
    QSignalSpy disconnectedSpy(&socket, SIGNAL(disconnected()));
    QSignalSpy stateChangedSpy(&socket, SIGNAL(stateChanged(QAbstractSocket::SocketState)));
    QSignalSpy readChannelFinishedSpy(&socket, SIGNAL(readChannelFinished()));
    QSignalSpy textFrameReceivedSpy(&socket, SIGNAL(textFrameReceived(QString,bool)));
    QSignalSpy binaryFrameReceivedSpy(&socket, SIGNAL(binaryFrameReceived(QByteArray,bool)));
    QSignalSpy textMessageReceivedSpy(&socket, SIGNAL(textMessageReceived(QString)));
    QSignalSpy binaryMessageReceivedSpy(&socket, SIGNAL(binaryMessageReceived(QByteArray)));
    QSignalSpy pongSpy(&socket, SIGNAL(pong(quint64,QByteArray)));
    QSignalSpy bytesWrittenSpy(&socket, SIGNAL(bytesWritten(qint64)));

    socket.open(QUrl(QStringLiteral("ws://127.0.0.1:1/")));

    //at this point the socket is in a connecting state
    //so, there should no error at this point
    QCOMPARE(socket.error(), QAbstractSocket::UnknownSocketError);
    QVERIFY(!socket.errorString().isEmpty());
    QVERIFY(!socket.isValid());
    QVERIFY(socket.localAddress().isNull());
    QCOMPARE(socket.localPort(), quint16(0));
    QCOMPARE(socket.pauseMode(), QAbstractSocket::PauseNever);
    QVERIFY(socket.peerAddress().isNull());
    QCOMPARE(socket.peerPort(), quint16(0));
    QCOMPARE(socket.peerName(), QStringLiteral("127.0.0.1"));
    QCOMPARE(socket.state(), QAbstractSocket::ConnectingState);
    QCOMPARE(socket.readBufferSize(), 0);
    QCOMPARE(socket.resourceName(), QStringLiteral("/"));
    QCOMPARE(socket.requestUrl(), QUrl(QStringLiteral("ws://127.0.0.1:1/")));
    QCOMPARE(socket.closeCode(), QWebSocketProtocol::CloseCodeNormal);

    QVERIFY(errorSpy.wait());

    QCOMPARE(errorSpy.count(), 1);
    QList<QVariant> arguments = errorSpy.takeFirst();
    QAbstractSocket::SocketError socketError =
            qvariant_cast<QAbstractSocket::SocketError>(arguments.at(0));
    QCOMPARE(socketError, QAbstractSocket::ConnectionRefusedError);
    QCOMPARE(aboutToCloseSpy.count(), 0);
    QCOMPARE(connectedSpy.count(), 0);
    QCOMPARE(disconnectedSpy.count(), 1);
    QCOMPARE(stateChangedSpy.count(), 2);   //connectingstate, unconnectedstate
    arguments = stateChangedSpy.takeFirst();
    QAbstractSocket::SocketState socketState =
            qvariant_cast<QAbstractSocket::SocketState>(arguments.at(0));
    arguments = stateChangedSpy.takeFirst();
    socketState = qvariant_cast<QAbstractSocket::SocketState>(arguments.at(0));
    QCOMPARE(socketState, QAbstractSocket::UnconnectedState);
    QCOMPARE(readChannelFinishedSpy.count(), 0);
    QCOMPARE(textFrameReceivedSpy.count(), 0);
    QCOMPARE(binaryFrameReceivedSpy.count(), 0);
    QCOMPARE(textMessageReceivedSpy.count(), 0);
    QCOMPARE(binaryMessageReceivedSpy.count(), 0);
    QCOMPARE(pongSpy.count(), 0);
    QCOMPARE(bytesWrittenSpy.count(), 0);
}

void tst_QWebSocket::tst_sendTextMessage()
{
    EchoServer echoServer;

    QWebSocket socket;

    //should return 0 because socket is not open yet
    QCOMPARE(socket.sendTextMessage(QStringLiteral("1234")), 0);

    QSignalSpy socketConnectedSpy(&socket, SIGNAL(connected()));
    QSignalSpy serverConnectedSpy(&echoServer, SIGNAL(newConnection(QUrl)));
    QSignalSpy textMessageReceived(&socket, SIGNAL(textMessageReceived(QString)));
    QSignalSpy textFrameReceived(&socket, SIGNAL(textFrameReceived(QString,bool)));
    QSignalSpy binaryMessageReceived(&socket, SIGNAL(binaryMessageReceived(QByteArray)));
    QSignalSpy binaryFrameReceived(&socket, SIGNAL(binaryFrameReceived(QByteArray,bool)));
    QSignalSpy socketError(&socket, SIGNAL(error(QAbstractSocket::SocketError)));

    QUrl url = QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
                    QStringLiteral(":") + QString::number(echoServer.port()));
    url.setPath("/segment/with spaces");
    QUrlQuery query;
    query.addQueryItem("queryitem", "with encoded characters");
    url.setQuery(query);

    socket.open(url);

    QTRY_COMPARE(socketConnectedSpy.count(), 1);
    QCOMPARE(socketError.count(), 0);
    QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
    QList<QVariant> arguments = serverConnectedSpy.takeFirst();
    QUrl urlConnected = arguments.at(0).toUrl();
    QCOMPARE(urlConnected, url);

    QCOMPARE(socket.bytesToWrite(), 0);
    socket.sendTextMessage(QStringLiteral("Hello world!"));
    QVERIFY(socket.bytesToWrite() > 12); // 12 + a few extra bytes for header

    QVERIFY(textMessageReceived.wait(500));
    QCOMPARE(socket.bytesToWrite(), 0);

    QCOMPARE(textMessageReceived.count(), 1);
    QCOMPARE(binaryMessageReceived.count(), 0);
    QCOMPARE(binaryFrameReceived.count(), 0);
    arguments = textMessageReceived.takeFirst();
    QString messageReceived = arguments.at(0).toString();
    QCOMPARE(messageReceived, QStringLiteral("Hello world!"));

    QCOMPARE(textFrameReceived.count(), 1);
    arguments = textFrameReceived.takeFirst();
    QString frameReceived = arguments.at(0).toString();
    bool isLastFrame = arguments.at(1).toBool();
    QCOMPARE(frameReceived, QStringLiteral("Hello world!"));
    QVERIFY(isLastFrame);

    socket.close();
    socketConnectedSpy.clear();
    textMessageReceived.clear();
    textFrameReceived.clear();

    // QTBUG-74464 QWebsocket doesn't receive text (binary) message with size > 32 kb
    socket.open(url);

    QTRY_COMPARE(socketConnectedSpy.count(), 1);
    QCOMPARE(socketError.count(), 0);
    QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
    arguments = serverConnectedSpy.takeFirst();
    urlConnected = arguments.at(0).toUrl();
    QCOMPARE(urlConnected, url);
    QCOMPARE(socket.bytesToWrite(), 0);

    // transmit a long text message with 1 MB
    QString longString(0x100000, 'a');
    socket.sendTextMessage(longString);
    QVERIFY(socket.bytesToWrite() > longString.length());
    QVERIFY(textMessageReceived.wait());
    QCOMPARE(socket.bytesToWrite(), 0);

    QCOMPARE(textMessageReceived.count(), 1);
    QCOMPARE(binaryMessageReceived.count(), 0);
    QCOMPARE(binaryFrameReceived.count(), 0);
    arguments = textMessageReceived.takeFirst();
    messageReceived = arguments.at(0).toString();
    QCOMPARE(messageReceived.length(), longString.length());
    QCOMPARE(messageReceived, longString);

    arguments = textFrameReceived.takeLast();
    isLastFrame = arguments.at(1).toBool();
    QVERIFY(isLastFrame);

    socket.close();
    socketConnectedSpy.clear();
    textMessageReceived.clear();
    textFrameReceived.clear();

    //QTBUG-36762: QWebSocket emits multiplied signals when socket was reopened
    socket.open(QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
                     QStringLiteral(":") + QString::number(echoServer.port())));

    QTRY_COMPARE(socketConnectedSpy.count(), 1);
    QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);

    socket.sendTextMessage(QStringLiteral("Hello world!"));

    QVERIFY(textMessageReceived.wait(500));
    QCOMPARE(textMessageReceived.count(), 1);
    QCOMPARE(binaryMessageReceived.count(), 0);
    QCOMPARE(binaryFrameReceived.count(), 0);
    arguments = textMessageReceived.takeFirst();
    messageReceived = arguments.at(0).toString();
    QCOMPARE(messageReceived, QStringLiteral("Hello world!"));

    QCOMPARE(textFrameReceived.count(), 1);
    arguments = textFrameReceived.takeFirst();
    frameReceived = arguments.at(0).toString();
    isLastFrame = arguments.at(1).toBool();
    QCOMPARE(frameReceived, QStringLiteral("Hello world!"));
    QVERIFY(isLastFrame);

    QString reason = QStringLiteral("going away");
    socket.close(QWebSocketProtocol::CloseCodeGoingAway, reason);
    QCOMPARE(socket.closeCode(), QWebSocketProtocol::CloseCodeGoingAway);
    QCOMPARE(socket.closeReason(), reason);
}

void tst_QWebSocket::tst_sendBinaryMessage()
{
    EchoServer echoServer;

    QWebSocket socket;

    //should return 0 because socket is not open yet
    QCOMPARE(socket.sendBinaryMessage(QByteArrayLiteral("1234")), 0);

    QSignalSpy socketConnectedSpy(&socket, SIGNAL(connected()));
    QSignalSpy textMessageReceived(&socket, SIGNAL(textMessageReceived(QString)));
    QSignalSpy textFrameReceived(&socket, SIGNAL(textFrameReceived(QString,bool)));
    QSignalSpy binaryMessageReceived(&socket, SIGNAL(binaryMessageReceived(QByteArray)));
    QSignalSpy binaryFrameReceived(&socket, SIGNAL(binaryFrameReceived(QByteArray,bool)));

    socket.open(QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
                     QStringLiteral(":") + QString::number(echoServer.port())));

    QTRY_COMPARE(socketConnectedSpy.count(), 1);
    QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);

    QCOMPARE(socket.bytesToWrite(), 0);
    socket.sendBinaryMessage(QByteArrayLiteral("Hello world!"));
    QVERIFY(socket.bytesToWrite() > 12); // 12 + a few extra bytes for header

    QVERIFY(binaryMessageReceived.wait(500));
    QCOMPARE(socket.bytesToWrite(), 0);

    QCOMPARE(textMessageReceived.count(), 0);
    QCOMPARE(textFrameReceived.count(), 0);
    QCOMPARE(binaryMessageReceived.count(), 1);
    QList<QVariant> arguments = binaryMessageReceived.takeFirst();
    QByteArray messageReceived = arguments.at(0).toByteArray();
    QCOMPARE(messageReceived, QByteArrayLiteral("Hello world!"));

    QCOMPARE(binaryFrameReceived.count(), 1);
    arguments = binaryFrameReceived.takeFirst();
    QByteArray frameReceived = arguments.at(0).toByteArray();
    bool isLastFrame = arguments.at(1).toBool();
    QCOMPARE(frameReceived, QByteArrayLiteral("Hello world!"));
    QVERIFY(isLastFrame);

    socket.close();

    //QTBUG-36762: QWebSocket emits multiple signals when socket is reopened
    socketConnectedSpy.clear();
    binaryMessageReceived.clear();
    binaryFrameReceived.clear();

    socket.open(QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
                     QStringLiteral(":") + QString::number(echoServer.port())));

    QTRY_COMPARE(socketConnectedSpy.count(), 1);
    QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);

    socket.sendBinaryMessage(QByteArrayLiteral("Hello world!"));

    QVERIFY(binaryMessageReceived.wait(500));
    QCOMPARE(textMessageReceived.count(), 0);
    QCOMPARE(textFrameReceived.count(), 0);
    QCOMPARE(binaryMessageReceived.count(), 1);
    arguments = binaryMessageReceived.takeFirst();
    messageReceived = arguments.at(0).toByteArray();
    QCOMPARE(messageReceived, QByteArrayLiteral("Hello world!"));

    QCOMPARE(binaryFrameReceived.count(), 1);
    arguments = binaryFrameReceived.takeFirst();
    frameReceived = arguments.at(0).toByteArray();
    isLastFrame = arguments.at(1).toBool();
    QCOMPARE(frameReceived, QByteArrayLiteral("Hello world!"));
    QVERIFY(isLastFrame);
}

void tst_QWebSocket::tst_errorString()
{
    //Check for QTBUG-37228: QWebSocket returns "Unknown Error" for known errors
    QWebSocket socket;

    //check that the default error string is empty
    QVERIFY(socket.errorString().isEmpty());

    QSignalSpy errorSpy(&socket, SIGNAL(error(QAbstractSocket::SocketError)));

    socket.open(QUrl(QStringLiteral("ws://someserver.on.mars:9999")));

    QTRY_COMPARE(errorSpy.count(), 1);
    QList<QVariant> arguments = errorSpy.takeFirst();
    QAbstractSocket::SocketError socketError =
            qvariant_cast<QAbstractSocket::SocketError>(arguments.at(0));
    QCOMPARE(socketError, QAbstractSocket::HostNotFoundError);
    QCOMPARE(socket.errorString(), QStringLiteral("Host not found"));
}

void tst_QWebSocket::tst_openRequest()
{
    EchoServer echoServer;

    QWebSocket socket;

    QSignalSpy socketConnectedSpy(&socket, SIGNAL(connected()));
    QSignalSpy serverRequestSpy(&echoServer, SIGNAL(newConnection(QNetworkRequest)));

    QUrl url = QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
                    QLatin1Char(':') + QString::number(echoServer.port()));
    QUrlQuery query;
    query.addQueryItem("queryitem", "with encoded characters");
    url.setQuery(query);
    QNetworkRequest req(url);
    req.setRawHeader("X-Custom-Header", "A custom header");
    socket.open(req);

    QTRY_COMPARE(socketConnectedSpy.count(), 1);
    QTRY_COMPARE(serverRequestSpy.count(), 1);
    QCOMPARE(socket.state(), QAbstractSocket::ConnectedState);
    QList<QVariant> arguments = serverRequestSpy.takeFirst();
    QNetworkRequest requestConnected = arguments.at(0).value<QNetworkRequest>();
    QCOMPARE(requestConnected.url(), req.url());
    QCOMPARE(requestConnected.rawHeader("X-Custom-Header"), req.rawHeader("X-Custom-Header"));
    socket.close();
}

class WebSocket : public QWebSocket
{
    Q_OBJECT

public:
    explicit WebSocket()
    {
        connect(this, SIGNAL(triggerClose()), SLOT(onClose()), Qt::QueuedConnection);
        connect(this, SIGNAL(triggerOpen(QUrl)), SLOT(onOpen(QUrl)), Qt::QueuedConnection);
        connect(this, SIGNAL(triggerSendTextMessage(QString)), SLOT(onSendTextMessage(QString)), Qt::QueuedConnection);
        connect(this, SIGNAL(textMessageReceived(QString)), this, SLOT(onTextMessageReceived(QString)), Qt::QueuedConnection);
    }

    void asyncClose() { triggerClose(); }
    void asyncOpen(const QUrl &url) { triggerOpen(url); }
    void asyncSendTextMessage(const QString &msg) { triggerSendTextMessage(msg); }

    QString receivedMessage;

Q_SIGNALS:
    void triggerClose();
    void triggerOpen(const QUrl &);
    void triggerSendTextMessage(const QString &);
    void done();

private Q_SLOTS:
    void onClose() { close(); }
    void onOpen(const QUrl &url) { open(url); }
    void onSendTextMessage(const QString &msg) { sendTextMessage(msg); }
    void onTextMessageReceived(const QString &msg) { receivedMessage = msg; done(); }
};

struct Warned
{
    static QtMessageHandler origHandler;
    static bool warned;
    static void messageHandler(QtMsgType type, const QMessageLogContext& context, const QString& str)
    {
        if (type == QtWarningMsg) {
            warned = true;
        }
        if (origHandler)
            origHandler(type, context, str);
    }
};
QtMessageHandler Warned::origHandler = nullptr;
bool Warned::warned = false;


void tst_QWebSocket::tst_moveToThread()
{
    Warned::origHandler = qInstallMessageHandler(&Warned::messageHandler);

    EchoServer echoServer;

    QThread* thread = new QThread(this);
    thread->start();

    WebSocket* socket = new WebSocket;
    socket->moveToThread(thread);

    const QString textMessage = QStringLiteral("Hello world!");
    QSignalSpy socketConnectedSpy(socket, SIGNAL(connected()));
    QUrl url = QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
                    QStringLiteral(":") + QString::number(echoServer.port()));
    url.setPath("/segment/with spaces");
    QUrlQuery query;
    query.addQueryItem("queryitem", "with encoded characters");
    url.setQuery(query);

    socket->asyncOpen(url);
    if (socketConnectedSpy.count() == 0)
        QVERIFY(socketConnectedSpy.wait(500));

    socket->asyncSendTextMessage(textMessage);

    QTimer timer;
    timer.setInterval(1000);
    timer.start();
    QEventLoop loop;
    connect(socket, SIGNAL(done()), &loop, SLOT(quit()));
    connect(socket, SIGNAL(done()), &timer, SLOT(stop()));
    connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
    loop.exec();

    socket->asyncClose();

    QTRY_COMPARE_WITH_TIMEOUT(loop.isRunning(), false, 200);
    QCOMPARE(socket->receivedMessage, textMessage);

    socket->deleteLater();
    thread->quit();
    thread->wait();
}

void tst_QWebSocket::tst_moveToThreadNoWarning()
{
    // check for warnings in tst_moveToThread()
    // couldn't done there because warnings are processed after the test run
    QCOMPARE(Warned::warned, false);
}


#ifndef QT_NO_NETWORKPROXY
void tst_QWebSocket::tst_setProxy()
{
    // check if property assignment works as expected.
    QWebSocket socket;
    QCOMPARE(socket.proxy(), QNetworkProxy(QNetworkProxy::DefaultProxy));

    QNetworkProxy proxy;
    proxy.setPort(123);
    socket.setProxy(proxy);
    QCOMPARE(socket.proxy(), proxy);

    proxy.setPort(321);
    QCOMPARE(socket.proxy().port(), quint16(123));
    socket.setProxy(proxy);
    QCOMPARE(socket.proxy(), proxy);
}

void tst_QWebSocket::overlongCloseReason()
{
    EchoServer echoServer;

    QWebSocket socket;

    //should return 0 because socket is not open yet
    QCOMPARE(socket.sendTextMessage(QStringLiteral("1234")), 0);

    QSignalSpy socketConnectedSpy(&socket, SIGNAL(connected()));
    QSignalSpy socketDisconnectedSpy(&socket, SIGNAL(disconnected()));
    QSignalSpy serverConnectedSpy(&echoServer, SIGNAL(newConnection(QUrl)));

    QUrl url = QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
                    QStringLiteral(":") + QString::number(echoServer.port()));
    socket.open(url);
    QTRY_COMPARE(socketConnectedSpy.count(), 1);
    QTRY_COMPARE(serverConnectedSpy.count(), 1);

    const QString reason(200, QChar::fromLatin1('a'));
    socket.close(QWebSocketProtocol::CloseCodeGoingAway, reason);
    QCOMPARE(socket.closeCode(), QWebSocketProtocol::CloseCodeGoingAway);
    // Max length of a control frame is 125, but 2 bytes are used for the close code:
    QCOMPARE(socket.closeReason().length(), 123);
    QCOMPARE(socket.closeReason(), reason.leftRef(123));
    QTRY_COMPARE(socketDisconnectedSpy.count(), 1);
}

void tst_QWebSocket::incomingMessageTooLong()
{
//QTBUG-70693
    quint64 maxAllowedIncomingMessageSize = 1024;
    quint64 maxAllowedIncomingFrameSize = QWebSocket::maxIncomingFrameSize();

    EchoServer echoServer(nullptr, maxAllowedIncomingMessageSize, maxAllowedIncomingFrameSize);

    QWebSocket socket;

    QSignalSpy socketConnectedSpy(&socket, &QWebSocket::connected);
    QSignalSpy serverConnectedSpy(&echoServer, QOverload<QUrl>::of(&EchoServer::newConnection));
    QSignalSpy socketDisconnectedSpy(&socket, &QWebSocket::disconnected);

    QUrl url = QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
                    QStringLiteral(":") + QString::number(echoServer.port()));
    socket.open(url);
    QTRY_COMPARE(socketConnectedSpy.count(), 1);
    QTRY_COMPARE(serverConnectedSpy.count(), 1);

    QString payload(maxAllowedIncomingMessageSize+1, 'a');
    QCOMPARE(socket.sendTextMessage(payload), payload.size());

    QTRY_COMPARE(socketDisconnectedSpy.count(), 1);
    QCOMPARE(socket.closeCode(), QWebSocketProtocol::CloseCodeTooMuchData);
}

void tst_QWebSocket::incomingFrameTooLong()
{
//QTBUG-70693
    quint64 maxAllowedIncomingMessageSize = QWebSocket::maxIncomingMessageSize();
    quint64 maxAllowedIncomingFrameSize = 1024;

    EchoServer echoServer(nullptr, maxAllowedIncomingMessageSize, maxAllowedIncomingFrameSize);

    QWebSocket socket;
    socket.setOutgoingFrameSize(maxAllowedIncomingFrameSize+1);

    QSignalSpy socketConnectedSpy(&socket, &QWebSocket::connected);
    QSignalSpy serverConnectedSpy(&echoServer, QOverload<QUrl>::of(&EchoServer::newConnection));
    QSignalSpy socketDisconnectedSpy(&socket, &QWebSocket::disconnected);

    QUrl url = QUrl(QStringLiteral("ws://") + echoServer.hostAddress().toString() +
                    QStringLiteral(":") + QString::number(echoServer.port()));
    socket.open(url);
    QTRY_COMPARE(socketConnectedSpy.count(), 1);
    QTRY_COMPARE(serverConnectedSpy.count(), 1);

    QString payload(maxAllowedIncomingFrameSize+1, 'a');
    QCOMPARE(socket.sendTextMessage(payload), payload.size());

    QTRY_COMPARE(socketDisconnectedSpy.count(), 1);
    QCOMPARE(socket.closeCode(), QWebSocketProtocol::CloseCodeTooMuchData);
}

void tst_QWebSocket::testingFrameAndMessageSizeApi()
{
//requested by André Hartmann, QTBUG-70693
    QWebSocket socket;

    const quint64 outgoingFrameSize = 5;
    socket.setOutgoingFrameSize(outgoingFrameSize);
    QTRY_COMPARE(outgoingFrameSize, socket.outgoingFrameSize());

    const quint64 maxAllowedIncomingFrameSize = 9;
    socket.setMaxAllowedIncomingFrameSize(maxAllowedIncomingFrameSize);
    QTRY_COMPARE(maxAllowedIncomingFrameSize, socket.maxAllowedIncomingFrameSize());

    const quint64 maxAllowedIncomingMessageSize = 889;
    socket.setMaxAllowedIncomingMessageSize(maxAllowedIncomingMessageSize);
    QTRY_COMPARE(maxAllowedIncomingMessageSize, socket.maxAllowedIncomingMessageSize());
}

#endif // QT_NO_NETWORKPROXY

QTEST_MAIN(tst_QWebSocket)

#include "tst_qwebsocket.moc"
