/*
 *    This software is in the public domain, furnished "as is", without technical
 *    support, and with no warranty, express or implied, as to its usefulness for
 *    any purpose.
 *
 */

#include <QtTest/QtTest>
#include <QDesktopServices>

#include "gui/creds/oauth.h"
#include "syncenginetestutils.h"
#include "theme.h"
#include "common/asserts.h"

using namespace OCC;

class DesktopServiceHook : public QObject
{
    Q_OBJECT
signals:
    void hooked(const QUrl &);
public:
    DesktopServiceHook() { QDesktopServices::setUrlHandler("oauthtest", this, "hooked"); }
};

static const QUrl sOAuthTestServer("oauthtest://someserver/owncloud");


class FakePostReply : public QNetworkReply
{
    Q_OBJECT
public:
    std::unique_ptr<QIODevice> payload;
    bool aborted = false;
    bool redirectToPolicy = false;
    bool redirectToToken = false;

    FakePostReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request,
                  std::unique_ptr<QIODevice> payload_, QObject *parent)
        : QNetworkReply{parent}, payload{std::move(payload_)}
    {
        setRequest(request);
        setUrl(request.url());
        setOperation(op);
        open(QIODevice::ReadOnly);
        payload->open(QIODevice::ReadOnly);
        QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
    }

    Q_INVOKABLE virtual void respond() {
        if (aborted) {
            setError(OperationCanceledError, "Operation Canceled");
            emit metaDataChanged();
            emit finished();
            return;
        } else if (redirectToPolicy) {
            setHeader(QNetworkRequest::LocationHeader, "/my.policy");
            setAttribute(QNetworkRequest::RedirectionTargetAttribute, "/my.policy");
            setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 302); // 302 might or might not lose POST data in rfc
            setHeader(QNetworkRequest::ContentLengthHeader, 0);
            emit metaDataChanged();
            emit finished();
            return;
        } else if (redirectToToken) {
            // Redirect to self
            QVariant destination = QVariant(sOAuthTestServer.toString()+QLatin1String("/index.php/apps/oauth2/api/v1/token"));
            setHeader(QNetworkRequest::LocationHeader, destination);
            setAttribute(QNetworkRequest::RedirectionTargetAttribute, destination);
            setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 307); // 307 explicitly in rfc says to not lose POST data
            setHeader(QNetworkRequest::ContentLengthHeader, 0);
            emit metaDataChanged();
            emit finished();
            return;
        }
        setHeader(QNetworkRequest::ContentLengthHeader, payload->size());
        setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
        emit metaDataChanged();
        if (bytesAvailable())
            emit readyRead();
        emit finished();
    }

    void abort() override {
        aborted = true;
    }
    qint64 bytesAvailable() const override {
        if (aborted)
            return 0;
        return payload->bytesAvailable();
    }

    qint64 readData(char *data, qint64 maxlen) override {
        return payload->read(data, maxlen);
    }
};

// Reply with a small delay
class SlowFakePostReply : public FakePostReply {
    Q_OBJECT
public:
    using FakePostReply::FakePostReply;
    void respond() override {
        // override of FakePostReply::respond, will call the real one with a delay.
        QTimer::singleShot(100, this, [this] { this->FakePostReply::respond(); });
    }
};


class OAuthTestCase : public QObject
{
    Q_OBJECT
    DesktopServiceHook desktopServiceHook;
public:
    enum State { StartState, BrowserOpened, TokenAsked, CustomState } state = StartState;
    Q_ENUM(State);
    bool replyToBrowserOk = false;
    bool gotAuthOk = false;
    virtual bool done() const { return replyToBrowserOk && gotAuthOk; }

    FakeQNAM *fakeQnam = nullptr;
    QNetworkAccessManager realQNAM;
    QPointer<QNetworkReply> browserReply = nullptr;
    QString code = generateEtag();
    OCC::AccountPtr account;

    QScopedPointer<OAuth> oauth;

    virtual void test() {
        fakeQnam = new FakeQNAM({});
        account = OCC::Account::create();
        account->setUrl(sOAuthTestServer);
        account->setCredentials(new FakeCredentials{fakeQnam});
        fakeQnam->setParent(this);
        fakeQnam->setOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
            ASSERT(device);
            ASSERT(device->bytesAvailable()>0); // OAuth2 always sends around POST data.
            return this->tokenReply(op, req);
        });

        QObject::connect(&desktopServiceHook, &DesktopServiceHook::hooked,
                         this, &OAuthTestCase::openBrowserHook);

        oauth.reset(new OAuth(account.data(), nullptr));
        QObject::connect(oauth.data(), &OAuth::result, this, &OAuthTestCase::oauthResult);
        oauth->start();
        QTRY_VERIFY(done());
    }

    virtual void openBrowserHook(const QUrl &url) {
        QCOMPARE(state, StartState);
        state = BrowserOpened;
        QCOMPARE(url.path(), QString(sOAuthTestServer.path() + "/index.php/apps/oauth2/authorize"));
        QVERIFY(url.toString().startsWith(sOAuthTestServer.toString()));
        QUrlQuery query(url);
        QCOMPARE(query.queryItemValue(QLatin1String("response_type")), QLatin1String("code"));
        QCOMPARE(query.queryItemValue(QLatin1String("client_id")), Theme::instance()->oauthClientId());
        QUrl redirectUri(query.queryItemValue(QLatin1String("redirect_uri")));
        QCOMPARE(redirectUri.host(), QLatin1String("localhost"));
        redirectUri.setQuery("code=" + code);
        createBrowserReply(QNetworkRequest(redirectUri));
    }

    virtual QNetworkReply *createBrowserReply(const QNetworkRequest &request) {
        browserReply = realQNAM.get(request);
        QObject::connect(browserReply, &QNetworkReply::finished, this, &OAuthTestCase::browserReplyFinished);
        return browserReply;
    }

    virtual void browserReplyFinished() {
        QCOMPARE(sender(), browserReply.data());
        QCOMPARE(state, TokenAsked);
        browserReply->deleteLater();
        QCOMPARE(browserReply->rawHeader("Location"), QByteArray("owncloud://success"));
        replyToBrowserOk = true;
    };

    virtual QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req)
    {
        ASSERT(state == BrowserOpened);
        state = TokenAsked;
        ASSERT(op == QNetworkAccessManager::PostOperation);
        ASSERT(req.url().toString().startsWith(sOAuthTestServer.toString()));
        ASSERT(req.url().path() == sOAuthTestServer.path() + "/index.php/apps/oauth2/api/v1/token");
        std::unique_ptr<QBuffer> payload(new QBuffer());
        payload->setData(tokenReplyPayload());
        return new FakePostReply(op, req, std::move(payload), fakeQnam);
    }

    virtual QByteArray tokenReplyPayload() const {
        QJsonDocument jsondata(QJsonObject{
                { "access_token", "123" },
                { "refresh_token" , "456" },
                { "message_url",  "owncloud://success"},
                { "user_id", "789" },
                { "token_type", "Bearer" }
        });
        return jsondata.toJson();
    }

    virtual void oauthResult(OAuth::Result result, const QString &user, const QString &token , const QString &refreshToken) {
        QCOMPARE(state, TokenAsked);
        QCOMPARE(result, OAuth::LoggedIn);
        QCOMPARE(user, QString("789"));
        QCOMPARE(token, QString("123"));
        QCOMPARE(refreshToken, QString("456"));
        gotAuthOk = true;
    }
};

class TestOAuth: public QObject
{
    Q_OBJECT

private slots:
    void testBasic()
    {
        OAuthTestCase test;
        test.test();
    }

    // Test for https://github.com/owncloud/client/pull/6057
    void testCloseBrowserDontCrash()
    {
        struct Test : OAuthTestCase {
            QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest & req) override
            {
                ASSERT(browserReply);
                // simulate the fact that the browser is closing the connection
                browserReply->abort();
                QCoreApplication::processEvents();

                ASSERT(state == BrowserOpened);
                state = TokenAsked;

                std::unique_ptr<QBuffer> payload(new QBuffer);
                payload->setData(tokenReplyPayload());
                return new SlowFakePostReply(op, req, std::move(payload), fakeQnam);
            }

            void browserReplyFinished() override
            {
                QCOMPARE(sender(), browserReply.data());
                QCOMPARE(browserReply->error(), QNetworkReply::OperationCanceledError);
                replyToBrowserOk = true;
            }
        } test;
        test.test();
    }

    void testRandomConnections()
    {
        // Test that we can send random garbage to the litening socket and it does not prevent the connection
        struct Test : OAuthTestCase {
            virtual QNetworkReply *createBrowserReply(const QNetworkRequest &request) override {
                QTimer::singleShot(0, this, [this, request] {
                    auto port = request.url().port();
                    state = CustomState;
                    QVector<QByteArray> payloads = {
                        "GET FOFOFO HTTP 1/1\n\n",
                        "GET /?code=invalie HTTP 1/1\n\n",
                        "GET /?code=xxxxx&bar=fff",
                        QByteArray("\0\0\0", 3),
                        QByteArray("GET \0\0\0 \n\n\n\n\n\0", 14),
                        QByteArray("GET /?code=éléphant\xa5 HTTP\n"),
                        QByteArray("\n\n\n\n"),
                    };
                    foreach (const auto &x, payloads) {
                        auto socket = new QTcpSocket(this);
                        socket->connectToHost("localhost", port);
                        QVERIFY(socket->waitForConnected());
                        socket->write(x);
                    }

                    // Do the actual request a bit later
                    QTimer::singleShot(100, this, [this, request] {
                        QCOMPARE(state, CustomState);
                        state = BrowserOpened;
                        this->OAuthTestCase::createBrowserReply(request);
                    });
               });
               return nullptr;
            }

            QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest &req) override
            {
                if (state == CustomState)
                    return new FakeErrorReply{op, req, this, 500};
                return OAuthTestCase::tokenReply(op, req);
            }

            void oauthResult(OAuth::Result result, const QString &user, const QString &token ,
                             const QString &refreshToken) override {
                if (state != CustomState)
                    return OAuthTestCase::oauthResult(result, user, token, refreshToken);
                QCOMPARE(result, OAuth::Error);
            }
        } test;
        test.test();
    }

    void testTokenUrlHasRedirect()
    {
        struct Test : OAuthTestCase {
            int redirectsDone = 0;
            QNetworkReply *tokenReply(QNetworkAccessManager::Operation op, const QNetworkRequest & request) override
            {
                ASSERT(browserReply);
                // Kind of reproduces what we had in https://github.com/owncloud/enterprise/issues/2951 (not 1:1)
                if (redirectsDone == 0) {
                    std::unique_ptr<QBuffer> payload(new QBuffer());
                    payload->setData("");
                    SlowFakePostReply *reply = new SlowFakePostReply(op, request, std::move(payload), this);
                    reply->redirectToPolicy = true;
                    redirectsDone++;
                    return reply;
                } else if  (redirectsDone == 1) {
                    std::unique_ptr<QBuffer> payload(new QBuffer());
                    payload->setData("");
                    SlowFakePostReply *reply = new SlowFakePostReply(op, request, std::move(payload), this);
                    reply->redirectToToken = true;
                    redirectsDone++;
                    return reply;
                } else {
                    // ^^ This is with a custom reply and not actually HTTP, so we're testing the HTTP redirect code
                    // we have in AbstractNetworkJob::slotFinished()
                    redirectsDone++;
                    return OAuthTestCase::tokenReply(op, request);
                }
            }
        } test;
        test.test();
    }
};

QTEST_GUILESS_MAIN(TestOAuth)
#include "testoauth.moc"
