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

#include "delegatemodelkinds.h"

#include <QtTest/QTest>
#include <QtQmlModels/private/qqmlobjectmodel_p.h>
#include <QtQuick/qquickview.h>
#include <QtQuick/private/qquickitemview_p_p.h>
#include <QtQuick/private/qquicklistview_p.h>
#include <QtQuickTest/QtQuickTest>
#include <QStringListModel>
#include <QQmlApplicationEngine>
#include <QtQml/QQmlComponent>

#include <QtQuickTestUtils/private/viewtestutils_p.h>
#include <QtQuickTestUtils/private/visualtestutils_p.h>
#include <QtQuickTestUtils/private/qmlutils_p.h>

Q_LOGGING_CATEGORY(lcTests, "qt.quick.tests")

using namespace QQuickViewTestUtils;
using namespace QQuickVisualTestUtils;

static const int oneSecondInMs = 1000;

class tst_QQuickListView2 : public QQmlDataTest
{
    Q_OBJECT
public:
    tst_QQuickListView2();

private slots:
    void urlListModel();
    void dragDelegateWithMouseArea_data();
    void dragDelegateWithMouseArea();
    void delegateChooserEnumRole();
    void QTBUG_92809();
    void footerUpdate();
    void singletonModelLifetime();
    void delegateModelRefresh();
    void wheelSnap();
    void wheelSnap_data();
    void nestedWheelSnap();

    void sectionsNoOverlap();
    void metaSequenceAsModel();
    void noCrashOnIndexChange();
    void innerRequired();
    void boundDelegateComponent();
    void tapDelegateDuringFlicking_data();
    void tapDelegateDuringFlicking();
    void flickDuringFlicking_data();
    void flickDuringFlicking();
    void maxExtent_data();
    void maxExtent();
    void isCurrentItem_DelegateModel();
    void isCurrentItem_NoRegressionWithDelegateModelGroups();

    void pullbackSparseList();
    void highlightWithBound();
    void sectionIsCompatibleWithBoundComponents();
    void sectionGeometryChange();
    void areaZeroviewDoesNotNeedlesslyPopulateWholeModel();
    void viewportAvoidUndesiredMovementOnSetCurrentIndex();

    void delegateContextHandling();
    void fetchMore_data();
    void fetchMore();

    void changingOrientationResetsPreviousAxisValues_data();
    void changingOrientationResetsPreviousAxisValues();
    void bindingDirectlyOnPositionInHeaderAndFooterDelegates_data();
    void bindingDirectlyOnPositionInHeaderAndFooterDelegates();

    void clearObjectListModel();
    void gadgetModelSections();
    void visibleBoundToCountGreaterThanZero();

    void setDelegateAfterModel();

    void delegateModelAccess_data();
    void delegateModelAccess();
    void removeAndDestroyObjectModelItem_data();
    void removeAndDestroyObjectModelItem();

    void fastMouseWheel();

private:
    void flickWithTouch(QQuickWindow *window, const QPoint &from, const QPoint &to);
    std::unique_ptr<QPointingDevice> touchscreen{QTest::createTouchDevice()};
};

tst_QQuickListView2::tst_QQuickListView2()
    : QQmlDataTest(QT_QMLTEST_DATADIR)
{
}

void tst_QQuickListView2::urlListModel()
{
    QScopedPointer<QQuickView> window(createView());
    QVERIFY(window);

    QList<QUrl> model = { QUrl::fromLocalFile("abc"), QUrl::fromLocalFile("123") };
    window->setInitialProperties({{ "model", QVariant::fromValue(model) }});

    window->setSource(testFileUrl("urlListModel.qml"));
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window.data()));

    QQuickListView *view = window->rootObject()->property("view").value<QQuickListView*>();
    QVERIFY(view);
    if (QQuickTest::qIsPolishScheduled(view))
        QVERIFY(QQuickTest::qWaitForPolish(view));
    QCOMPARE(view->count(), model.size());
}

static void dragListView(QWindow *window, QPoint *startPos, const QPoint &delta)
{
    auto drag_helper = [&](QWindow *window, QPoint *startPos, const QPoint &d) {
        QPoint pos = *startPos;
        const int dragDistance = d.manhattanLength();
        const QPoint unitVector(qBound(-1, d.x(), 1), qBound(-1, d.y(), 1));
        for (int i = 0; i < dragDistance; ++i) {
            QTest::mouseMove(window, pos);
            pos += unitVector;
        }
        // Move to the final position
        pos = *startPos + d;
        QTest::mouseMove(window, pos);
        *startPos = pos;
    };

    if (delta.manhattanLength() == 0)
        return;
    const int dragThreshold = QGuiApplication::styleHints()->startDragDistance();
    const QPoint unitVector(qBound(-1, delta.x(), 1), qBound(-1, delta.y(), 1));
    // go just beyond the drag theshold
    drag_helper(window, startPos, unitVector * (dragThreshold + 1));
    drag_helper(window, startPos, unitVector);

    // next drag will actually scroll the listview
    drag_helper(window, startPos, delta);
}

void tst_QQuickListView2::dragDelegateWithMouseArea_data()
{
    QTest::addColumn<QQuickItemView::LayoutDirection>("layoutDirection");

    for (int layDir = QQuickItemView::LeftToRight; layDir <= (int)QQuickItemView::VerticalBottomToTop; layDir++) {
        const char *enumValueName = QMetaEnum::fromType<QQuickItemView::LayoutDirection>().valueToKey(layDir);
        QTest::newRow(enumValueName) << static_cast<QQuickItemView::LayoutDirection>(layDir);
    }
}

void tst_QQuickListView2::viewportAvoidUndesiredMovementOnSetCurrentIndex()
{
    QScopedPointer<QQuickView> window(createView());
    QVERIFY(window);
    window->setFlag(Qt::FramelessWindowHint);
    window->setSource(testFileUrl("viewportAvoidUndesiredMovementOnSetCurrentIndex.qml"));
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window.data()));
    QVERIFY(window->rootObject());
    QQuickListView *listview = findItem<QQuickListView>(window->rootObject(), "list");
    QVERIFY(listview);
    listview->setCurrentIndex(2); // change current item
    // partially obscure first item
    QCOMPARE(listview->contentY(), 0);
    listview->setContentY(50);
    QTRY_COMPARE(listview->contentY(), 50);
    listview->setCurrentIndex(0); // change current item back to first one
    QVERIFY(QQuickTest::qWaitForPolish(listview));
    // that shouldn't have caused any movement
    QCOMPARE(listview->contentY(), 50);

    // that even applies to the case where the current item is completely out of the viewport
    listview->setCurrentIndex(25);
    QVERIFY(QQuickTest::qWaitForPolish(listview));
    QCOMPARE(listview->contentY(), 50);
}

void tst_QQuickListView2::dragDelegateWithMouseArea()
{
    QFETCH(QQuickItemView::LayoutDirection, layoutDirection);

    QScopedPointer<QQuickView> window(createView());
    QVERIFY(window);
    window->setFlag(Qt::FramelessWindowHint);
    window->setSource(testFileUrl("delegateWithMouseArea.qml"));
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window.data()));

    QQuickListView *listview = findItem<QQuickListView>(window->rootObject(), "list");
    QVERIFY(listview != nullptr);

    const bool horizontal = layoutDirection < QQuickItemView::VerticalTopToBottom;
    listview->setOrientation(horizontal ? QQuickListView::Horizontal : QQuickListView::Vertical);

    if (horizontal)
        listview->setLayoutDirection(static_cast<Qt::LayoutDirection>(layoutDirection));
    else
        listview->setVerticalLayoutDirection(static_cast<QQuickItemView::VerticalLayoutDirection>(layoutDirection));

    QVERIFY(QQuickTest::qWaitForPolish(listview));

    auto contentPosition = [&](QQuickListView *listview) {
        return (listview->orientation() == QQuickListView::Horizontal ? listview->contentX(): listview->contentY());
    };

    qreal expectedContentPosition = contentPosition(listview);
    QPoint startPos = (QPointF(listview->width(), listview->height())/2).toPoint();
    QTest::mousePress(window.data(), Qt::LeftButton, Qt::NoModifier, startPos, 200);

    QPoint dragDelta(0, -10);

    if (layoutDirection == QQuickItemView::RightToLeft || layoutDirection == QQuickItemView::VerticalBottomToTop)
        dragDelta = -dragDelta;
    expectedContentPosition -= dragDelta.y();
    if (horizontal)
        dragDelta = dragDelta.transposed();

    dragListView(window.data(), &startPos, dragDelta);

    QTest::mouseRelease(window.data(), Qt::LeftButton, Qt::NoModifier, startPos, 200);     // Wait 200 ms before we release to avoid trigger a flick

    // wait for the "fixup" animation to finish
    QVERIFY(QTest::qWaitFor([&]()
        { return !listview->isMoving();}
    ));

    QCOMPARE(contentPosition(listview), expectedContentPosition);
}


void tst_QQuickListView2::delegateChooserEnumRole()
{
    QQuickView window;
    QVERIFY(QQuickTest::showView(window, testFileUrl("delegateChooserEnumRole.qml")));
    QQuickListView *listview = qobject_cast<QQuickListView*>(window.rootObject());
    QVERIFY(listview);
    QTRY_COMPARE(listview->count(), 3);
    QCOMPARE(listview->itemAtIndex(0)->property("delegateType").toInt(), 0);
    QCOMPARE(listview->itemAtIndex(1)->property("delegateType").toInt(), 1);
    QCOMPARE(listview->itemAtIndex(2)->property("delegateType").toInt(), 2);
}

void tst_QQuickListView2::QTBUG_92809()
{
    QScopedPointer<QQuickView> window(createView());
    QTRY_VERIFY(window);
    window->setSource(testFileUrl("qtbug_92809.qml"));
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window.data()));

    QQuickListView *listview = findItem<QQuickListView>(window->rootObject(), "list");
    QTRY_VERIFY(listview != nullptr);
    QVERIFY(QQuickTest::qWaitForPolish(listview));
    listview->setCurrentIndex(1);
    QVERIFY(QQuickTest::qWaitForPolish(listview));
    listview->setCurrentIndex(2);
    QVERIFY(QQuickTest::qWaitForPolish(listview));
    listview->setCurrentIndex(3);
    QVERIFY(QQuickTest::qWaitForPolish(listview));
    QTest::qWait(500);
    listview->setCurrentIndex(10);
    QVERIFY(QQuickTest::qWaitForPolish(listview));
    QTest::qWait(500);
    int currentIndex = listview->currentIndex();
    QTRY_COMPARE(currentIndex, 9);
}

void tst_QQuickListView2::footerUpdate()
{
    QScopedPointer<QQuickView> window(createView());
    QTRY_VERIFY(window);
    window->setSource(testFileUrl("footerUpdate.qml"));
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window.data()));

    QQuickListView *listview = findItem<QQuickListView>(window->rootObject(), "list");
    QTRY_VERIFY(listview != nullptr);
    QVERIFY(QQuickTest::qWaitForPolish(listview));
    QQuickItem *footer = listview->footerItem();
    QTRY_VERIFY(footer);
    QVERIFY(QQuickTest::qWaitForPolish(footer));
    QTRY_COMPARE(footer->y(), 0);
}

void tst_QQuickListView2::sectionsNoOverlap()
{
    QScopedPointer<QQuickView> window(createView());
    QTRY_VERIFY(window);
    window->setSource(testFileUrl("sectionsNoOverlap.qml"));
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window.data()));

    QQuickListView *listview = findItem<QQuickListView>(window->rootObject(), "list");
    QTRY_VERIFY(listview != nullptr);

    QQuickItem *contentItem = listview->contentItem();
    QTRY_VERIFY(contentItem != nullptr);
    QVERIFY(QQuickTest::qWaitForPolish(listview));

    const unsigned int sectionCount = 2, normalDelegateCount = 2;
    const unsigned int expectedSectionHeight = 48;
    const unsigned int expectedNormalDelegateHeight = 40;

    unsigned int normalDelegateCounter = 0;
    for (unsigned int sectionIndex = 0; sectionIndex < sectionCount; ++sectionIndex) {
        QQuickItem *sectionDelegate =
                findItem<QQuickItem>(contentItem, "section" + QString::number(sectionIndex + 1));
        QVERIFY(sectionDelegate);

        QCOMPARE(sectionDelegate->height(), expectedSectionHeight);
        QVERIFY(sectionDelegate->isVisible());
        QCOMPARE(sectionDelegate->y(),
                 qreal(sectionIndex * expectedSectionHeight
                       + (sectionIndex * normalDelegateCount * expectedNormalDelegateHeight)));

        for (; normalDelegateCounter < ((sectionIndex + 1) * normalDelegateCount);
             ++normalDelegateCounter) {
            QQuickItem *normalDelegate = findItem<QQuickItem>(
                    contentItem, "element" + QString::number(normalDelegateCounter + 1));
            QVERIFY(normalDelegate);

            QCOMPARE(normalDelegate->height(), expectedNormalDelegateHeight);
            QVERIFY(normalDelegate->isVisible());
            QCOMPARE(normalDelegate->y(),
                     qreal((sectionIndex + 1) * expectedSectionHeight
                           + normalDelegateCounter * expectedNormalDelegateHeight
                           + listview->spacing() * normalDelegateCounter));
        }
    }
}

void tst_QQuickListView2::metaSequenceAsModel()
{
    QQmlEngine engine;
    QQmlComponent c(&engine, testFileUrl("metaSequenceAsModel.qml"));
    QVERIFY2(c.isReady(), qPrintable(c.errorString()));
    QScopedPointer<QObject> o(c.create());
    QVERIFY(!o.isNull());
    QStringList strings = qvariant_cast<QStringList>(o->property("texts"));
    QCOMPARE(strings.size(), 2);
    QCOMPARE(strings[0], QStringLiteral("1/2"));
    QCOMPARE(strings[1], QStringLiteral("5/6"));
}

void tst_QQuickListView2::noCrashOnIndexChange()
{
    QQmlEngine engine;
    QQmlComponent c(&engine, testFileUrl("noCrashOnIndexChange.qml"));
    QVERIFY2(c.isReady(), qPrintable(c.errorString()));
    QScopedPointer<QObject> o(c.create());
    QVERIFY(!o.isNull());

    QObject *delegateModel = qmlContext(o.data())->objectForName("displayDelegateModel");
    QVERIFY(delegateModel);

    QObject *items = qvariant_cast<QObject *>(delegateModel->property("items"));
    QCOMPARE(items->property("name").toString(), QStringLiteral("items"));
    QCOMPARE(items->property("count").toInt(), 4);
}

void tst_QQuickListView2::innerRequired()
{
    QQmlEngine engine;
    const QUrl url(testFileUrl("innerRequired.qml"));
    QQmlComponent component(&engine, url);
    QVERIFY2(component.isReady(), qPrintable(component.errorString()));

    QScopedPointer<QObject> o(component.create());
    QVERIFY2(!o.isNull(), qPrintable(component.errorString()));

    QQuickListView *a = qobject_cast<QQuickListView *>(
            qmlContext(o.data())->objectForName(QStringLiteral("listView")));
    QVERIFY(a);

    QCOMPARE(a->count(), 2);
    QCOMPARE(a->itemAtIndex(0)->property("age").toInt(), 8);
    QCOMPARE(a->itemAtIndex(0)->property("text").toString(), u"meow");
    QCOMPARE(a->itemAtIndex(1)->property("age").toInt(), 5);
    QCOMPARE(a->itemAtIndex(1)->property("text").toString(), u"woof");
}

void tst_QQuickListView2::boundDelegateComponent()
{
    QQmlEngine engine;
    const QUrl url(testFileUrl("boundDelegateComponent.qml"));
    QQmlComponent c(&engine, url);
    QVERIFY2(c.isReady(), qPrintable(c.errorString()));

    QTest::ignoreMessage(
            QtWarningMsg, qPrintable(QLatin1String("%1:14: ReferenceError: index is not defined")
                                             .arg(url.toString())));

    QScopedPointer<QObject> o(c.create());
    QVERIFY(!o.isNull());

    QQmlContext *context = qmlContext(o.data());

    QObject *inner = context->objectForName(QLatin1String("listView"));
    QVERIFY(inner != nullptr);
    QQuickListView *listView = qobject_cast<QQuickListView *>(inner);
    QVERIFY(listView != nullptr);
    QObject *item = listView->itemAtIndex(0);
    QVERIFY(item);
    QCOMPARE(item->objectName(), QLatin1String("fooouterundefined"));

    QObject *inner2 = context->objectForName(QLatin1String("listView2"));
    QVERIFY(inner2 != nullptr);
    QQuickListView *listView2 = qobject_cast<QQuickListView *>(inner2);
    QVERIFY(listView2 != nullptr);
    QObject *item2 = listView2->itemAtIndex(0);
    QVERIFY(item2);
    QCOMPARE(item2->objectName(), QLatin1String("fooouter0"));

    QQmlComponent *comp = qobject_cast<QQmlComponent *>(
            context->objectForName(QLatin1String("outerComponent")));
    QVERIFY(comp != nullptr);

    for (int i = 0; i < 3; ++i) {
        QTest::ignoreMessage(
                QtWarningMsg,
                qPrintable(QLatin1String("%1:51:21: ReferenceError: model is not defined")
                                   .arg(url.toString())));
    }

    QScopedPointer<QObject> outerItem(comp->create(context));
    QVERIFY(!outerItem.isNull());
    QQuickListView *innerListView = qobject_cast<QQuickListView *>(
            qmlContext(outerItem.data())->objectForName(QLatin1String("innerListView")));
    QVERIFY(innerListView != nullptr);
    QCOMPARE(innerListView->count(), 3);
    for (int i = 0; i < 3; ++i)
        QVERIFY(innerListView->itemAtIndex(i)->objectName().isEmpty());
}

void tst_QQuickListView2::tapDelegateDuringFlicking_data()
{
    QTest::addColumn<QByteArray>("qmlFile");
    QTest::addColumn<QQuickFlickable::BoundsBehavior>("boundsBehavior");
    QTest::addColumn<bool>("expectCanceled");

    QTest::newRow("Button StopAtBounds") << QByteArray("buttonDelegate.qml")
                                         << QQuickFlickable::BoundsBehavior(QQuickFlickable::StopAtBounds) << false;
    QTest::newRow("MouseArea StopAtBounds") << QByteArray("mouseAreaDelegate.qml")
                                            << QQuickFlickable::BoundsBehavior(QQuickFlickable::StopAtBounds) << true;
    QTest::newRow("Button DragOverBounds") << QByteArray("buttonDelegate.qml")
                                           << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragOverBounds) << false;
    QTest::newRow("MouseArea DragOverBounds") << QByteArray("mouseAreaDelegate.qml")
                                              << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragOverBounds) << true;
    QTest::newRow("Button OvershootBounds") << QByteArray("buttonDelegate.qml")
                                            << QQuickFlickable::BoundsBehavior(QQuickFlickable::OvershootBounds) << false;
    QTest::newRow("MouseArea OvershootBounds") << QByteArray("mouseAreaDelegate.qml")
                                               << QQuickFlickable::BoundsBehavior(QQuickFlickable::OvershootBounds) << true;
    QTest::newRow("Button DragAndOvershootBounds") << QByteArray("buttonDelegate.qml")
                                                   << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragAndOvershootBounds) << false;
    QTest::newRow("MouseArea DragAndOvershootBounds") << QByteArray("mouseAreaDelegate.qml")
                                                      << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragAndOvershootBounds) << true;
}

void tst_QQuickListView2::tapDelegateDuringFlicking() // QTBUG-103832
{
    QFETCH(QByteArray, qmlFile);
    QFETCH(QQuickFlickable::BoundsBehavior, boundsBehavior);
    QFETCH(bool, expectCanceled);

    QQuickView window;
    QVERIFY(QQuickTest::showView(window, testFileUrl(qmlFile.constData())));
    QQuickListView *listView = qobject_cast<QQuickListView*>(window.rootObject());
    QVERIFY(listView);
    listView->setBoundsBehavior(boundsBehavior);

    flickWithTouch(&window, {100, 400}, {100, 100});
    QTRY_VERIFY(listView->contentY() > 501); // let it flick some distance
    QVERIFY(listView->isFlicking()); // we want to test the case when it's still moving while we tap
    QVERIFY(listView->isMoving());
    // @y = 400 we pressed the 4th delegate; started flicking, and the press was canceled
    QCOMPARE(listView->property("pressedDelegates").toList().first(), 4);
    // At first glance one would expect MouseArea and Button would be consistent about this;
    // but in fact, before ListView takes over the grab via filtering,
    // Button.pressed transitions to false because QQuickAbstractButtonPrivate::handleMove
    // sees that the touchpoint has strayed outside its bounds, but it does NOT emit the canceled signal
    if (expectCanceled) {
        const QVariantList canceledDelegates = listView->property("canceledDelegates").toList();
        QCOMPARE(canceledDelegates.size(), 1);
        QCOMPARE(canceledDelegates.first(), 4);
    }
    QCOMPARE(listView->property("releasedDelegates").toList().size(), 0);

    // press a delegate during flicking (at y > 501 + 100, so likely delegate 6)
    QTest::touchEvent(&window, touchscreen.get()).press(0, {100, 100});
    QQuickTouchUtils::flush(&window);
    // The press will stop listView from flicking, but it will still be "moving", which
    // means that the user is still interacting with it
    QVERIFY(!listView->isFlicking());
    QVERIFY(!listView->isDragging());
    QVERIFY(listView->isMoving());
    QTest::touchEvent(&window, touchscreen.get()).release(0, {100, 100});
    QQuickTouchUtils::flush(&window);
    // Releasing while "flicking" is false will stop the flicking
    // session, and set "moving" to false
    QVERIFY(!listView->isMoving());
    QVERIFY(!listView->isFlicking());
    QVERIFY(!listView->isDragging());

    const QVariantList pressedDelegates = listView->property("pressedDelegates").toList();
    const QVariantList releasedDelegates = listView->property("releasedDelegates").toList();
    const QVariantList tappedDelegates = listView->property("tappedDelegates").toList();
    const QVariantList canceledDelegates = listView->property("canceledDelegates").toList();

    qCDebug(lcTests) << "pressed" << pressedDelegates; // usually [4, 6]
    qCDebug(lcTests) << "released" << releasedDelegates;
    qCDebug(lcTests) << "tapped" << tappedDelegates;
    qCDebug(lcTests) << "canceled" << canceledDelegates;

    // Since the flickable was already moving (that is, the user was interacting with
    // it), the second tap was only used to stop the flicking. Hence, no delegates
    // received any pointer events.
    const int lastPressed = pressedDelegates.last().toInt();
    QCOMPARE(lastPressed, 4); // Nothing changed since the beginning
    QVERIFY(releasedDelegates.isEmpty());
    QVERIFY(tappedDelegates.isEmpty());
    QCOMPARE(canceledDelegates.size(), expectCanceled ? 1 : 0); // only the first press was canceled, not the second
}

void tst_QQuickListView2::flickDuringFlicking_data()
{
    QTest::addColumn<QByteArray>("qmlFile");
    QTest::addColumn<QQuickFlickable::BoundsBehavior>("boundsBehavior");

    QTest::newRow("Button StopAtBounds") << QByteArray("buttonDelegate.qml")
                                         << QQuickFlickable::BoundsBehavior(QQuickFlickable::StopAtBounds);
    QTest::newRow("MouseArea StopAtBounds") << QByteArray("mouseAreaDelegate.qml")
                                            << QQuickFlickable::BoundsBehavior(QQuickFlickable::StopAtBounds);
    QTest::newRow("Button DragOverBounds") << QByteArray("buttonDelegate.qml")
                                           << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragOverBounds);
    QTest::newRow("MouseArea DragOverBounds") << QByteArray("mouseAreaDelegate.qml")
                                              << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragOverBounds);
    QTest::newRow("Button OvershootBounds") << QByteArray("buttonDelegate.qml")
                                            << QQuickFlickable::BoundsBehavior(QQuickFlickable::OvershootBounds);
    QTest::newRow("MouseArea OvershootBounds") << QByteArray("mouseAreaDelegate.qml")
                                               << QQuickFlickable::BoundsBehavior(QQuickFlickable::OvershootBounds);
    QTest::newRow("Button DragAndOvershootBounds") << QByteArray("buttonDelegate.qml")
                                                   << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragAndOvershootBounds);
    QTest::newRow("MouseArea DragAndOvershootBounds") << QByteArray("mouseAreaDelegate.qml")
                                                      << QQuickFlickable::BoundsBehavior(QQuickFlickable::DragAndOvershootBounds);
}

void tst_QQuickListView2::flickDuringFlicking() // QTBUG-103832
{
    QFETCH(QByteArray, qmlFile);
    QFETCH(QQuickFlickable::BoundsBehavior, boundsBehavior);

    QQuickView window;
    QVERIFY(QQuickTest::showView(window, testFileUrl(qmlFile.constData())));
    QQuickListView *listView = qobject_cast<QQuickListView*>(window.rootObject());
    QVERIFY(listView);
    listView->setBoundsBehavior(boundsBehavior);

    flickWithTouch(&window, {100, 400}, {100, 100});
    // let it flick some distance
    QTRY_COMPARE_GT(listView->contentY(), 500);
    QVERIFY(listView->isFlicking()); // we want to test the case when it's moving and then we flick again
    const qreal posBeforeSecondFlick = listView->contentY();

    // flick again during flicking, and make sure that it doesn't jump back to the first delegate,
    // but flicks incrementally further from the position at that time
    QTest::touchEvent(&window, touchscreen.get()).press(0, {100, 400});
    QQuickTouchUtils::flush(&window);
    qCDebug(lcTests) << "second press: contentY" << posBeforeSecondFlick << "->" << listView->contentY();
    qCDebug(lcTests) << "pressed delegates" << listView->property("pressedDelegates").toList();
    QVERIFY(listView->contentY() >= posBeforeSecondFlick);

    QTest::qWait(20);
    QTest::touchEvent(&window, touchscreen.get()).move(0, {100, 300});
    QQuickTouchUtils::flush(&window);
    qCDebug(lcTests) << "first move after second press: contentY" << posBeforeSecondFlick << "->" << listView->contentY();
    QVERIFY(listView->contentY() >= posBeforeSecondFlick);

    QTest::qWait(20);
    QTest::touchEvent(&window, touchscreen.get()).move(0, {100, 200});
    QQuickTouchUtils::flush(&window);
    qCDebug(lcTests) << "second move after second press: contentY" << posBeforeSecondFlick << "->" << listView->contentY();
    QVERIFY(listView->contentY() >= posBeforeSecondFlick + 100);

    QTest::touchEvent(&window, touchscreen.get()).release(0, {100, 100});
}

void tst_QQuickListView2::flickWithTouch(QQuickWindow *window, const QPoint &from, const QPoint &to)
{
    QTest::touchEvent(window, touchscreen.get()).press(0, from, window);
    QQuickTouchUtils::flush(window);

    QPoint diff = to - from;
    for (int i = 1; i <= 8; ++i) {
        QTest::touchEvent(window, touchscreen.get()).move(0, from + i * diff / 8, window);
        QQuickTouchUtils::flush(window);
    }
    QTest::touchEvent(window, touchscreen.get()).release(0, to, window);
    QQuickTouchUtils::flush(window);
}

class SingletonModel : public QStringListModel
{
    Q_OBJECT
public:
    SingletonModel(QObject* parent = nullptr) : QStringListModel(parent) { }
};

void tst_QQuickListView2::singletonModelLifetime()
{
    // this does not really test any functionality of listview, but we do not have a good way
    // to unit test QQmlAdaptorModel in isolation.
    qmlRegisterSingletonType<SingletonModel>("SingletonModelLifeTimeTest", 1, 0, "SingletonModel",
            [](QQmlEngine* , QJSEngine*) -> QObject* { return new SingletonModel; });

    QQmlApplicationEngine engine(testFile("singletonModelLifetime.qml"));
    // needs event loop iteration for callLater to execute
    QTRY_VERIFY(engine.rootObjects().first()->property("alive").toBool());
}

void tst_QQuickListView2::delegateModelRefresh()
{
    // Test case originates from QTBUG-100161
    QQmlApplicationEngine engine(testFile("delegateModelRefresh.qml"));
    QVERIFY(!engine.rootObjects().isEmpty());
    // needs event loop iteration for callLater to execute
    QTRY_VERIFY(engine.rootObjects().first()->property("done").toBool());
}

void tst_QQuickListView2::wheelSnap()
{
    QFETCH(QQuickListView::Orientation, orientation);
    QFETCH(Qt::LayoutDirection, layoutDirection);
    QFETCH(QQuickItemView::VerticalLayoutDirection, verticalLayoutDirection);
    QFETCH(QQuickItemView::HighlightRangeMode, highlightRangeMode);
    QFETCH(QPoint, forwardAngleDelta);
    QFETCH(qreal, snapAlignment);
    QFETCH(qreal, endExtent);
    QFETCH(qreal, startExtent);
    QFETCH(qreal, preferredHighlightBegin);
    QFETCH(qreal, preferredHighlightEnd);

    // Helpers begin
    quint64 timestamp = 10;
    auto sendWheelEvent = [&timestamp](QQuickView *window, const QPoint &angleDelta) {
        QPoint pos(100, 100);
        QWheelEvent event(pos, window->mapToGlobal(pos), QPoint(), angleDelta, Qt::NoButton,
                          Qt::NoModifier, Qt::NoScrollPhase, false);
        event.setAccepted(false);
        event.setTimestamp(timestamp);
        QGuiApplication::sendEvent(window, &event);
        timestamp += 50;
    };

    auto atEnd = [&layoutDirection, &orientation,
                  &verticalLayoutDirection](QQuickListView *listview) {
        if (orientation == QQuickListView::Horizontal) {
            if (layoutDirection == Qt::LeftToRight)
                return listview->isAtXEnd();

            return listview->isAtXBeginning();
        } else {
            if (verticalLayoutDirection == QQuickItemView::VerticalLayoutDirection::TopToBottom)
                return listview->isAtYEnd();

            return listview->isAtYBeginning();
        }
    };

    auto atBegin = [&layoutDirection, &orientation,
                    &verticalLayoutDirection](QQuickListView *listview) {
        if (orientation == QQuickListView::Horizontal) {
            if (layoutDirection == Qt::LeftToRight)
                return listview->isAtXBeginning();

            return listview->isAtXEnd();
        } else {
            if (verticalLayoutDirection == QQuickItemView::VerticalLayoutDirection::TopToBottom)
                return listview->isAtYBeginning();

            return listview->isAtYEnd();
        }
    };
    // Helpers end

    QScopedPointer<QQuickView> window(createView());
    QTRY_VERIFY(window);
    QQuickViewTestUtils::moveMouseAway(window.data());
    window->setSource(testFileUrl("snapOneItem.qml"));
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window.data()));

    QQuickListView *listview = qobject_cast<QQuickListView *>(window->rootObject());
    QTRY_VERIFY(listview);

    listview->setOrientation(orientation);
    listview->setVerticalLayoutDirection(verticalLayoutDirection);
    listview->setLayoutDirection(layoutDirection);
    listview->setHighlightRangeMode(highlightRangeMode);
    listview->setPreferredHighlightBegin(preferredHighlightBegin);
    listview->setPreferredHighlightEnd(preferredHighlightEnd);
    QVERIFY(QQuickTest::qWaitForPolish(listview));

    QQuickItem *contentItem = listview->contentItem();
    QTRY_VERIFY(contentItem);

    QSignalSpy currentIndexSpy(listview, &QQuickListView::currentIndexChanged);

    // confirm that a flick hits the next item boundary
    int indexCounter = 0;
    sendWheelEvent(window.data(), forwardAngleDelta);
    QTRY_VERIFY(listview->isMoving() == false); // wait until it stops

    if (orientation == QQuickListView::Vertical)
        QCOMPARE(listview->contentY(), snapAlignment);
    else
        QCOMPARE(listview->contentX(), snapAlignment);

    if (highlightRangeMode == QQuickItemView::StrictlyEnforceRange) {
        ++indexCounter;
        QTRY_VERIFY(listview->currentIndex() == indexCounter);
    }

    // flick to end
    do {
        sendWheelEvent(window.data(), forwardAngleDelta);
        QTRY_VERIFY(listview->isMoving() == false); // wait until it stops
        if (highlightRangeMode == QQuickItemView::StrictlyEnforceRange) {
            ++indexCounter;
            QTRY_VERIFY(listview->currentIndex() == indexCounter);
        }
    } while (!atEnd(listview));

    if (orientation == QQuickListView::Vertical)
        QCOMPARE(listview->contentY(), endExtent);
    else
        QCOMPARE(listview->contentX(), endExtent);

    if (highlightRangeMode == QQuickItemView::StrictlyEnforceRange) {
        QCOMPARE(listview->currentIndex(), listview->count() - 1);
        QCOMPARE(currentIndexSpy.count(), listview->count() - 1);
    }

    // flick to start
    const QPoint backwardAngleDelta(-forwardAngleDelta.x(), -forwardAngleDelta.y());
    do {
        sendWheelEvent(window.data(), backwardAngleDelta);
        QTRY_VERIFY(listview->isMoving() == false); // wait until it stops
        if (highlightRangeMode == QQuickItemView::StrictlyEnforceRange) {
            --indexCounter;
            QTRY_VERIFY(listview->currentIndex() == indexCounter);
        }
    } while (!atBegin(listview));

    if (orientation == QQuickListView::Vertical)
        QCOMPARE(listview->contentY(), startExtent);
    else
        QCOMPARE(listview->contentX(), startExtent);

    if (highlightRangeMode == QQuickItemView::StrictlyEnforceRange) {
        QCOMPARE(listview->currentIndex(), 0);
        QCOMPARE(currentIndexSpy.count(), (listview->count() - 1) * 2);
    }
}

void tst_QQuickListView2::wheelSnap_data()
{
    QTest::addColumn<QQuickListView::Orientation>("orientation");
    QTest::addColumn<Qt::LayoutDirection>("layoutDirection");
    QTest::addColumn<QQuickItemView::VerticalLayoutDirection>("verticalLayoutDirection");
    QTest::addColumn<QQuickItemView::HighlightRangeMode>("highlightRangeMode");
    QTest::addColumn<QPoint>("forwardAngleDelta");
    QTest::addColumn<qreal>("snapAlignment");
    QTest::addColumn<qreal>("endExtent");
    QTest::addColumn<qreal>("startExtent");
    QTest::addColumn<qreal>("preferredHighlightBegin");
    QTest::addColumn<qreal>("preferredHighlightEnd");

    QTest::newRow("vertical, top to bottom")
            << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom
            << QQuickItemView::NoHighlightRange << QPoint(20, -240) << 200.0 << 600.0 << 0.0 << 0.0
            << 0.0;

    QTest::newRow("vertical, bottom to top")
            << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop
            << QQuickItemView::NoHighlightRange << QPoint(20, 240) << -400.0 << -800.0 << -200.0
            << 0.0 << 0.0;

    QTest::newRow("horizontal, left to right")
            << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom
            << QQuickItemView::NoHighlightRange << QPoint(-240, 20) << 200.0 << 600.0 << 0.0 << 0.0
            << 0.0;

    QTest::newRow("horizontal, right to left")
            << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom
            << QQuickItemView::NoHighlightRange << QPoint(240, 20) << -400.0 << -800.0 << -200.0
            << 0.0 << 0.0;

    QTest::newRow("vertical, top to bottom, enforce range")
            << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom
            << QQuickItemView::StrictlyEnforceRange << QPoint(20, -240) << 200.0 << 600.0 << 0.0
            << 0.0 << 0.0;

    QTest::newRow("vertical, bottom to top, enforce range")
            << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop
            << QQuickItemView::StrictlyEnforceRange << QPoint(20, 240) << -400.0 << -800.0 << -200.0
            << 0.0 << 0.0;

    QTest::newRow("horizontal, left to right, enforce range")
            << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom
            << QQuickItemView::StrictlyEnforceRange << QPoint(-240, 20) << 200.0 << 600.0 << 0.0
            << 0.0 << 0.0;

    QTest::newRow("horizontal, right to left, enforce range")
            << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom
            << QQuickItemView::StrictlyEnforceRange << QPoint(240, 20) << -400.0 << -800.0 << -200.0
            << 0.0 << 0.0;

    QTest::newRow("vertical, top to bottom, apply range")
            << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom
            << QQuickItemView::ApplyRange << QPoint(20, -240) << 200.0 << 600.0 << 0.0 << 0.0
            << 0.0;

    QTest::newRow("vertical, bottom to top, apply range")
            << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop
            << QQuickItemView::ApplyRange << QPoint(20, 240) << -400.0 << -800.0 << -200.0 << 0.0
            << 0.0;

    QTest::newRow("horizontal, left to right, apply range")
            << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom
            << QQuickItemView::ApplyRange << QPoint(-240, 20) << 200.0 << 600.0 << 0.0 << 0.0
            << 0.0;

    QTest::newRow("horizontal, right to left, apply range")
            << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom
            << QQuickItemView::ApplyRange << QPoint(240, 20) << -400.0 << -800.0 << -200.0 << 0.0
            << 0.0;

    QTest::newRow("vertical, top to bottom with highlightRange")
            << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom
            << QQuickItemView::NoHighlightRange << QPoint(20, -240) << 190.0 << 600.0 << 0.0 << 10.0
            << 210.0;

    QTest::newRow("vertical, bottom to top with highlightRange")
            << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop
            << QQuickItemView::NoHighlightRange << QPoint(20, 240) << -390.0 << -800.0 << -200.0
            << 10.0 << 210.0;

    QTest::newRow("horizontal, left to right with highlightRange")
            << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom
            << QQuickItemView::NoHighlightRange << QPoint(-240, 20) << 190.0 << 600.0 << 0.0 << 10.0
            << 210.0;

    QTest::newRow("horizontal, right to left with highlightRange")
            << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom
            << QQuickItemView::NoHighlightRange << QPoint(240, 20) << -390.0 << -800.0 << -200.0
            << 10.0 << 210.0;

    QTest::newRow("vertical, top to bottom, enforce range with highlightRange")
            << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom
            << QQuickItemView::StrictlyEnforceRange << QPoint(20, -240) << 190.0 << 590.0 << -10.0
            << 10.0 << 210.0;

    QTest::newRow("vertical, bottom to top, enforce range with highlightRange")
            << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop
            << QQuickItemView::StrictlyEnforceRange << QPoint(20, 240) << -390.0 << -790.0 << -190.0
            << 10.0 << 210.0;

    QTest::newRow("horizontal, left to right, enforce range with highlightRange")
            << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom
            << QQuickItemView::StrictlyEnforceRange << QPoint(-240, 20) << 190.0 << 590.0 << -10.0
            << 10.0 << 210.0;

    QTest::newRow("horizontal, right to left, enforce range with highlightRange")
            << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom
            << QQuickItemView::StrictlyEnforceRange << QPoint(240, 20) << -390.0 << -790.0 << -190.0
            << 10.0 << 210.0;

    QTest::newRow("vertical, top to bottom, apply range with highlightRange")
            << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::TopToBottom
            << QQuickItemView::ApplyRange << QPoint(20, -240) << 190.0 << 600.0 << 0.0 << 10.0
            << 210.0;

    QTest::newRow("vertical, bottom to top, apply range with highlightRange")
            << QQuickListView::Vertical << Qt::LeftToRight << QQuickItemView::BottomToTop
            << QQuickItemView::ApplyRange << QPoint(20, 240) << -390.0 << -800.0 << -200.0 << 10.0
            << 210.0;

    QTest::newRow("horizontal, left to right, apply range with highlightRange")
            << QQuickListView::Horizontal << Qt::LeftToRight << QQuickItemView::TopToBottom
            << QQuickItemView::ApplyRange << QPoint(-240, 20) << 190.0 << 600.0 << 0.0 << 10.0
            << 210.0;

    QTest::newRow("horizontal, right to left, apply range with highlightRange")
            << QQuickListView::Horizontal << Qt::RightToLeft << QQuickItemView::TopToBottom
            << QQuickItemView::ApplyRange << QPoint(240, 20) << -390.0 << -800.0 << -200.0 << 10.0
            << 210.0;
}

void tst_QQuickListView2::nestedWheelSnap()
{
    QQuickView window;
    QVERIFY(QQuickTest::showView(window, testFileUrl("nestedSnap.qml")));

    quint64 timestamp = 10;
    auto sendWheelEvent = [&timestamp, &window](const QPoint &pixelDelta, Qt::ScrollPhase phase) {
        const QPoint pos(100, 100);
        QWheelEvent event(pos, window.mapToGlobal(pos), pixelDelta, pixelDelta, Qt::NoButton,
                          Qt::NoModifier, phase, false, Qt::MouseEventSynthesizedBySystem);
        event.setAccepted(false);
        event.setTimestamp(timestamp);
        QGuiApplication::sendEvent(&window, &event);
        timestamp += 50;
    };

    QQuickListView *outerListView = qobject_cast<QQuickListView *>(window.rootObject());
    QTRY_VERIFY(outerListView);
    QSignalSpy outerCurrentIndexSpy(outerListView, &QQuickListView::currentIndexChanged);
    int movingAtIndex = -1;

    // send horizontal pixel-delta wheel events with phases; confirm that ListView hits the next item boundary
    sendWheelEvent({}, Qt::ScrollBegin);
    for (int i = 1; i < 4; ++i) {
        sendWheelEvent({-50, 0}, Qt::ScrollUpdate);
        if (movingAtIndex < 0 && outerListView->isMoving())
            movingAtIndex = i;
    }
    QVERIFY(outerListView->isDragging());
    sendWheelEvent({}, Qt::ScrollEnd);
    QCOMPARE(outerListView->isDragging(), false);
    QTRY_COMPARE(outerListView->isMoving(), false); // wait until it stops
    qCDebug(lcTests) << "outer got moving after" << movingAtIndex
                     << "horizontal events; stopped at" << outerListView->contentX() << outerListView->currentIndex();
    QCOMPARE_GT(movingAtIndex, 0);
    QCOMPARE(outerListView->contentX(), 300);
    QCOMPARE(outerCurrentIndexSpy.size(), 1);

    movingAtIndex = -1;
    QQuickListView *innerListView = qobject_cast<QQuickListView *>(outerListView->currentItem());
    QTRY_VERIFY(innerListView);
    QSignalSpy innerCurrentIndexSpy(innerListView, &QQuickListView::currentIndexChanged);

    // send vertical pixel-delta wheel events with phases; confirm that ListView hits the next item boundary
    sendWheelEvent({}, Qt::ScrollBegin);
    for (int i = 1; i < 4; ++i) {
        sendWheelEvent({0, -50}, Qt::ScrollUpdate);
        if (movingAtIndex < 0 && innerListView->isMoving())
            movingAtIndex = i;
    }
    QVERIFY(innerListView->isDragging());
    sendWheelEvent({}, Qt::ScrollEnd);
    QCOMPARE(innerListView->isDragging(), false);
    QTRY_COMPARE(innerListView->isMoving(), false); // wait until it stops
    qCDebug(lcTests) << "inner got moving after" << movingAtIndex
                     << "vertical events; stopped at" << innerListView->contentY() << innerListView->currentIndex();
    QCOMPARE_GT(movingAtIndex, 0);
    QCOMPARE(innerListView->contentY(), 300);
    QCOMPARE(innerCurrentIndexSpy.size(), 1);
}

class FriendlyItemView : public QQuickItemView
{
    friend class ItemViewAccessor;
};

class ItemViewAccessor
{
public:
    ItemViewAccessor(QQuickItemView *itemView) :
        mItemView(reinterpret_cast<FriendlyItemView*>(itemView))
    {
    }

    qreal maxXExtent() const
    {
        return mItemView->maxXExtent();
    }

    qreal maxYExtent() const
    {
        return mItemView->maxYExtent();
    }

private:
    FriendlyItemView *mItemView = nullptr;
};

void tst_QQuickListView2::maxExtent_data()
{
    QTest::addColumn<QString>("qmlFilePath");
    QTest::addRow("maxXExtent") << "maxXExtent.qml";
    QTest::addRow("maxYExtent") << "maxYExtent.qml";
}

void tst_QQuickListView2::maxExtent()
{
    QFETCH(QString, qmlFilePath);

    QScopedPointer<QQuickView> window(createView());
    QVERIFY(window);
    window->setSource(testFileUrl(qmlFilePath));
    QVERIFY2(window->status() == QQuickView::Ready, qPrintable(QDebug::toString(window->errors())));
    window->resize(640, 480);
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window.data()));

    QQuickListView *view = window->rootObject()->property("view").value<QQuickListView*>();
    QVERIFY(view);
    ItemViewAccessor viewAccessor(view);
    if (view->orientation() == QQuickListView::Vertical)
        QCOMPARE(viewAccessor.maxXExtent(), 0);
    else if (view->orientation() == QQuickListView::Horizontal)
        QCOMPARE(viewAccessor.maxYExtent(), 0);
}

void tst_QQuickListView2::isCurrentItem_DelegateModel()
{
    QScopedPointer<QQuickView> window(createView());
    window->setSource(testFileUrl("qtbug86744.qml"));
    window->resize(640, 480);
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window.data()));
    QQuickListView* listView = window->rootObject()->findChild<QQuickListView*>("listView");
    QVERIFY(listView);
    QVariant value = listView->itemAtIndex(1)->property("isCurrent");
    QVERIFY(value.toBool() == true);
}

void tst_QQuickListView2::isCurrentItem_NoRegressionWithDelegateModelGroups()
{
    QScopedPointer<QQuickView> window(createView());
    window->setSource(testFileUrl("qtbug98315.qml"));
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window.data()));
    QQuickListView* listView = window->rootObject()->findChild<QQuickListView*>("listView");
    QVERIFY(listView);

    QQuickItem *item3 = listView->itemAtIndex(1);
    QVERIFY(item3);
    QCOMPARE(item3->property("isCurrent").toBool(), true);

    QObject *item0 = listView->itemAtIndex(0);
    QVERIFY(item0);
    QCOMPARE(item0->property("isCurrent").toBool(), false);

    // Press left arrow key -> Item 1 should become current, Item 3 should not
    // be current anymore. After a previous fix of QTBUG-86744 it was working
    // incorrectly - see QTBUG-98315
    QVERIFY(QTest::qWaitForWindowActive(window.get()));
    QTest::keyPress(window.get(), Qt::Key_Left);

    QTRY_COMPARE(item0->property("isCurrent").toBool(), true);
    QCOMPARE(item3->property("isCurrent").toBool(), false);
}

void tst_QQuickListView2::pullbackSparseList() // QTBUG_104679
{
    // check if PullbackHeader crashes
    QScopedPointer<QQuickView> window(createView());
    QVERIFY(window);
    window->setSource(testFileUrl("qtbug104679_header.qml"));
    QVERIFY2(window->status() == QQuickView::Ready, qPrintable(QDebug::toString(window->errors())));
    window->resize(640, 480);
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window.data()));

    // check if PullbackFooter crashes
    window.reset(createView());
    QVERIFY(window);
    window->setSource(testFileUrl("qtbug104679_footer.qml"));
    QVERIFY2(window->status() == QQuickView::Ready, qPrintable(QDebug::toString(window->errors())));
    window->resize(640, 480);
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window.data()));
}

void tst_QQuickListView2::highlightWithBound()
{
    QQmlEngine engine;
    QQmlComponent c(&engine, testFileUrl("highlightWithBound.qml"));
    QVERIFY2(c.isReady(), qPrintable(c.errorString()));
    QScopedPointer<QObject> o(c.create());
    QVERIFY(!o.isNull());
    QQuickListView *listView = qobject_cast<QQuickListView *>(o.data());
    QVERIFY(listView);
    QQuickItem *highlight = listView->highlightItem();
    QVERIFY(highlight);
    QCOMPARE(highlight->objectName(), QStringLiteral("highlight"));
}

void tst_QQuickListView2::sectionIsCompatibleWithBoundComponents()
{
    QTest::failOnWarning(".?");
    QQmlEngine engine;
    QQmlComponent c(&engine, testFileUrl("sectionBoundComponent.qml"));
    QVERIFY2(c.isReady(), qPrintable(c.errorString()));
    QScopedPointer<QObject> o(c.create());
    QVERIFY(!o.isNull());
    QQuickListView *listView = qobject_cast<QQuickListView *>(o.data());
    QVERIFY(listView);
    QTRY_COMPARE(listView->currentSection(), "42");
}

void tst_QQuickListView2::sectionGeometryChange()
{
    QScopedPointer<QQuickView> window(createView());
    QTRY_VERIFY(window);
    window->setSource(testFileUrl("sectionGeometryChange.qml"));
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window.data()));

    QQuickListView *listview = findItem<QQuickListView>(window->rootObject(), "list");
    QTRY_VERIFY(listview);

    QQuickItem *contentItem = listview->contentItem();
    QTRY_VERIFY(contentItem);
    QVERIFY(QQuickTest::qWaitForPolish(listview));

    QQuickItem *section1 = findItem<QQuickItem>(contentItem, "Section1");
    QVERIFY(section1);
    QQuickItem *element1 = findItem<QQuickItem>(contentItem, "Element1");
    QVERIFY(element1);

    QCOMPARE(element1->y(), section1->y() + section1->height());

    // Update the height of the section delegate and verify that the next element is not overlapping
    section1->setHeight(section1->height() + 10);
    QTRY_COMPARE(element1->y(), section1->y() + section1->height());
}

void tst_QQuickListView2::areaZeroviewDoesNotNeedlesslyPopulateWholeModel()
{
    QTest::failOnWarning(QRegularExpression(".*"));
    QQmlEngine engine;
    QQmlComponent c(&engine, testFileUrl("areaZeroView.qml"));
    QVERIFY2(c.isReady(), qPrintable(c.errorString()));
    std::unique_ptr<QObject> root(c.create());
    QVERIFY(root);
    auto delegateCreationCounter = [&]() {
        return root->property("delegateCreationCounter").toInt();
    };
    // wait for onComplete to be settled
    QTRY_VERIFY(delegateCreationCounter() != 0);
    auto view = qobject_cast<QQuickListView *>(qmlContext(root.get())->objectForName("lv"));
    QVERIFY(view);
    QCOMPARE(view->count(), 6'000);
    // we use 100, which is < 6000, but larger than the actual expected value
    // that's to give the test some leniency in case the ListView implementation
    // changes in the future to instantiate a few more items outside of the viewport
    QVERIFY(delegateCreationCounter() < 100);
}

void tst_QQuickListView2::delegateContextHandling()
{
    QQmlEngine engine;
    QQmlComponent c(&engine, testFileUrl("delegateContextHandling.qml"));
    QVERIFY2(c.isReady(), qPrintable(c.errorString()));
    std::unique_ptr<QObject> o(c.create());
    QVERIFY(o);

    for (int i = 0; i < 10; ++i) {
        QQuickItem *delegate = nullptr;
        QMetaObject::invokeMethod(o.get(), "toggle", Q_RETURN_ARG(QQuickItem *, delegate));
        QVERIFY(delegate);
    }

}

class TestFetchMoreModel : public QAbstractListModel
{
    Q_OBJECT

public:
    QVariant data(const QModelIndex& index, int role) const override
    {
        if (role == Qt::DisplayRole)
            return QString::number(index.row());
        return {};
    }

    int columnCount(const QModelIndex&) const override { return 1; }

    int rowCount(const QModelIndex& parent) const override
    {
        return parent.isValid() ? 0 : m_lines;
    }

    QModelIndex parent(const QModelIndex&) const override { return {}; }

    bool canFetchMore(const QModelIndex &) const override { return true; }

    void fetchMore(const QModelIndex & parent) override
    {
        if (Q_UNLIKELY(parent.isValid()))
            return;
        beginInsertRows(parent, m_lines, m_lines);
        m_lines++;
        endInsertRows();
    }

    int m_lines = 3;
};

void tst_QQuickListView2::fetchMore_data()
{
    QTest::addColumn<bool>("reuseItems");
    QTest::addColumn<int>("cacheBuffer");

    QTest::newRow("no reuseItems, default buffer") << false << -1;
    QTest::newRow("reuseItems, default buffer") << true << -1;
    QTest::newRow("no reuseItems, no buffer") << false << 0;
    QTest::newRow("reuseItems, no buffer") << true << 0;
    QTest::newRow("no reuseItems, buffer 100 px") << false << 100;
    QTest::newRow("reuseItems, buffer 100 px") << true << 100;
}

void tst_QQuickListView2::fetchMore() // QTBUG-95107
{
    QFETCH(bool, reuseItems);
    QFETCH(int, cacheBuffer);

    TestFetchMoreModel model;
    qmlRegisterSingletonInstance("org.qtproject.Test", 1, 0, "FetchMoreModel", &model);
    QQuickView window;
    QVERIFY(QQuickTest::showView(window, testFileUrl("fetchMore.qml")));
    auto *listView = qobject_cast<QQuickListView*>(window.rootObject());
    QVERIFY(listView);
    listView->setReuseItems(reuseItems);
    if (cacheBuffer >= 0)
        listView->setCacheBuffer(cacheBuffer);

    for (int i = 0; i < 3; ++i) {
        const int rowCount = listView->count();
        if (lcTests().isDebugEnabled()) QTest::qWait(1000);
        listView->flick(0, -5000);
        QTRY_VERIFY(!listView->isMoving());
        qCDebug(lcTests) << "after flick: contentY" << listView->contentY()
                         << "rows" << rowCount << "->" << listView->count();
        QCOMPARE_GT(listView->count(), rowCount);
        QCOMPARE_GE(model.m_lines, listView->count()); // fetchMore() was called
    }
}

void tst_QQuickListView2::changingOrientationResetsPreviousAxisValues_data()
{
    QTest::addColumn<QByteArray>("sourceFile");
    QTest::newRow("ObjectModel") << QByteArray("changingOrientationWithObjectModel.qml");
    QTest::newRow("ListModel") << QByteArray("changingOrientationWithListModel.qml");
}

void tst_QQuickListView2::changingOrientationResetsPreviousAxisValues() // QTBUG-115696
{
    QFETCH(QByteArray, sourceFile);

    QQuickView window;
    QVERIFY(QQuickTest::showView(window, testFileUrl(QString::fromLatin1(sourceFile))));
    auto *listView = qobject_cast<QQuickListView *>(window.rootObject());
    QVERIFY(listView);

    // Starts of with vertical orientation. X should be 0 for all delegates, but not Y.
    QVERIFY(listView->property("isXReset").toBool());
    QVERIFY(!listView->property("isYReset").toBool());

    listView->setOrientation(QQuickListView::Orientation::Horizontal);

    // Y should be 0 for all delegates, but not X.
    QVERIFY(!listView->property("isXReset").toBool());
    QVERIFY(listView->property("isYReset").toBool());

    listView->setOrientation(QQuickListView::Orientation::Vertical);

    // X should be 0 for all delegates, but not Y.
    QVERIFY(listView->property("isXReset").toBool());
    QVERIFY(!listView->property("isYReset").toBool());
}

void tst_QQuickListView2::bindingDirectlyOnPositionInHeaderAndFooterDelegates_data()
{
    QTest::addColumn<QByteArray>("sourceFile");
    QTest::addColumn<qreal(QQuickItem::*)()const>("pos");
    QTest::addColumn<qreal(QQuickItem::*)()const>("size");
    QTest::newRow("XPosition") << QByteArray("bindOnHeaderAndFooterXPosition.qml") << &QQuickItem::x << &QQuickItem::width;
    QTest::newRow("YPosition") << QByteArray("bindOnHeaderAndFooterYPosition.qml") << &QQuickItem::y << &QQuickItem::height;
}
void tst_QQuickListView2::bindingDirectlyOnPositionInHeaderAndFooterDelegates()
{

    typedef qreal (QQuickItem::*position_func_t)() const;
    QFETCH(QByteArray, sourceFile);
    QFETCH(position_func_t, pos);
    QFETCH(position_func_t, size);

    QQuickView window;
    QVERIFY(QQuickTest::showView(window, testFileUrl(QString::fromLatin1(sourceFile))));
    auto *listView = qobject_cast<QQuickListView *>(window.rootObject());
    QVERIFY(listView);

    const qreal widthOrHeight = (listView->*size)();

    QCOMPARE((listView->headerItem()->*pos)(), (widthOrHeight - 50) / 2);
    QCOMPARE((listView->footerItem()->*pos)(), (widthOrHeight - 50) / 2);

    // Verify that the "regular" delegate items, don't honor x and y bindings.
    // This should only be allowed for header and footer delegates.
    for (int i = 0; i < listView->count(); ++i)
        QCOMPARE((listView->itemAtIndex(i)->*pos)(), 0);
}

void tst_QQuickListView2::clearObjectListModel()
{
    QQmlEngine engine;
    QQmlComponent delegate(&engine);

    // Need one required property to trigger the incremental rebuilding of metaobjects.
    delegate.setData("import QtQuick\nItem { required property int index }", QUrl());

    QQuickListView list;
    engine.setContextForObject(&list, engine.rootContext());
    list.setDelegate(&delegate);
    list.setWidth(640);
    list.setHeight(480);

    QScopedPointer modelObject(new QObject);

    // Use a list that might also carry something non-QObject

    list.setModel(QVariantList {
        QVariant::fromValue(modelObject.data()),
        QVariant::fromValue(modelObject.data())
    });

    QVERIFY(list.itemAtIndex(0));

    modelObject.reset();

    // list should not access dangling pointer from old model data anymore.
    list.setModel(QVariantList());

    QVERIFY(!list.itemAtIndex(0));
}

void tst_QQuickListView2::gadgetModelSections()
{
    QQuickView window;
    QVERIFY(QQuickTest::showView(window, testFileUrl("gadgetlist.qml")));
    QQuickListView *listview = qobject_cast<QQuickListView*>(window.rootObject());
    QTRY_VERIFY(listview);
    QVERIFY(QQuickTest::qWaitForPolish(listview));
    QCOMPARE(listview->count(), 4);
    QCOMPARE(listview->currentSection(), "small");
}

void tst_QQuickListView2::visibleBoundToCountGreaterThanZero()
{
    QQuickView window;
    QVERIFY(QQuickTest::showView(window, testFileUrl("visibleBoundToCountGreaterThanZero.qml")));

    auto *listView = window.rootObject()->property("listView").value<QQuickListView *>();
    QVERIFY(listView);

    QSignalSpy countChangedSpy(listView, SIGNAL(countChanged()));
    QVERIFY(countChangedSpy.isValid());

    QTRY_COMPARE_GT_WITH_TIMEOUT(listView->count(), 1, oneSecondInMs);
    // Using the TRY variant here as well is necessary.
    QTRY_COMPARE_GT_WITH_TIMEOUT(countChangedSpy.count(), 1, oneSecondInMs);
    QVERIFY(listView->isVisible());
}

void tst_QQuickListView2::setDelegateAfterModel()
{
    QQmlEngine engine;
    const QUrl url = testFileUrl("setDelegateAfterModel.qml");
    QQmlComponent component(&engine, url);
    QVERIFY2(component.isReady(), qPrintable(component.errorString()));

    QScopedPointer<QObject> object(component.create());
    QVERIFY(!object.isNull());

    // If the model was lost by setting the delegate, the count would be 0.
    QCOMPARE(object->property("count").toInt(), 3);

    QTest::ignoreMessage(
            QtWarningMsg,
            qPrintable(url.toString() + ":17:5: QML ListView: Cannot retain explicitly set "
                                        "delegate on non-DelegateModel"));
    object->setProperty("useObjectModel", QVariant::fromValue<bool>(true));
    QCOMPARE(object->property("count").toInt(), 2);

    // The old model must not mess with the view anymore.
    QTest::failOnWarning(qPrintable(url.toString() + ":17:5: QML ListView: Explicitly set delegate "
                                                     "is externally overridden"));
    QMetaObject::invokeMethod(object.data(), "plantDelegate");
    QCOMPARE(object->property("count").toInt(), 4);
}

template<typename Enum>
const char *enumKey(Enum value) {
    const QMetaObject *mo = qt_getEnumMetaObject(value);
    const QMetaEnum metaEnum = mo->enumerator(mo->indexOfEnumerator(qt_getEnumName(value)));
    return metaEnum.valueToKey(value);
}

void tst_QQuickListView2::delegateModelAccess_data()
{
    QTest::addColumn<QQmlDelegateModel::DelegateModelAccess>("access");
    QTest::addColumn<Model::Kind>("modelKind");
    QTest::addColumn<Delegate::Kind>("delegateKind");

    using Access = QQmlDelegateModel::DelegateModelAccess;
    for (auto access : { Access::Qt5ReadWrite, Access::ReadOnly, Access::ReadWrite }) {
        for (auto model : { Model::Singular, Model::List, Model::Array, Model::Object }) {
            for (auto delegate : { Delegate::Untyped, Delegate::Typed }) {
                QTest::addRow("%s-%s-%s", enumKey(access), enumKey(model), enumKey(delegate))
                    << access << model << delegate;
            }
        }
    }
}

void tst_QQuickListView2::delegateModelAccess()
{
    QFETCH(QQmlDelegateModel::DelegateModelAccess, access);
    QFETCH(Model::Kind, modelKind);
    QFETCH(Delegate::Kind, delegateKind);

    QQmlEngine engine;
    const QUrl url = testFileUrl("delegateModelAccess.qml");
    QQmlComponent c(&engine, url);
    QVERIFY2(c.isReady(), qPrintable(c.errorString()));
    QScopedPointer<QObject> object(c.create());

    QQuickListView *listView = qobject_cast<QQuickListView *>(object.data());
    QVERIFY(listView);

    QSignalSpy modelChangedSpy(listView, &QQuickItemView::modelChanged);

    if (delegateKind == Delegate::Untyped && modelKind == Model::Array)
        QSKIP("Properties of objects in arrays are not exposed as context properties");

    if (access == QQmlDelegateModel::ReadOnly) {
        const QRegularExpression message(
                url.toString() + ":[0-9]+: TypeError: Cannot assign to read-only property \"a\"");

        QTest::ignoreMessage(QtWarningMsg, message);
        if (delegateKind == Delegate::Untyped)
            QTest::ignoreMessage(QtWarningMsg, message);
    }
    object->setProperty("delegateModelAccess", access);
    object->setProperty("modelIndex", modelKind);
    object->setProperty("delegateIndex", delegateKind);

    listView->setCurrentIndex(0);
    QObject *delegate = listView->itemAtIndex(0);
    QVERIFY(delegate);

    const bool modelWritable = access != QQmlDelegateModel::ReadOnly;
    const bool immediateWritable = (delegateKind == Delegate::Untyped)
            ? access != QQmlDelegateModel::ReadOnly
            : access == QQmlDelegateModel::ReadWrite;

    // Only the array is actually updated itself. The other models are pointers
    const bool writeShouldSignal = modelKind == Model::Kind::Array;

    double expected = 11;

    // Initial setting of the model, signals one update
    int expectedModelUpdates = 1;
    QCOMPARE(modelChangedSpy.count(), expectedModelUpdates);

    QCOMPARE(delegate->property("immediateX").toDouble(), expected);
    QCOMPARE(delegate->property("modelX").toDouble(), expected);

    if (modelWritable) {
        expected = 3;
        if (writeShouldSignal)
            ++expectedModelUpdates;
    }

    QMetaObject::invokeMethod(delegate, "writeThroughModel");
    QCOMPARE(delegate->property("immediateX").toDouble(), expected);
    QCOMPARE(delegate->property("modelX").toDouble(), expected);
    QCOMPARE(modelChangedSpy.count(), expectedModelUpdates);

    if (immediateWritable) {
        expected = 1;
        if (writeShouldSignal)
            ++expectedModelUpdates;
    }

    QMetaObject::invokeMethod(delegate, "writeImmediate");

    // Writes to required properties always succeed, but might not be propagated to the model
    QCOMPARE(delegate->property("immediateX").toDouble(),
             delegateKind == Delegate::Untyped ? expected : 1);

    QCOMPARE(delegate->property("modelX").toDouble(), expected);
    QCOMPARE(modelChangedSpy.count(), expectedModelUpdates);
}

enum RemovalPolicy {
    RemoveAndDestroy,
    OnlyDestroy
};

void tst_QQuickListView2::removeAndDestroyObjectModelItem_data()
{
    QTest::addColumn<QString>("qmlFilePath");
    QTest::addColumn<RemovalPolicy>("removalPolicy");

    QTest::addRow("remove and destroy, no transitions")
        << "removeAndDestroyObjectModelItem.qml" << RemoveAndDestroy;
    QTest::addRow("destroy, no transitions")
        << "removeAndDestroyObjectModelItem.qml" << OnlyDestroy;
    QTest::addRow("remove and destroy, transitions")
        << "removeAndDestroyObjectModelItemWithTransitions.qml" << RemoveAndDestroy;
    QTest::addRow("destroy, transitions")
        << "removeAndDestroyObjectModelItemWithTransitions.qml" << OnlyDestroy;
}

// QTBUG-46798, QTBUG-133256
void tst_QQuickListView2::removeAndDestroyObjectModelItem()
{
    QFETCH(QString, qmlFilePath);
    QFETCH(RemovalPolicy, removalPolicy);

    QQuickView window;
    QVERIFY(QQuickTest::showView(window, testFileUrl(qmlFilePath)));

    QQuickListView *listView = qobject_cast<QQuickListView*>(window.rootObject());
    QVERIFY(listView);

    auto *objectModel = listView->model().value<QQmlObjectModel *>();
    QVERIFY(objectModel);
    QPointer<QObject> firstItem = objectModel->get(0);
    QVERIFY(firstItem);
    // Shouldn't crash.
    if (removalPolicy == RemoveAndDestroy)
        objectModel->remove(0);
    firstItem->deleteLater();
    QTRY_VERIFY(!firstItem);

    // Now try moving the view. It also shouldn't crash.
    if (removalPolicy == RemoveAndDestroy) {
        if (QQuickTest::qIsPolishScheduled(listView))
            QVERIFY(QQuickTest::qWaitForPolish(listView));
    }
    listView->positionViewAtEnd();
}

void tst_QQuickListView2::fastMouseWheel()
{
    QScopedPointer<QQuickView> window(createView());
    QTRY_VERIFY(window);
    window->setSource(testFileUrl("qtbug134502.qml"));
    window->resize(640, 480);
    window->show();
    QQuickViewTestUtils::moveMouseAway(window.data());
    QVERIFY(QTest::qWaitForWindowExposed(window.data()));
    auto sendWheelEvent = [](QQuickView *window, const QPoint &angleDelta, quint64 timestamp) {
        QPoint pos(100, 100);
        QWheelEvent event(pos, window->mapToGlobal(pos), QPoint(), angleDelta, Qt::NoButton,
                          Qt::NoModifier, Qt::NoScrollPhase, false);
        event.setAccepted(false);
        event.setTimestamp(timestamp);
        QGuiApplication::sendEvent(window, &event);
    };

    QQuickListView *listview = findItem<QQuickListView>(window->rootObject(), "objects");
    QTRY_VERIFY(listview != nullptr);

    QGuiApplication::styleHints()->setWheelScrollLines(60);
    sendWheelEvent(window.data(), QPoint(0, -120), 100);
    QTRY_VERIFY(listview->isMoving() == false);
    sendWheelEvent(window.data(), QPoint(0, -120), 132);
    QTRY_VERIFY(listview->isMoving() == false);
    sendWheelEvent(window.data(), QPoint(0, -240), 194);
    QTRY_VERIFY(listview->isMoving() == false);
}

QTEST_MAIN(tst_QQuickListView2)

#include "tst_qquicklistview2.moc"
