// Copyright (C) 2022 The Qt Company Ltd.
// Copyright (C) 2012 Intel Corporation.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include <QtCore/QUrlQuery>
#include <QTest>

typedef QList<QPair<QString, QString> > QueryItems;
Q_DECLARE_METATYPE(QueryItems)
Q_DECLARE_METATYPE(QUrl::ComponentFormattingOptions)

using namespace Qt::StringLiterals;

class tst_QUrlQuery : public QObject
{
    Q_OBJECT

public:
    tst_QUrlQuery()
    {
        qRegisterMetaType<QueryItems>();
    }

private Q_SLOTS:
    void constructing();
    void addRemove();
    void multiAddRemove();
    void multiplyAddSamePair();
    void setQueryItems_data();
    void setQueryItems();
    void basicParsing_data();
    void basicParsing();
    void reconstructQuery_data();
    void reconstructQuery();
    void encodedSetQueryItems_data();
    void encodedSetQueryItems();
    void encodedParsing_data();
    void encodedParsing();
    void differentDelimiters();

    // old tests from tst_qurl.cpp
    // add new tests above
    void old_queryItems();
    void old_hasQueryItem_data();
    void old_hasQueryItem();
};

static QString prettyPair(const QPair<QString, QString> &pair)
{
    const auto represent = [](const QString &s) {
        return s.isNull() ? u"null"_s : u'"' + s + u'"';
    };
    return represent(pair.first) + " -> "_L1 + represent(pair.second);
}

static QByteArray prettyList(const QueryItems &items)
{
    if (items.isEmpty())
        return "()";
    auto it = items.constBegin();
    QString result = u'(' + prettyPair(*it);
    for (++it; it != items.constEnd(); ++it)
        result += ", "_L1 + prettyPair(*it);
    result += u')';
    return result.toLocal8Bit();
}

static bool compare(const QueryItems &actual, const QueryItems &expected,
                    const char *actualStr, const char *expectedStr, const char *file, int line)
{
    return QTest::compare_helper(actual == expected, "Compared values are not the same",
                                 [&actual] { return qstrdup(prettyList(actual).constData()); },
                                 [&expected] { return qstrdup(prettyList(expected).constData()); },
                                 actualStr, expectedStr, file, line);
}

#define COMPARE_ITEMS(actual, expected) \
    do { \
        if (!compare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \
            return; \
    } while (0)

inline QueryItems operator+(QueryItems items, const QPair<QString, QString> &pair)
{
    // items is already a copy
    items.append(pair);
    return items;
}

inline QueryItems operator+(const QPair<QString, QString> &pair, QueryItems items)
{
    // items is already a copy
    items.prepend(pair);
    return items;
}

inline QPair<QString, QString> qItem(const QString &first, const QString &second)
{
    return qMakePair(first, second);
}

inline QPair<QString, QString> qItem(const char *first, const QString &second)
{
    return qMakePair(QString::fromUtf8(first), second);
}

inline QPair<QString, QString> qItem(const char *first, const char *second)
{
    return qMakePair(QString::fromUtf8(first), QString::fromUtf8(second));
}

inline QPair<QString, QString> qItem(const QString &first, const char *second)
{
    return qMakePair(first, QString::fromUtf8(second));
}

static QUrlQuery emptyQuery()
{
    return QUrlQuery();
}

void tst_QUrlQuery::constructing()
{
    QUrlQuery empty;
    QVERIFY(empty.isEmpty());
    QCOMPARE(empty.queryPairDelimiter(), QUrlQuery::defaultQueryPairDelimiter());
    QCOMPARE(empty.queryValueDelimiter(), QUrlQuery::defaultQueryValueDelimiter());
    // undefined whether it is detached, but don't crash
    QVERIFY(empty.isDetached() || !empty.isDetached());

    empty.clear();
    QVERIFY(empty.isEmpty());

    {
        QUrlQuery copy(empty);
        QVERIFY(copy.isEmpty());
        QVERIFY(!copy.isDetached());
        QCOMPARE(copy, empty);
        QCOMPARE(qHash(copy), qHash(empty));
        QVERIFY(!(copy != empty));

        copy = empty;
        QCOMPARE(copy, empty);

        copy = QUrlQuery();
        QCOMPARE(copy, empty);
        QCOMPARE(qHash(copy), qHash(empty));
    }
    {
        QUrlQuery copy(emptyQuery());
        QCOMPARE(copy, empty);
    }

    QVERIFY(!empty.hasQueryItem("a"));
    QVERIFY(empty.queryItemValue("a").isEmpty());
    QVERIFY(empty.allQueryItemValues("a").isEmpty());

    QVERIFY(!empty.hasQueryItem(""));
    QVERIFY(empty.queryItemValue("").isEmpty());
    QVERIFY(empty.allQueryItemValues("").isEmpty());

    QVERIFY(!empty.hasQueryItem(QString()));
    QVERIFY(empty.queryItemValue(QString()).isEmpty());
    QVERIFY(empty.allQueryItemValues(QString()).isEmpty());

    QVERIFY(empty.queryItems().isEmpty());

    QUrlQuery other;
    other.addQueryItem("a", "b");
    QVERIFY(!other.isEmpty());
    QVERIFY(other.isDetached());
    QCOMPARE_NE(other, empty);
    QVERIFY(!(other == empty));

    // copy-construct
    QUrlQuery copy(other);
    QCOMPARE(copy, other);

    copy.clear();
    QVERIFY(copy.isEmpty());
    QCOMPARE_NE(copy, other);

    // copy-assign
    copy = other;
    QVERIFY(!copy.isEmpty());
    QCOMPARE(copy, other);

    // move-construct
    QUrlQuery moved(std::move(other));
    QCOMPARE(moved, copy);

    // self move-assign
    {
        auto &self = moved; // prevent -Wself-move
        moved = std::move(self);
    }
    QCOMPARE(moved, copy);

    // self move-assign of moved-from (Hinnant Criterion)
    {
        auto &self = other; // prevent -Wself-move
        other = std::move(self);
    }
    // shouldn't crash; here, or further down

    // copy-assign to moved-from object
    other = copy;
    QCOMPARE(other, copy);
    QCOMPARE(other, moved);

    // move-assign
    moved = std::move(other);
    QCOMPARE(moved, copy);

    // (move-)assign default-constructed
    copy = QUrlQuery();
    QVERIFY(copy.isEmpty());

    empty.setQueryDelimiters('(', ')');
    QCOMPARE(empty.queryValueDelimiter(), QChar(QLatin1Char('(')));
    QCOMPARE(empty.queryPairDelimiter(), QChar(QLatin1Char(')')));

    QList<QPair<QString, QString> > query;
    query += qMakePair(QString("type"), QString("login"));
    query += qMakePair(QString("name"), QString::fromUtf8("åge nissemannsen"));
    query += qMakePair(QString("ole&du"), QString::fromUtf8("anne+jørgen=sant"));
    query += qMakePair(QString("prosent"), QString("%"));
    copy.setQueryItems(query);
    QVERIFY(!copy.isEmpty());

    QUrlQuery fromList = {
        {QString("type"), QString("login")},
        {QString("name"), QString::fromUtf8("åge nissemannsen")},
        {QString("ole&du"), QString::fromUtf8("anne+jørgen=sant")},
        {QString("prosent"), QString("%")}
    };
    QCOMPARE(fromList, copy);
}

void tst_QUrlQuery::addRemove()
{
    QUrlQuery query;
    QCOMPARE(query, query);

    {
        // one item
        query.addQueryItem("a", "b");
        QVERIFY(!query.isEmpty());
        QVERIFY(query.hasQueryItem("a"));
        QCOMPARE_NE(query, QUrlQuery());
        QCOMPARE(query.queryItemValue("a"), QString("b"));
        QCOMPARE(query.allQueryItemValues("a"), QStringList() << "b");

        QList<QPair<QString, QString> > allItems = query.queryItems();
        QCOMPARE(allItems.size(), 1);
        QCOMPARE(allItems.at(0).first, QString("a"));
        QCOMPARE(allItems.at(0).second, QString("b"));
    }

    QUrlQuery original = query;
    QCOMPARE(query, original);

    {
        // two items
        query.addQueryItem("c", "d");
        QVERIFY(query.hasQueryItem("a"));
        QCOMPARE(query.queryItemValue("a"), QString("b"));
        QCOMPARE(query.allQueryItemValues("a"), QStringList() << "b");
        QVERIFY(query.hasQueryItem("c"));
        QCOMPARE(query.queryItemValue("c"), QString("d"));
        QCOMPARE(query.allQueryItemValues("c"), QStringList() << "d");

        QList<QPair<QString, QString> > allItems = query.queryItems();
        QCOMPARE(allItems.size(), 2);
        QVERIFY(allItems.contains(qItem("a", "b")));
        QVERIFY(allItems.contains(qItem("c", "d")));

        QCOMPARE_NE(query, original);
        QVERIFY(!(query == original));
    }

    {
        // remove an item that isn't there
        QUrlQuery copy = query;
        query.removeQueryItem("e");
        QCOMPARE(query, copy);
    }

    {
        // remove an item
        query.removeQueryItem("c");
        QVERIFY(query.hasQueryItem("a"));
        QCOMPARE(query.queryItemValue("a"), QString("b"));
        QCOMPARE(query.allQueryItemValues("a"), QStringList() << "b");

        QList<QPair<QString, QString> > allItems = query.queryItems();
        QCOMPARE(allItems.size(), 1);
        QCOMPARE(allItems.at(0).first, QString("a"));
        QCOMPARE(allItems.at(0).second, QString("b"));

        QCOMPARE(query, original);
        QVERIFY(!(query != original));
        QCOMPARE(qHash(query), qHash(original));
    }

    {
        // add an item with en empty value
        QString emptyButNotNull(0, Qt::Uninitialized);
        QVERIFY(emptyButNotNull.isEmpty());
        QVERIFY(!emptyButNotNull.isNull());

        query.addQueryItem("e", "");
        QVERIFY(query.hasQueryItem("a"));
        QCOMPARE(query.queryItemValue("a"), QString("b"));
        QCOMPARE(query.allQueryItemValues("a"), QStringList() << "b");
        QVERIFY(query.hasQueryItem("e"));
        QCOMPARE(query.queryItemValue("e"), emptyButNotNull);
        QCOMPARE(query.allQueryItemValues("e"), QStringList() << emptyButNotNull);

        QList<QPair<QString, QString> > allItems = query.queryItems();
        QCOMPARE(allItems.size(), 2);
        QVERIFY(allItems.contains(qItem("a", "b")));
        QVERIFY(allItems.contains(qItem("e", emptyButNotNull)));

        QCOMPARE_NE(query, original);
        QVERIFY(!(query == original));
    }

    {
        // remove the items
        query.removeQueryItem("a");
        query.removeQueryItem("e");
        QVERIFY(query.isEmpty());
        QVERIFY(query.isDetached());
        QCOMPARE_NE(query, original);
        QCOMPARE(query, QUrlQuery());
    }
}

void tst_QUrlQuery::multiAddRemove()
{
    QUrlQuery query;

    {
        // one item, two values
        query.addQueryItem("a", "b");
        query.addQueryItem("a", "c");
        QVERIFY(!query.isEmpty());
        QVERIFY(query.hasQueryItem("a"));

        // returns the first one
        QCOMPARE(query.queryItemValue("a"), QLatin1String("b"));

        // order is the order we set them in
        QVERIFY(query.allQueryItemValues("a") == QStringList() << "b" << "c");
    }

    {
        // add another item, two values
        query.addQueryItem("A", "B");
        query.addQueryItem("A", "C");
        QVERIFY(query.hasQueryItem("A"));
        QVERIFY(query.hasQueryItem("a"));

        QCOMPARE(query.queryItemValue("a"), QLatin1String("b"));
        QVERIFY(query.allQueryItemValues("a") == QStringList() << "b" << "c");
        QCOMPARE(query.queryItemValue("A"), QLatin1String("B"));
        QVERIFY(query.allQueryItemValues("A") == QStringList() << "B" << "C");
    }

    {
        // remove one of the original items
        query.removeQueryItem("a");
        QVERIFY(query.hasQueryItem("a"));

        // it must have removed the first one
        QCOMPARE(query.queryItemValue("a"), QLatin1String("c"));
    }

    {
        // remove the items we added later
        query.removeAllQueryItems("A");
        QVERIFY(!query.isEmpty());
        QVERIFY(!query.hasQueryItem("A"));
    }

    {
        // add one element to the current, then remove them
        query.addQueryItem("a", "d");
        query.removeAllQueryItems("a");
        QVERIFY(!query.hasQueryItem("a"));
        QVERIFY(query.isEmpty());
    }
}

void tst_QUrlQuery::multiplyAddSamePair()
{
    QUrlQuery query;
    query.addQueryItem("a", "a");
    query.addQueryItem("a", "a");
    QCOMPARE(query.allQueryItemValues("a"), QStringList() << "a" << "a");

    query.addQueryItem("a", "a");
    QCOMPARE(query.allQueryItemValues("a"), QStringList() << "a" << "a" << "a");

    query.removeQueryItem("a");
    QCOMPARE(query.allQueryItemValues("a"), QStringList() << "a" << "a");
}

void tst_QUrlQuery::setQueryItems_data()
{
    QTest::addColumn<QueryItems>("items");
    QString emptyButNotNull(0, Qt::Uninitialized);

    QTest::newRow("empty") << QueryItems();
    QTest::newRow("1-novalue") << (QueryItems() << qItem("a", QString()));
    QTest::newRow("1-emptyvalue") << (QueryItems() << qItem("a", emptyButNotNull));

    QueryItems list;
    list << qItem("a", "b");
    QTest::newRow("1-value") << list;
    QTest::newRow("1-multi") << (list + qItem("a", "c"));
    QTest::newRow("1-duplicated") << (list + qItem("a", "b"));

    list << qItem("c", "d");
    QTest::newRow("2") << list;

    list << qItem("c", "e");
    QTest::newRow("2-multi") << list;
}

void tst_QUrlQuery::setQueryItems()
{
    QFETCH(QueryItems, items);
    QUrlQuery query;

    QueryItems::const_iterator it = items.constBegin();
    for ( ; it != items.constEnd(); ++it)
        query.addQueryItem(it->first, it->second);
    COMPARE_ITEMS(query.queryItems(), items);

    query.clear();

    query.setQueryItems(items);
    COMPARE_ITEMS(query.queryItems(), items);
}

void tst_QUrlQuery::basicParsing_data()
{
    QTest::addColumn<QString>("queryString");
    QTest::addColumn<QueryItems>("items");
    QString emptyButNotNull(0, Qt::Uninitialized);

    QTest::newRow("null") << QString() << QueryItems();
    QTest::newRow("empty") << "" << QueryItems();

    QTest::newRow("1-novalue") << "a" << (QueryItems() << qItem("a", QString()));
    QTest::newRow("1-emptyvalue") << "a=" << (QueryItems() << qItem("a", emptyButNotNull));
    QTest::newRow("1-value") << "a=b" << (QueryItems() << qItem("a", "b"));

    // some longer keys
    QTest::newRow("1-longkey-novalue") << "thisisalongkey" << (QueryItems() << qItem("thisisalongkey", QString()));
    QTest::newRow("1-longkey-emptyvalue") << "thisisalongkey=" << (QueryItems() << qItem("thisisalongkey", emptyButNotNull));
    QTest::newRow("1-longkey-value") << "thisisalongkey=b" << (QueryItems() << qItem("thisisalongkey", "b"));

    // longer values
    QTest::newRow("1-longvalue-value") << "a=thisisalongreasonablyvalue"
                                       << (QueryItems() << qItem("a", "thisisalongreasonablyvalue"));
    QTest::newRow("1-longboth-value") << "thisisalongkey=thisisalongreasonablyvalue"
                                      << (QueryItems() << qItem("thisisalongkey", "thisisalongreasonablyvalue"));

    // two or more entries
    QueryItems baselist;
    baselist << qItem("a", "b") << qItem("c", "d");
    QTest::newRow("2-ab-cd") << "a=b&c=d" << baselist;
    QTest::newRow("2-cd-ab") << "c=d&a=b" << (QueryItems() << qItem("c", "d") << qItem("a", "b"));

    // the same entry multiply defined
    QTest::newRow("2-a-a") << "a&a" << (QueryItems() << qItem("a", QString()) << qItem("a", QString()));
    QTest::newRow("2-ab-a") << "a=b&a" << (QueryItems() << qItem("a", "b") << qItem("a", QString()));
    QTest::newRow("2-ab-ab") << "a=b&a=b" << (QueryItems() << qItem("a", "b") << qItem("a", "b"));
    QTest::newRow("2-ab-ac") << "a=b&a=c" << (QueryItems() << qItem("a", "b") << qItem("a", "c"));

    QPair<QString, QString> novalue = qItem("somekey", QString());
    QueryItems list2 = baselist + novalue;
    QTest::newRow("3-novalue-ab-cd") << "somekey&a=b&c=d" << (novalue + baselist);
    QTest::newRow("3-ab-novalue-cd") << "a=b&somekey&c=d" << (QueryItems() << qItem("a", "b") << novalue << qItem("c", "d"));
    QTest::newRow("3-ab-cd-novalue") << "a=b&c=d&somekey" << list2;

    list2 << qItem("otherkeynovalue", QString());
    QTest::newRow("4-ab-cd-novalue-novalue") << "a=b&c=d&somekey&otherkeynovalue" << list2;

    QPair<QString, QString> emptyvalue = qItem("somekey", emptyButNotNull);
    list2 = baselist + emptyvalue;
    QTest::newRow("3-emptyvalue-ab-cd") << "somekey=&a=b&c=d" << (emptyvalue + baselist);
    QTest::newRow("3-ab-emptyvalue-cd") << "a=b&somekey=&c=d" << (QueryItems() << qItem("a", "b") << emptyvalue << qItem("c", "d"));
    QTest::newRow("3-ab-cd-emptyvalue") << "a=b&c=d&somekey=" << list2;
}

void tst_QUrlQuery::basicParsing()
{
    QFETCH(QString, queryString);
    QFETCH(QueryItems, items);

    QUrlQuery query(queryString);
    QCOMPARE(query.isEmpty(), items.isEmpty());
    COMPARE_ITEMS(query.queryItems(), items);
}

void tst_QUrlQuery::reconstructQuery_data()
{
    QTest::addColumn<QString>("queryString");
    QTest::addColumn<QueryItems>("items");
    QString emptyButNotNull(0, Qt::Uninitialized);

    QTest::newRow("null") << QString() << QueryItems();
    QTest::newRow("empty") << "" << QueryItems();

    QTest::newRow("1-novalue") << "a" << (QueryItems() << qItem("a", QString()));
    QTest::newRow("1-emptyvalue") << "a=" << (QueryItems() << qItem("a", emptyButNotNull));
    QTest::newRow("1-value") << "a=b" << (QueryItems() << qItem("a", "b"));

    // some longer keys
    QTest::newRow("1-longkey-novalue") << "thisisalongkey" << (QueryItems() << qItem("thisisalongkey", QString()));
    QTest::newRow("1-longkey-emptyvalue") << "thisisalongkey=" << (QueryItems() << qItem("thisisalongkey", emptyButNotNull));
    QTest::newRow("1-longkey-value") << "thisisalongkey=b" << (QueryItems() << qItem("thisisalongkey", "b"));

    // longer values
    QTest::newRow("1-longvalue-value") << "a=thisisalongreasonablyvalue"
                                       << (QueryItems() << qItem("a", "thisisalongreasonablyvalue"));
    QTest::newRow("1-longboth-value") << "thisisalongkey=thisisalongreasonablyvalue"
                                      << (QueryItems() << qItem("thisisalongkey", "thisisalongreasonablyvalue"));

    // two or more entries
    QueryItems baselist;
    baselist << qItem("a", "b") << qItem("c", "d");
    QTest::newRow("2-ab-cd") << "a=b&c=d" << baselist;

    // The same entry multiply defined
    QTest::newRow("2-a-a") << "a&a" << (QueryItems() << qItem("a", QString()) << qItem("a", QString()));
    QTest::newRow("2-ab-ab") << "a=b&a=b" << (QueryItems() << qItem("a", "b") << qItem("a", "b"));
    QTest::newRow("2-ab-ac") << "a=b&a=c" << (QueryItems() << qItem("a", "b") << qItem("a", "c"));
    QTest::newRow("2-ac-ab") << "a=c&a=b" << (QueryItems() << qItem("a", "c") << qItem("a", "b"));
    QTest::newRow("2-cd-ab") << "c=d&a=b" << (QueryItems() << qItem("c", "d") << qItem("a", "b"));

    QueryItems list2 = baselist + qItem("somekey", QString());
    QTest::newRow("3-ab-cd-novalue") << "a=b&c=d&somekey" << list2;

    list2 << qItem("otherkeynovalue", QString());
    QTest::newRow("4-ab-cd-novalue-novalue") << "a=b&c=d&somekey&otherkeynovalue" << list2;

    list2 = baselist + qItem("somekey", emptyButNotNull);
    QTest::newRow("3-ab-cd-emptyvalue") << "a=b&c=d&somekey=" << list2;
}

void tst_QUrlQuery::reconstructQuery()
{
    QFETCH(QString, queryString);
    QFETCH(QueryItems, items);

    QUrlQuery query;

    // add the items
    for (QueryItems::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) {
        query.addQueryItem(it->first, it->second);
    }
    QCOMPARE(query.query(), queryString);
}

void tst_QUrlQuery::encodedSetQueryItems_data()
{
    QTest::addColumn<QString>("queryString");
    QTest::addColumn<QString>("key");
    QTest::addColumn<QString>("value");
    QTest::addColumn<QUrl::ComponentFormattingOptions>("encoding");
    QTest::addColumn<QString>("expectedQuery");
    QTest::addColumn<QString>("expectedKey");
    QTest::addColumn<QString>("expectedValue");
    typedef QUrl::ComponentFormattingOptions F;

    QTest::newRow("nul") << "f%00=bar%00" << "f%00" << "bar%00" << F(QUrl::PrettyDecoded)
                         << "f%00=bar%00" << "f%00" << "bar%00";
    QTest::newRow("non-decodable-1") << "foo%01%7f=b%1ar" << "foo%01%7f" << "b%1ar" << F(QUrl::PrettyDecoded)
                                     << "foo%01%7F=b%1Ar" << "foo%01%7F" << "b%1Ar";
    QTest::newRow("non-decodable-2") << "foo\x01\x7f=b\x1ar" << "foo\x01\x7f" << "b\x1Ar" << F(QUrl::PrettyDecoded)
                                     << "foo%01%7F=b%1Ar" << "foo%01%7F" << "b%1Ar";

    QTest::newRow("space") << "%20=%20" << "%20" << "%20" << F(QUrl::PrettyDecoded)
                           << " = " << " " << " ";
    QTest::newRow("encode-space") << " = " << " " << " " << F(QUrl::FullyEncoded)
                                  << "%20=%20" << "%20" << "%20";

    // tri-state
    QTest::newRow("decode-non-delimiters") << "%3C%5C%3E=%7B%7C%7D%5E%60" << "%3C%5C%3E" << "%7B%7C%7D%5E%60" << F(QUrl::DecodeReserved)
                                    << "<\\>={|}^`" << "<\\>" << "{|}^`";
    QTest::newRow("encode-non-delimiters") << "<\\>={|}^`" << "<\\>" << "{|}^`" << F(QUrl::EncodeReserved)
                                           << "%3C%5C%3E=%7B%7C%7D%5E%60" << "%3C%5C%3E" << "%7B%7C%7D%5E%60";
    QTest::newRow("pretty-non-delimiters") << "<\\>={|}^`" << "<\\>" << "{|}^`" << F(QUrl::PrettyDecoded)
                                           << "%3C%5C%3E=%7B%7C%7D%5E%60" << "<\\>" << "{|}^`";

    QTest::newRow("equals") << "%3D=%3D" << "%3D" << "%3D" << F(QUrl::PrettyDecoded)
                            << "%3D=%3D" << "=" << "=";
    QTest::newRow("equals-2") << "%3D==" << "=" << "=" << F(QUrl::PrettyDecoded)
                              << "%3D=%3D" << "=" << "=";
    QTest::newRow("ampersand") << "%26=%26" << "%26" << "%26" << F(QUrl::PrettyDecoded)
                               << "%26=%26" << "&" << "&";
    QTest::newRow("hash") << "#=#" << "%23" << "%23" << F(QUrl::PrettyDecoded)
                            << "#=#" << "#" << "#";
    QTest::newRow("decode-hash") << "%23=%23" << "%23" << "%23" << F(QUrl::PrettyDecoded)
                                 << "#=#" << "#" << "#";

    QTest::newRow("percent") << "%25=%25" << "%25" << "%25" << F(QUrl::PrettyDecoded)
                             << "%25=%25" << "%25" << "%25";
    QTest::newRow("bad-percent-1") << "%=%" << "%" << "%" << F(QUrl::PrettyDecoded)
                                   << "%25=%25" << "%25" << "%25";
    QTest::newRow("bad-percent-2") << "%2=%2" << "%2" << "%2" << F(QUrl::PrettyDecoded)
                                   << "%252=%252" << "%252" << "%252";

    QTest::newRow("plus") << "+=+" << "+" << "+" << F(QUrl::PrettyDecoded)
                            << "+=+" << "+" << "+";
    QTest::newRow("2b") << "%2b=%2b" << "%2b" << "%2b" << F(QUrl::PrettyDecoded)
                            << "%2B=%2B" << "%2B" << "%2B";
    // plus signs must not be touched
    QTest::newRow("encode-plus") << "+=+" << "+" << "+" << F(QUrl::FullyEncoded)
                            << "+=+" << "+" << "+";
    QTest::newRow("decode-2b") << "%2b=%2b" << "%2b" << "%2b" << F(QUrl::PrettyDecoded)
                            << "%2B=%2B" << "%2B" << "%2B";


    QTest::newRow("unicode") << "q=R%C3%a9sum%c3%A9" << "q" << "R%C3%a9sum%c3%A9" << F(QUrl::PrettyDecoded)
                             << QString::fromUtf8("q=R\xc3\xa9sum\xc3\xa9") << "q" << QString::fromUtf8("R\xc3\xa9sum\xc3\xa9");
    QTest::newRow("encode-unicode") << QString::fromUtf8("q=R\xc3\xa9sum\xc3\xa9") << "q" << QString::fromUtf8("R\xc3\xa9sum\xc3\xa9")
                                    << F(QUrl::FullyEncoded)
                                    << "q=R%C3%A9sum%C3%A9" << "q" << "R%C3%A9sum%C3%A9";
}

void tst_QUrlQuery::encodedSetQueryItems()
{
    QFETCH(QString, key);
    QFETCH(QString, value);
    QFETCH(QString, expectedQuery);
    QFETCH(QString, expectedKey);
    QFETCH(QString, expectedValue);
    QFETCH(QUrl::ComponentFormattingOptions, encoding);
    QUrlQuery query;

    query.addQueryItem(key, value);
    COMPARE_ITEMS(query.queryItems(encoding), QueryItems() << qItem(expectedKey, expectedValue));
    QCOMPARE(query.query(encoding), expectedQuery);
}

void tst_QUrlQuery::encodedParsing_data()
{
    encodedSetQueryItems_data();
}

void tst_QUrlQuery::encodedParsing()
{
    QFETCH(QString, queryString);
    QFETCH(QString, expectedQuery);
    QFETCH(QString, expectedKey);
    QFETCH(QString, expectedValue);
    QFETCH(QUrl::ComponentFormattingOptions, encoding);

    QUrlQuery query(queryString);
    COMPARE_ITEMS(query.queryItems(encoding), QueryItems() << qItem(expectedKey, expectedValue));
    QCOMPARE(query.query(encoding), expectedQuery);
}

void tst_QUrlQuery::differentDelimiters()
{
    QUrlQuery query;
    query.setQueryDelimiters('(', ')');

    {
        // parse:
        query.setQuery("foo(bar)hello(world)");

        QueryItems expected;
        expected << qItem("foo", "bar") << qItem("hello", "world");
        COMPARE_ITEMS(query.queryItems(), expected);
        COMPARE_ITEMS(query.queryItems(QUrl::FullyEncoded), expected);
        COMPARE_ITEMS(query.queryItems(QUrl::PrettyDecoded), expected);
    }

    {
        // reconstruct:
        // note the final ')' is missing because there are no further items
        QCOMPARE(query.query(), QString("foo(bar)hello(world"));
    }

    {
        // set items containing the new delimiters and the old ones
        query.clear();
        query.addQueryItem("z(=)", "y(&)");
        QCOMPARE(query.query(), QString("z%28=%29(y%28&%29"));

        QUrlQuery copy = query;
        QCOMPARE(query.query(), QString("z%28=%29(y%28&%29"));

        copy.setQueryDelimiters(QUrlQuery::defaultQueryValueDelimiter(),
                                QUrlQuery::defaultQueryPairDelimiter());
        QCOMPARE(copy.query(), QString("z(%3D)=y(%26)"));
    }
}

void tst_QUrlQuery::old_queryItems()
{
    // test imported from old tst_qurl.cpp
    QUrlQuery url;

    QList<QPair<QString, QString> > newItems;
    newItems += qMakePair(QString("1"), QString("a"));
    newItems += qMakePair(QString("2"), QString("b"));
    newItems += qMakePair(QString("3"), QString("c"));
    newItems += qMakePair(QString("4"), QString("a b"));
    newItems += qMakePair(QString("5"), QString("&"));
    newItems += qMakePair(QString("foo bar"), QString("hello world"));
    newItems += qMakePair(QString("foo+bar"), QString("hello+world"));
    newItems += qMakePair(QString("tex"), QString("a + b = c"));
    url.setQueryItems(newItems);
    QVERIFY(!url.isEmpty());

    QList<QPair<QString, QString> > setItems = url.queryItems();
    QCOMPARE(newItems, setItems);

    url.addQueryItem("1", "z");

#if 0
    // undefined behaviour in the new QUrlQuery

    QVERIFY(url.hasQueryItem("1"));
    QCOMPARE(url.queryItemValue("1").toLatin1().constData(), "a");

    url.addQueryItem("1", "zz");

    QStringList expected;
    expected += "a";
    expected += "z";
    expected += "zz";
    QCOMPARE(url.allQueryItemValues("1"), expected);

    url.removeQueryItem("1");
    QCOMPARE(url.allQueryItemValues("1").size(), 2);
    QCOMPARE(url.queryItemValue("1").toLatin1().constData(), "z");
#endif

    url.removeAllQueryItems("1");
    QVERIFY(!url.hasQueryItem("1"));

    QCOMPARE(url.queryItemValue("4"), QLatin1String("a b"));
    QCOMPARE(url.queryItemValue("5"), QLatin1String("&"));
    QCOMPARE(url.queryItemValue("tex"), QLatin1String("a + b = c"));
    QCOMPARE(url.queryItemValue("foo bar"), QLatin1String("hello world"));

    //url.setUrl("http://www.google.com/search?q=a+b");
    url.setQuery("q=a+b");
    QCOMPARE(url.queryItemValue("q"), QLatin1String("a+b"));

    //url.setUrl("http://www.google.com/search?q=a=b"); // invalid, but should be tolerated
    url.setQuery("q=a=b");
    QCOMPARE(url.queryItemValue("q"), QLatin1String("a=b"));
}

void tst_QUrlQuery::old_hasQueryItem_data()
{
    QTest::addColumn<QString>("url");
    QTest::addColumn<QString>("item");
    QTest::addColumn<bool>("trueFalse");

    // the old tests started with "http://www.foo.bar"
    QTest::newRow("no query items") << "" << "baz" << false;
    QTest::newRow("query item: hello") << "hello=world" << "hello" << true;
    QTest::newRow("no query item: world") << "hello=world" << "world" << false;
    QTest::newRow("query item: qt") << "hello=world&qt=rocks" << "qt" << true;
}

void tst_QUrlQuery::old_hasQueryItem()
{
    QFETCH(QString, url);
    QFETCH(QString, item);
    QFETCH(bool, trueFalse);

    QCOMPARE(QUrlQuery(url).hasQueryItem(item), trueFalse);
}

#if 0
// this test doesn't make sense anymore
void tst_QUrl::removeAllEncodedQueryItems_data()
{
    QTest::addColumn<QUrl>("url");
    QTest::addColumn<QByteArray>("key");
    QTest::addColumn<QUrl>("result");

    QTest::newRow("test1") << QUrl::fromEncoded("http://qt-project.org/foo?aaa=a&bbb=b&ccc=c") << QByteArray("bbb") << QUrl::fromEncoded("http://qt-project.org/foo?aaa=a&ccc=c");
    QTest::newRow("test2") << QUrl::fromEncoded("http://qt-project.org/foo?aaa=a&bbb=b&ccc=c") << QByteArray("aaa") << QUrl::fromEncoded("http://qt-project.org/foo?bbb=b&ccc=c");
//    QTest::newRow("test3") << QUrl::fromEncoded("http://qt-project.org/foo?aaa=a&bbb=b&ccc=c") << QByteArray("ccc") << QUrl::fromEncoded("http://qt-project.org/foo?aaa=a&bbb=b");
    QTest::newRow("test4") << QUrl::fromEncoded("http://qt-project.org/foo?aaa=a&bbb=b&ccc=c") << QByteArray("b%62b") << QUrl::fromEncoded("http://qt-project.org/foo?aaa=a&bbb=b&ccc=c");
    QTest::newRow("test5") << QUrl::fromEncoded("http://qt-project.org/foo?aaa=a&b%62b=b&ccc=c") << QByteArray("b%62b") << QUrl::fromEncoded("http://qt-project.org/foo?aaa=a&ccc=c");
    QTest::newRow("test6") << QUrl::fromEncoded("http://qt-project.org/foo?aaa=a&b%62b=b&ccc=c") << QByteArray("bbb") << QUrl::fromEncoded("http://qt-project.org/foo?aaa=a&b%62b=b&ccc=c");
}

void tst_QUrl::removeAllEncodedQueryItems()
{
    QFETCH(QUrl, url);
    QFETCH(QByteArray, key);
    QFETCH(QUrl, result);
    url.removeAllEncodedQueryItems(key);
    QCOMPARE(url, result);
}
#endif

QTEST_APPLESS_MAIN(tst_QUrlQuery)

#include "tst_qurlquery.moc"
