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

#include <QDomDocument>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QTcpServer>
#include <QTcpSocket>
#include <QTimer>
#include <QElapsedTimer>
#include <QtDebug>
#include <QtTest/QtTest>
#include <QXmlDefaultHandler>
#include <QXmlInputSource>
#include <QXmlSimpleReader>

class tst_QXmlInputSource : public QObject
{
    Q_OBJECT

private slots:
    void reset() const;
    void resetSimplified() const;
    void waitForReadyIODevice() const;
    void inputFromSlowDevice() const;
};

/*!
  \internal
  \since 4.4

  See task 166278.
 */
void tst_QXmlInputSource::reset() const
{
    const QString input(QString::fromLatin1("<element attribute1='value1' attribute2='value2'/>"));

    QXmlSimpleReader reader;
    QXmlDefaultHandler handler;
    reader.setContentHandler(&handler);

    QXmlInputSource source;
    source.setData(input);

    QCOMPARE(source.data(), input);

    source.reset();
    QCOMPARE(source.data(), input);

    source.reset();
    QVERIFY(reader.parse(source));
    source.reset();
    QCOMPARE(source.data(), input);
}

/*!
  \internal
  \since 4.4

  See task 166278.
 */
void tst_QXmlInputSource::resetSimplified() const
{
    const QString input(QString::fromLatin1("<element/>"));

    QXmlSimpleReader reader;

    QXmlInputSource source;
    source.setData(input);

    QVERIFY(reader.parse(source));
    source.reset();
    QCOMPARE(source.data(), input);
}

class ServerAndClient : public QObject
{
    Q_OBJECT

public:
    ServerAndClient(QEventLoop &ev) : success(false)
                                    , eventLoop(ev)
                                    , bodyBytesRead(0)
                                    , bodyLength(-1)
                                    , isBody(false)
    {
        setObjectName("serverAndClient");
        tcpServer = new QTcpServer(this);
        connect(tcpServer, SIGNAL(newConnection()), this, SLOT(newConnection()));
        tcpServer->listen(QHostAddress::LocalHost, 1088);
        httpClient = new QNetworkAccessManager(this);
        connect(httpClient, SIGNAL(finished(QNetworkReply*)), SLOT(requestFinished(QNetworkReply*)));
    }

    bool success;
    QEventLoop &eventLoop;

public slots:
    void doIt()
    {
        QUrl url("http://127.0.0.1:1088");
        QNetworkRequest req(url);
        req.setRawHeader("POST", url.path().toLatin1());
        req.setRawHeader("user-agent", "xml-test");
        req.setRawHeader("keep-alive", "false");
        req.setRawHeader("host", url.host().toLatin1());

        QByteArray xmlrpc("<methodCall>\r\n\
                <methodName>SFD.GetVersion</methodName>\r\n\
                <params/>\r\n\
                </methodCall>");
        req.setHeader(QNetworkRequest::ContentLengthHeader, xmlrpc.size());
        req.setHeader(QNetworkRequest::ContentTypeHeader, "text/xml");

        httpClient->post(req, xmlrpc);
    }

    void requestFinished(QNetworkReply *reply)
    {
        QCOMPARE(reply->error(), QNetworkReply::NoError);
        reply->deleteLater();
    }

private slots:
    void newConnection()
    {
        QTcpSocket *const s = tcpServer->nextPendingConnection();

        if (s)
            connect(s, SIGNAL(readyRead()), this, SLOT(readyRead()));
    }

    void readyRead()
    {
        QTcpSocket *const s = static_cast<QTcpSocket *>(sender());

        while (s->bytesAvailable())
        {
            const QString line(s->readLine());

            if (line.startsWith("Content-Length:", Qt::CaseInsensitive))
                bodyLength = line.mid(15).toInt();

            if (isBody)
            {
                body.append(line.toUtf8());
                bodyBytesRead += line.size();
            }
            else if (line == "\r\n")
            {
                isBody = true;
                if (bodyLength == -1)
                {
                    qFatal("No length was specified in the header.");
                }
            }
        }

        if (bodyBytesRead == bodyLength)
        {
            QDomDocument domDoc;
            success = bool(domDoc.setContent(body));
            eventLoop.exit();
        }
    }

private:
    QByteArray body;
    int bodyBytesRead, bodyLength;
    bool isBody;
    QTcpServer *tcpServer;
    QNetworkAccessManager* httpClient;
};

void tst_QXmlInputSource::waitForReadyIODevice() const
{
    QEventLoop el;
    ServerAndClient sv(el);
    QTimer::singleShot(1, &sv, SLOT(doIt()));

    el.exec();
    QVERIFY(sv.success);
}

// This class is used to emulate a case where less than 4 bytes are sent in
// a single packet to ensure it is still parsed correctly
class SlowIODevice : public QIODevice
{
public:
    SlowIODevice(const QString &expectedData, QObject *parent = 0)
        : QIODevice(parent), currentPos(0), readyToSend(true)
    {
        stringData = expectedData.toUtf8();
        dataTimer = new QTimer(this);
        connect(dataTimer, &QTimer::timeout, this, [this] {
            readyToSend = true;
            emit readyRead();
            dataTimer->stop();
        });
        dataTimer->start(1000);
    }
    bool open(SlowIODevice::OpenMode) override
    {
        setOpenMode(ReadOnly);
        return true;
    }
    bool isSequential() const override
    {
        return true;
    }
    qint64 bytesAvailable() const override
    {
        if (readyToSend && stringData.size() != currentPos)
            return qMax(3, stringData.size() - currentPos);
        return 0;
    }
    qint64 readData(char *data, qint64 maxSize) override
    {
        if (!readyToSend)
            return 0;
        const qint64 readSize = qMin(qMin((qint64)3, maxSize), (qint64)(stringData.size() - currentPos));
        if (readSize > 0)
            memcpy(data, &stringData.constData()[currentPos], readSize);
        currentPos += readSize;
        readyToSend = false;
        if (currentPos != stringData.size())
            dataTimer->start(1000);
        return readSize;
    }
    qint64 writeData(const char *, qint64) override { return 0; }
    bool waitForReadyRead(int msecs) override
    {
        // Delibrately wait a maximum of 10 seconds for the sake
        // of the test, so it doesn't unduly hang
        const int waitTime = qMax(10000, msecs);
        QElapsedTimer t;
        t.start();
        while (t.elapsed() < waitTime) {
            QCoreApplication::processEvents();
            if (readyToSend)
                return true;
        }
        return false;
    }
private:
    QByteArray stringData;
    int currentPos;
    bool readyToSend;
    QTimer *dataTimer;
};

void tst_QXmlInputSource::inputFromSlowDevice() const
{
    QString expectedData = QStringLiteral("<foo><bar>kake</bar><bar>ja</bar></foo>");
    SlowIODevice slowDevice(expectedData);
    QXmlInputSource source(&slowDevice);
    QString data;
    while (true) {
        const QChar nextChar = source.next();
        if (nextChar == QXmlInputSource::EndOfDocument)
            break;
        else if (nextChar != QXmlInputSource::EndOfData)
            data += nextChar;
    }
    QCOMPARE(data, expectedData);
}

QTEST_MAIN(tst_QXmlInputSource)
#include "tst_qxmlinputsource.moc"
