// Copyright (C) 2016 Kurt Pattyn <pattyn.kurt@gmail.com>.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QtTest/QtTest>
#include <QtTest/qtestcase.h>
#include <QDebug>
#include <QByteArray>
#include <QtEndian>

#include "private/qwebsocketframe_p.h"
#include "private/qwebsocketprotocol_p.h"
#include "qwebsocketprotocol.h"

QT_USE_NAMESPACE

Q_DECLARE_METATYPE(QWebSocketProtocol::CloseCode)
Q_DECLARE_METATYPE(QWebSocketProtocol::OpCode)

/*!
 * \brief class FrameHelper is used to encode a single frame.
 *
 * \internal
 */
class FrameHelper
{
public:
    FrameHelper();

    QByteArray wireRepresentation();

    void setRsv1(int value) { m_rsv1 = value; }
    void setRsv2(int value) { m_rsv2 = value; }
    void setRsv3(int value) { m_rsv3 = value; }
    void setMask(quint32 mask) { m_mask = mask; }
    void setOpCode(QWebSocketProtocol::OpCode opCode) { m_opCode = opCode; }
    void setPayload(const QByteArray &payload) { m_payload = payload; }
    void setFinalFrame(bool isFinal) { m_isFinalFrame = isFinal; }

private:
    int m_rsv1;
    int m_rsv2;
    int m_rsv3;
    quint32 m_mask;
    QWebSocketProtocol::OpCode m_opCode;
    QByteArray m_payload;
    bool m_isFinalFrame;
};

FrameHelper::FrameHelper() :
    m_rsv1(0), m_rsv2(0), m_rsv3(0),
    m_mask(0), m_opCode(QWebSocketProtocol::OpCodeReserved3),
    m_payload(), m_isFinalFrame(false)
{}

QByteArray FrameHelper::wireRepresentation()
{
    quint8 byte = 0x00;
    QByteArray wireRep;
    quint64 payloadLength = m_payload.size();

    //FIN, opcode
    byte = static_cast<quint8>((m_opCode & 0x0F) | (m_isFinalFrame ? 0x80 : 0x00)); //FIN, opcode
    //RSV1-3
    byte |= static_cast<quint8>(((m_rsv1 & 0x01) << 6) | ((m_rsv2 & 0x01) << 5) |
                                ((m_rsv3 & 0x01) << 4));
    wireRep.append(static_cast<char>(byte));

    byte = 0x00;
    if (m_mask != 0)
    {
        byte |= 0x80;
    }
    if (payloadLength <= 125)
    {
        byte |= static_cast<quint8>(payloadLength);
        wireRep.append(static_cast<char>(byte));
    }
    else if (payloadLength <= 0xFFFFU)
    {
        byte |= 126;
        wireRep.append(static_cast<char>(byte));
        quint16 swapped = qToBigEndian<quint16>(static_cast<quint16>(payloadLength));
        wireRep.append(static_cast<const char *>(static_cast<const void *>(&swapped)), 2);
    }
    else
    {
        byte |= 127;
        wireRep.append(static_cast<char>(byte));
        quint64 swapped = qToBigEndian<quint64>(payloadLength);
        wireRep.append(static_cast<const char *>(static_cast<const void *>(&swapped)), 8);
    }
    //Write mask
    if (m_mask != 0)
    {
        wireRep.append(static_cast<const char *>(static_cast<const void *>(&m_mask)),
                       sizeof(quint32));
    }
    QByteArray tmpData = m_payload;
    if (m_mask)
    {
        tmpData.detach();
        QWebSocketProtocol::mask(&tmpData, m_mask);
    }
    wireRep.append(tmpData);
    return wireRep;
}

class tst_WebSocketFrame : public QObject
{
    Q_OBJECT

public:
    tst_WebSocketFrame();

private Q_SLOTS:
    void initTestCase();
    void cleanupTestCase();
    void init();
    void cleanup();

    void tst_initialization();
    void tst_copyConstructorAndAssignment();

    void tst_goodFrames_data();
    void tst_goodFrames();

    void tst_invalidFrames_data();
    void tst_invalidFrames();

    void tst_malformedFrames_data();
    void tst_malformedFrames();
};

tst_WebSocketFrame::tst_WebSocketFrame()
{}

void tst_WebSocketFrame::initTestCase()
{
}

void tst_WebSocketFrame::cleanupTestCase()
{}

void tst_WebSocketFrame::init()
{
    qRegisterMetaType<QWebSocketProtocol::OpCode>("QWebSocketProtocol::OpCode");
    qRegisterMetaType<QWebSocketProtocol::CloseCode>("QWebSocketProtocol::CloseCode");
}

void tst_WebSocketFrame::cleanup()
{
}

void tst_WebSocketFrame::tst_initialization()
{
    QWebSocketFrame frame;
    QVERIFY(!frame.isValid());
    QCOMPARE(frame.payload().size(), 0);
}

void tst_WebSocketFrame::tst_copyConstructorAndAssignment()
{
    FrameHelper frameHelper;
    frameHelper.setRsv1(0);
    frameHelper.setRsv2(0);
    frameHelper.setRsv3(0);
    frameHelper.setFinalFrame(true);
    frameHelper.setMask(1234u);
    frameHelper.setOpCode(QWebSocketProtocol::OpCodeBinary);
    frameHelper.setPayload(QByteArrayLiteral("12345"));

    QByteArray payload = frameHelper.wireRepresentation();
    QBuffer buffer(&payload);
    buffer.open(QIODevice::ReadOnly);

    QWebSocketFrame frame;
    frame.readFrame(&buffer);
    buffer.close();

    auto compareFrames = [](const QWebSocketFrame &other, const QWebSocketFrame &frame)
    {
        QCOMPARE(other.closeCode(), frame.closeCode());
        QCOMPARE(other.closeReason(), frame.closeReason());
        QCOMPARE(other.hasMask(), frame.hasMask());
        QCOMPARE(other.isContinuationFrame(), frame.isContinuationFrame());
        QCOMPARE(other.isControlFrame(), frame.isControlFrame());
        QCOMPARE(other.isDataFrame(), frame.isDataFrame());
        QCOMPARE(other.isFinalFrame(), frame.isFinalFrame());
        QCOMPARE(other.isValid(), frame.isValid());
        QCOMPARE(other.mask(), frame.mask());
        QCOMPARE(other.opCode(), frame.opCode());
        QCOMPARE(other.payload(), frame.payload());
        QCOMPARE(other.rsv1(), frame.rsv1());
        QCOMPARE(other.rsv2(), frame.rsv2());
        QCOMPARE(other.rsv3(), frame.rsv3());
    };

    {
        QWebSocketFrame other(frame);
        compareFrames(other, frame);
    }
    {
        QWebSocketFrame other;
        other = frame;
        compareFrames(other, frame);
        QWebSocketFrame other2 = std::move(other);
        compareFrames(other2, frame);
        QWebSocketFrame other3(std::move(other2));
        compareFrames(other3, frame);
    }
}

void tst_WebSocketFrame::tst_goodFrames_data()
{
    QTest::addColumn<bool>("rsv1");
    QTest::addColumn<bool>("rsv2");
    QTest::addColumn<bool>("rsv3");
    QTest::addColumn<quint32>("mask");
    QTest::addColumn<QWebSocketProtocol::OpCode>("opCode");
    QTest::addColumn<bool>("isFinal");
    QTest::addColumn<QByteArray>("payload");
    QTest::addColumn<bool>("isControlFrame");
    QTest::addColumn<bool>("isDataFrame");
    QTest::addColumn<bool>("isContinuationFrame");

    QTest::newRow("Non masked final text frame with small payload")
            << false << false << false
            << 0U << QWebSocketProtocol::OpCodeText
            << true << QStringLiteral("Hello world!").toUtf8()
            << false << true << false;
    QTest::newRow("Non masked final binary frame with small payload")
            << false << false << false
            << 0U << QWebSocketProtocol::OpCodeBinary
            << true << QByteArrayLiteral("\x00\x01\x02\x03\x04")
            << false << true << false;
    QTest::newRow("Non masked final text frame with no payload")
            << false << false << false
            << 0U << QWebSocketProtocol::OpCodeText
            << true << QByteArray()
            << false << true << false;
    QTest::newRow("Non masked final binary frame with no payload")
            << false << false << false
            << 0U << QWebSocketProtocol::OpCodeBinary
            << true << QByteArray()
            << false << true << false;

    QTest::newRow("Non masked final close frame with small payload")
            << false << false << false
            << 0U << QWebSocketProtocol::OpCodeClose
            << true << QStringLiteral("Hello world!").toUtf8()
            << true << false << false;
    QTest::newRow("Non masked final close frame with no payload")
            << false << false << false
            << 0U << QWebSocketProtocol::OpCodeClose
            << true << QByteArray()
            << true << false << false;
    QTest::newRow("Non masked final ping frame with small payload")
            << false << false << false
            << 0U << QWebSocketProtocol::OpCodePing
            << true << QStringLiteral("Hello world!").toUtf8()
            << true << false << false;
    QTest::newRow("Non masked final pong frame with no payload")
            << false << false << false
            << 0U << QWebSocketProtocol::OpCodePong
            << true << QByteArray()
            << true << false << false;

    QTest::newRow("Non masked final continuation frame with small payload")
            << false << false << false
            << 0U << QWebSocketProtocol::OpCodeContinue
            << true << QStringLiteral("Hello world!").toUtf8()
            << false << true << true;
    QTest::newRow("Non masked non-final continuation frame with small payload")
            << false << false << false
            << 0U << QWebSocketProtocol::OpCodeContinue
            << false << QStringLiteral("Hello world!").toUtf8()
            << false << true << true;
}

void tst_WebSocketFrame::tst_goodFrames()
{
    QFETCH(bool, rsv1);
    QFETCH(bool, rsv2);
    QFETCH(bool, rsv3);
    QFETCH(quint32, mask);
    QFETCH(QWebSocketProtocol::OpCode, opCode);
    QFETCH(bool, isFinal);
    QFETCH(QByteArray, payload);
    QFETCH(bool, isControlFrame);
    QFETCH(bool, isDataFrame);
    QFETCH(bool, isContinuationFrame);

    FrameHelper helper;
    helper.setRsv1(rsv1);
    helper.setRsv2(rsv2);
    helper.setRsv3(rsv3);
    helper.setMask(mask);
    helper.setOpCode(opCode);
    helper.setFinalFrame(isFinal);
    helper.setPayload(payload);

    QByteArray wireRepresentation = helper.wireRepresentation();
    QBuffer buffer;
    buffer.setData(wireRepresentation);
    buffer.open(QIODevice::ReadOnly);
    QWebSocketFrame frame;
    frame.readFrame(&buffer);
    buffer.close();
    QVERIFY(frame.isValid());
    QCOMPARE(frame.rsv1(), rsv1);
    QCOMPARE(frame.rsv2(), rsv2);
    QCOMPARE(frame.rsv3(), rsv3);
    QCOMPARE(frame.hasMask(), (mask != 0));
    QCOMPARE(frame.opCode(), opCode);
    QCOMPARE(frame.isFinalFrame(), isFinal);
    QCOMPARE(frame.isControlFrame(), isControlFrame);
    QCOMPARE(frame.isDataFrame(), isDataFrame);
    QCOMPARE(frame.isContinuationFrame(), isContinuationFrame);
    QCOMPARE(frame.payload().size(), payload.size());
    QCOMPARE(frame.payload(), payload);
}

void tst_WebSocketFrame::tst_invalidFrames_data()
{
    QTest::addColumn<int>("rsv1");
    QTest::addColumn<int>("rsv2");
    QTest::addColumn<int>("rsv3");
    QTest::addColumn<quint32>("mask");
    QTest::addColumn<QWebSocketProtocol::OpCode>("opCode");
    QTest::addColumn<bool>("isFinal");
    QTest::addColumn<QByteArray>("payload");
    QTest::addColumn<QWebSocketProtocol::CloseCode>("expectedError");

    QTest::newRow("RSV1 != 0")
            << 1 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodeText
            << true << QStringLiteral("Hello world!").toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("RSV2 != 0")
            << 0 << 1 << 0
            << 0U << QWebSocketProtocol::OpCodeText
            << true << QStringLiteral("Hello world!").toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("RSV3 != 0")
            << 0 << 0 << 1
            << 0U << QWebSocketProtocol::OpCodeText
            << true << QStringLiteral("Hello world!").toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("RSV1 != 0 and RSV2 != 0")
            << 1 << 1 << 0
            << 0U << QWebSocketProtocol::OpCodeText
            << true << QStringLiteral("Hello world!").toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("RSV1 != 0 and RSV3 != 0")
            << 1 << 0 << 1
            << 0U << QWebSocketProtocol::OpCodeText
            << true << QStringLiteral("Hello world!").toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("RSV2 != 0 and RSV3 != 0")
            << 0 << 1 << 1
            << 0U << QWebSocketProtocol::OpCodeText
            << true << QStringLiteral("Hello world!").toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;

    QTest::newRow("Reserved OpCode 3")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodeReserved3
            << true << QStringLiteral("Hello world!").toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("Reserved OpCode 4")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodeReserved4
            << true << QStringLiteral("Hello world!").toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("Reserved OpCode 5")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodeReserved5
            << true << QStringLiteral("Hello world!").toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("Reserved OpCode 6")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodeReserved6
            << true << QStringLiteral("Hello world!").toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("Reserved OpCode 7")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodeReserved7
            << true << QStringLiteral("Hello world!").toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("Reserved OpCode B")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodeReservedB
            << true << QStringLiteral("Hello world!").toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("Reserved OpCode C")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodeReservedC
            << true << QStringLiteral("Hello world!").toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("Reserved OpCode D")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodeReservedD
            << true << QStringLiteral("Hello world!").toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("Reserved OpCode E")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodeReservedE
            << true << QStringLiteral("Hello world!").toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("Reserved OpCode F")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodeReservedF
            << true << QStringLiteral("Hello world!").toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;

    QTest::newRow("Close Frame with payload > 125 bytes")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodeClose
            << true << QString(126, 'a').toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("Non-final Close Frame")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodeClose
            << false << QString(126, 'a').toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("Ping Frame with payload > 125 bytes")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodePing
            << true << QString(126, 'a').toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("Non-final Ping Frame")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodePing
            << false << QString(126, 'a').toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("Pong Frame with payload > 125 bytes")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodePong
            << true << QString(126, 'a').toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
    QTest::newRow("Non-final Pong Frame")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodePong
            << false << QString(126, 'a').toUtf8()
            << QWebSocketProtocol::CloseCodeProtocolError;
}

void tst_WebSocketFrame::tst_invalidFrames()
{
    QFETCH(int, rsv1);
    QFETCH(int, rsv2);
    QFETCH(int, rsv3);
    QFETCH(quint32, mask);
    QFETCH(QWebSocketProtocol::OpCode, opCode);
    QFETCH(bool, isFinal);
    QFETCH(QByteArray, payload);
    QFETCH(QWebSocketProtocol::CloseCode, expectedError);

    FrameHelper helper;
    helper.setRsv1(rsv1);
    helper.setRsv2(rsv2);
    helper.setRsv3(rsv3);
    helper.setMask(mask);
    helper.setOpCode(opCode);
    helper.setFinalFrame(isFinal);
    helper.setPayload(payload);

    QByteArray wireRepresentation = helper.wireRepresentation();
    QBuffer buffer;
    buffer.setData(wireRepresentation);
    buffer.open(QIODevice::ReadOnly);
    QWebSocketFrame frame;
    frame.readFrame(&buffer);
    buffer.close();

    QVERIFY(!frame.isValid());
    QCOMPARE(frame.closeCode(), expectedError);
}


/*
 * Incomplete or overly large frames
 * Payload must be crafted manually
 *
    QTest::newRow("Frame Too Big")
            << 0 << 0 << 0
            << 0U << QWebSocketProtocol::OpCodeText
            << true << QString(MAX_FRAME_SIZE_IN_BYTES + 1, 'a').toUtf8()
            << QWebSocketProtocol::CloseCodeTooMuchData;

 */
void tst_WebSocketFrame::tst_malformedFrames_data()
{
    QTest::addColumn<QByteArray>("payload");
    QTest::addColumn<QWebSocketProtocol::CloseCode>("expectedError");

    //too little data
    QTest::newRow("No data") << QByteArray() << QWebSocketProtocol::CloseCodeGoingAway;
    FrameHelper helper;
    helper.setRsv1(0);
    helper.setRsv2(0);
    helper.setRsv3(0);
    helper.setMask(0U);
    helper.setOpCode(QWebSocketProtocol::OpCodeText);
    helper.setFinalFrame(true);
    helper.setPayload(QString(10, 'a').toUtf8());
    QByteArray wireRep = helper.wireRepresentation();

    //too little data
    //header + payload should be 12 bytes for non-masked payloads < 126 bytes
    for (int i = 1; i < 12; ++i)
    {
        QTest::newRow(QStringLiteral("Header too small - %1 byte(s)").arg(i).toLatin1().constData())
                << wireRep.left(i)
                << QWebSocketProtocol::CloseCodeGoingAway;
    }
    //too much data
    {
        const char bigpayloadIndicator = char(127);
        const quint64 payloadSize = QWebSocketFrame::maxFrameSize() + 1;
        uchar swapped[8] = {0};
        qToBigEndian<quint64>(payloadSize, swapped);
        QTest::newRow("Frame too big")
                << wireRep.left(1).append(bigpayloadIndicator)
                                  .append(reinterpret_cast<char *>(swapped), 8)
                << QWebSocketProtocol::CloseCodeTooMuchData;
    }
    //invalid size field
    {
        const char bigpayloadIndicator = char(127);
        quint64 payloadSize = quint64(1) << 63;
        uchar swapped[8] = {0};
        qToBigEndian<quint64>(payloadSize, swapped);
        QTest::newRow("Highest bit of payload length is set")
                << wireRep.left(1).append(bigpayloadIndicator)
                                  .append(reinterpret_cast<char *>(swapped), 8)
                << QWebSocketProtocol::CloseCodeProtocolError;

        payloadSize = 256;
        qToBigEndian<quint64>(payloadSize, swapped);
        QTest::newRow("Overlong 64-bit size field; should be 16-bit")
                << wireRep.left(1).append(bigpayloadIndicator)
                                  .append(reinterpret_cast<char *>(swapped), 8)
                << QWebSocketProtocol::CloseCodeProtocolError;
    }
    //overlong size field
    {
        const char largepayloadIndicator = char(126);
        const quint16 payloadSize = 120;
        uchar swapped[2] = {0};
        qToBigEndian<quint16>(payloadSize, swapped);
        QTest::newRow("Overlong 16-bit size field")
                << wireRep.left(1).append(largepayloadIndicator)
                                  .append(reinterpret_cast<char *>(swapped), 2)
                << QWebSocketProtocol::CloseCodeProtocolError;
    }
    {
        const char bigpayloadIndicator = char(127);
        quint64 payloadSize = 120;
        uchar swapped[8] = {0};
        qToBigEndian<quint64>(payloadSize, swapped);
        QTest::newRow("Overlong 64-bit size field; should be 7-bit")
                << wireRep.left(1).append(bigpayloadIndicator)
                                  .append(reinterpret_cast<char *>(swapped), 8)
                << QWebSocketProtocol::CloseCodeProtocolError;

        payloadSize = 256;
        qToBigEndian<quint64>(payloadSize, swapped);
        QTest::newRow("Overlong 64-bit size field; should be 16-bit")
                << wireRep.left(1).append(bigpayloadIndicator)
                                  .append(reinterpret_cast<char *>(swapped), 8)
                << QWebSocketProtocol::CloseCodeProtocolError;
    }
}

void tst_WebSocketFrame::tst_malformedFrames()
{
    QFETCH(QByteArray, payload);
    QFETCH(QWebSocketProtocol::CloseCode, expectedError);

    QBuffer buffer;
    buffer.setData(payload);
    buffer.open(QIODevice::ReadOnly);
    QWebSocketFrame frame;
    frame.readFrame(&buffer);
    buffer.close();

    QVERIFY(!frame.isValid());
    QCOMPARE(frame.closeCode(), expectedError);
}

QTEST_MAIN(tst_WebSocketFrame)

#include "tst_websocketframe.moc"

