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

#include "viewtestutils_p.h"

#include <QtCore/QRandomGenerator>
#include <QtCore/QTimer>
#include <QtQuick/QQuickView>
#include <QtQuick/QQuickView>
#include <QtGui/QScreen>
#include <QtGui/qpa/qwindowsysteminterface.h>
#include <QtGui/private/qhighdpiscaling_p.h>

#include <QtTest/QTest>

#include <QtQuick/private/qquickdeliveryagent_p_p.h>
#if QT_CONFIG(quick_itemview)
#include <QtQuick/private/qquickitemview_p_p.h>
#endif
#include <QtQuick/private/qquickwindow_p.h>

#include <QtQuickTestUtils/private/visualtestutils_p.h>

QT_BEGIN_NAMESPACE

QQuickView *QQuickViewTestUtils::createView()
{
    QQuickView *window = new QQuickView(0);
    const QSize size(240, 320);
    window->resize(size);
    QQuickViewTestUtils::centerOnScreen(window, size);
    return window;
}

void QQuickViewTestUtils::centerOnScreen(QQuickWindow *window, const QSize &size)
{
    const QRect screenGeometry = window->screen()->availableGeometry();
    const QPoint offset = QPoint(size.width() / 2, size.height() / 2);
    window->setFramePosition(screenGeometry.center() - offset);
}

void QQuickViewTestUtils::centerOnScreen(QQuickWindow *window)
{
    QQuickViewTestUtils::centerOnScreen(window, window->size());
}

void QQuickViewTestUtils::moveMouseAway(QQuickView *window)
{
#if QT_CONFIG(cursor) // Get the cursor out of the way.
    QCursor::setPos(window->geometry().topRight() + QPoint(100, 100));
#else
    Q_UNUSED(window);
#endif
}

QList<int> QQuickViewTestUtils::adjustIndexesForAddDisplaced(const QList<int> &indexes, int index, int count)
{
    QList<int> result;
    for (int i=0; i<indexes.size(); i++) {
        int num = indexes[i];
        if (num >= index) {
            num += count;
        }
        result << num;
    }
    return result;
}

QList<int> QQuickViewTestUtils::adjustIndexesForMove(const QList<int> &indexes, int from, int to, int count)
{
    QList<int> result;
    for (int i=0; i<indexes.size(); i++) {
        int num = indexes[i];
        if (from < to) {
            if (num >= from && num < from + count)
                num += (to - from); // target
            else if (num >= from && num < to + count)
                num -= count;   // displaced
        } else if (from > to) {
            if (num >= from && num < from + count)
                num -= (from - to);  // target
            else if (num >= to && num < from + count)
                num += count;   // displaced
        }
        result << num;
    }
    return result;
}

QList<int> QQuickViewTestUtils::adjustIndexesForRemoveDisplaced(const QList<int> &indexes, int index, int count)
{
    QList<int> result;
    for (int i=0; i<indexes.size(); i++) {
        int num = indexes[i];
        if (num >= index)
            num -= count;
        result << num;
    }
    return result;
}

QQuickViewTestUtils::QaimModel::QaimModel(QObject *parent)
    : QAbstractListModel(parent)
{
}

int QQuickViewTestUtils::QaimModel::rowCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return list.size();
}

int QQuickViewTestUtils::QaimModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return columns;
}

QHash<int,QByteArray> QQuickViewTestUtils::QaimModel::roleNames() const
{
    QHash<int,QByteArray> roles = QAbstractListModel::roleNames();
    roles.insert(Name, "name");
    roles.insert(Number, "number");
    return roles;
}

QVariant QQuickViewTestUtils::QaimModel::data(const QModelIndex &index, int role) const
{
    QVariant rv;
    if (role == Name)
        rv = list.at(index.row()).first;
    else if (role == Number)
        rv = list.at(index.row()).second;

    return rv;
}

int QQuickViewTestUtils::QaimModel::count() const
{
    return rowCount() * columnCount();
}

QString QQuickViewTestUtils::QaimModel::name(int index) const
{
    return list.at(index).first;
}

QString QQuickViewTestUtils::QaimModel::number(int index) const
{
    return list.at(index).second;
}

void QQuickViewTestUtils::QaimModel::addItem(const QString &name, const QString &number)
{
    emit beginInsertRows(QModelIndex(), list.size(), list.size());
    list.append(std::pair<QString,QString>(name, number));
    emit endInsertRows();
}

void QQuickViewTestUtils::QaimModel::addItems(const QList<std::pair<QString, QString> > &items)
{
    emit beginInsertRows(QModelIndex(), list.size(), list.size()+items.size()-1);
    for (int i=0; i<items.size(); i++)
        list.append(std::pair<QString,QString>(items[i].first, items[i].second));
    emit endInsertRows();
}

void QQuickViewTestUtils::QaimModel::insertItem(int index, const QString &name, const QString &number)
{
    emit beginInsertRows(QModelIndex(), index, index);
    list.insert(index, std::pair<QString,QString>(name, number));
    emit endInsertRows();
}

void QQuickViewTestUtils::QaimModel::insertItems(int index, const QList<std::pair<QString, QString> > &items)
{
    emit beginInsertRows(QModelIndex(), index, index+items.size()-1);
    for (int i=0; i<items.size(); i++)
        list.insert(index + i, std::pair<QString,QString>(items[i].first, items[i].second));
    emit endInsertRows();
}

void QQuickViewTestUtils::QaimModel::removeItem(int index)
{
    emit beginRemoveRows(QModelIndex(), index, index);
    list.removeAt(index);
    emit endRemoveRows();
}

void QQuickViewTestUtils::QaimModel::removeItems(int index, int count)
{
    emit beginRemoveRows(QModelIndex(), index, index+count-1);
    while (count--)
        list.removeAt(index);
    emit endRemoveRows();
}

void QQuickViewTestUtils::QaimModel::moveItem(int from, int to)
{
    emit beginMoveRows(QModelIndex(), from, from, QModelIndex(), to);
    list.move(from, to);
    emit endMoveRows();
}

void QQuickViewTestUtils::QaimModel::moveItems(int from, int to, int count)
{
    emit beginMoveRows(QModelIndex(), from, from+count-1, QModelIndex(), to > from ? to+count : to);
    qquickmodelviewstestutil_move(from, to, count, &list);
    emit endMoveRows();
}

void QQuickViewTestUtils::QaimModel::modifyItem(int idx, const QString &name, const QString &number)
{
    list[idx] = std::pair<QString,QString>(name, number);
    emit dataChanged(index(idx,0), index(idx,0));
}

void QQuickViewTestUtils::QaimModel::clear()
{
    int count = list.size();
    if (count > 0) {
        beginRemoveRows(QModelIndex(), 0, count-1);
        list.clear();
        endRemoveRows();
    }
}

void QQuickViewTestUtils::QaimModel::reset()
{
    emit beginResetModel();
    emit endResetModel();
}

void QQuickViewTestUtils::QaimModel::resetItems(const QList<std::pair<QString, QString> > &items)
{
    beginResetModel();
    list = items;
    endResetModel();
}

class ScopedPrintable
{
    Q_DISABLE_COPY_MOVE(ScopedPrintable)

public:
    ScopedPrintable(const QString &string) : data(QTest::toString(string)) {}
    ~ScopedPrintable() { delete[] data; }

    operator const char*() const { return data; }

private:
    const char *data;
};

void QQuickViewTestUtils::QaimModel::matchAgainst(const QList<std::pair<QString, QString> > &other, const QString &error1, const QString &error2) {
    for (int i=0; i<other.size(); i++) {
        QVERIFY2(list.contains(other[i]),
                 ScopedPrintable(other[i].first + QLatin1Char(' ') + other[i].second + QLatin1Char(' ') + error1));
    }
    for (int i=0; i<list.size(); i++) {
        QVERIFY2(other.contains(list[i]),
                 ScopedPrintable(list[i].first + QLatin1Char(' ') + list[i].second + QLatin1Char(' ') + error2));
    }
}



QQuickViewTestUtils::ListRange::ListRange()
    : valid(false)
{
}

QQuickViewTestUtils::ListRange::ListRange(const ListRange &other)
    : valid(other.valid)
{
    indexes = other.indexes;
}

QQuickViewTestUtils::ListRange::ListRange(int start, int end)
    : valid(true)
{
    for (int i=start; i<=end; i++)
        indexes << i;
}

QQuickViewTestUtils::ListRange::~ListRange()
{
}

QQuickViewTestUtils::ListRange QQuickViewTestUtils::ListRange::operator+(const ListRange &other) const
{
    if (other == *this)
        return *this;
    ListRange a(*this);
    a.indexes.append(other.indexes);
    return a;
}

bool QQuickViewTestUtils::ListRange::operator==(const ListRange &other) const
{
    return QSet<int>(indexes.cbegin(), indexes.cend())
        == QSet<int>(other.indexes.cbegin(), other.indexes.cend());
}

bool QQuickViewTestUtils::ListRange::operator!=(const ListRange &other) const
{
    return !(*this == other);
}

bool QQuickViewTestUtils::ListRange::isValid() const
{
    return valid;
}

int QQuickViewTestUtils::ListRange::count() const
{
    return indexes.size();
}

QList<std::pair<QString,QString> > QQuickViewTestUtils::ListRange::getModelDataValues(const QaimModel &model)
{
    QList<std::pair<QString,QString> > data;
    if (!valid)
        return data;
    for (int i=0; i<indexes.size(); i++)
        data.append(std::make_pair(model.name(indexes[i]), model.number(indexes[i])));
    return data;
}

QQuickViewTestUtils::StressTestModel::StressTestModel()
    : QAbstractListModel()
    , m_rowCount(20)
{
    QTimer *t = new QTimer(this);
    t->setInterval(500);
    t->start();

    connect(t, &QTimer::timeout, this, &StressTestModel::updateModel);
}

int QQuickViewTestUtils::StressTestModel::rowCount(const QModelIndex &) const
{
    return m_rowCount;
}

QVariant QQuickViewTestUtils::StressTestModel::data(const QModelIndex &, int) const
{
    return QVariant();
}

void QQuickViewTestUtils::StressTestModel::updateModel()
{
    if (m_rowCount > 10) {
        for (int i = 0; i < 10; ++i) {
            int rnum = QRandomGenerator::global()->bounded(m_rowCount);
            beginRemoveRows(QModelIndex(), rnum, rnum);
            m_rowCount--;
            endRemoveRows();
        }
    }
    if (m_rowCount < 20) {
        for (int i = 0; i < 10; ++i) {
            int rnum = QRandomGenerator::global()->bounded(m_rowCount);
            beginInsertRows(QModelIndex(), rnum, rnum);
            m_rowCount++;
            endInsertRows();
        }
    }
}

#if QT_CONFIG(quick_itemview) && defined(QT_BUILD_INTERNAL)
bool QQuickViewTestUtils::testVisibleItems(const QQuickItemViewPrivate *priv, bool *nonUnique, FxViewItem **failItem, int *expectedIdx)
{
    QHash<QQuickItem*, int> uniqueItems;

    int skip = 0;
    for (int i = 0; i < priv->visibleItems.size(); ++i) {
        FxViewItem *item = priv->visibleItems.at(i);
        if (!item) {
            *failItem = nullptr;
            return false;
        }
#if 0
        qDebug() << "\t" << item->index
                 << item->item
                 << item->position()
                 << (!item->item || QQuickItemPrivate::get(item->item)->culled ? "hidden" : "visible");
#endif
        if (item->index == -1) {
            ++skip;
        } else if (item->index != priv->visibleIndex + i - skip) {
            *nonUnique = false;
            *failItem = item;
            *expectedIdx = priv->visibleIndex + i - skip;
            return false;
        } else if (uniqueItems.contains(item->item)) {
            *nonUnique = true;
            *failItem = item;
            *expectedIdx = uniqueItems.find(item->item).value();
            return false;
        }

        uniqueItems.insert(item->item, item->index);
    }

    return true;
}
#endif

namespace QQuickTouchUtils {

    /* QQuickWindow does event compression and only delivers events just
     * before it is about to render the next frame. Since some tests
     * rely on events being delivered immediately AND that no other
     * event processing has occurred in the meanwhile, we flush the
     * event manually and immediately.
     */
    void flush(QQuickWindow *window) {
        if (!window)
            return;
        QQuickDeliveryAgentPrivate *da = QQuickWindowPrivate::get(window)->deliveryAgentPrivate();
        if (!da || !da->delayedTouch)
            return;
        da->deliverDelayedTouchEvent();
    }

}

namespace QTest {
    int Q_TESTLIB_EXPORT defaultMouseDelay();
}

namespace QQuickTest {

    /*! \internal
        Initialize \a view, set \a url, center in available geometry, move mouse away if desired.
        If \a errorMessage is given, QQuickView::errors() will be concatenated into it;
        otherwise, the QWARN messages are generally enough to debug the test.

        Returns \c false if the view fails to load the QML.  That should be fatal in most tests,
        so normally the return value should be checked with QVERIFY.
    */
    bool initView(QQuickView &view, const QUrl &url, bool moveMouseOut, QByteArray *errorMessage)
    {
        const bool platformIsWayland = !QGuiApplication::platformName().compare(QLatin1String("wayland"), Qt::CaseInsensitive);
        view.setSource(url);
        while (view.status() == QQuickView::Loading)
            QTest::qWait(10);
        if (view.status() != QQuickView::Ready) {
            if (errorMessage) {
                for (const QQmlError &e : view.errors())
                    errorMessage->append(e.toString().toLocal8Bit() + '\n');
            }
            return false;
        }
        if (view.width() == 0)
            view.setWidth(100);
        if (view.height() == 0)
            view.setHeight(100);
        if (!platformIsWayland) {
            const QSize size = view.size();
            const QRect screenGeometry = view.screen()->availableGeometry();
            const QPoint offset = QPoint(size.width() / 2, size.height() / 2);
            view.setFramePosition(screenGeometry.center() - offset);
#if QT_CONFIG(cursor) // Get the cursor out of the way. But it's not possible on Wayland.
            if (moveMouseOut)
                QCursor::setPos(view.geometry().topRight() + QPoint(100, 100));
#else
            Q_UNUSED(moveMouseOut);
#endif
        }
        return true;
    }

    /*! \internal
        Initialize \a view, set \a url, center in available geometry, move mouse away,
        show the \a view, wait for it to be exposed, and verify that its rootObject is not null.

        Returns \c false if anything fails, which should be fatal in most tests.
        The usual way to call this function is
        \code
        QQuickView window;
        QVERIFY(QQuickTest::showView(window, testFileUrl("myitems.qml")));
        \endcode
    */
    bool showView(QQuickView &view, const QUrl &url)
    {
        QByteArray errorMessage;
        if (!initView(view, url, true, &errorMessage)) {
            qCritical() << errorMessage;
            return false;
        }
        const QPoint framePos(view.framePosition());
        view.show();
        if (!QTest::qWaitForWindowExposed(&view)) {
            qCritical() << "qWaitForWindowExposed() failed";
            return false;
        }
        if (!view.rootObject()) {
            qCritical() << "View has no root object";
            const auto errors = view.errors();
            for (const auto &error : errors)
                qCritical() << qPrintable(error.toString());
            return false;
        }
        if (view.flags().testFlag(Qt::FramelessWindowHint))
            return true;
        const bool positionOk = QTest::qWaitFor([&]{ return framePos != view.position(); });
        if (!positionOk) {
            if (!QGuiApplication::platformName().compare(QLatin1String("wayland"), Qt::CaseInsensitive)) {
                // Setting position is not supported on Wayland; message intentionaly not printed to
                // avoid clutter in tests.
                return true;
            }
            qCritical() << "Position failed to update";
            return false;
        }
        return true;
    }

    // TODO maybe move the generic pointerPress/Move/Release functions to QTestLib later on

    static Qt::MouseButton pressedTabletButton = Qt::NoButton;
    static Qt::KeyboardModifiers pressedTabletModifiers = Qt::NoModifier;

    void pointerPress(const QPointingDevice *dev, QQuickWindow *window, int pointId, const QPoint &p,
                      Qt::MouseButton button, Qt::KeyboardModifiers modifiers, int delay)
    {
        const auto defaultDelay = QTest::defaultMouseDelay();
        switch (dev->type()) {
        case QPointingDevice::DeviceType::Mouse:
        case QPointingDevice::DeviceType::TouchPad:
            QTest::mousePress(window, button, modifiers, p, delay >= 0 ? delay : defaultDelay ? defaultDelay : 1);
            break;
        case QPointingDevice::DeviceType::TouchScreen:
            // TODO apply delay when QTBUG-95421 is fixed
            QTest::touchEvent(window, const_cast<QPointingDevice *>(dev)).press(pointId, p, window);
            QQuickTouchUtils::flush(window);
            break;
        case QPointingDevice::DeviceType::Puck:
        case QPointingDevice::DeviceType::Stylus:
        case QPointingDevice::DeviceType::Airbrush:{
            const QPointF nativeLocal = QHighDpi::toNativeLocalPosition(p, window);
            const QPointF nativeGlobal = QHighDpi::toNativeGlobalPosition(window->mapToGlobal(p), window);
            QTest::lastMouseTimestamp += delay >= 0 ? delay : defaultDelay ? defaultDelay : 1;
            pressedTabletButton = button;
            pressedTabletModifiers = modifiers;
            QWindowSystemInterface::handleTabletEvent(window, QTest::lastMouseTimestamp, dev, nativeLocal, nativeGlobal,
                                                      button, 0.8, 0, 0, 0, 0, 0, modifiers);
            break;
        }
        default:
            qWarning() << "can't send a press event from" << dev;
            break;
        }
    }

    void pointerMove(const QPointingDevice *dev, QQuickWindow *window, int pointId, const QPoint &p, int delay)
    {
        const auto defaultDelay = QTest::defaultMouseDelay();
        switch (dev->type()) {
        case QPointingDevice::DeviceType::Mouse:
        case QPointingDevice::DeviceType::TouchPad:
            QTest::mouseMove(window, p, delay >= 0 ? delay : defaultDelay ? defaultDelay : 1);
            break;
        case QPointingDevice::DeviceType::TouchScreen:
            // TODO apply delay when QTBUG-95421 is fixed
            QTest::touchEvent(window, const_cast<QPointingDevice *>(dev)).move(pointId, p, window);
            QQuickTouchUtils::flush(window);
            break;
        case QPointingDevice::DeviceType::Puck:
        case QPointingDevice::DeviceType::Stylus:
        case QPointingDevice::DeviceType::Airbrush: {
            const QPointF nativeLocal = QHighDpi::toNativeLocalPosition(p, window);
            const QPointF nativeGlobal = QHighDpi::toNativeGlobalPosition(window->mapToGlobal(p), window);
            const auto delay = QTest::defaultMouseDelay();
            // often QTest::defaultMouseDelay() == 0; but avoid infinite velocity
            QTest::lastMouseTimestamp += delay >= 0 ? delay : defaultDelay ? defaultDelay : 1;
            QWindowSystemInterface::handleTabletEvent(window, QTest::lastMouseTimestamp, dev, nativeLocal, nativeGlobal,
                                                      pressedTabletButton, pressedTabletButton == Qt::NoButton ? 0 : 0.75,
                                                      0, 0, 0, 0, 0, pressedTabletModifiers);
            break;
        }
        default:
            qWarning() << "can't send a move event from" << dev;
            break;
        }
    }

    void pointerRelease(const QPointingDevice *dev, QQuickWindow *window, int pointId, const QPoint &p,
                        Qt::MouseButton button, Qt::KeyboardModifiers modifiers, int delay)
    {
        const auto defaultDelay = QTest::defaultMouseDelay();
        switch (dev->type()) {
        case QPointingDevice::DeviceType::Mouse:
        case QPointingDevice::DeviceType::TouchPad:
            QTest::mouseRelease(window, button, modifiers, p, delay >= 0 ? delay : defaultDelay ? defaultDelay : 1);
            break;
        case QPointingDevice::DeviceType::TouchScreen:
            // TODO apply delay when QTBUG-95421 is fixed
            QTest::touchEvent(window, const_cast<QPointingDevice *>(dev)).release(pointId, p, window);
            QQuickTouchUtils::flush(window);
            break;
        case QPointingDevice::DeviceType::Puck:
        case QPointingDevice::DeviceType::Stylus:
        case QPointingDevice::DeviceType::Airbrush: {
            const QPointF nativeLocal = QHighDpi::toNativeLocalPosition(p, window);
            const QPointF nativeGlobal = QHighDpi::toNativeGlobalPosition(window->mapToGlobal(p), window);
            QTest::lastMouseTimestamp += delay >= 0 ? delay : defaultDelay ? defaultDelay : 1;
            QWindowSystemInterface::handleTabletEvent(window, QTest::lastMouseTimestamp, dev, nativeLocal, nativeGlobal,
                                                      Qt::NoButton, 0, 0, 0, 0, 0, 0, modifiers);
            break;
        }
        default:
            qWarning() << "can't send a press event from" << dev;
            break;
        }
    }

    void pointerMoveAndPress(const QPointingDevice *dev, QQuickWindow *window,
                             int pointId, const QPoint &p, Qt::MouseButton button,
                             Qt::KeyboardModifiers modifiers, int delay)
    {
        pointerMove(dev, window, pointId, p, delay);
        pointerPress(dev, window, pointId, p, button, modifiers);
    }

    void pointerMoveAndRelease(const QPointingDevice *dev, QQuickWindow *window,
                               int pointId, const QPoint &p, Qt::MouseButton button,
                               Qt::KeyboardModifiers modifiers, int delay)
    {
        pointerMove(dev, window, pointId, p, delay);
        pointerRelease(dev, window, pointId, p, button, modifiers);
    }

    void pointerFlick(const QPointingDevice *dev, QQuickWindow *window,
                      int pointId, const QPoint &from, const QPoint &to, int duration,
                      Qt::MouseButton button, Qt::KeyboardModifiers modifiers, int delay)
    {
        const int pointCount = 5;
        const QPoint diff = to - from;

        // send press, five equally spaced moves, and release.
        pointerMoveAndPress(dev, window, pointId, from, button, modifiers, delay);

        for (int i = 0; i < pointCount; ++i)
            pointerMove(dev, window, pointId, from + (i + 1) * diff / pointCount, duration / pointCount);

        pointerMoveAndRelease(dev, window, pointId, to, button, modifiers);
    }
}

QT_END_NAMESPACE

#include "moc_viewtestutils_p.cpp"
