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

#include <QtTest/QTest>
#include <QtTest/QSignalSpy>
#include <QtQuickTest/quicktest.h>

#include <QtQuick/qquickview.h>
#include <QtQuick/private/qquickmousearea_p.h>
#include <QtQuick/private/qquickpointerhandler_p.h>
#include <QtQuick/private/qquicktaphandler_p.h>

#include <QtQml/qqmlengine.h>
#include <QtQml/qqmlcontext.h>

#include <QtQuickTemplates2/private/qquickbutton_p.h>
#include <QtQuickTemplates2/private/qquickmenu_p.h>

#include <QtGui/qguiapplication.h>
#include <QtGui/private/qpointingdevice_p.h>

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

Q_LOGGING_CATEGORY(lcPointerTests, "qt.quick.pointer.tests")

using namespace QQuickViewTestUtils;
using namespace QQuickVisualTestUtils;

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

private slots:
    void hover_controlInsideControl();
    void hover_controlAndMouseArea();
    void buttonTapHandler_data();
    void buttonTapHandler();
    void checkableButtonRightClickTapHandler();
    void buttonDragHandler_data();
    void buttonDragHandler();

private:
    QScopedPointer<QPointingDevice> touchscreen = QScopedPointer<QPointingDevice>(QTest::createTouchDevice());
};

tst_pointerhandlers::tst_pointerhandlers()
    : QQmlDataTest(QT_QMLTEST_DATADIR)
{
}

void tst_pointerhandlers::hover_controlInsideControl()
{
    SKIP_IF_NO_MOUSE_HOVER;

    // Test that if you move the mouse over a control that is
    // a child of another control, both controls end up hovered.
    // A control should basically not block (accept) hover events.
    QQuickView view(testFileUrl("controlinsidecontrol.qml"));
    QCOMPARE(view.status(), QQuickView::Ready);
    view.show();
    QVERIFY(QTest::qWaitForWindowExposed(&view));

    QQuickItem *rootItem = view.rootObject();
    QVERIFY(rootItem);
    QQuickWindow *window = rootItem->window();
    QVERIFY(window);

    const auto context = qmlContext(rootItem);
    auto outerButton = context->contextProperty("outerButton").value<QQuickButton *>();
    auto innerButton = context->contextProperty("innerButton").value<QQuickButton *>();
    QVERIFY(outerButton);
    QVERIFY(innerButton);

    const QPoint posInWindow(1, 1);
    const QPoint posOnOuterButton = rootItem->mapFromItem(outerButton, QPointF(0, 0)).toPoint();
    const QPoint posOnInnerButton = rootItem->mapFromItem(innerButton, QPointF(0, 0)).toPoint();

    // Start by moving the mouse to the window
    QTest::mouseMove(window, posInWindow);
    QCOMPARE(outerButton->isHovered(), false);
    QCOMPARE(innerButton->isHovered(), false);

    // Move the mouse over the outer control
    QTest::mouseMove(window, posOnOuterButton);
    QCOMPARE(outerButton->isHovered(), true);
    QCOMPARE(innerButton->isHovered(), false);

    // Move the mouse over the inner control
    QTest::mouseMove(window, posOnInnerButton);
    QCOMPARE(outerButton->isHovered(), true);
    QCOMPARE(innerButton->isHovered(), true);

    // Move the mouse over the outer control again
    QTest::mouseMove(window, posOnOuterButton);
    QCOMPARE(outerButton->isHovered(), true);
    QCOMPARE(innerButton->isHovered(), false);

    // Move the mouse outside all controls
    QTest::mouseMove(window, posInWindow);
    QCOMPARE(outerButton->isHovered(), false);
    QCOMPARE(innerButton->isHovered(), false);
}

void tst_pointerhandlers::hover_controlAndMouseArea()
{
    SKIP_IF_NO_MOUSE_HOVER;

    QQuickView view(testFileUrl("controlandmousearea.qml"));
    QCOMPARE(view.status(), QQuickView::Ready);
    view.show();
    QVERIFY(QTest::qWaitForWindowExposed(&view));

    QQuickItem *rootItem = view.rootObject();
    QVERIFY(rootItem);
    QQuickWindow *window = rootItem->window();
    QVERIFY(window);

    const auto context = qmlContext(rootItem);
    auto outerMouseArea = context->contextProperty("outerMouseArea").value<QQuickMouseArea *>();
    auto buttonInTheMiddle = context->contextProperty("buttonInTheMiddle").value<QQuickButton *>();
    auto innerMouseArea = context->contextProperty("innerMouseArea").value<QQuickMouseArea *>();
    QVERIFY(outerMouseArea);
    QVERIFY(buttonInTheMiddle);
    QVERIFY(innerMouseArea);

    const QPoint posInWindow(1, 1);
    const QPoint posOnOuterMouseArea = rootItem->mapFromItem(outerMouseArea, QPointF(0, 0)).toPoint();
    const QPoint posOnButtonInTheMiddle = rootItem->mapFromItem(buttonInTheMiddle, QPointF(0, 0)).toPoint();
    const QPoint posOnInnerMouseArea = rootItem->mapFromItem(innerMouseArea, QPointF(0, 0)).toPoint();

    // Start by moving the mouse to the window
    QTest::mouseMove(window, posInWindow);
    QCOMPARE(outerMouseArea->hovered(), false);
    QCOMPARE(buttonInTheMiddle->isHovered(), false);
    QCOMPARE(innerMouseArea->hovered(), false);

    // Move the mouse over the outer mousearea
    QTest::mouseMove(window, posOnOuterMouseArea);
    QCOMPARE(outerMouseArea->hovered(), true);
    QCOMPARE(buttonInTheMiddle->isHovered(), false);
    QCOMPARE(innerMouseArea->hovered(), false);

    // Move the mouse over the button in the middle
    QTest::mouseMove(window, posOnButtonInTheMiddle);
    QCOMPARE(outerMouseArea->hovered(), true);
    QCOMPARE(buttonInTheMiddle->isHovered(), true);
    QCOMPARE(innerMouseArea->hovered(), false);

    // Move the mouse over the inner mousearea
    QTest::mouseMove(window, posOnInnerMouseArea);
    QCOMPARE(outerMouseArea->hovered(), true);
    QCOMPARE(buttonInTheMiddle->isHovered(), true);
    QCOMPARE(innerMouseArea->hovered(), true);

    // Move the mouse over the button in the middle again
    QTest::mouseMove(window, posOnButtonInTheMiddle);
    QCOMPARE(outerMouseArea->hovered(), true);
    QCOMPARE(buttonInTheMiddle->isHovered(), true);
    QCOMPARE(innerMouseArea->hovered(), false);

    // Move the mouse over the outer mousearea again
    QTest::mouseMove(window, posOnOuterMouseArea);
    QCOMPARE(outerMouseArea->hovered(), true);
    QCOMPARE(buttonInTheMiddle->isHovered(), false);
    QCOMPARE(innerMouseArea->hovered(), false);

    // Move the mouse outside all items
    QTest::mouseMove(window, posInWindow);
    QCOMPARE(outerMouseArea->hovered(), false);
    QCOMPARE(buttonInTheMiddle->isHovered(), false);
    QCOMPARE(innerMouseArea->hovered(), false);
}

void tst_pointerhandlers::buttonTapHandler_data()
{
    QTest::addColumn<QPointingDevice::DeviceType>("deviceType");
    QTest::addColumn<Qt::MouseButton>("mouseButton");

    QTest::newRow("left mouse") << QPointingDevice::DeviceType::Mouse << Qt::LeftButton;
    QTest::newRow("right mouse") << QPointingDevice::DeviceType::Mouse << Qt::RightButton;
    QTest::newRow("touch") << QPointingDevice::DeviceType::TouchScreen << Qt::NoButton;
}

void tst_pointerhandlers::buttonTapHandler() // QTBUG-105609
{
    QFETCH(QPointingDevice::DeviceType, deviceType);
    QFETCH(Qt::MouseButton, mouseButton);

    QQuickView window;
    QVERIFY(QQuickTest::showView(window, testFileUrl("tapHandlerButton.qml")));

    QPointer<QQuickTapHandler> handler = window.rootObject()->findChild<QQuickTapHandler*>();
    QVERIFY(handler);
    handler->setAcceptedButtons(mouseButton);
    QQuickItem *target = handler->target();
    QVERIFY(target);
    QSignalSpy tappedSpy(handler, &QQuickTapHandler::tapped);
    QSignalSpy clickedSpy(target, SIGNAL(clicked())); // avoid #include for this signal

    const QPoint pos(10, 10);
    switch (static_cast<QPointingDevice::DeviceType>(deviceType)) {
    case QPointingDevice::DeviceType::Mouse:
        // click it
        QTest::mouseClick(&window, mouseButton, Qt::NoModifier, pos);
        QTRY_COMPARE(clickedSpy.size(), mouseButton == Qt::RightButton ? 0 : 1);
        QCOMPARE(tappedSpy.size(), 1);
        break;

    case QPointingDevice::DeviceType::TouchScreen: {
        // tap it
        QTest::QTouchEventSequence touch = QTest::touchEvent(&window, touchscreen.data());
        touch.press(0, pos, &window).commit();
        QTRY_COMPARE(target->property("pressed").toBool(), true);
        touch.release(0, pos, &window).commit();
        QTRY_COMPARE(clickedSpy.size(), 1);
        QCOMPARE(tappedSpy.size(), 1);
        break;
    }
    default:
        break;
    }
    QCOMPARE(handler->isPressed(), false);
}

void tst_pointerhandlers::checkableButtonRightClickTapHandler()
{
    QQuickView window;
    QVERIFY(QQuickTest::showView(window, testFileUrl("tapHandlerContextMenuCheckableButton.qml")));
    QQuickItem *rootItem = window.rootObject();
    QVERIFY(rootItem);

    QQuickTapHandler *inButtonHandler = rootItem->findChild<QQuickTapHandler*>("in button");
    QVERIFY(inButtonHandler);
    QQuickAbstractButton *leftButton = qmlobject_cast<QQuickAbstractButton *>(inButtonHandler->parentItem());
    QVERIFY(leftButton);
    QQuickMenu *leftMenu = leftButton->findChild<QQuickMenu *>();
    QVERIFY(leftMenu);
    QSignalSpy inButtonTappedSpy(inButtonHandler, &QQuickTapHandler::tapped);

    QQuickTapHandler *buttonParentHandler = rootItem->findChild<QQuickTapHandler*>("in button parent");
    QVERIFY(buttonParentHandler);
    QQuickAbstractButton *rightButton = buttonParentHandler->parentItem()->findChild<QQuickAbstractButton *>();
    QVERIFY(rightButton);
    QQuickMenu *rightMenu = rightButton->findChild<QQuickMenu *>();
    QVERIFY(rightMenu);
    QSignalSpy buttonParentTappedSpy(buttonParentHandler, &QQuickTapHandler::tapped);

    // right-click the left-side button which has a TapHandler inside:
    // TapHandler opens the context menu, and the Button itself does not react
    QPoint pos = rootItem->mapFromItem(inButtonHandler->parentItem(), QPointF(10, 10)).toPoint();
    QTest::mouseClick(&window, Qt::RightButton, Qt::NoModifier, pos);
    QCOMPARE(inButtonTappedSpy.size(), 1);
    QTRY_VERIFY(leftMenu->isOpened());
    QCOMPARE(leftButton->isChecked(), false);

    // right-click the right-side button which has a TapHandler in its parent:
    // TapHandler opens the context menu, and the Button itself does not react
    pos = rootItem->mapFromItem(buttonParentHandler->parentItem(), QPointF(10, 10)).toPoint();
    QTest::mouseClick(&window, Qt::RightButton, Qt::NoModifier, pos);
    QCOMPARE(buttonParentTappedSpy.size(), 1);
    QTRY_VERIFY(rightMenu->isOpened());
    QCOMPARE(rightButton->isChecked(), false);
}

void tst_pointerhandlers::buttonDragHandler_data()
{
    QTest::addColumn<QPointingDevice::DeviceType>("deviceType");

    QTest::newRow("mouse") << QPointingDevice::DeviceType::Mouse;
    QTest::newRow("touch") << QPointingDevice::DeviceType::TouchScreen;
}

void tst_pointerhandlers::buttonDragHandler() // QTBUG-105610
{
    QFETCH(QPointingDevice::DeviceType, deviceType);

    QQuickView window;
    QVERIFY(QQuickTest::showView(window, testFileUrl("draggableButton.qml")));

    const int dragThreshold = QGuiApplication::styleHints()->startDragDistance();

    QPointer<QQuickPointerHandler> handler = window.rootObject()->findChild<QQuickPointerHandler*>();
    QVERIFY(handler);
    QQuickItem *target = handler->target();
    QVERIFY(target);
    QSignalSpy clickedSpy(target, SIGNAL(clicked()));

    QPoint dragPos(10, 10);
    switch (static_cast<QPointingDevice::DeviceType>(deviceType)) {
    case QPointingDevice::DeviceType::Mouse:
        // click it
        QTest::mouseClick(&window, Qt::LeftButton, Qt::NoModifier, dragPos);
        QTRY_COMPARE(clickedSpy.size(), 1);

        // drag it
        QTest::mousePress(&window, Qt::LeftButton, Qt::NoModifier, dragPos);
        dragPos += QPoint(dragThreshold, dragThreshold);
        QTest::mouseMove(&window, dragPos);
        dragPos += QPoint(1, 1);
        QTest::mouseMove(&window, dragPos);
        qCDebug(lcPointerTests) << handler << "dragged" << target << "to" << target->position();
        QTRY_VERIFY(handler->active());
        QTest::mouseRelease(&window, Qt::LeftButton, Qt::NoModifier, dragPos);
        break;

    case QPointingDevice::DeviceType::TouchScreen: {
        QTest::QTouchEventSequence touch = QTest::touchEvent(&window, touchscreen.data());

        // tap it
        touch.press(0, dragPos, &window).commit();
        touch.release(0, dragPos, &window).commit();
        QTRY_COMPARE(clickedSpy.size(), 1);

        // drag it
        touch.press(0, dragPos, &window).commit();
        dragPos += QPoint(dragThreshold, dragThreshold);
        touch.move(0, dragPos, &window).commit();
        dragPos += QPoint(1, 1);
        touch.move(0, dragPos, &window).commit();
        qCDebug(lcPointerTests) << handler << "dragged" << target << "to" << target->position();
        QTRY_VERIFY(handler->active());
        touch.release(0, dragPos, &window).commit();
        break;
    }
    default:
        break;
    }
    QTRY_COMPARE(handler->active(), false);

    // click it again
    QTest::mouseClick(&window, Qt::LeftButton, Qt::NoModifier, dragPos);
    QTRY_COMPARE(clickedSpy.size(), 2);
}

QTEST_MAIN(tst_pointerhandlers)

#include "tst_pointerhandlers.moc"
