/*
    KWin - the KDE window manager
    This file is part of the KDE project.

    SPDX-FileCopyrightText: 2016 Martin Gräßlin <mgraesslin@kde.org>

    SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "kwin_wayland_test.h"

#include "core/output.h"
#include "keyboard_input.h"
#include "pointer_input.h"
#include "wayland_server.h"
#include "window.h"
#include "workspace.h"

#include <KWayland/Client/compositor.h>
#include <KWayland/Client/keyboard.h>
#include <KWayland/Client/pointer.h>
#include <KWayland/Client/seat.h>
#include <KWayland/Client/shm_pool.h>
#include <KWayland/Client/surface.h>
#include <KWayland/Client/touch.h>

#include <linux/input.h>

using namespace KWin;

static const QString s_socketName = QStringLiteral("wayland_test_kwin_window_selection-0");

class TestWindowSelection : public QObject
{
    Q_OBJECT
private Q_SLOTS:
    void initTestCase();
    void init();
    void cleanup();

    void testSelectOnWindowPointer();
    void testSelectOnWindowKeyboard_data();
    void testSelectOnWindowKeyboard();
    void testSelectOnWindowTouch();
    void testCancelOnWindowPointer();
    void testCancelOnWindowKeyboard();

    void testSelectPointPointer();
    void testSelectPointTouch();
};

void TestWindowSelection::initTestCase()
{
    qRegisterMetaType<KWin::Window *>();
    QVERIFY(waylandServer()->init(s_socketName));
    Test::setOutputConfig({
        QRect(0, 0, 1280, 1024),
        QRect(1280, 0, 1280, 1024),
    });
    qputenv("XKB_DEFAULT_RULES", "evdev");

    kwinApp()->start();
    const auto outputs = workspace()->outputs();
    QCOMPARE(outputs.count(), 2);
    QCOMPARE(outputs[0]->geometry(), QRect(0, 0, 1280, 1024));
    QCOMPARE(outputs[1]->geometry(), QRect(1280, 0, 1280, 1024));
}

void TestWindowSelection::init()
{
    QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat));
    QVERIFY(Test::waitForWaylandPointer());

    workspace()->setActiveOutput(QPoint(640, 512));
    KWin::input()->pointer()->warp(QPoint(640, 512));
}

void TestWindowSelection::cleanup()
{
    Test::destroyWaylandConnection();
}

void TestWindowSelection::testSelectOnWindowPointer()
{
    // this test verifies window selection through pointer works
    std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
    std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
    std::unique_ptr<KWayland::Client::Pointer> pointer(Test::waylandSeat()->createPointer());
    std::unique_ptr<KWayland::Client::Keyboard> keyboard(Test::waylandSeat()->createKeyboard());
    QSignalSpy pointerEnteredSpy(pointer.get(), &KWayland::Client::Pointer::entered);
    QSignalSpy pointerLeftSpy(pointer.get(), &KWayland::Client::Pointer::left);
    QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered);
    QSignalSpy keyboardLeftSpy(keyboard.get(), &KWayland::Client::Keyboard::left);

    auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
    QVERIFY(window);
    QVERIFY(keyboardEnteredSpy.wait());
    KWin::input()->pointer()->warp(window->frameGeometry().center());
    QCOMPARE(input()->pointer()->focus(), window);
    QVERIFY(pointerEnteredSpy.wait());

    Window *selectedWindow = nullptr;
    auto callback = [&selectedWindow](Window *t) {
        selectedWindow = t;
    };

    // start the interaction
    QCOMPARE(input()->isSelectingWindow(), false);
    kwinApp()->startInteractiveWindowSelection(callback);
    QCOMPARE(input()->isSelectingWindow(), true);
    QVERIFY(!selectedWindow);
    QCOMPARE(keyboardLeftSpy.count(), 0);
    QVERIFY(pointerLeftSpy.wait());
    if (keyboardLeftSpy.isEmpty()) {
        QVERIFY(keyboardLeftSpy.wait());
    }
    QCOMPARE(pointerLeftSpy.count(), 1);
    QCOMPARE(keyboardLeftSpy.count(), 1);

    // simulate left button press
    quint32 timestamp = 0;
    Test::pointerButtonPressed(BTN_LEFT, timestamp++);
    // should not have ended the mode
    QCOMPARE(input()->isSelectingWindow(), true);
    QVERIFY(!selectedWindow);
    QVERIFY(!input()->pointer()->focus());

    // updating the pointer should not change anything
    input()->pointer()->update();
    QVERIFY(!input()->pointer()->focus());
    // updating keyboard should also not change
    input()->keyboard()->update();

    // perform a right button click
    Test::pointerButtonPressed(BTN_RIGHT, timestamp++);
    Test::pointerButtonReleased(BTN_RIGHT, timestamp++);
    // should not have ended the mode
    QCOMPARE(input()->isSelectingWindow(), true);
    QVERIFY(!selectedWindow);
    // now release
    Test::pointerButtonReleased(BTN_LEFT, timestamp++);
    QCOMPARE(input()->isSelectingWindow(), false);
    QCOMPARE(selectedWindow, window);
    QCOMPARE(input()->pointer()->focus(), window);
    // should give back keyboard and pointer
    QVERIFY(pointerEnteredSpy.wait());
    if (keyboardEnteredSpy.count() != 2) {
        QVERIFY(keyboardEnteredSpy.wait());
    }
    QCOMPARE(pointerLeftSpy.count(), 1);
    QCOMPARE(keyboardLeftSpy.count(), 1);
    QCOMPARE(pointerEnteredSpy.count(), 2);
    QCOMPARE(keyboardEnteredSpy.count(), 2);
}

void TestWindowSelection::testSelectOnWindowKeyboard_data()
{
    QTest::addColumn<qint32>("key");

    QTest::newRow("enter") << KEY_ENTER;
    QTest::newRow("keypad enter") << KEY_KPENTER;
    QTest::newRow("space") << KEY_SPACE;
}

void TestWindowSelection::testSelectOnWindowKeyboard()
{
    // this test verifies window selection through keyboard key
    std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
    std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
    std::unique_ptr<KWayland::Client::Pointer> pointer(Test::waylandSeat()->createPointer());
    std::unique_ptr<KWayland::Client::Keyboard> keyboard(Test::waylandSeat()->createKeyboard());
    QSignalSpy pointerEnteredSpy(pointer.get(), &KWayland::Client::Pointer::entered);
    QSignalSpy pointerLeftSpy(pointer.get(), &KWayland::Client::Pointer::left);
    QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered);
    QSignalSpy keyboardLeftSpy(keyboard.get(), &KWayland::Client::Keyboard::left);

    auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
    QVERIFY(window);
    QVERIFY(keyboardEnteredSpy.wait());
    QVERIFY(!exclusiveContains(window->frameGeometry(), KWin::Cursors::self()->mouse()->pos()));

    Window *selectedWindow = nullptr;
    auto callback = [&selectedWindow](Window *t) {
        selectedWindow = t;
    };

    // start the interaction
    QCOMPARE(input()->isSelectingWindow(), false);
    kwinApp()->startInteractiveWindowSelection(callback);
    QCOMPARE(input()->isSelectingWindow(), true);
    QVERIFY(!selectedWindow);
    QCOMPARE(keyboardLeftSpy.count(), 0);
    QVERIFY(keyboardLeftSpy.wait());
    QCOMPARE(pointerLeftSpy.count(), 0);
    QCOMPARE(keyboardLeftSpy.count(), 1);

    // simulate key press
    quint32 timestamp = 0;
    // move cursor through keys
    auto keyPress = [&timestamp](qint32 key) {
        Test::keyboardKeyPressed(key, timestamp++);
        Test::keyboardKeyReleased(key, timestamp++);
    };
    while (KWin::Cursors::self()->mouse()->pos().x() >= window->frameGeometry().x() + window->frameGeometry().width()) {
        keyPress(KEY_LEFT);
    }
    while (KWin::Cursors::self()->mouse()->pos().x() <= window->frameGeometry().x()) {
        keyPress(KEY_RIGHT);
    }
    while (KWin::Cursors::self()->mouse()->pos().y() <= window->frameGeometry().y()) {
        keyPress(KEY_DOWN);
    }
    while (KWin::Cursors::self()->mouse()->pos().y() >= window->frameGeometry().y() + window->frameGeometry().height()) {
        keyPress(KEY_UP);
    }
    QFETCH(qint32, key);
    Test::keyboardKeyPressed(key, timestamp++);
    QCOMPARE(input()->isSelectingWindow(), false);
    QCOMPARE(selectedWindow, window);
    QCOMPARE(input()->pointer()->focus(), window);
    // should give back keyboard and pointer
    QVERIFY(pointerEnteredSpy.wait());
    if (keyboardEnteredSpy.count() != 2) {
        QVERIFY(keyboardEnteredSpy.wait());
    }
    QCOMPARE(pointerLeftSpy.count(), 0);
    QCOMPARE(keyboardLeftSpy.count(), 1);
    QCOMPARE(pointerEnteredSpy.count(), 1);
    QCOMPARE(keyboardEnteredSpy.count(), 2);
    Test::keyboardKeyReleased(key, timestamp++);
}

void TestWindowSelection::testSelectOnWindowTouch()
{
    // this test verifies window selection through touch
    std::unique_ptr<KWayland::Client::Touch> touch(Test::waylandSeat()->createTouch());
    QSignalSpy touchStartedSpy(touch.get(), &KWayland::Client::Touch::sequenceStarted);
    QSignalSpy touchCanceledSpy(touch.get(), &KWayland::Client::Touch::sequenceCanceled);
    std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
    std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
    auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
    QVERIFY(window);

    Window *selectedWindow = nullptr;
    auto callback = [&selectedWindow](Window *t) {
        selectedWindow = t;
    };

    // start the interaction
    QCOMPARE(input()->isSelectingWindow(), false);
    kwinApp()->startInteractiveWindowSelection(callback);
    QCOMPARE(input()->isSelectingWindow(), true);
    QVERIFY(!selectedWindow);

    // simulate touch down
    quint32 timestamp = 0;
    Test::touchDown(0, window->frameGeometry().center(), timestamp++);
    QVERIFY(!selectedWindow);
    Test::touchUp(0, timestamp++);
    QCOMPARE(input()->isSelectingWindow(), false);
    QCOMPARE(selectedWindow, window);

    // with movement
    selectedWindow = nullptr;
    kwinApp()->startInteractiveWindowSelection(callback);
    Test::touchDown(0, window->frameGeometry().bottomRight() + QPoint(20, 20), timestamp++);
    QVERIFY(!selectedWindow);
    Test::touchMotion(0, window->frameGeometry().bottomRight() - QPoint(1, 1), timestamp++);
    QVERIFY(!selectedWindow);
    Test::touchUp(0, timestamp++);
    QCOMPARE(selectedWindow, window);
    QCOMPARE(input()->isSelectingWindow(), false);

    // it cancels active touch sequence on the window
    Test::touchDown(0, window->frameGeometry().center(), timestamp++);
    QVERIFY(touchStartedSpy.wait());
    selectedWindow = nullptr;
    kwinApp()->startInteractiveWindowSelection(callback);
    QCOMPARE(input()->isSelectingWindow(), true);
    QVERIFY(touchCanceledSpy.wait());
    QVERIFY(!selectedWindow);
    // this touch up does not yet select the window, it was started prior to the selection
    Test::touchUp(0, timestamp++);
    QVERIFY(!selectedWindow);
    Test::touchDown(0, window->frameGeometry().center(), timestamp++);
    Test::touchUp(0, timestamp++);
    QCOMPARE(selectedWindow, window);
    QCOMPARE(input()->isSelectingWindow(), false);

    QCOMPARE(touchStartedSpy.count(), 1);
    QCOMPARE(touchCanceledSpy.count(), 1);
}

void TestWindowSelection::testCancelOnWindowPointer()
{
    // this test verifies that window selection cancels through right button click
    std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
    std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
    std::unique_ptr<KWayland::Client::Pointer> pointer(Test::waylandSeat()->createPointer());
    std::unique_ptr<KWayland::Client::Keyboard> keyboard(Test::waylandSeat()->createKeyboard());
    QSignalSpy pointerEnteredSpy(pointer.get(), &KWayland::Client::Pointer::entered);
    QSignalSpy pointerLeftSpy(pointer.get(), &KWayland::Client::Pointer::left);
    QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered);
    QSignalSpy keyboardLeftSpy(keyboard.get(), &KWayland::Client::Keyboard::left);

    auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
    QVERIFY(window);
    QVERIFY(keyboardEnteredSpy.wait());
    KWin::input()->pointer()->warp(window->frameGeometry().center());
    QCOMPARE(input()->pointer()->focus(), window);
    QVERIFY(pointerEnteredSpy.wait());

    Window *selectedWindow = nullptr;
    auto callback = [&selectedWindow](Window *t) {
        selectedWindow = t;
    };

    // start the interaction
    QCOMPARE(input()->isSelectingWindow(), false);
    kwinApp()->startInteractiveWindowSelection(callback);
    QCOMPARE(input()->isSelectingWindow(), true);
    QVERIFY(!selectedWindow);
    QCOMPARE(keyboardLeftSpy.count(), 0);
    QVERIFY(pointerLeftSpy.wait());
    if (keyboardLeftSpy.isEmpty()) {
        QVERIFY(keyboardLeftSpy.wait());
    }
    QCOMPARE(pointerLeftSpy.count(), 1);
    QCOMPARE(keyboardLeftSpy.count(), 1);

    // simulate left button press
    quint32 timestamp = 0;
    Test::pointerButtonPressed(BTN_RIGHT, timestamp++);
    Test::pointerButtonReleased(BTN_RIGHT, timestamp++);
    QCOMPARE(input()->isSelectingWindow(), false);
    QVERIFY(!selectedWindow);
    QCOMPARE(input()->pointer()->focus(), window);
    // should give back keyboard and pointer
    QVERIFY(pointerEnteredSpy.wait());
    if (keyboardEnteredSpy.count() != 2) {
        QVERIFY(keyboardEnteredSpy.wait());
    }
    QCOMPARE(pointerLeftSpy.count(), 1);
    QCOMPARE(keyboardLeftSpy.count(), 1);
    QCOMPARE(pointerEnteredSpy.count(), 2);
    QCOMPARE(keyboardEnteredSpy.count(), 2);
}

void TestWindowSelection::testCancelOnWindowKeyboard()
{
    // this test verifies that cancel window selection through escape key works
    std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
    std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
    std::unique_ptr<KWayland::Client::Pointer> pointer(Test::waylandSeat()->createPointer());
    std::unique_ptr<KWayland::Client::Keyboard> keyboard(Test::waylandSeat()->createKeyboard());
    QSignalSpy pointerEnteredSpy(pointer.get(), &KWayland::Client::Pointer::entered);
    QSignalSpy pointerLeftSpy(pointer.get(), &KWayland::Client::Pointer::left);
    QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered);
    QSignalSpy keyboardLeftSpy(keyboard.get(), &KWayland::Client::Keyboard::left);

    auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
    QVERIFY(window);
    QVERIFY(keyboardEnteredSpy.wait());
    KWin::input()->pointer()->warp(window->frameGeometry().center());
    QCOMPARE(input()->pointer()->focus(), window);
    QVERIFY(pointerEnteredSpy.wait());

    Window *selectedWindow = nullptr;
    auto callback = [&selectedWindow](Window *t) {
        selectedWindow = t;
    };

    // start the interaction
    QCOMPARE(input()->isSelectingWindow(), false);
    kwinApp()->startInteractiveWindowSelection(callback);
    QCOMPARE(input()->isSelectingWindow(), true);
    QVERIFY(!selectedWindow);
    QCOMPARE(keyboardLeftSpy.count(), 0);
    QVERIFY(pointerLeftSpy.wait());
    if (keyboardLeftSpy.isEmpty()) {
        QVERIFY(keyboardLeftSpy.wait());
    }
    QCOMPARE(pointerLeftSpy.count(), 1);
    QCOMPARE(keyboardLeftSpy.count(), 1);

    // simulate left button press
    quint32 timestamp = 0;
    Test::keyboardKeyPressed(KEY_ESC, timestamp++);
    QCOMPARE(input()->isSelectingWindow(), false);
    QVERIFY(!selectedWindow);
    QCOMPARE(input()->pointer()->focus(), window);
    // should give back keyboard and pointer
    QVERIFY(pointerEnteredSpy.wait());
    if (keyboardEnteredSpy.count() != 2) {
        QVERIFY(keyboardEnteredSpy.wait());
    }
    QCOMPARE(pointerLeftSpy.count(), 1);
    QCOMPARE(keyboardLeftSpy.count(), 1);
    QCOMPARE(pointerEnteredSpy.count(), 2);
    QCOMPARE(keyboardEnteredSpy.count(), 2);
    Test::keyboardKeyReleased(KEY_ESC, timestamp++);
}

void TestWindowSelection::testSelectPointPointer()
{
    // this test verifies point selection through pointer works
    std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
    std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
    std::unique_ptr<KWayland::Client::Pointer> pointer(Test::waylandSeat()->createPointer());
    std::unique_ptr<KWayland::Client::Keyboard> keyboard(Test::waylandSeat()->createKeyboard());
    QSignalSpy pointerEnteredSpy(pointer.get(), &KWayland::Client::Pointer::entered);
    QSignalSpy pointerLeftSpy(pointer.get(), &KWayland::Client::Pointer::left);
    QSignalSpy keyboardEnteredSpy(keyboard.get(), &KWayland::Client::Keyboard::entered);
    QSignalSpy keyboardLeftSpy(keyboard.get(), &KWayland::Client::Keyboard::left);

    auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
    QVERIFY(window);
    QVERIFY(keyboardEnteredSpy.wait());
    KWin::input()->pointer()->warp(window->frameGeometry().center());
    QCOMPARE(input()->pointer()->focus(), window);
    QVERIFY(pointerEnteredSpy.wait());

    // start the interaction
    QCOMPARE(input()->isSelectingWindow(), false);
    QPointF point;
    kwinApp()->startInteractivePositionSelection([&point](const QPointF &p) {
        point = p;
    });
    QCOMPARE(input()->isSelectingWindow(), true);
    QCOMPARE(point, QPoint());
    QCOMPARE(keyboardLeftSpy.count(), 0);
    QVERIFY(pointerLeftSpy.wait());
    if (keyboardLeftSpy.isEmpty()) {
        QVERIFY(keyboardLeftSpy.wait());
    }
    QCOMPARE(pointerLeftSpy.count(), 1);
    QCOMPARE(keyboardLeftSpy.count(), 1);

    // trying again should not be allowed
    QPointF point2;
    kwinApp()->startInteractivePositionSelection([&point2](const QPointF &p) {
        point2 = p;
    });
    QCOMPARE(point2, QPoint(-1, -1));

    // simulate left button press
    quint32 timestamp = 0;
    Test::pointerButtonPressed(BTN_LEFT, timestamp++);
    // should not have ended the mode
    QCOMPARE(input()->isSelectingWindow(), true);
    QCOMPARE(point, QPoint());
    QVERIFY(!input()->pointer()->focus());

    // updating the pointer should not change anything
    input()->pointer()->update();
    QVERIFY(!input()->pointer()->focus());
    // updating keyboard should also not change
    input()->keyboard()->update();

    // perform a right button click
    Test::pointerButtonPressed(BTN_RIGHT, timestamp++);
    Test::pointerButtonReleased(BTN_RIGHT, timestamp++);
    // should not have ended the mode
    QCOMPARE(input()->isSelectingWindow(), true);
    QCOMPARE(point, QPoint());
    // now release
    Test::pointerButtonReleased(BTN_LEFT, timestamp++);
    QCOMPARE(input()->isSelectingWindow(), false);
    QCOMPARE(point, input()->globalPointer().toPoint());
    QCOMPARE(input()->pointer()->focus(), window);
    // should give back keyboard and pointer
    QVERIFY(pointerEnteredSpy.wait());
    if (keyboardEnteredSpy.count() != 2) {
        QVERIFY(keyboardEnteredSpy.wait());
    }
    QCOMPARE(pointerLeftSpy.count(), 1);
    QCOMPARE(keyboardLeftSpy.count(), 1);
    QCOMPARE(pointerEnteredSpy.count(), 2);
    QCOMPARE(keyboardEnteredSpy.count(), 2);
}

void TestWindowSelection::testSelectPointTouch()
{
    // this test verifies point selection through touch works
    // start the interaction
    QCOMPARE(input()->isSelectingWindow(), false);
    QPointF point;
    kwinApp()->startInteractivePositionSelection([&point](const QPointF &p) {
        point = p;
    });
    QCOMPARE(input()->isSelectingWindow(), true);
    QCOMPARE(point, QPoint());

    // let's create multiple touch points
    quint32 timestamp = 0;
    Test::touchDown(0, QPointF(0, 1), timestamp++);
    QCOMPARE(input()->isSelectingWindow(), true);
    Test::touchDown(1, QPointF(10, 20), timestamp++);
    QCOMPARE(input()->isSelectingWindow(), true);
    Test::touchDown(2, QPointF(30, 40), timestamp++);
    QCOMPARE(input()->isSelectingWindow(), true);

    // let's move our points
    Test::touchMotion(0, QPointF(5, 10), timestamp++);
    Test::touchMotion(2, QPointF(20, 25), timestamp++);
    Test::touchMotion(1, QPointF(25, 35), timestamp++);
    QCOMPARE(input()->isSelectingWindow(), true);
    Test::touchUp(0, timestamp++);
    QCOMPARE(input()->isSelectingWindow(), true);
    Test::touchUp(2, timestamp++);
    QCOMPARE(input()->isSelectingWindow(), true);
    Test::touchUp(1, timestamp++);
    QCOMPARE(input()->isSelectingWindow(), false);
    QCOMPARE(point, QPoint(25, 35));
}

WAYLANDTEST_MAIN(TestWindowSelection)
#include "window_selection_test.moc"
