/*
    SPDX-FileCopyrightText: 2016 Martin Gräßlin <mgraesslin@kde.org>
    SPDX-FileCopyrightText: 2023 Vlad Zahorodnii <vlad.zahorodnii@kde.org>

    SPDX-License-Identifier: GPL-2.0-or-later
*/

#include "kwin_wayland_test.h"

#include "core/inputdevice.h"
#include "input.h"
#include "main.h"
#include "wayland_server.h"

#include <linux/input-event-codes.h>
#include <xkbcommon/xkbcommon-keysyms.h>

namespace KWin
{

class FakeInputTest : public QObject
{
    Q_OBJECT

private Q_SLOTS:
    void initTestCase();
    void init();
    void cleanup();
    void testPointerMotion();
    void testMotionAbsolute();
    void testPointerButton_data();
    void testPointerButton();
    void testPointerVerticalAxis();
    void testPointerHorizontalAxis();
    void testTouch();
    void testKeyboardKey_data();
    void testKeyboardKey();
    void testKeySym();

private:
    InputDevice *m_inputDevice = nullptr;
};

void FakeInputTest::initTestCase()
{
    QVERIFY(waylandServer()->init(qAppName()));
    kwinApp()->start();
    Test::setOutputConfig({
        Rect(0, 0, 1280, 1024),
        Rect(1280, 0, 1280, 1024),
    });
}

void FakeInputTest::init()
{
    QSignalSpy deviceAddedSpy(input(), &InputRedirection::deviceAdded);
    QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::FakeInput | Test::AdditionalWaylandInterface::Seat));

    QVERIFY(deviceAddedSpy.wait());
    m_inputDevice = deviceAddedSpy.last().at(0).value<InputDevice *>();
}

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

void FakeInputTest::testPointerMotion()
{
    Test::FakeInput *fakeInput = Test::waylandFakeInput();

    // without an authentication we shouldn't get the signals
    QSignalSpy pointerMotionSpy(m_inputDevice, &InputDevice::pointerMotion);
    fakeInput->pointer_motion(wl_fixed_from_double(1), wl_fixed_from_double(2));
    QVERIFY(Test::waylandSync());
    QVERIFY(pointerMotionSpy.isEmpty());

    // now let's authenticate the interface
    fakeInput->authenticate(QStringLiteral("org.kde.foobar"), QStringLiteral("foobar"));
    fakeInput->pointer_motion(wl_fixed_from_double(1), wl_fixed_from_double(2));
    QVERIFY(pointerMotionSpy.wait());
    QCOMPARE(pointerMotionSpy.last().first().toPointF(), QPointF(1, 2));

    // just for the fun: once more
    fakeInput->pointer_motion(wl_fixed_from_double(0), wl_fixed_from_double(0));
    QVERIFY(pointerMotionSpy.wait());
    QCOMPARE(pointerMotionSpy.last().first().toPointF(), QPointF(0, 0));
}

void FakeInputTest::testMotionAbsolute()
{
    Test::FakeInput *fakeInput = Test::waylandFakeInput();

    // without an authentication we shouldn't get the signals
    QSignalSpy pointerMotionAbsoluteSpy(m_inputDevice, &InputDevice::pointerMotionAbsolute);
    fakeInput->pointer_motion_absolute(wl_fixed_from_double(1), wl_fixed_from_double(2));
    QVERIFY(Test::waylandSync());
    QVERIFY(pointerMotionAbsoluteSpy.isEmpty());

    // now let's authenticate the interface
    fakeInput->authenticate(QStringLiteral("org.kde.foobar"), QStringLiteral("foobar"));
    fakeInput->pointer_motion_absolute(wl_fixed_from_double(1), wl_fixed_from_double(2));
    QVERIFY(pointerMotionAbsoluteSpy.wait());
    QCOMPARE(pointerMotionAbsoluteSpy.last().first().toPointF(), QPointF(1, 2));
}

void FakeInputTest::testPointerButton_data()
{
    QTest::addColumn<quint32>("linuxButton");

    QTest::newRow("left") << quint32(BTN_LEFT);
    QTest::newRow("right") << quint32(BTN_RIGHT);
    QTest::newRow("middle") << quint32(BTN_MIDDLE);
    QTest::newRow("side") << quint32(BTN_SIDE);
    QTest::newRow("extra") << quint32(BTN_EXTRA);
    QTest::newRow("forward") << quint32(BTN_FORWARD);
    QTest::newRow("back") << quint32(BTN_BACK);
    QTest::newRow("task") << quint32(BTN_TASK);
}

void FakeInputTest::testPointerButton()
{
    Test::FakeInput *fakeInput = Test::waylandFakeInput();

    // without an authentication we shouldn't get the signals
    QSignalSpy pointerButtonSpy(m_inputDevice, &InputDevice::pointerButtonChanged);
    QFETCH(quint32, linuxButton);
    fakeInput->button(linuxButton, WL_POINTER_BUTTON_STATE_PRESSED);
    QVERIFY(Test::waylandSync());
    QVERIFY(pointerButtonSpy.isEmpty());

    // now authenticate
    fakeInput->authenticate(QStringLiteral("org.kde.foobar"), QStringLiteral("foobar"));
    fakeInput->button(linuxButton, WL_POINTER_BUTTON_STATE_PRESSED);
    QVERIFY(pointerButtonSpy.wait());
    QCOMPARE(pointerButtonSpy.last().at(0).value<quint32>(), linuxButton);
    QCOMPARE(pointerButtonSpy.last().at(1).value<PointerButtonState>(), PointerButtonState::Pressed);

    // and release
    fakeInput->button(linuxButton, WL_POINTER_BUTTON_STATE_RELEASED);
    QVERIFY(pointerButtonSpy.wait());
    QCOMPARE(pointerButtonSpy.last().at(0).value<quint32>(), linuxButton);
    QCOMPARE(pointerButtonSpy.last().at(1).value<PointerButtonState>(), PointerButtonState::Released);
}

void FakeInputTest::testPointerVerticalAxis()
{
    Test::FakeInput *fakeInput = Test::waylandFakeInput();

    // without an authentication we shouldn't get the signals
    QSignalSpy pointerAxisSpy(m_inputDevice, &InputDevice::pointerAxisChanged);
    fakeInput->axis(WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_double(15));
    QVERIFY(Test::waylandSync());
    QVERIFY(pointerAxisSpy.isEmpty());

    // now authenticate
    fakeInput->authenticate(QStringLiteral("org.kde.foobar"), QStringLiteral("foobar"));
    fakeInput->axis(WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_double(15));
    QVERIFY(pointerAxisSpy.wait());
    QCOMPARE(pointerAxisSpy.last().at(0).value<PointerAxis>(), PointerAxis::Vertical);
    QCOMPARE(pointerAxisSpy.last().at(1).value<qreal>(), 15);
}

void FakeInputTest::testPointerHorizontalAxis()
{
    Test::FakeInput *fakeInput = Test::waylandFakeInput();

    // without an authentication we shouldn't get the signals
    QSignalSpy pointerAxisSpy(m_inputDevice, &InputDevice::pointerAxisChanged);
    fakeInput->axis(WL_POINTER_AXIS_HORIZONTAL_SCROLL, wl_fixed_from_double(15));
    QVERIFY(Test::waylandSync());
    QVERIFY(pointerAxisSpy.isEmpty());

    // now authenticate
    fakeInput->authenticate(QStringLiteral("org.kde.foobar"), QStringLiteral("foobar"));
    fakeInput->axis(WL_POINTER_AXIS_HORIZONTAL_SCROLL, wl_fixed_from_double(15));
    QVERIFY(pointerAxisSpy.wait());
    QCOMPARE(pointerAxisSpy.last().at(0).value<PointerAxis>(), PointerAxis::Horizontal);
    QCOMPARE(pointerAxisSpy.last().at(1).value<qreal>(), 15);
}

void FakeInputTest::testTouch()
{
    Test::FakeInput *fakeInput = Test::waylandFakeInput();

    // without an authentication we shouldn't get the signals
    QSignalSpy touchDownSpy(m_inputDevice, &InputDevice::touchDown);
    QSignalSpy touchUpSpy(m_inputDevice, &InputDevice::touchUp);
    QSignalSpy touchMotionSpy(m_inputDevice, &InputDevice::touchMotion);
    QSignalSpy touchFrameSpy(m_inputDevice, &InputDevice::touchFrame);
    QSignalSpy touchCanceledSpy(m_inputDevice, &InputDevice::touchCanceled);
    fakeInput->touch_down(0, wl_fixed_from_double(1), wl_fixed_from_double(2));
    QVERIFY(Test::waylandSync());
    QVERIFY(touchDownSpy.isEmpty());

    fakeInput->touch_motion(0, wl_fixed_from_double(3), wl_fixed_from_double(4));
    QVERIFY(Test::waylandSync());
    QVERIFY(touchMotionSpy.isEmpty());

    fakeInput->touch_up(0);
    QVERIFY(Test::waylandSync());
    QVERIFY(touchUpSpy.isEmpty());

    fakeInput->touch_down(1, wl_fixed_from_double(5), wl_fixed_from_double(6));
    QVERIFY(Test::waylandSync());
    QVERIFY(touchDownSpy.isEmpty());

    fakeInput->touch_frame();
    QVERIFY(Test::waylandSync());
    QVERIFY(touchFrameSpy.isEmpty());

    fakeInput->touch_cancel();
    QVERIFY(Test::waylandSync());
    QVERIFY(touchCanceledSpy.isEmpty());

    // now let's authenticate the interface
    fakeInput->authenticate(QStringLiteral("org.kde.foobar"), QStringLiteral("foobar"));
    fakeInput->touch_down(0, wl_fixed_from_double(1), wl_fixed_from_double(2));
    QVERIFY(touchDownSpy.wait());
    QCOMPARE(touchDownSpy.count(), 1);
    QCOMPARE(touchDownSpy.last().at(0).value<quint32>(), quint32(0));
    QCOMPARE(touchDownSpy.last().at(1).toPointF(), QPointF(1, 2));

    // Same id should not trigger another touchDown.
    fakeInput->touch_down(0, wl_fixed_from_double(5), wl_fixed_from_double(6));
    QVERIFY(Test::waylandSync());
    QCOMPARE(touchDownSpy.count(), 1);

    fakeInput->touch_motion(0, wl_fixed_from_double(3), wl_fixed_from_double(4));
    QVERIFY(touchMotionSpy.wait());
    QCOMPARE(touchMotionSpy.count(), 1);
    QCOMPARE(touchMotionSpy.last().at(0).value<quint32>(), quint32(0));
    QCOMPARE(touchMotionSpy.last().at(1).toPointF(), QPointF(3, 4));

    // touchMotion with bogus id should not trigger signal.
    fakeInput->touch_motion(1, wl_fixed_from_double(3), wl_fixed_from_double(4));
    QVERIFY(Test::waylandSync());
    QCOMPARE(touchMotionSpy.count(), 1);

    fakeInput->touch_up(0);
    QVERIFY(touchUpSpy.wait());
    QCOMPARE(touchUpSpy.count(), 1);
    QCOMPARE(touchUpSpy.last().at(0).value<quint32>(), quint32(0));

    // touchUp with bogus id should not trigger signal.
    fakeInput->touch_up(1);
    QVERIFY(Test::waylandSync());
    QCOMPARE(touchUpSpy.count(), 1);

    fakeInput->touch_down(1, wl_fixed_from_double(5), wl_fixed_from_double(6));
    QVERIFY(touchDownSpy.wait());
    QCOMPARE(touchDownSpy.count(), 2);
    QCOMPARE(touchDownSpy.last().at(0).value<quint32>(), quint32(1));
    QCOMPARE(touchDownSpy.last().at(1).toPointF(), QPointF(5, 6));

    fakeInput->touch_frame();
    QVERIFY(touchFrameSpy.wait());
    QCOMPARE(touchFrameSpy.count(), 1);

    fakeInput->touch_cancel();
    QVERIFY(touchCanceledSpy.wait());
    QCOMPARE(touchCanceledSpy.count(), 1);
}

void FakeInputTest::testKeyboardKey_data()
{
    QTest::addColumn<quint32>("linuxKey");

    QTest::newRow("A") << quint32(KEY_A);
    QTest::newRow("S") << quint32(KEY_S);
    QTest::newRow("D") << quint32(KEY_D);
    QTest::newRow("F") << quint32(KEY_F);
}

void FakeInputTest::testKeyboardKey()
{
    Test::FakeInput *fakeInput = Test::waylandFakeInput();

    // without an authentication we shouldn't get the signals
    QSignalSpy keyboardKeySpy(m_inputDevice, &InputDevice::keyChanged);
    QFETCH(quint32, linuxKey);
    fakeInput->keyboard_key(linuxKey, WL_KEYBOARD_KEY_STATE_PRESSED);
    QVERIFY(Test::waylandSync());
    QVERIFY(keyboardKeySpy.isEmpty());

    // now authenticate
    fakeInput->authenticate(QStringLiteral("org.kde.foobar"), QStringLiteral("foobar"));
    fakeInput->keyboard_key(linuxKey, WL_KEYBOARD_KEY_STATE_PRESSED);
    QVERIFY(keyboardKeySpy.wait());
    QCOMPARE(keyboardKeySpy.last().at(0).value<quint32>(), linuxKey);
    QCOMPARE(keyboardKeySpy.last().at(1).value<KeyboardKeyState>(), KeyboardKeyState::Pressed);

    // and release
    fakeInput->keyboard_key(linuxKey, WL_KEYBOARD_KEY_STATE_RELEASED);
    QVERIFY(keyboardKeySpy.wait());
    QCOMPARE(keyboardKeySpy.last().at(0).value<quint32>(), linuxKey);
    QCOMPARE(keyboardKeySpy.last().at(1).value<KeyboardKeyState>(), KeyboardKeyState::Released);
}

void FakeInputTest::testKeySym()
{
    // as keyboard input is complex, test the client gets the right keys as well as kwin
    Test::FakeInput *fakeInput = Test::waylandFakeInput();
    fakeInput->authenticate(QStringLiteral("org.kde.foobar"), QStringLiteral("foobar"));

    std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
    std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
    Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red);
    QVERIFY(window);
    QVERIFY(window->isActive());
    QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024));

    auto keyboard = new Test::SimpleKeyboard(window);

    auto sendKey = [fakeInput](uint32_t keySym) {
        fakeInput->keyboard_keysym(keySym, WL_KEYBOARD_KEY_STATE_PRESSED);
        fakeInput->keyboard_keysym(keySym, WL_KEYBOARD_KEY_STATE_RELEASED);
    };

    sendKey(XKB_KEY_a);
    sendKey(XKB_KEY_B);
    sendKey(XKB_KEY_space);
    sendKey(XKB_KEY_adiaeresis); // lowercase a with umlauts
    sendKey(XKB_KEY_Adiaeresis); // uppercase a with umlauts
    sendKey(XKB_KEY_space);
    sendKey(0x01000000 | 0xC548); // korean symbol
    sendKey(0x01000000 | 0x1F60A); // smily face
    sendKey(XKB_KEY_f);

    QSignalSpy receivedTextChangedSpy(keyboard, &Test::SimpleKeyboard::receviedTextChanged);
    bool matched = false;
    for (int i = 0; i < 10; ++i) {
        if (keyboard->receviedText() == "aB äÄ 안😊f") {
            matched = true;
            break;
        }
        receivedTextChangedSpy.wait();
    }
    QVERIFY(matched);

    QSignalSpy keySymReceivedSpy(keyboard, &Test::SimpleKeyboard::keySymRecevied);
    QSignalSpy modifiersChangedSpy(keyboard->keyboard(), &KWayland::Client::Keyboard::modifiersChanged);

    fakeInput->keyboard_keysym(XKB_KEY_Control_L, WL_KEYBOARD_KEY_STATE_PRESSED);
    QVERIFY(keySymReceivedSpy.count() == 1 || keySymReceivedSpy.wait());
    QCOMPARE(keySymReceivedSpy.last().at(0).toUInt(), XKB_KEY_Control_L);
    QVERIFY(modifiersChangedSpy.count() == 1 || modifiersChangedSpy.wait());
    const auto modifiersAfterPress = modifiersChangedSpy.last();
    QVERIFY(modifiersAfterPress.at(0).toUInt() != 0);

    sendKey(XKB_KEY_a);
    QVERIFY(keySymReceivedSpy.count() == 2 || keySymReceivedSpy.wait());
    QCOMPARE(keySymReceivedSpy.last().at(0).toUInt(), XKB_KEY_a);
    QCOMPARE(modifiersChangedSpy.count(), 1);
    QCOMPARE(modifiersChangedSpy.last().at(0).toUInt(), modifiersAfterPress.at(0).toUInt());

    fakeInput->keyboard_keysym(XKB_KEY_Control_L, WL_KEYBOARD_KEY_STATE_RELEASED);
    QVERIFY(modifiersChangedSpy.count() == 2 || modifiersChangedSpy.wait());
    QCOMPARE(keySymReceivedSpy.count(), 2);
    QCOMPARE(modifiersChangedSpy.last().at(0).toUInt(), 0U);

}

} // namespace KWin

WAYLANDTEST_MAIN(KWin::FakeInputTest)
#include "fakeinput_test.moc"
