// Copyright (C) 2016 The Qt Company Ltd.
// Copyright (C) 2014 Governikus GmbH & Co. KG.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QTest>
#include <QRandomGenerator>

#include <QtNetwork/private/bitstreams_p.h>
#include <QtNetwork/private/hpack_p.h>

#include <QtCore/qbytearray.h>

#include <cstdlib>
#include <vector>
#include <string>

QT_USE_NAMESPACE

using namespace HPack;

class tst_Hpack: public QObject
{
    Q_OBJECT

public:
    tst_Hpack();
private Q_SLOTS:
    void bitstreamConstruction();
    void bitstreamWrite();
    void bitstreamReadWrite();
    void bitstreamCompression();
    void bitstreamErrors();

    void lookupTableConstructor();

    void lookupTableStatic();
    void lookupTableDynamic();

    void dynamicTableSizeUpdate();

    void hpackEncodeRequest_data();
    void hpackEncodeRequest();
    void hpackDecodeRequest_data();
    void hpackDecodeRequest();

    void hpackEncodeResponse_data();
    void hpackEncodeResponse();
    void hpackDecodeResponse_data();
    void hpackDecodeResponse();

    // TODO: more-more-more tests needed!

private:
    void hpackEncodeRequest(bool withHuffman);
    void hpackEncodeResponse(bool withHuffman);

    HttpHeader header1;
    std::vector<uchar> buffer1;
    BitOStream request1;

    HttpHeader header2;
    std::vector<uchar> buffer2;
    BitOStream request2;

    HttpHeader header3;
    std::vector<uchar> buffer3;
    BitOStream request3;
};

using StreamError = BitIStream::Error;

tst_Hpack::tst_Hpack()
    : request1(buffer1),
      request2(buffer2),
      request3(buffer3)
{
}

void tst_Hpack::bitstreamConstruction()
{
    const uchar bytes[] = {0xDE, 0xAD, 0xBE, 0xEF};
    const int size = int(sizeof bytes);

    // Default ctors:
    std::vector<uchar> buffer;
    {
        const BitOStream out(buffer);
        QVERIFY(out.bitLength() == 0);
        QVERIFY(out.byteLength() == 0);

        const BitIStream in;
        QVERIFY(in.bitLength() == 0);
        QVERIFY(in.streamOffset() == 0);
        QVERIFY(in.error() == StreamError::NoError);
    }

    // Create istream with some data:
    {
        BitIStream in(bytes, bytes + size);
        QVERIFY(in.bitLength() == size * 8);
        QVERIFY(in.streamOffset() == 0);
        QVERIFY(in.error() == StreamError::NoError);
        // 'Read' some data back:
        for (int i = 0; i < size; ++i) {
            uchar bitPattern = 0;
            const auto bitsRead = in.peekBits(quint64(i * 8), 8, &bitPattern);
            QVERIFY(bitsRead == 8);
            QVERIFY(bitPattern == bytes[i]);
        }
    }

    // Copy ctors:
    {
        // Ostreams - copy is disabled.
        // Istreams:
        const BitIStream in1;
        const BitIStream in2(in1);
        QVERIFY(in2.bitLength() == in1.bitLength());
        QVERIFY(in2.streamOffset() == in1.streamOffset());
        QVERIFY(in2.error() == StreamError::NoError);

        const BitIStream in3(bytes, bytes + size);
        const BitIStream in4(in3);
        QVERIFY(in4.bitLength() == in3.bitLength());
        QVERIFY(in4.streamOffset() == in3.streamOffset());
        QVERIFY(in4.error() == StreamError::NoError);
    }
}

void tst_Hpack::bitstreamWrite()
{
    // Known representations,
    // https://http2.github.io/http2-spec/compression.html.
    // 5.1 Integer Representation

    // Test bit/byte lengths of the
    // resulting data:
    std::vector<uchar> buffer;
    BitOStream out(buffer);
    out.write(3);
    // 11, fits into 8-bit prefix:
    QVERIFY(out.bitLength() == 8);
    QVERIFY(out.byteLength() == 1);
    QVERIFY(out.begin()[0] == 3);

    out.clear();
    QVERIFY(out.bitLength() == 0);
    QVERIFY(out.byteLength() == 0);

    // This number does not fit into 8-bit
    // prefix we'll need 2 bytes:
    out.write(256);
    QVERIFY(out.byteLength() == 2);
    QVERIFY(out.bitLength() == 16);
    QVERIFY(out.begin()[0] == 0xff);
    QVERIFY(out.begin()[1] == 1);

    out.clear();

    // See 5.2   String Literal Representation.

    // We use Huffman code,
    // char 'a' has a prefix code 00011 (5 bits)
    out.write(QByteArray("aaa", 3), true);
    QVERIFY(out.byteLength() == 3);
    QVERIFY(out.bitLength() == 24);
    // Now we must have in our stream:
    // 10000010 | 00011000| 11000111
    const uchar *encoded = out.begin();
    QVERIFY(encoded[0] == 0x82);
    QVERIFY(encoded[1] == 0x18);
    QVERIFY(encoded[2] == 0xC7);
    // TODO: add more tests ...
}

void tst_Hpack::bitstreamReadWrite()
{
    // We can write into the bit stream:
    // 1) bit patterns
    // 2) integers (see HPACK, 5.1)
    // 3) string (see HPACK, 5.2)
    std::vector<uchar> buffer;
    BitOStream out(buffer);
    out.writeBits(0xf, 3);
    QVERIFY(out.byteLength() == 1);
    QVERIFY(out.bitLength() == 3);

    // Now, read it back:
    {
        BitIStream in(out.begin(), out.end());
        uchar bitPattern = 0;
        const auto bitsRead = in.peekBits(0, 3, &bitPattern);
        // peekBits pack into the most significant byte/bit:
        QVERIFY(bitsRead == 3);
        QVERIFY((bitPattern >> 5) == 7);
    }

    const quint32 testInt = 133;
    out.write(testInt);

    // This integer does not fit into the current 5-bit prefix,
    // so byteLength == 2.
    QVERIFY(out.byteLength() == 2);
    const auto bitLength = out.bitLength();
    QVERIFY(bitLength > 3);

    // Now, read it back:
    {
        BitIStream in(out.begin(), out.end());
        in.skipBits(3); // Bit pattern
        quint32 value = 0;
        QVERIFY(in.read(&value));
        QVERIFY(in.error() == StreamError::NoError);
        QCOMPARE(value, testInt);
    }

    const QByteArray testString("ABCDE", 5);
    out.write(testString, true); // Compressed
    out.write(testString, false); // Non-compressed
    QVERIFY(out.byteLength() > 2);
    QVERIFY(out.bitLength() > bitLength);

    // Now, read it back:
    {
        BitIStream in(out.begin(), out.end());
        in.skipBits(bitLength); // Bit pattern and integer
        QByteArray value;
        // Read compressed string first ...
        QVERIFY(in.read(&value));
        QCOMPARE(value, testString);
        QCOMPARE(in.error(), StreamError::NoError);
        // Now non-compressed ...
        QVERIFY(in.read(&value));
        QCOMPARE(value, testString);
        QCOMPARE(in.error(), StreamError::NoError);
    }
}

void tst_Hpack::bitstreamCompression()
{
    // Similar to bitstreamReadWrite but
    // writes/reads a lot of mixed strings/integers.
    std::vector<std::string> strings;
    std::vector<quint32> integers;
    std::vector<bool> isA; // integer or string.
    const std::string bytes("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789()[]/*");
    const unsigned nValues = 100000;

    quint64 totalStringBytes = 0;
    std::vector<uchar> buffer;
    BitOStream out(buffer);
    for (unsigned i = 0; i < nValues; ++i) {
        const bool isString = QRandomGenerator::global()->bounded(1000) > 500;
        isA.push_back(isString);
        if (!isString) {
            integers.push_back(QRandomGenerator::global()->bounded(1000u));
            out.write(integers.back());
        } else {
            const auto start = QRandomGenerator::global()->bounded(uint(bytes.length()) / 2);
            auto end = start * 2;
            if (!end)
                end = unsigned(bytes.length() / 2);
            strings.push_back(bytes.substr(start, end - start));
            const auto &s = strings.back();
            totalStringBytes += s.size();
            QByteArray data(s.c_str(), int(s.size()));
            const bool compressed(QRandomGenerator::global()->bounded(1000) > 500);
            out.write(data, compressed);
        }
    }

    qDebug() << "Compressed(?) byte length:" << out.byteLength()
             << "total string bytes:" << totalStringBytes;
    qDebug() << "total integer bytes (for quint32):" << integers.size() * sizeof(quint32);

    QVERIFY(out.byteLength() > 0);
    QVERIFY(out.bitLength() > 0);

    BitIStream in(out.begin(), out.end());

    for (unsigned i = 0, iS = 0, iI = 0; i < nValues; ++i) {
        if (isA[i]) {
            QByteArray data;
            QVERIFY(in.read(&data));
            QCOMPARE(in.error(), StreamError::NoError);
            QCOMPARE(data.toStdString(), strings[iS]);
            ++iS;
        } else {
            quint32 value = 0;
            QVERIFY(in.read(&value));
            QCOMPARE(in.error(), StreamError::NoError);
            QCOMPARE(value, integers[iI]);
            ++iI;
        }
    }
}

void tst_Hpack::bitstreamErrors()
{
    {
        BitIStream in;
        quint32 val = 0;
        QVERIFY(!in.read(&val));
        QCOMPARE(in.error(), StreamError::NotEnoughData);
    }
    {
        // Integer in a stream, that does not fit into quint32.
        const uchar bytes[] = {0xff, 0xff, 0xff, 0xff, 0xff, 0xff};
        BitIStream in(bytes, bytes + sizeof bytes);
        quint32 val = 0;
        QVERIFY(!in.read(&val));
        QCOMPARE(in.error(), StreamError::InvalidInteger);
    }
    {
        const uchar byte = 0x82; // 1 - Huffman compressed, 2 - the (fake) byte length.
        BitIStream in(&byte, &byte + 1);
        QByteArray val;
        QVERIFY(!in.read(&val));
        QCOMPARE(in.error(), StreamError::NotEnoughData);
    }
}

void tst_Hpack::lookupTableConstructor()
{
    {
        FieldLookupTable nonIndexed(4096, false);
        QVERIFY(nonIndexed.dynamicDataSize() == 0);
        QVERIFY(nonIndexed.numberOfDynamicEntries() == 0);
        QVERIFY(nonIndexed.numberOfStaticEntries() != 0);
        QVERIFY(nonIndexed.numberOfStaticEntries() == nonIndexed.numberOfEntries());
        // Now we add some fake field and verify what 'non-indexed' means ... no search
        // by name.
        QVERIFY(nonIndexed.prependField("custom-key", "custom-value"));
        // 54: 10 + 12 in name/value pair above + 32 required by HPACK specs ...
        QVERIFY(nonIndexed.dynamicDataSize() == 54);
        QVERIFY(nonIndexed.numberOfDynamicEntries() == 1);
        QCOMPARE(nonIndexed.numberOfEntries(), nonIndexed.numberOfStaticEntries() + 1);
        // Should fail to find it (invalid index 0) - search is disabled.
        QVERIFY(nonIndexed.indexOf("custom-key", "custom-value") == 0);
    }
    {
        // "key" + "value" == 8 bytes, + 32 (HPACK's requirement) == 40.
        // Let's ask for a max-size 32 so that entry does not fit:
        FieldLookupTable nonIndexed(32, false);
        QVERIFY(nonIndexed.prependField("key", "value"));
        QVERIFY(nonIndexed.numberOfEntries() == nonIndexed.numberOfStaticEntries());
        QVERIFY(nonIndexed.indexOf("key", "value") == 0);
    }
    {
        FieldLookupTable indexed(4096, true);
        QVERIFY(indexed.dynamicDataSize() == 0);
        QVERIFY(indexed.numberOfDynamicEntries() == 0);
        QVERIFY(indexed.numberOfStaticEntries() != 0);
        QVERIFY(indexed.numberOfStaticEntries() == indexed.numberOfEntries());
        QVERIFY(indexed.prependField("custom-key", "custom-value"));
        QVERIFY(indexed.dynamicDataSize() == 54);
        QVERIFY(indexed.numberOfDynamicEntries() == 1);
        QVERIFY(indexed.numberOfEntries() == indexed.numberOfStaticEntries() + 1);
        QVERIFY(indexed.indexOf("custom-key") == indexed.numberOfStaticEntries() + 1);
        QVERIFY(indexed.indexOf("custom-key", "custom-value") == indexed.numberOfStaticEntries() + 1);
    }
}

void tst_Hpack::lookupTableStatic()
{
    const FieldLookupTable table(0, false /*all static, no need in 'search index'*/);
    const auto &staticTable = FieldLookupTable::staticPart();
    QByteArray name, value;
    quint32 currentIndex = 1; // HPACK is indexing starting from 1.
    for (const HeaderField &field : staticTable) {
        const quint32 index = table.indexOf(field.name, field.value);
        QVERIFY(index != 0);
        QCOMPARE(index, currentIndex);
        QVERIFY(table.field(index, &name, &value));
        QCOMPARE(name, field.name);
        QCOMPARE(value, field.value);
        ++currentIndex;
    }
}

void tst_Hpack::lookupTableDynamic()
{
    // HPACK's table size:
    // for every field -> size += field.name.length() + field.value.length() + 32.
    // Let's set some size limit and try to fill table with enough entries to have several
    // items evicted.
    const quint32 tableSize = 8192;
    const char stringData[] = "abcdefghijklmnopABCDEFGHIJKLMNOP0123456789()[]:";
    const quint32 dataSize = sizeof stringData - 1;

    FieldLookupTable table(tableSize, true);

    std::vector<QByteArray> fieldsToFind;
    quint32 evicted = 0;

    while (true) {
        // Strings are repeating way too often, I want to
        // have at least some items really evicted and not found,
        // therefore these weird dances with start/len.
        const quint32 start = QRandomGenerator::global()->bounded(dataSize - 10);
        quint32 len = QRandomGenerator::global()->bounded(dataSize - start);
        if (!len)
            len = 1;

        const QByteArray val(stringData + start, len);
        fieldsToFind.push_back(val);
        const quint32 entriesBefore = table.numberOfDynamicEntries();
        QVERIFY(table.prependField(val, val));
        QVERIFY(table.indexOf(val));
        QVERIFY(table.indexOf(val) == table.indexOf(val, val));
        QByteArray fieldName, fieldValue;
        table.field(table.indexOf(val), &fieldName, &fieldValue);

        QVERIFY(val == fieldName);
        QVERIFY(val == fieldValue);

        if (table.numberOfDynamicEntries() <= entriesBefore) {
            // We had to evict several items ...
            evicted += entriesBefore - table.numberOfDynamicEntries() + 1;
            if (evicted >= 200)
                break;
        }
    }

    QVERIFY(table.dynamicDataSize() <= tableSize);
    QVERIFY(table.numberOfDynamicEntries() > 0);
    QVERIFY(table.indexOf(fieldsToFind.back())); // We MUST have it in a table!

    using size_type = std::vector<QByteArray>::size_type;
    for (size_type i = 0, e = fieldsToFind.size(); i < e; ++i) {
        const auto &val = fieldsToFind[i];
        const quint32 index = table.indexOf(val);
        if (!index) {
            QVERIFY(i < size_type(evicted));
        } else {
            QVERIFY(index == table.indexOf(val, val));
            QByteArray fieldName, fieldValue;
            QVERIFY(table.field(index, &fieldName, &fieldValue));
            QVERIFY(val == fieldName);
            QVERIFY(val == fieldValue);
        }
    }

    table.clearDynamicTable();

    QVERIFY(table.numberOfDynamicEntries() == 0);
    QVERIFY(table.dynamicDataSize() == 0);
    QVERIFY(table.indexOf(fieldsToFind.back()) == 0);

    QVERIFY(table.prependField("name1", "value1"));
    QVERIFY(table.prependField("name2", "value2"));

    QVERIFY(table.indexOf("name1") == table.numberOfStaticEntries() + 2);
    QVERIFY(table.indexOf("name2", "value2") == table.numberOfStaticEntries() + 1);
    QVERIFY(table.indexOf("name1", "value2") == 0);
    QVERIFY(table.indexOf("name2", "value1") == 0);
    QVERIFY(table.indexOf("name3") == 0);

    QVERIFY(!table.indexIsValid(table.numberOfEntries() + 1));

    QVERIFY(table.prependField("name1", "value1"));
    QVERIFY(table.numberOfDynamicEntries() == 3);
    table.evictEntry();
    QVERIFY(table.indexOf("name1") != 0);
    table.evictEntry();
    QVERIFY(table.indexOf("name2") == 0);
    QVERIFY(table.indexOf("name1") != 0);
    table.evictEntry();
    QVERIFY(table.dynamicDataSize() == 0);
    QVERIFY(table.numberOfDynamicEntries() == 0);
    QVERIFY(table.indexOf("name1") == 0);
}

void tst_Hpack::dynamicTableSizeUpdate()
{
    std::vector<uchar> buffer;
    buffer.reserve(4096);

    BitOStream out(buffer);

    HPack::Encoder encoder(4096u, true);

    QVERIFY(encoder.encodeSizeUpdate(out, 2048u));
    QVERIFY(encoder.encodeRequest(out, {{":method", "GET"}, {":path", "/"}, {":scheme", "https"}}));

    BitIStream in(buffer.data(), buffer.data() + buffer.size());
    HPack::Decoder decoder(4096u);
    QCOMPARE(decoder.dynamicTableCapacity(), 4096u);
    QCOMPARE(decoder.maxDynamicTableCapacity(), 4096u);

    QVERIFY(decoder.decodeHeaderFields(in));
    QCOMPARE(decoder.dynamicTableCapacity(), 2048u);
    // The max is adjusted out-of-band, so it's not changed here:
    QCOMPARE(decoder.maxDynamicTableCapacity(), 4096u);
}


void  tst_Hpack::hpackEncodeRequest_data()
{
    QTest::addColumn<bool>("compression");
    QTest::newRow("no-string-compression") << false;
    QTest::newRow("with-string-compression") << true;
}

void tst_Hpack::hpackEncodeRequest(bool withHuffman)
{
    // This function uses examples from HPACK specs
    // (see appendix).

    Encoder encoder(4096, withHuffman);
    // HPACK, C.3.1 First Request
    /*
    :method: GET
    :scheme: http
    :path: /
    :authority: www.example.com

    Hex dump of encoded data (without Huffman):

    8286 8441 0f77 7777 2e65 7861 6d70 6c65 | ...A.www.example
    2e63 6f6d

    Hex dump of encoded data (with Huffman):

    8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4 ff
    */
    request1.clear();
    header1 = {{":method", "GET"},
               {":scheme", "http"},
               {":path", "/"},
               {":authority", "www.example.com"}};
    QVERIFY(encoder.encodeRequest(request1, header1));
    QVERIFY(encoder.dynamicTableSize() == 57);

    // HPACK, C.3.2 Second Request
    /*
    Header list to encode:

    :method: GET
    :scheme: http
    :path: /
    :authority: www.example.com
    cache-control: no-cache

    Hex dump of encoded data (without Huffman):

    8286 84be 5808 6e6f 2d63 6163 6865

    Hex dump of encoded data (with Huffman):

    8286 84be 5886 a8eb 1064 9cbf
    */

    request2.clear();
    header2 = {{":method", "GET"},
               {":scheme", "http"},
               {":path", "/"},
               {":authority", "www.example.com"},
               {"cache-control", "no-cache"}};
    encoder.encodeRequest(request2, header2);
    QVERIFY(encoder.dynamicTableSize() == 110);

    // HPACK, C.3.3 Third Request
    /*
    Header list to encode:

    :method: GET
    :scheme: https
    :path: /index.html
    :authority: www.example.com
    custom-key: custom-value

    Hex dump of encoded data (without Huffman):

    8287 85bf 400a 6375 7374 6f6d 2d6b 6579
    0c63 7573 746f 6d2d 7661 6c75 65

    Hex dump of encoded data (with Huffman):

    8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925
    a849 e95b b8e8 b4bf
    */
    request3.clear();
    header3 = {{":method", "GET"},
               {":scheme", "https"},
               {":path", "/index.html"},
               {":authority", "www.example.com"},
               {"custom-key", "custom-value"}};
    encoder.encodeRequest(request3, header3);
    QVERIFY(encoder.dynamicTableSize() == 164);
}

void tst_Hpack::hpackEncodeRequest()
{
    QFETCH(bool, compression);

    hpackEncodeRequest(compression);

    // See comments above about these hex dumps ...
    const uchar bytes1NH[] = {0x82, 0x86, 0x84, 0x41,
                              0x0f, 0x77, 0x77, 0x77,
                              0x2e, 0x65, 0x78, 0x61,
                              0x6d, 0x70, 0x6c, 0x65,
                              0x2e, 0x63, 0x6f, 0x6d};

    const uchar bytes1WH[] = {0x82, 0x86, 0x84, 0x41,
                              0x8c, 0xf1, 0xe3, 0xc2,
                              0xe5, 0xf2, 0x3a, 0x6b,
                              0xa0, 0xab, 0x90, 0xf4,
                              0xff};

    const uchar *hexDump1 = compression ? bytes1WH : bytes1NH;
    const quint64 byteLength1 = compression ? sizeof bytes1WH : sizeof bytes1NH;

    QCOMPARE(request1.byteLength(), byteLength1);
    QCOMPARE(request1.bitLength(), byteLength1 * 8);

    for (quint32 i = 0, e = request1.byteLength(); i < e; ++i)
        QCOMPARE(hexDump1[i], request1.begin()[i]);

    const uchar bytes2NH[] = {0x82, 0x86, 0x84, 0xbe,
                              0x58, 0x08, 0x6e, 0x6f,
                              0x2d, 0x63, 0x61, 0x63,
                              0x68, 0x65};

    const uchar bytes2WH[] = {0x82, 0x86, 0x84, 0xbe,
                              0x58, 0x86, 0xa8, 0xeb,
                              0x10, 0x64, 0x9c, 0xbf};

    const uchar *hexDump2 = compression ? bytes2WH : bytes2NH;
    const auto byteLength2 = compression ? sizeof bytes2WH : sizeof bytes2NH;
    QVERIFY(request2.byteLength() == byteLength2);
    QVERIFY(request2.bitLength() == byteLength2 * 8);
    for (quint32 i = 0, e = request2.byteLength(); i < e; ++i)
        QCOMPARE(hexDump2[i], request2.begin()[i]);

    const uchar bytes3NH[] = {0x82, 0x87, 0x85, 0xbf,
                              0x40, 0x0a, 0x63, 0x75,
                              0x73, 0x74, 0x6f, 0x6d,
                              0x2d, 0x6b, 0x65, 0x79,
                              0x0c, 0x63, 0x75, 0x73,
                              0x74, 0x6f, 0x6d, 0x2d,
                              0x76, 0x61, 0x6c, 0x75,
                              0x65};
    const uchar bytes3WH[] = {0x82, 0x87, 0x85, 0xbf,
                              0x40, 0x88, 0x25, 0xa8,
                              0x49, 0xe9, 0x5b, 0xa9,
                              0x7d, 0x7f, 0x89, 0x25,
                              0xa8, 0x49, 0xe9, 0x5b,
                              0xb8, 0xe8, 0xb4, 0xbf};

    const uchar *hexDump3 = compression ? bytes3WH : bytes3NH;
    const quint64 byteLength3 = compression ? sizeof bytes3WH : sizeof bytes3NH;
    QCOMPARE(request3.byteLength(), byteLength3);
    QCOMPARE(request3.bitLength(), byteLength3 * 8);
    for (quint32 i = 0, e = request3.byteLength(); i < e; ++i)
        QCOMPARE(hexDump3[i], request3.begin()[i]);
}

void tst_Hpack::hpackDecodeRequest_data()
{
    QTest::addColumn<bool>("compression");
    QTest::newRow("no-string-compression") << false;
    QTest::newRow("with-string-compression") << true;
}

void tst_Hpack::hpackDecodeRequest()
{
    QFETCH(bool, compression);
    hpackEncodeRequest(compression);

    QVERIFY(request1.byteLength());
    QVERIFY(request2.byteLength());
    QVERIFY(request3.byteLength());

    Decoder decoder(4096);
    BitIStream inputStream1(request1.begin(), request1.end());
    QVERIFY(decoder.decodeHeaderFields(inputStream1));
    QCOMPARE(decoder.dynamicTableSize(), quint32(57));
    {
        const auto &decoded = decoder.decodedHeader();
        QVERIFY(decoded == header1);
    }

    BitIStream inputStream2{request2.begin(), request2.end()};
    QVERIFY(decoder.decodeHeaderFields(inputStream2));
    QCOMPARE(decoder.dynamicTableSize(), quint32(110));
    {
        const auto &decoded = decoder.decodedHeader();
        QVERIFY(decoded == header2);
    }

    BitIStream inputStream3(request3.begin(), request3.end());
    QVERIFY(decoder.decodeHeaderFields(inputStream3));
    QCOMPARE(decoder.dynamicTableSize(), quint32(164));
    {
        const auto &decoded = decoder.decodedHeader();
        QVERIFY(decoded == header3);
    }
}

void tst_Hpack::hpackEncodeResponse_data()
{
    hpackEncodeRequest_data();
}

void tst_Hpack::hpackEncodeResponse()
{
    QFETCH(bool, compression);

    hpackEncodeResponse(compression);

    // TODO: we can also test bytes - using hex dumps from HPACK's specs,
    // for now only test a table behavior/expected sizes.
}

void tst_Hpack::hpackEncodeResponse(bool withCompression)
{
    Encoder encoder(256, withCompression); // 256 - this will result in entries evicted.

    // HPACK, C.5.1 First Response
    /*
    Header list to encode:

    :status: 302
    cache-control: private
    date: Mon, 21 Oct 2013 20:13:21 GMT
    location: https://www.example.com
    */
    request1.clear();
    header1 = {{":status", "302"},
               {"cache-control", "private"},
               {"date", "Mon, 21 Oct 2013 20:13:21 GMT"},
               {"location", "https://www.example.com"}};

    QVERIFY(encoder.encodeResponse(request1, header1));
    QCOMPARE(encoder.dynamicTableSize(), quint32(222));

    // HPACK, C.5.2 Second Response
    /*


    The (":status", "302") header field is evicted from the dynamic
    table to free space to allow adding the (":status", "307") header field.

    Header list to encode:

    :status: 307
    cache-control: private
    date: Mon, 21 Oct 2013 20:13:21 GMT
    location: https://www.example.com
    */
    request2.clear();
    header2 = {{":status", "307"},
               {"cache-control", "private"},
               {"date", "Mon, 21 Oct 2013 20:13:21 GMT"},
               {"location", "https://www.example.com"}};
    QVERIFY(encoder.encodeResponse(request2, header2));
    QCOMPARE(encoder.dynamicTableSize(), quint32(222));

    // HPACK, C.5.3 Third Response
    /*
    Several header fields are evicted from the dynamic table
    during the processing of this header list.

    Header list to encode:

    :status: 200
    cache-control: private
    date: Mon, 21 Oct 2013 20:13:22 GMT
    location: https://www.example.com
    content-encoding: gzip
    set-cookie: foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1
    */
    request3.clear();
    header3 = {{":status", "200"},
               {"cache-control", "private"},
               {"date", "Mon, 21 Oct 2013 20:13:22 GMT"},
               {"location", "https://www.example.com"},
               {"content-encoding", "gzip"},
               {"set-cookie", "foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1"}};
    QVERIFY(encoder.encodeResponse(request3, header3));
    QCOMPARE(encoder.dynamicTableSize(), quint32(215));
}

void tst_Hpack::hpackDecodeResponse_data()
{
    hpackEncodeRequest_data();
}

void tst_Hpack::hpackDecodeResponse()
{
    QFETCH(bool, compression);

    hpackEncodeResponse(compression);

    QVERIFY(request1.byteLength());
    Decoder decoder(256); // This size will result in entries evicted.
    BitIStream inputStream1(request1.begin(), request1.end());
    QVERIFY(decoder.decodeHeaderFields(inputStream1));
    QCOMPARE(decoder.dynamicTableSize(), quint32(222));

    {
        const auto &decoded = decoder.decodedHeader();
        QVERIFY(decoded == header1);
    }

    QVERIFY(request2.byteLength());
    BitIStream inputStream2(request2.begin(), request2.end());
    QVERIFY(decoder.decodeHeaderFields(inputStream2));
    QCOMPARE(decoder.dynamicTableSize(), quint32(222));

    {
        const auto &decoded = decoder.decodedHeader();
        QVERIFY(decoded == header2);
    }

    QVERIFY(request3.byteLength());
    BitIStream inputStream3(request3.begin(), request3.end());
    QVERIFY(decoder.decodeHeaderFields(inputStream3));
    QCOMPARE(decoder.dynamicTableSize(), quint32(215));

    {
        const auto &decoded = decoder.decodedHeader();
        QVERIFY(decoded == header3);
    }
}

QTEST_MAIN(tst_Hpack)

#include "tst_hpack.moc"
