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

#include <QtTest/qtest.h>
#include <QtTest/qsignalspy.h>
#include <QtTest/private/qtesthelpers_p.h>

#include <QtQuick/qquickwindow.h>
#include <QtQuick/qquickitem.h>
#include <QtQuick/private/qquickpointerdevicehandler_p.h>
#include <QtQuick/private/qquicktaphandler_p.h>
#include <QtQuickTestUtils/private/qmlutils_p.h>
#include <QtQuickTestUtils/private/visualtestutils_p.h>
#include <QtQuickControlsTestUtils/private/controlstestutils_p.h>
#include <QtQuickTemplates2/private/qquickapplicationwindow_p.h>
#include <QtQuickTemplates2/private/qquickoverlay_p.h>

using namespace QQuickControlsTestUtils;
using namespace QQuickVisualTestUtils;

class tst_QQuickOverlay : public QQmlDataTest
{
    Q_OBJECT

public:
    tst_QQuickOverlay();

private slots:
    void clearGrabbers();
    void retainOrientation();
    void pressedAndReleased();
    void eventsToWindowlessPopupInAsyncLoader();
};

class TestInputHandler : public QQuickPointerDeviceHandler
{
public:
    TestInputHandler(QQuickItem * parent)
        : QQuickPointerDeviceHandler(parent)
    {
        setGrabPermissions(GrabPermission::TakeOverForbidden);
    }

    bool wantsPointerEvent(QPointerEvent * event) override
    {
        return QQuickPointerDeviceHandler::wantsPointerEvent(event)
               && !eligiblePoints(event).isEmpty();
    }

    bool wantsEventPoint(const QPointerEvent * event, const QEventPoint & point) override
    {
        bool ret = QQuickPointerDeviceHandler::wantsEventPoint(event, point);
        return ret || (ret && event->exclusiveGrabber(point) != this);
    }

    bool grabPoints(QPointerEvent * event, const QVector<QEventPoint> & points)
    {
        if (points.isEmpty())
            return false;

        bool allowed = true;

        for (const auto & point : points) {
            if (event->exclusiveGrabber(point) != this && !canGrab(event, point)) {
                allowed = false;
                break;
            }
        }

        if (allowed) {
            for (auto point : points) {
                if (event->exclusiveGrabber(point) != this)
                    event->setExclusiveGrabber(point, this);
                point.setAccepted(true);
            }
        }

        return allowed;
    }

    bool grabEligiblePoints(QPointerEvent * event)
    {
        return grabPoints(event, eligiblePoints(event));
    }

    QList<QEventPoint> eligiblePoints(QPointerEvent * event)
    {
        QList<QEventPoint> ret;

        // elligible points should return all points which either
        // 1. have no owner
        // 2. are owned by us
        // 3. we can steal

        for (int i = 0; i < event->pointCount(); ++i) {
            auto p = event->point(i);
            auto * exclusiveGrabber = event->exclusiveGrabber(p);

            if (exclusiveGrabber && exclusiveGrabber != this && !canGrab(event, p))
                continue;

            if (p.state() != QEventPoint::Released && wantsEventPoint(event, p))
                ret << p;
        }

        return ret;
    }

    void handlePointerEventImpl(QPointerEvent * event) override
    {
        QQuickPointerDeviceHandler::handlePointerEventImpl(event);
        auto grabbed = grabEligiblePoints(event);

        if (!active()) {
            setActive(grabbed);
            return;
        }

        for (int i = 0; i < event->pointCount(); ++i) {
            auto point = event->point(i);

            if (event->exclusiveGrabber(point) == this)
                Q_ASSERT(m_points.contains(point.id()));
        }
    }

    void onGrabChanged(QQuickPointerHandler * grabber,
                        QPointingDevice::GrabTransition transition,
                        QPointerEvent * event,
                        QEventPoint & point) override
    {
        if (grabber == this) {
            switch (transition) {
            case QPointingDevice::GrabTransition::GrabExclusive:
                Q_ASSERT(!m_points.contains(point.id()));
                m_points.append(point.id());
                break;

            case QPointingDevice::GrabTransition::UngrabExclusive:
            case QPointingDevice::GrabTransition::CancelGrabExclusive:
                if (m_points.contains(point.id()))
                    m_points.removeOne(point.id());

                break;

            default:
                break;
            }
        }

        QQuickPointerDeviceHandler::onGrabChanged(grabber, transition, event, point);
    }

    QList<int> m_points;
};

tst_QQuickOverlay::tst_QQuickOverlay()
    : QQmlDataTest(QT_QMLTEST_DATADIR, FailOnWarningsPolicy::FailOnWarnings)
{
}

void tst_QQuickOverlay::clearGrabbers()
{
    QQuickWindow window;

    auto *overlay = QQuickOverlay::overlay(&window);
    auto *overlayItem = new QQuickItem(overlay);
    QVERIFY(overlayItem);

    const auto size = QSize(640, 480);
    window.resize(size);

    auto *item = new QQuickItem(window.contentItem());
    item->setSize(size);

    auto testPointerhandler = TestInputHandler(item);
    window.show();
    QVERIFY(QTest::qWaitForWindowExposed(&window));

    auto *dev = QTest::createTouchDevice();

    QTest::touchEvent(&window, dev)
        .press(0, QPoint(10, 10));

    QCOMPARE(testPointerhandler.m_points.size(), 1);
    QCOMPARE(testPointerhandler.m_points[0], 0);

    QTest::touchEvent(&window, dev)
        .stationary(0)
        .press(1, QPoint(20, 20));

    QCOMPARE(testPointerhandler.m_points.size(), 2);
    QCOMPARE(testPointerhandler.m_points[0], 0);
    QCOMPARE(testPointerhandler.m_points[1], 1);

    QTest::touchEvent(&window, dev)
        .move(0, QPoint(20, 20))
        .move(1, QPoint(30, 30));

    QCOMPARE(testPointerhandler.m_points.size(), 2);
    QCOMPARE(testPointerhandler.m_points[0], 0);
    QCOMPARE(testPointerhandler.m_points[1], 1);

    QTest::touchEvent(&window, dev)
        .release(0, QPoint(30, 30))
        .stationary(1);

    QCOMPARE(testPointerhandler.m_points.size(), 1);
    QCOMPARE(testPointerhandler.m_points[0], 1);

    QTest::touchEvent(&window, dev)
        .release(1, QPoint(40, 40));

    QVERIFY(testPointerhandler.m_points.isEmpty());
}

void tst_QQuickOverlay::retainOrientation()
{
    QQuickWindow window;
    auto *overlay = QQuickOverlay::overlay(&window);

    window.show();
    QVERIFY(QTest::qWaitForWindowExposed(&window));

    qreal rot = 10;
    overlay->setRotation(rot);
    const QSizeF sz = overlay->size();
    window.resize(window.size() + QSize(10, 10));
    // wait for the resize event to call QQuickOverlay::updateGeometry
    QTRY_COMPARE_NE(overlay->size(), sz);

    QCOMPARE(overlay->rotation(), rot);
}

void tst_QQuickOverlay::pressedAndReleased()
{
    QQuickControlsApplicationHelper helper(this, "tapHandler.qml");
    QVERIFY2(helper.ready, helper.failureMessage());
    QQuickApplicationWindow *window = helper.appWindow;
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window));
    // Verify that the position and framePosition are correct
    // to avoid flakiness on openSuSe KDE.
    QVERIFY(QTestPrivate::ensurePositionTopLeft(window));

    auto *overlay = QQuickOverlay::overlay(window);
    QVERIFY(overlay);
    const QSignalSpy pressedSpy(overlay, SIGNAL(pressed()));
    QVERIFY(pressedSpy.isValid());
    const QSignalSpy releasedSpy(overlay, SIGNAL(released()));
    QVERIFY(releasedSpy.isValid());

    auto *tapHandler = window->findChild<QQuickTapHandler *>();
    QVERIFY(tapHandler);
    const QSignalSpy tappedSpy(tapHandler, SIGNAL(tapped(QEventPoint,Qt::MouseButton)));
    QVERIFY(tappedSpy.isValid());

    // Left click should cause pressed to be emitted.
    const QPoint &windowCenter = mapCenterToWindow(window->contentItem());
    QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, windowCenter);
    QCOMPARE(pressedSpy.count(), 1);
    QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, windowCenter);
    QCOMPARE(releasedSpy.count(), 1);
    QCOMPARE(tappedSpy.count(), 1);

    // Right press shouldn't cause pressed and released to be emitted
    // now that we need that to synthesize QContextMenuEvents for ContextMenu.
    QTest::mousePress(window, Qt::RightButton, Qt::NoModifier, windowCenter);
    QCOMPARE(pressedSpy.count(), 1);
    QTest::mouseRelease(window, Qt::RightButton, Qt::NoModifier, windowCenter);
    QCOMPARE(releasedSpy.count(), 1);
    QCOMPARE(tappedSpy.count(), 1);
}

void tst_QQuickOverlay::eventsToWindowlessPopupInAsyncLoader()
{
    QQuickControlsApplicationHelper helper(this, "eventsToWindowlessPopupInAsyncLoader.qml");
    QVERIFY2(helper.ready, helper.failureMessage());
    QQuickApplicationWindow *window = helper.appWindow;
    window->show();
    QVERIFY(QTest::qWaitForWindowExposed(window));

    const QPoint startPos(0, 0);
    const QPoint targetPos(40, 40);
    const int steps = 80;
    const int delay = 1;

    // Test various event types. None of them should cause crashes.
    PointLerper mousePointLerper(window, startPos);
    mousePointLerper.move(targetPos, steps, delay);

    std::unique_ptr<QPointingDevice> touchscreen { QTest::createTouchDevice() };
    PointLerper touchPointLerper(window, startPos, touchscreen.get());
    touchPointLerper.move(targetPos, steps, delay);

    forEachStep(steps, [&](qreal progress) {
        QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier,
            lerpPoints(startPos, targetPos, progress), delay);
    });
}

QTEST_MAIN(tst_QQuickOverlay)

#include "tst_qquickoverlay.moc"
