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

#include <QtCore>
#include <QtTest>
#include <QtNetwork>
#include <QHostInfo>

#include <QtNetworkAuth/qoauth1.h>
#include <QtNetworkAuth/qoauth1signature.h>

#include <private/qoauth1_p.h>

#include "webserver.h"

Q_DECLARE_METATYPE(QNetworkAccessManager::Operation)
Q_DECLARE_METATYPE(QAbstractOAuth::Error)

// TODO: Test PUT and DELETE operations.
// TODO: Write tests to test errors.
// TODO: Remove common event loop

typedef QSharedPointer<QNetworkReply> QNetworkReplyPtr;

class tst_OAuth1 : public QObject
{
    Q_OBJECT

    using StringPair = QPair<QString, QString>;

    QEventLoop *loop = nullptr;
    enum RunSimpleRequestReturn { Timeout = 0, Success, Failure };
    int returnCode;

    using QObject::connect;
    static bool connect(const QNetworkReplyPtr &ptr,
                        const char *signal,
                        const QObject *receiver,
                        const char *slot,
                        Qt::ConnectionType ct = Qt::AutoConnection)
    {
        return connect(ptr.data(), signal, receiver, slot, ct);
    }
    bool connect(const QNetworkReplyPtr &ptr,
                 const char *signal,
                 const char *slot,
                 Qt::ConnectionType ct = Qt::AutoConnection)
    {
        return connect(ptr.data(), signal, slot, ct);
    }

public:
    int waitForFinish(QNetworkReplyPtr &reply);
    void fillParameters(QVariantMap *parameters, const QUrlQuery &query);

    template<class Type>
    struct PropertyTester
    {
        typedef Type InnerType;
        typedef void(QOAuth1::*ConstSignalType)(const Type &);
        typedef void(QOAuth1::*SignalType)(Type);
        typedef QList<std::function<void(Type *, QOAuth1 *object)>> SetterFunctions;

    private:
        // Each entry in setters should set its first parameter to an expected value
        // and act on its second, a QOAuth1 object, to trigger signal; this
        // function shall check that signal is passed the value the setter previously
        // told us to expect.
        template<class SignalType>
        static void runImpl(SignalType signal, const SetterFunctions &setters)
        {
            QOAuth1 obj;
            Type expectedValue;
            QSignalSpy spy(&obj, signal);
            connect(&obj, signal, [&](const Type &value) {
                QCOMPARE(expectedValue, value);
            });
            for (const auto &setter : setters) {
                const auto previous = expectedValue;
                setter(&expectedValue, &obj);
                QVERIFY(previous != expectedValue); // To check if the value was modified
            }
            QCOMPARE(spy.size(), setters.size()); // The signal should be emitted
        }

    public:

        static void run(ConstSignalType signal, const SetterFunctions &setters)
        {
            runImpl(signal, setters);
        }

        static void run(SignalType signal, const SetterFunctions &setters)
        {
            runImpl(signal, setters);
        }
    };

    QMultiMap<QString, QVariant> parseAuthorizationString(const QString &string)
    {
        const QString prefix = QStringLiteral("OAuth ");
        QMultiMap<QString, QVariant> ret;
        Q_ASSERT(string.startsWith(prefix));
        QRegularExpression rx("(?<key>.[^=]*)=\"(?<value>.[^\"]*)\",?");
        auto globalMatch = rx.globalMatch(string, prefix.size());
        while (globalMatch.hasNext()) {
            const auto match = globalMatch.next();
            auto key = match.captured("key");
            QString value = match.captured("value");
            value = QString::fromUtf8(QByteArray::fromPercentEncoding(value.toUtf8()));
            ret.insert(key, value);
        }
        return ret;
    }

public Q_SLOTS:
    void finished();
    void gotError();

private Q_SLOTS:
    void clientIdentifierSignal();
    void clientSharedSecretSignal();
    void tokenSignal();
    void tokenSecretSignal();
    void temporaryCredentialsUrlSignal();
    void temporaryTokenCredentialsUrlSignal();
    void tokenCredentialsUrlSignal();
    void signatureMethodSignal();

    void getToken_data();
    void getToken();

    void prepareRequestSignature_data();
    void prepareRequestSignature();

    void grant_data();
    void grant();

    void authenticatedCalls_data();
    void authenticatedCalls();

    void prepareRequestCalls_data();
    void prepareRequestCalls();

    void secondTemporaryToken();
};

const auto oauthVersion = QStringLiteral("oauth_version");
const auto oauthConsumerKey = QStringLiteral("oauth_consumer_key");
const auto oauthNonce = QStringLiteral("oauth_nonce");
const auto oauthSignatureMethod = QStringLiteral("oauth_signature_method");
const auto oauthTimestamp = QStringLiteral("oauth_timestamp");
const auto oauthToken = QStringLiteral("oauth_token");
const auto oauthSignature = QStringLiteral("oauth_signature");

bool hostReachable(const QLatin1String &host)
{
    // check host exists
    QHostInfo hostInfo = QHostInfo::fromName(host);
    if (hostInfo.error() != QHostInfo::NoError)
        return false;

    // try to connect to host
    QTcpSocket socket;
    socket.connectToHost(host, 80);
    if (!socket.waitForConnected(1000))
        return false;

    return true;
}

int tst_OAuth1::waitForFinish(QNetworkReplyPtr &reply)
{
    int count = 0;

    connect(reply, SIGNAL(finished()), SLOT(finished()));
    connect(reply, SIGNAL(errorOccurred(QNetworkReply::NetworkError)), SLOT(gotError()));
    returnCode = Success;
    loop = new QEventLoop;
    QSignalSpy spy(reply.data(), SIGNAL(downloadProgress(qint64,qint64)));
    while (!reply->isFinished()) {
        QTimer::singleShot(5000, loop, SLOT(quit()));
        if (loop->exec() == Timeout && count == spy.size() && !reply->isFinished()) {
            returnCode = Timeout;
            break;
        }
        count = spy.size();
    }
    delete loop;
    loop = nullptr;

    return returnCode;
}

void tst_OAuth1::fillParameters(QVariantMap *parameters, const QUrlQuery &query)
{
    const auto list = query.queryItems();
    for (auto it = list.begin(), end = list.end(); it != end; ++it)
        parameters->insert(it->first, it->second);
}

void tst_OAuth1::finished()
{
    if (loop)
        loop->exit(returnCode = Success);
}

void tst_OAuth1::gotError()
{
    if (loop)
        loop->exit(returnCode = Failure);
    disconnect(QObject::sender(), SIGNAL(finished()), this, nullptr);
}

void tst_OAuth1::clientIdentifierSignal()
{
    using PropertyTester = PropertyTester<QString>;
    PropertyTester::SetterFunctions setters {
        [](QString *expectedValue, QOAuth1 *object) {
            *expectedValue = "setClientIdentifier";
            object->setClientIdentifier(*expectedValue);
        },
        [](QString *expectedValue, QOAuth1 *object) {
            *expectedValue = "setClientCredentials";
            object->setClientCredentials(qMakePair(*expectedValue, QString()));
        }
    };
    PropertyTester::run(&QOAuth1::clientIdentifierChanged, setters);
}

void tst_OAuth1::clientSharedSecretSignal()
{
    using PropertyTester = PropertyTester<QString>;
    PropertyTester::SetterFunctions setters {
        [](QString *expectedValue, QOAuth1 *object) {
            *expectedValue = "setClientSharedSecret";
            object->setClientSharedSecret(*expectedValue);
        },
        [](QString *expectedValue, QOAuth1 *object) {
            *expectedValue = "setClientCredentials";
            object->setClientCredentials(qMakePair(QString(), *expectedValue));
        }
    };
    PropertyTester::run(&QOAuth1::clientSharedSecretChanged, setters);
}

void tst_OAuth1::tokenSignal()
{
    using PropertyTester = PropertyTester<QString>;
    PropertyTester::SetterFunctions setters {
        [](QString *expectedValue, QOAuth1 *object) {
            *expectedValue = "setToken";
            object->setToken(*expectedValue);
        },
        [](QString *expectedValue, QOAuth1 *object) {
            *expectedValue = "setTokenCredentials";
            object->setTokenCredentials(qMakePair(*expectedValue, QString()));
        }
    };
    PropertyTester::run(&QOAuth1::tokenChanged, setters);
}

void tst_OAuth1::tokenSecretSignal()
{
    using PropertyTester = PropertyTester<QString>;
    PropertyTester::SetterFunctions setters {
        [](QString *expectedValue, QOAuth1 *object) {
            *expectedValue = "setTokenSecret";
            object->setTokenSecret(*expectedValue);
        },
        [](QString *expectedValue, QOAuth1 *object) {
            *expectedValue = "setTokenCredentials";
            object->setTokenCredentials(qMakePair(QString(), *expectedValue));
        }
    };
    PropertyTester::run(&QOAuth1::tokenSecretChanged, setters);
}

void tst_OAuth1::temporaryCredentialsUrlSignal()
{
    using PropertyTester = PropertyTester<QUrl>;
    PropertyTester::SetterFunctions setters {
        [](QUrl *expectedValue, QOAuth1 *object) {
            *expectedValue = QUrl("http://example.net/");
            object->setTemporaryCredentialsUrl(*expectedValue);
        }
    };
    PropertyTester::run(&QOAuth1::temporaryCredentialsUrlChanged, setters);
}

void tst_OAuth1::temporaryTokenCredentialsUrlSignal()
{
    using PropertyTester = PropertyTester<QUrl>;
    PropertyTester::SetterFunctions setters {
        [](QUrl *expectedValue, QOAuth1 *object) {
            *expectedValue = QUrl("http://example.net/");
            object->setTemporaryCredentialsUrl(*expectedValue);
        }
    };
    PropertyTester::run(&QOAuth1::temporaryCredentialsUrlChanged, setters);
}

void tst_OAuth1::tokenCredentialsUrlSignal()
{
    using PropertyTester = PropertyTester<QUrl>;
    PropertyTester::SetterFunctions setters {
        [](QUrl *expectedValue, QOAuth1 *object) {
            *expectedValue = QUrl("http://example.net/");
            object->setTokenCredentialsUrl(*expectedValue);
        }
    };
    PropertyTester::run(&QOAuth1::tokenCredentialsUrlChanged, setters);
}

void tst_OAuth1::signatureMethodSignal()
{
    using PropertyTester = PropertyTester<QOAuth1::SignatureMethod>;
    PropertyTester::SetterFunctions setters {
        [](PropertyTester::InnerType *expectedValue, QOAuth1 *object) {
            QVERIFY(object->signatureMethod() != QOAuth1::SignatureMethod::PlainText);
            *expectedValue = QOAuth1::SignatureMethod::PlainText;
            object->setSignatureMethod(*expectedValue);
        }
    };
    PropertyTester::run(&QOAuth1::signatureMethodChanged, setters);
}

void tst_OAuth1::getToken_data()
{
    QTest::addColumn<StringPair>("clientCredentials");
    QTest::addColumn<StringPair>("token");
    QTest::addColumn<StringPair>("expectedToken");
    QTest::addColumn<QOAuth1::SignatureMethod>("signatureMethod");
    QTest::addColumn<QNetworkAccessManager::Operation>("requestType");

    const StringPair emptyCredentials;
    QTest::newRow("temporary_get_plainText")
            << qMakePair(QStringLiteral("key"), QStringLiteral("secret"))
            << emptyCredentials
            << qMakePair(QStringLiteral("requestkey"), QStringLiteral("requestsecret"))
            << QOAuth1::SignatureMethod::PlainText
            << QNetworkAccessManager::GetOperation;

    QTest::newRow("temporary_get_hmacSha1")
            << qMakePair(QStringLiteral("key"), QStringLiteral("secret"))
            << emptyCredentials
            << qMakePair(QStringLiteral("requestkey"), QStringLiteral("requestsecret"))
            << QOAuth1::SignatureMethod::Hmac_Sha1
            << QNetworkAccessManager::GetOperation;

    QTest::newRow("temporary_post_plainText")
            << qMakePair(QStringLiteral("key"), QStringLiteral("secret"))
            << emptyCredentials
            << qMakePair(QStringLiteral("requestkey"), QStringLiteral("requestsecret"))
            << QOAuth1::SignatureMethod::PlainText
            << QNetworkAccessManager::PostOperation;

    QTest::newRow("temporary_post_hmacSha1")
            << qMakePair(QStringLiteral("key"), QStringLiteral("secret"))
            << emptyCredentials
            << qMakePair(QStringLiteral("requestkey"), QStringLiteral("requestsecret"))
            << QOAuth1::SignatureMethod::Hmac_Sha1
            << QNetworkAccessManager::PostOperation;

    QTest::newRow("token_get_plainText")
            << qMakePair(QStringLiteral("key"), QStringLiteral("secret"))
            << qMakePair(QStringLiteral("requestkey"), QStringLiteral("requestsecret"))
            << qMakePair(QStringLiteral("accesskey"), QStringLiteral("accesssecret"))
            << QOAuth1::SignatureMethod::PlainText
            << QNetworkAccessManager::GetOperation;

    QTest::newRow("token_get_hmacSha1")
            << qMakePair(QStringLiteral("key"), QStringLiteral("secret"))
            << qMakePair(QStringLiteral("requestkey"), QStringLiteral("requestsecret"))
            << qMakePair(QStringLiteral("accesskey"), QStringLiteral("accesssecret"))
            << QOAuth1::SignatureMethod::Hmac_Sha1
            << QNetworkAccessManager::GetOperation;

    QTest::newRow("token_post_plainText")
            << qMakePair(QStringLiteral("key"), QStringLiteral("secret"))
            << qMakePair(QStringLiteral("requestkey"), QStringLiteral("requestsecret"))
            << qMakePair(QStringLiteral("accesskey"), QStringLiteral("accesssecret"))
            << QOAuth1::SignatureMethod::PlainText
            << QNetworkAccessManager::PostOperation;

    QTest::newRow("token_post_hmacSha1")
            << qMakePair(QStringLiteral("key"), QStringLiteral("secret"))
            << qMakePair(QStringLiteral("requestkey"), QStringLiteral("requestsecret"))
            << qMakePair(QStringLiteral("accesskey"), QStringLiteral("accesssecret"))
            << QOAuth1::SignatureMethod::Hmac_Sha1
            << QNetworkAccessManager::PostOperation;
}

void tst_OAuth1::getToken()
{
    QFETCH(StringPair, clientCredentials);
    QFETCH(StringPair, token);
    QFETCH(StringPair, expectedToken);
    QFETCH(QOAuth1::SignatureMethod, signatureMethod);
    QFETCH(QNetworkAccessManager::Operation, requestType);

    StringPair tokenReceived;
    QNetworkAccessManager networkAccessManager;
    QNetworkReplyPtr reply;
    QMultiMap<QString, QVariant> oauthHeaders;

    WebServer webServer([&](const WebServer::HttpRequest &request, QTcpSocket *socket) {
        oauthHeaders = parseAuthorizationString(request.headers["Authorization"]);
        const QString format = "oauth_token=%1&oauth_token_secret=%2";
        const QByteArray text = format.arg(expectedToken.first, expectedToken.second).toUtf8();
        const QByteArray replyMessage {
            "HTTP/1.0 200 OK\r\n"
            "Content-Type: application/x-www-form-urlencoded; charset=\"utf-8\"\r\n"
            "Content-Length: " + QByteArray::number(text.size()) + "\r\n\r\n"
            + text
        };
        socket->write(replyMessage);
    });

    struct OAuth1 : QOAuth1
    {
        OAuth1(QNetworkAccessManager *manager) : QOAuth1(manager) {}
        using QOAuth1::requestTokenCredentials;
    } o1(&networkAccessManager);
    const auto url = webServer.url(QStringLiteral("token"));

    o1.setSignatureMethod(signatureMethod);
    o1.setClientCredentials(clientCredentials.first, clientCredentials.second);
    o1.setTokenCredentials(token);
    o1.setTemporaryCredentialsUrl(url);
    QVariantMap parameters {{ "c2&a3", "c2=a3" }};
    reply.reset(o1.requestTokenCredentials(requestType, url, token, parameters));
    QVERIFY(!reply.isNull());
    connect(&o1, &QOAuth1::tokenChanged, [&tokenReceived](const QString &token){
        tokenReceived.first = token;
    });
    connect(&o1, &QOAuth1::tokenSecretChanged, [&tokenReceived](const QString &tokenSecret) {
        tokenReceived.second = tokenSecret;
    });
    QVERIFY(waitForFinish(reply) == Success);
    QCOMPARE(tokenReceived, expectedToken);
    QCOMPARE(oauthHeaders.value("oauth_consumer_key"), clientCredentials.first);
    QCOMPARE(oauthHeaders.value("oauth_version"), "1.0");
    QString expectedSignature;
    {
        QMultiMap<QString, QVariant> modifiedHeaders = oauthHeaders;
        modifiedHeaders.unite(QMultiMap<QString, QVariant>(parameters));
        modifiedHeaders.remove("oauth_signature");
        QOAuth1Signature signature(url,
                                   clientCredentials.second,
                                   token.second,
                                   static_cast<QOAuth1Signature::HttpRequestMethod>(requestType),
                                   modifiedHeaders);
        switch (signatureMethod) {
        case QOAuth1::SignatureMethod::PlainText:
            expectedSignature = signature.plainText();
            break;
        case QOAuth1::SignatureMethod::Hmac_Sha1:
            expectedSignature = signature.hmacSha1().toBase64();
            break;
        case QOAuth1::SignatureMethod::Rsa_Sha1:
            expectedSignature = signature.rsaSha1();
            break;
        }
    }
    QCOMPARE(oauthHeaders.value("oauth_signature"), expectedSignature);
}

void tst_OAuth1::prepareRequestSignature_data()
{
    QTest::addColumn<QString>("consumerKey");
    QTest::addColumn<QString>("consumerSecret");
    QTest::addColumn<QString>("accessKey");
    QTest::addColumn<QString>("accessKeySecret");
    QTest::addColumn<QNetworkRequest>("request");
    QTest::addColumn<QByteArray>("operation");
    QTest::addColumn<QByteArray>("body");
    QTest::addColumn<QVariantMap>("extraParams");

    QTest::newRow("get_simple")
            << "key"
            << "secret"
            << "accesskey"
            << "accesssecret"
            << QNetworkRequest(QUrl("http://term.ie/oauth/example/echo_api.php"))
            << QByteArray("GET")
            << QByteArray()
            << QVariantMap();

    QTest::newRow("get_params")
            << "key"
            << "secret"
            << "accesskey"
            << "accesssecret"
            << QNetworkRequest(QUrl("http://term.ie/oauth/example/echo_api.php?"
                                    "first=first&second=second"))
            << QByteArray("GET")
            << QByteArray()
            << QVariantMap();

    QNetworkRequest postRequest(QUrl("http://term.ie/oauth/example/echo_api.php"));
    postRequest.setHeader(QNetworkRequest::ContentTypeHeader,
                          QByteArray("application/x-www-form-urlencoded"));
    QTest::newRow("post_params")
            << "key"
            << "secret"
            << "accesskey"
            << "accesssecret"
            << postRequest
            << QByteArray("POST")
            << QByteArray("first=first&second=second")
            << QVariantMap({
                               {"first", "first"},
                               {"second", "second"}
                           });

    QTest::newRow("patch_param")
            << "key"
            << "secret"
            << "accesskey"
            << "accesssecret"
            << QNetworkRequest(QUrl("http://term.ie/oauth/example/echo_api.php?"
                                    "first=first&second=second"))
            << QByteArray("PATCH")
            << QByteArray()
            << QVariantMap();
}

void tst_OAuth1::prepareRequestSignature()
{
    QFETCH(QString, consumerKey);
    QFETCH(QString, consumerSecret);
    QFETCH(QString, accessKey);
    QFETCH(QString, accessKeySecret);
    QFETCH(QNetworkRequest, request);
    QFETCH(QByteArray, operation);
    QFETCH(QByteArray, body);
    QFETCH(QVariantMap, extraParams);

    QOAuth1 o1;
    o1.setClientCredentials(consumerKey, consumerSecret);
    o1.setTokenCredentials(accessKey, accessKeySecret);

    o1.prepareRequest(&request, operation, body);

    // extract oauth parameters from the headers
    QMultiMap<QString, QVariant> authArgs;
    const auto authHeader = request.rawHeader("Authorization");
    QCOMPARE(authHeader.mid(0, 6), "OAuth ");
    const auto values = authHeader.mid(6).split(',');
    for (const auto &pair : values) {
        const auto argPair = pair.split('=');
        QCOMPARE(argPair.size(), 2);
        QCOMPARE(argPair[1].front(), '\"');
        QCOMPARE(argPair[1].back(), '\"');
        authArgs.insert(argPair[0], argPair[1].mid(1, argPair[1].size() - 2));
    }

    //compare known parameters
    QCOMPARE(authArgs.value(oauthConsumerKey).toByteArray(), consumerKey.toLatin1());
    QCOMPARE(authArgs.value(oauthToken).toByteArray(), accessKey.toLatin1());
    QCOMPARE(authArgs.value(oauthSignatureMethod).toByteArray(), QByteArray("HMAC-SHA1"));
    QCOMPARE(authArgs.value(oauthVersion).toByteArray(), QByteArray("1.0"));
    QVERIFY(authArgs.contains(oauthNonce));
    QVERIFY(authArgs.contains(oauthTimestamp));
    QVERIFY(authArgs.contains(oauthSignature));

    // verify the signature
    const auto sigString = QUrl::fromPercentEncoding(authArgs.take(oauthSignature)
                                                     .toByteArray()).toUtf8();

    authArgs.unite(QMultiMap<QString, QVariant>(extraParams));
    QOAuth1Signature signature(request.url(),
                               consumerSecret,
                               accessKeySecret,
                               QOAuth1Signature::HttpRequestMethod::Custom,
                               authArgs);
    signature.setCustomMethodString(operation);
    const auto signatureData = signature.hmacSha1();
    QCOMPARE(signatureData.toBase64(), sigString);
}

void tst_OAuth1::grant_data()
{
    QTest::addColumn<QString>("consumerKey");
    QTest::addColumn<QString>("consumerSecret");
    QTest::addColumn<QString>("requestToken");
    QTest::addColumn<QString>("requestTokenSecret");
    QTest::addColumn<QString>("accessToken");
    QTest::addColumn<QString>("accessTokenSecret");
    QTest::addColumn<QUrl>("requestTokenUrl");
    QTest::addColumn<QUrl>("accessTokenUrl");
    QTest::addColumn<QUrl>("authenticatedCallUrl");
    QTest::addColumn<QNetworkAccessManager::Operation>("requestType");

    if (hostReachable(QLatin1String("term.ie"))) {
        QTest::newRow("term.ie_get") << "key"
                                    << "secret"
                                    << "requestkey"
                                    << "requestsecret"
                                    << "accesskey"
                                    << "accesssecret"
                                    << QUrl("http://term.ie/oauth/example/request_token.php")
                                    << QUrl("http://term.ie/oauth/example/access_token.php")
                                    << QUrl("http://term.ie/oauth/example/echo_api.php")
                                    << QNetworkAccessManager::GetOperation;
        QTest::newRow("term.ie_post") << "key"
                                    << "secret"
                                    << "requestkey"
                                    << "requestsecret"
                                    << "accesskey"
                                    << "accesssecret"
                                    << QUrl("http://term.ie/oauth/example/request_token.php")
                                    << QUrl("http://term.ie/oauth/example/access_token.php")
                                    << QUrl("http://term.ie/oauth/example/echo_api.php")
                                    << QNetworkAccessManager::PostOperation;
    } else {
        QSKIP("Skipping test due to unreachable term.ie host");
    }
}

void tst_OAuth1::grant()
{
    QFETCH(QString, consumerKey);
    QFETCH(QString, consumerSecret);
    QFETCH(QString, requestToken);
    QFETCH(QString, requestTokenSecret);
    QFETCH(QString, accessToken);
    QFETCH(QString, accessTokenSecret);
    QFETCH(QUrl, requestTokenUrl);
    QFETCH(QUrl, accessTokenUrl);

    bool tokenReceived = false;
    QNetworkAccessManager networkAccessManager;

    QOAuth1 o1(&networkAccessManager);

    {
        QSignalSpy clientIdentifierSpy(&o1, &QOAuth1::clientIdentifierChanged);
        QSignalSpy clientSharedSecretSpy(&o1, &QOAuth1::clientSharedSecretChanged);
        o1.setClientCredentials(consumerKey, consumerSecret);
        QCOMPARE(clientIdentifierSpy.size(), 1);
        QCOMPARE(clientSharedSecretSpy.size(), 1);
    }
    {
        QSignalSpy spy(&o1, &QOAuth1::temporaryCredentialsUrlChanged);
        o1.setTemporaryCredentialsUrl(requestTokenUrl);
        QCOMPARE(spy.size(), 1);
    }
    {
        QSignalSpy spy(&o1, &QOAuth1::tokenCredentialsUrlChanged);
        o1.setTokenCredentialsUrl(accessTokenUrl);
        QCOMPARE(spy.size(), 1);
    }
    connect(&o1, &QAbstractOAuth::statusChanged, [&](QAbstractOAuth::Status status) {
        if (status == QAbstractOAuth::Status::TemporaryCredentialsReceived) {
            if (!requestToken.isEmpty())
                QCOMPARE(requestToken, o1.tokenCredentials().first);
            if (!requestTokenSecret.isEmpty())
                QCOMPARE(requestTokenSecret, o1.tokenCredentials().second);
            tokenReceived = true;
        } else if (status == QAbstractOAuth::Status::Granted) {
            if (!accessToken.isEmpty())
                QCOMPARE(accessToken, o1.tokenCredentials().first);
            if (!accessTokenSecret.isEmpty())
                QCOMPARE(accessTokenSecret, o1.tokenCredentials().second);
            tokenReceived = true;
        }
    });

    QEventLoop eventLoop;

    QSignalSpy grantSignalSpy(&o1, &QOAuth1::granted);
    QTimer::singleShot(10000, &eventLoop, &QEventLoop::quit);
    connect(&o1, &QOAuth1::granted, &eventLoop, &QEventLoop::quit);
    o1.grant();
    eventLoop.exec();
    QVERIFY(tokenReceived);
    QCOMPARE(grantSignalSpy.size(), 1);
    QCOMPARE(o1.status(), QAbstractOAuth::Status::Granted);
}

void tst_OAuth1::authenticatedCalls_data()
{
    QTest::addColumn<QString>("consumerKey");
    QTest::addColumn<QString>("consumerSecret");
    QTest::addColumn<QString>("accessKey");
    QTest::addColumn<QString>("accessKeySecret");
    QTest::addColumn<QUrl>("url");
    QTest::addColumn<QVariantMap>("parameters");
    QTest::addColumn<QNetworkAccessManager::Operation>("operation");

    const QVariantMap parameters { { QStringLiteral("first"), QStringLiteral("first") },
                                   { QStringLiteral("second"), QStringLiteral("second") },
                                   { QStringLiteral("third"), QStringLiteral("third") },
                                   { QStringLiteral("c2&a3"), QStringLiteral("2=%$&@q") }
                                 };

    if (hostReachable(QLatin1String("term.ie"))) {
        QTest::newRow("term.ie_get") << "key"
                                    << "secret"
                                    << "accesskey"
                                    << "accesssecret"
                                    << QUrl("http://term.ie/oauth/example/echo_api.php")
                                    << parameters
                                    << QNetworkAccessManager::GetOperation;
        QTest::newRow("term.ie_post") << "key"
                                    << "secret"
                                    << "accesskey"
                                    << "accesssecret"
                                    << QUrl("http://term.ie/oauth/example/echo_api.php")
                                    << parameters
                                    << QNetworkAccessManager::PostOperation;
        QTest::newRow("term.ie_percent_encoded_query")
            << "key"
            << "secret"
            << "accesskey"
            << "accesssecret"
            << QUrl("http://term.ie/oauth/example/echo_api.php?key=%40value+1%2B2=3")
            << parameters
            << QNetworkAccessManager::GetOperation;
    } else {
        QSKIP("Skipping test due to unreachable term.ie host");
    }
}

void tst_OAuth1::authenticatedCalls()
{
    QFETCH(QString, consumerKey);
    QFETCH(QString, consumerSecret);
    QFETCH(QString, accessKey);
    QFETCH(QString, accessKeySecret);
    QFETCH(QUrl, url);
    QFETCH(QVariantMap, parameters);
    QFETCH(QNetworkAccessManager::Operation, operation);

    QNetworkAccessManager networkAccessManager;
    QNetworkReplyPtr reply;
    QString receivedData;
    QString parametersString;
    {
        if (url.hasQuery()) {
            parametersString = url.query(QUrl::FullyDecoded);
            if (!parameters.empty())
                parametersString.append(QLatin1Char('&'));
        }
        bool first = true;
        for (auto it = parameters.begin(), end = parameters.end(); it != end; ++it) {
            if (first)
                first = false;
            else
                parametersString += QLatin1Char('&');
            parametersString += it.key() + QLatin1Char('=') + it.value().toString();
        }
    }

    QOAuth1 o1(&networkAccessManager);
    o1.setClientCredentials(consumerKey, consumerSecret);
    o1.setTokenCredentials(accessKey, accessKeySecret);
    if (operation == QNetworkAccessManager::GetOperation)
        reply.reset(o1.get(url, parameters));
    else if (operation == QNetworkAccessManager::PostOperation)
        reply.reset(o1.post(url, parameters));
    QVERIFY(!reply.isNull());
    QVERIFY(!reply->isFinished());

    connect(&networkAccessManager, &QNetworkAccessManager::finished,
            [&](QNetworkReply *reply) {
        QByteArray data = reply->readAll();
        QUrlQuery query(QString::fromUtf8(data));
        receivedData = query.toString(QUrl::FullyDecoded);
    });
    QVERIFY(waitForFinish(reply) == Success);
    QCOMPARE(receivedData, parametersString);
    reply.clear();
}

void tst_OAuth1::prepareRequestCalls_data()
{
    QTest::addColumn<QString>("consumerKey");
    QTest::addColumn<QString>("consumerSecret");
    QTest::addColumn<QString>("accessKey");
    QTest::addColumn<QString>("accessKeySecret");
    QTest::addColumn<QUrl>("url");
    QTest::addColumn<QVariantMap>("parameters");
    QTest::addColumn<QByteArray>("operation");

    const QVariantMap parameters { { QStringLiteral("first"), QStringLiteral("first") },
                                   { QStringLiteral("second"), QStringLiteral("second") },
                                   { QStringLiteral("third"), QStringLiteral("third") },
                                   { QStringLiteral("c2&a3"), QStringLiteral("2=%$&@q") }
                                 };

    if (hostReachable(QLatin1String("term.ie"))) {
        QTest::newRow("term.ie_get") << "key"
                                     << "secret"
                                     << "accesskey"
                                     << "accesssecret"
                                     << QUrl("http://term.ie/oauth/example/echo_api.php")
                                     << parameters
                                     << QByteArray("GET");
        QTest::newRow("term.ie_post") << "key"
                                      << "secret"
                                      << "accesskey"
                                      << "accesssecret"
                                      << QUrl("http://term.ie/oauth/example/echo_api.php")
                                      << parameters
                                      << QByteArray("POST");
        QTest::newRow("term.ie_percent_encoded_query")
            << "key"
            << "secret"
            << "accesskey"
            << "accesssecret"
            << QUrl("http://term.ie/oauth/example/echo_api.php?key=%40value+1%2B2=3")
            << parameters
            << QByteArray("GET");
    } else {
        QSKIP("Skipping test due to unreachable term.ie host");
    }
}

void tst_OAuth1::prepareRequestCalls()
{
    QFETCH(QString, consumerKey);
    QFETCH(QString, consumerSecret);
    QFETCH(QString, accessKey);
    QFETCH(QString, accessKeySecret);
    QFETCH(QUrl, url);
    QFETCH(QVariantMap, parameters);
    QFETCH(QByteArray, operation);

    QNetworkAccessManager networkAccessManager;
    QNetworkReplyPtr reply;
    QString receivedData;
    QString parametersString;
    {
        if (url.hasQuery()) {
            parametersString = url.query(QUrl::FullyDecoded);
            if (!parameters.empty())
                parametersString.append(QLatin1Char('&'));
        }
        bool first = true;
        for (auto it = parameters.begin(), end = parameters.end(); it != end; ++it) {
            if (first)
                first = false;
            else
                parametersString += QLatin1Char('&');
            parametersString += it.key() + QLatin1Char('=') + it.value().toString();
        }
    }

    QOAuth1 o1(&networkAccessManager);
    o1.setClientCredentials(consumerKey, consumerSecret);
    o1.setTokenCredentials(accessKey, accessKeySecret);

    if (operation != "POST") {
        QUrlQuery query(url.query());
        for (auto it = parameters.begin(), end = parameters.end(); it != end; ++it)
            query.addQueryItem(it.key(), it.value().toString());
        url.setQuery(query);
    }
    QNetworkRequest request(url);
    QByteArray body;
    if (operation == "POST") {
        QUrlQuery query(url.query());
        for (auto it = parameters.begin(), end = parameters.end(); it != end; ++it)
            query.addQueryItem(it.key(), it.value().toString());
        body = query.toString().toUtf8();
        request.setHeader(QNetworkRequest::ContentTypeHeader,
                          QByteArray("application/x-www-form-urlencoded"));
    }

    o1.prepareRequest(&request, operation, body);
    if (body.isEmpty())
        reply.reset(o1.networkAccessManager()->sendCustomRequest(request, operation));
    else
        reply.reset(o1.networkAccessManager()->sendCustomRequest(request, operation, body));
    QVERIFY(!reply.isNull());
    QVERIFY(!reply->isFinished());

    connect(&networkAccessManager, &QNetworkAccessManager::finished,
            [&](QNetworkReply *reply) {
        QByteArray data = reply->readAll();
        QUrlQuery query(QString::fromUtf8(data));
        receivedData = query.toString(QUrl::FullyDecoded);
    });
    QVERIFY(waitForFinish(reply) == Success);
    QCOMPARE(receivedData, parametersString);
    reply.clear();
}

void tst_OAuth1::secondTemporaryToken()
{
    QNetworkAccessManager networkAccessManager;

    const StringPair expectedToken(qMakePair(QStringLiteral("temporaryKey"), QStringLiteral("temporaryToken")));
    WebServer webServer([&](const WebServer::HttpRequest &request, QTcpSocket *socket) {
        Q_UNUSED(request);
        const QString format = "oauth_token=%1&oauth_token_secret=%2&oauth_callback_confirmed=true";
        const QByteArray text = format.arg(expectedToken.first, expectedToken.second).toUtf8();
        const QByteArray replyMessage {
            "HTTP/1.0 200 OK\r\n"
            "Content-Type: application/x-www-form-urlencoded; charset=\"utf-8\"\r\n"
            "Content-Length: " + QByteArray::number(text.size()) + "\r\n\r\n"
            + text
        };
        socket->write(replyMessage);
    });

    QOAuth1 o1(&networkAccessManager);

    StringPair clientCredentials = qMakePair(QStringLiteral("user"), QStringLiteral("passwd"));
    o1.setClientCredentials(clientCredentials);
    o1.setTemporaryCredentialsUrl(webServer.url(QStringLiteral("temporary")));
    o1.setAuthorizationUrl(webServer.url(QStringLiteral("authorization")));
    o1.setTokenCredentialsUrl(webServer.url(QStringLiteral("token")));

    StringPair tokenReceived;
    connect(&o1, &QOAuth1::tokenChanged, [&tokenReceived](const QString &token) {
        tokenReceived.first = token;
    });
    bool replyReceived = false;
    connect(&o1, &QOAuth1::tokenSecretChanged, [&tokenReceived, &replyReceived](const QString &tokenSecret) {
        tokenReceived.second = tokenSecret;
        replyReceived = true;
    });

    o1.grant();
    QTRY_VERIFY(replyReceived);

    QVERIFY(!tokenReceived.first.isEmpty());
    QVERIFY(!tokenReceived.second.isEmpty());
    QCOMPARE(o1.status(), QAbstractOAuth::Status::TemporaryCredentialsReceived);
    QCOMPARE(tokenReceived, expectedToken);

    replyReceived = false; // reset this so we can 'synchronize' on it again
    // Do the same request again, should end up in the same state!!
    o1.grant();
    QTRY_VERIFY(replyReceived);

    QVERIFY(!tokenReceived.first.isEmpty());
    QVERIFY(!tokenReceived.second.isEmpty());
    QCOMPARE(o1.status(), QAbstractOAuth::Status::TemporaryCredentialsReceived);
    QCOMPARE(tokenReceived, expectedToken);
}

QTEST_MAIN(tst_OAuth1)
#include "tst_oauth1.moc"
