/********************************************************************
Copyright 2016  Martin Gräßlin <mgraesslin@kde.org>

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) version 3, or any
later version accepted by the membership of KDE e.V. (or its
successor approved by the membership of KDE e.V.), which shall
act as a proxy defined in Section 6 of version 3 of the license.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library.  If not, see <http://www.gnu.org/licenses/>.
*********************************************************************/
// Qt
#include <QtTest>
// client
#include "../../src/client/compositor.h"
#include "../../src/client/connection_thread.h"
#include "../../src/client/event_queue.h"
#include "../../src/client/keyboard.h"
#include "../../src/client/registry.h"
#include "../../src/client/seat.h"
#include "../../src/client/surface.h"
#include "../../src/client/textinput.h"
// server
#include "../../src/server/compositor_interface.h"
#include "../../src/server/display.h"
#include "../../src/server/seat_interface.h"
#include "../../src/server/textinput_interface.h"

using namespace KWayland::Client;
using namespace KWayland::Server;

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

    void testEnterLeave_data();
    void testEnterLeave();
    void testShowHidePanel_data();
    void testShowHidePanel();
    void testCursorRectangle_data();
    void testCursorRectangle();
    void testPreferredLanguage_data();
    void testPreferredLanguage();
    void testReset_data();
    void testReset();
    void testSurroundingText_data();
    void testSurroundingText();
    void testContentHints_data();
    void testContentHints();
    void testContentPurpose_data();
    void testContentPurpose();
    void testTextDirection_data();
    void testTextDirection();
    void testLanguage_data();
    void testLanguage();
    void testKeyEvent_data();
    void testKeyEvent();
    void testPreEdit_data();
    void testPreEdit();
    void testCommit_data();
    void testCommit();

private:
    SurfaceInterface *waitForSurface();
    TextInput *createTextInput(TextInputInterfaceVersion version);
    Display *m_display = nullptr;
    SeatInterface *m_seatInterface = nullptr;
    CompositorInterface *m_compositorInterface = nullptr;
    TextInputManagerInterface *m_textInputManagerV0Interface = nullptr;
    TextInputManagerInterface *m_textInputManagerV2Interface = nullptr;
    ConnectionThread *m_connection = nullptr;
    QThread *m_thread = nullptr;
    EventQueue *m_queue = nullptr;
    Seat *m_seat = nullptr;
    Keyboard *m_keyboard = nullptr;
    Compositor *m_compositor = nullptr;
    TextInputManager *m_textInputManagerV0 = nullptr;
    TextInputManager *m_textInputManagerV2 = nullptr;
};

static const QString s_socketName = QStringLiteral("kwayland-test-text-input-0");

void TextInputTest::init()
{
    delete m_display;
    m_display = new Display(this);
    m_display->setSocketName(s_socketName);
    m_display->start();
    QVERIFY(m_display->isRunning());
    m_display->createShm();
    m_seatInterface = m_display->createSeat();
    m_seatInterface->setHasKeyboard(true);
    m_seatInterface->setHasTouch(true);
    m_seatInterface->create();
    m_compositorInterface = m_display->createCompositor();
    m_compositorInterface->create();
    m_textInputManagerV0Interface = m_display->createTextInputManager(TextInputInterfaceVersion::UnstableV0);
    m_textInputManagerV0Interface->create();
    m_textInputManagerV2Interface = m_display->createTextInputManager(TextInputInterfaceVersion::UnstableV2);
    m_textInputManagerV2Interface->create();

    // setup connection
    m_connection = new KWayland::Client::ConnectionThread;
    QSignalSpy connectedSpy(m_connection, &ConnectionThread::connected);
    QVERIFY(connectedSpy.isValid());
    m_connection->setSocketName(s_socketName);

    m_thread = new QThread(this);
    m_connection->moveToThread(m_thread);
    m_thread->start();

    m_connection->initConnection();
    QVERIFY(connectedSpy.wait());

    m_queue = new EventQueue(this);
    m_queue->setup(m_connection);

    Registry registry;
    QSignalSpy interfacesAnnouncedSpy(&registry, &Registry::interfacesAnnounced);
    QVERIFY(interfacesAnnouncedSpy.isValid());
    registry.setEventQueue(m_queue);
    registry.create(m_connection);
    QVERIFY(registry.isValid());
    registry.setup();
    QVERIFY(interfacesAnnouncedSpy.wait());

    m_seat = registry.createSeat(registry.interface(Registry::Interface::Seat).name,
                                 registry.interface(Registry::Interface::Seat).version,
                                 this);
    QVERIFY(m_seat->isValid());
    QSignalSpy hasKeyboardSpy(m_seat, &Seat::hasKeyboardChanged);
    QVERIFY(hasKeyboardSpy.isValid());
    QVERIFY(hasKeyboardSpy.wait());
    m_keyboard = m_seat->createKeyboard(this);
    QVERIFY(m_keyboard->isValid());

    m_compositor = registry.createCompositor(registry.interface(Registry::Interface::Compositor).name,
                                             registry.interface(Registry::Interface::Compositor).version,
                                             this);
    QVERIFY(m_compositor->isValid());

    m_textInputManagerV0 = registry.createTextInputManager(registry.interface(Registry::Interface::TextInputManagerUnstableV0).name,
                                                           registry.interface(Registry::Interface::TextInputManagerUnstableV0).version,
                                                           this);
    QVERIFY(m_textInputManagerV0->isValid());

    m_textInputManagerV2 = registry.createTextInputManager(registry.interface(Registry::Interface::TextInputManagerUnstableV2).name,
                                                           registry.interface(Registry::Interface::TextInputManagerUnstableV2).version,
                                                           this);
    QVERIFY(m_textInputManagerV2->isValid());
}

void TextInputTest::cleanup()
{
#define CLEANUP(variable) \
    if (variable) { \
        delete variable; \
        variable = nullptr; \
    }
    CLEANUP(m_textInputManagerV0)
    CLEANUP(m_textInputManagerV2)
    CLEANUP(m_keyboard)
    CLEANUP(m_seat)
    CLEANUP(m_compositor)
    CLEANUP(m_queue)
    if (m_connection) {
        m_connection->deleteLater();
        m_connection = nullptr;
    }
    if (m_thread) {
        m_thread->quit();
        m_thread->wait();
        delete m_thread;
        m_thread = nullptr;
    }

    CLEANUP(m_textInputManagerV0Interface)
    CLEANUP(m_textInputManagerV2Interface)
    CLEANUP(m_compositorInterface)
    CLEANUP(m_seatInterface)
    CLEANUP(m_display)
#undef CLEANUP
}

SurfaceInterface *TextInputTest::waitForSurface()
{
    QSignalSpy surfaceCreatedSpy(m_compositorInterface, &CompositorInterface::surfaceCreated);
    if (!surfaceCreatedSpy.isValid()) {
        return nullptr;
    }
    if (!surfaceCreatedSpy.wait(500)) {
        return nullptr;
    }
    if (surfaceCreatedSpy.count() != 1) {
        return nullptr;
    }
    return surfaceCreatedSpy.first().first().value<SurfaceInterface*>();
}

TextInput *TextInputTest::createTextInput(TextInputInterfaceVersion version)
{
    switch (version) {
    case TextInputInterfaceVersion::UnstableV0:
        return m_textInputManagerV0->createTextInput(m_seat);
    case TextInputInterfaceVersion::UnstableV2:
        return m_textInputManagerV2->createTextInput(m_seat);
    default:
        Q_UNREACHABLE();
        return nullptr;
    }
}

void TextInputTest::testEnterLeave_data()
{
    QTest::addColumn<TextInputInterfaceVersion>("version");
    QTest::addColumn<bool>("updatesDirectly");

    QTest::newRow("UnstableV0") << TextInputInterfaceVersion::UnstableV0 << false;
    QTest::newRow("UnstableV2") << TextInputInterfaceVersion::UnstableV2 << true;
}

void TextInputTest::testEnterLeave()
{
    // this test verifies that enter leave are sent correctly
    QScopedPointer<Surface> surface(m_compositor->createSurface());
    QFETCH(TextInputInterfaceVersion, version);
    QScopedPointer<TextInput> textInput(createTextInput(version));
    auto serverSurface = waitForSurface();
    QVERIFY(serverSurface);
    QVERIFY(!textInput.isNull());
    QSignalSpy enteredSpy(textInput.data(), &TextInput::entered);
    QVERIFY(enteredSpy.isValid());
    QSignalSpy leftSpy(textInput.data(), &TextInput::left);
    QVERIFY(leftSpy.isValid());
    QSignalSpy textInputChangedSpy(m_seatInterface, &SeatInterface::focusedTextInputChanged);
    QVERIFY(textInputChangedSpy.isValid());

    // now let's try to enter it
    QVERIFY(!m_seatInterface->focusedTextInput());
    QVERIFY(!m_seatInterface->focusedTextInputSurface());
    m_seatInterface->setFocusedKeyboardSurface(serverSurface);
    QCOMPARE(m_seatInterface->focusedTextInputSurface(), serverSurface);
    // text input not yet set for the surface
    QFETCH(bool, updatesDirectly);
    QCOMPARE(bool(m_seatInterface->focusedTextInput()), updatesDirectly);
    QCOMPARE(textInputChangedSpy.isEmpty(), !updatesDirectly);
    textInput->enable(surface.data());
    // this should trigger on server side
    if (!updatesDirectly) {
        QVERIFY(textInputChangedSpy.wait());
    }
    QCOMPARE(textInputChangedSpy.count(), 1);
    auto serverTextInput = m_seatInterface->focusedTextInput();
    QVERIFY(serverTextInput);
    QCOMPARE(serverTextInput->interfaceVersion(), version);
    QSignalSpy enabledChangedSpy(serverTextInput, &TextInputInterface::enabledChanged);
    QVERIFY(enabledChangedSpy.isValid());
    if (updatesDirectly) {
        QVERIFY(enabledChangedSpy.wait());
        enabledChangedSpy.clear();
    }
    QCOMPARE(serverTextInput->surface().data(), serverSurface);
    QVERIFY(serverTextInput->isEnabled());

    // and trigger an enter
    if (enteredSpy.isEmpty()) {
        QVERIFY(enteredSpy.wait());
    }
    QCOMPARE(enteredSpy.count(), 1);
    QCOMPARE(textInput->enteredSurface(), surface.data());

    // now trigger a leave
    m_seatInterface->setFocusedKeyboardSurface(nullptr);
    QCOMPARE(textInputChangedSpy.count(), 2);
    QVERIFY(!m_seatInterface->focusedTextInput());
    QVERIFY(leftSpy.wait());
    QVERIFY(!textInput->enteredSurface());
    QVERIFY(serverTextInput->isEnabled());

    // if we enter again we should directly get the text input as it's still activated
    m_seatInterface->setFocusedKeyboardSurface(serverSurface);
    QCOMPARE(textInputChangedSpy.count(), 3);
    QVERIFY(m_seatInterface->focusedTextInput());
    QVERIFY(enteredSpy.wait());
    QCOMPARE(enteredSpy.count(), 2);
    QCOMPARE(textInput->enteredSurface(), surface.data());
    QVERIFY(serverTextInput->isEnabled());

    // let's deactivate on client side
    textInput->disable(surface.data());
    QVERIFY(enabledChangedSpy.wait());
    QCOMPARE(enabledChangedSpy.count(), 1);
    QVERIFY(!serverTextInput->isEnabled());
    // does not trigger a leave
    QCOMPARE(textInputChangedSpy.count(), 3);
    // should still be the same text input
    QCOMPARE(m_seatInterface->focusedTextInput(), serverTextInput);
    //reset
    textInput->enable(surface.data());
    QVERIFY(enabledChangedSpy.wait());

    //trigger an enter again and leave, but this
    //time we try sending an event after the surface is unbound
    //but not yet destroyed. It should work without errors
    QCOMPARE(textInput->enteredSurface(), surface.data());
    connect(serverSurface, &Resource::unbound, [=]() {
        m_seatInterface->setFocusedKeyboardSurface(nullptr);
    });
    //delete the client and wait for the server to catch up
    QSignalSpy unboundSpy(serverSurface, &QObject::destroyed);
    surface.reset();
    QVERIFY(unboundSpy.wait());
    QVERIFY(leftSpy.wait());
    QVERIFY(!textInput->enteredSurface());
}

void TextInputTest::testShowHidePanel_data()
{
    QTest::addColumn<TextInputInterfaceVersion>("version");

    QTest::newRow("UnstableV0") << TextInputInterfaceVersion::UnstableV0;
    QTest::newRow("UnstableV2") << TextInputInterfaceVersion::UnstableV2;
}

void TextInputTest::testShowHidePanel()
{
    // this test verifies that the requests for show/hide panel work
    // and that status is properly sent to the client
    QScopedPointer<Surface> surface(m_compositor->createSurface());
    auto serverSurface = waitForSurface();
    QVERIFY(serverSurface);
    QFETCH(TextInputInterfaceVersion, version);
    QScopedPointer<TextInput> textInput(createTextInput(version));
    QVERIFY(!textInput.isNull());
    textInput->enable(surface.data());
    m_connection->flush();
    m_display->dispatchEvents();

    m_seatInterface->setFocusedKeyboardSurface(serverSurface);
    auto ti = m_seatInterface->focusedTextInput();
    QVERIFY(ti);

    QSignalSpy showPanelRequestedSpy(ti, &TextInputInterface::requestShowInputPanel);
    QVERIFY(showPanelRequestedSpy.isValid());
    QSignalSpy hidePanelRequestedSpy(ti, &TextInputInterface::requestHideInputPanel);
    QVERIFY(hidePanelRequestedSpy.isValid());
    QSignalSpy inputPanelStateChangedSpy(textInput.data(), &TextInput::inputPanelStateChanged);
    QVERIFY(inputPanelStateChangedSpy.isValid());

    QCOMPARE(textInput->isInputPanelVisible(), false);
    textInput->showInputPanel();
    QVERIFY(showPanelRequestedSpy.wait());
    ti->setInputPanelState(true, QRect(0, 0, 0, 0));
    QVERIFY(inputPanelStateChangedSpy.wait());
    QCOMPARE(textInput->isInputPanelVisible(), true);

    textInput->hideInputPanel();
    QVERIFY(hidePanelRequestedSpy.wait());
    ti->setInputPanelState(false, QRect(0, 0, 0, 0));
    QVERIFY(inputPanelStateChangedSpy.wait());
    QCOMPARE(textInput->isInputPanelVisible(), false);
}

void TextInputTest::testCursorRectangle_data()
{
    QTest::addColumn<TextInputInterfaceVersion>("version");

    QTest::newRow("UnstableV0") << TextInputInterfaceVersion::UnstableV0;
    QTest::newRow("UnstableV2") << TextInputInterfaceVersion::UnstableV2;
}

void TextInputTest::testCursorRectangle()
{
    // this test verifies that passing the cursor rectangle from client to server works
    // and that setting visibility state from server to client works
    QScopedPointer<Surface> surface(m_compositor->createSurface());
    auto serverSurface = waitForSurface();
    QVERIFY(serverSurface);
    QFETCH(TextInputInterfaceVersion, version);
    QScopedPointer<TextInput> textInput(createTextInput(version));
    QVERIFY(!textInput.isNull());
    textInput->enable(surface.data());
    m_connection->flush();
    m_display->dispatchEvents();

    m_seatInterface->setFocusedKeyboardSurface(serverSurface);
    auto ti = m_seatInterface->focusedTextInput();
    QVERIFY(ti);
    QCOMPARE(ti->cursorRectangle(), QRect());
    QSignalSpy cursorRectangleChangedSpy(ti, &TextInputInterface::cursorRectangleChanged);
    QVERIFY(cursorRectangleChangedSpy.isValid());

    textInput->setCursorRectangle(QRect(10, 20, 30, 40));
    QVERIFY(cursorRectangleChangedSpy.wait());
    QCOMPARE(ti->cursorRectangle(), QRect(10, 20, 30, 40));
}

void TextInputTest::testPreferredLanguage_data()
{
    QTest::addColumn<TextInputInterfaceVersion>("version");

    QTest::newRow("UnstableV0") << TextInputInterfaceVersion::UnstableV0;
    QTest::newRow("UnstableV2") << TextInputInterfaceVersion::UnstableV2;
}

void TextInputTest::testPreferredLanguage()
{
    // this test verifies that passing the preferred language from client to server works
    QScopedPointer<Surface> surface(m_compositor->createSurface());
    auto serverSurface = waitForSurface();
    QVERIFY(serverSurface);
    QFETCH(TextInputInterfaceVersion, version);
    QScopedPointer<TextInput> textInput(createTextInput(version));
    QVERIFY(!textInput.isNull());
    textInput->enable(surface.data());
    m_connection->flush();
    m_display->dispatchEvents();

    m_seatInterface->setFocusedKeyboardSurface(serverSurface);
    auto ti = m_seatInterface->focusedTextInput();
    QVERIFY(ti);
    QVERIFY(ti->preferredLanguage().isEmpty());

    QSignalSpy preferredLanguageChangedSpy(ti, &TextInputInterface::preferredLanguageChanged);
    QVERIFY(preferredLanguageChangedSpy.isValid());
    textInput->setPreferredLanguage(QStringLiteral("foo"));
    QVERIFY(preferredLanguageChangedSpy.wait());
    QCOMPARE(ti->preferredLanguage(), QStringLiteral("foo").toUtf8());
}

void TextInputTest::testReset_data()
{
    QTest::addColumn<TextInputInterfaceVersion>("version");

    QTest::newRow("UnstableV0") << TextInputInterfaceVersion::UnstableV0;
    QTest::newRow("UnstableV2") << TextInputInterfaceVersion::UnstableV2;
}

void TextInputTest::testReset()
{
    // this test verifies that the reset request is properly passed from client to server
    QScopedPointer<Surface> surface(m_compositor->createSurface());
    auto serverSurface = waitForSurface();
    QVERIFY(serverSurface);
    QFETCH(TextInputInterfaceVersion, version);
    QScopedPointer<TextInput> textInput(createTextInput(version));
    QVERIFY(!textInput.isNull());
    textInput->enable(surface.data());
    m_connection->flush();
    m_display->dispatchEvents();

    m_seatInterface->setFocusedKeyboardSurface(serverSurface);
    auto ti = m_seatInterface->focusedTextInput();
    QVERIFY(ti);

    QSignalSpy resetRequestedSpy(ti, &TextInputInterface::requestReset);
    QVERIFY(resetRequestedSpy.isValid());

    textInput->reset();
    QVERIFY(resetRequestedSpy.wait());
}

void TextInputTest::testSurroundingText_data()
{
    QTest::addColumn<TextInputInterfaceVersion>("version");

    QTest::newRow("UnstableV0") << TextInputInterfaceVersion::UnstableV0;
    QTest::newRow("UnstableV2") << TextInputInterfaceVersion::UnstableV2;
}

void TextInputTest::testSurroundingText()
{
    // this test verifies that surrounding text is properly passed around
    QScopedPointer<Surface> surface(m_compositor->createSurface());
    auto serverSurface = waitForSurface();
    QVERIFY(serverSurface);
    QFETCH(TextInputInterfaceVersion, version);
    QScopedPointer<TextInput> textInput(createTextInput(version));
    QVERIFY(!textInput.isNull());
    textInput->enable(surface.data());
    m_connection->flush();
    m_display->dispatchEvents();

    m_seatInterface->setFocusedKeyboardSurface(serverSurface);
    auto ti = m_seatInterface->focusedTextInput();
    QVERIFY(ti);
    QVERIFY(ti->surroundingText().isEmpty());
    QCOMPARE(ti->surroundingTextCursorPosition(), 0);
    QCOMPARE(ti->surroundingTextSelectionAnchor(), 0);

    QSignalSpy surroundingTextChangedSpy(ti, &TextInputInterface::surroundingTextChanged);
    QVERIFY(surroundingTextChangedSpy.isValid());

    textInput->setSurroundingText(QStringLiteral("100 €, 100 $"), 5, 6);
    QVERIFY(surroundingTextChangedSpy.wait());
    QCOMPARE(ti->surroundingText(), QStringLiteral("100 €, 100 $").toUtf8());
    QCOMPARE(ti->surroundingTextCursorPosition(), QStringLiteral("100 €, 100 $").toUtf8().indexOf(','));
    QCOMPARE(ti->surroundingTextSelectionAnchor(), QStringLiteral("100 €, 100 $").toUtf8().indexOf(' ', ti->surroundingTextCursorPosition()));
}

void TextInputTest::testContentHints_data()
{
    QTest::addColumn<TextInputInterfaceVersion>("version");
    QTest::addColumn<TextInput::ContentHints>("clientHints");
    QTest::addColumn<TextInputInterface::ContentHints>("serverHints");

    QTest::newRow("completion/v0")     << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentHints(TextInput::ContentHint::AutoCompletion) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::AutoCompletion);
    QTest::newRow("Correction/v0")     << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentHints(TextInput::ContentHint::AutoCorrection) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::AutoCorrection);
    QTest::newRow("Capitalization/v0") << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentHints(TextInput::ContentHint::AutoCapitalization) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::AutoCapitalization);
    QTest::newRow("Lowercase/v0")      << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentHints(TextInput::ContentHint::LowerCase) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::LowerCase);
    QTest::newRow("Uppercase/v0")      << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentHints(TextInput::ContentHint::UpperCase) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::UpperCase);
    QTest::newRow("Titlecase/v0")      << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentHints(TextInput::ContentHint::TitleCase) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::TitleCase);
    QTest::newRow("HiddenText/v0")     << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentHints(TextInput::ContentHint::HiddenText) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::HiddenText);
    QTest::newRow("SensitiveData/v0")  << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentHints(TextInput::ContentHint::SensitiveData) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::SensitiveData);
    QTest::newRow("Latin/v0")          << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentHints(TextInput::ContentHint::Latin) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::Latin);
    QTest::newRow("Multiline/v0")      << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentHints(TextInput::ContentHint::MultiLine) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::MultiLine);

    QTest::newRow("autos/v0") << TextInputInterfaceVersion::UnstableV0
                              << (TextInput::ContentHint::AutoCompletion | TextInput::ContentHint::AutoCorrection | TextInput::ContentHint::AutoCapitalization)
                              << (TextInputInterface::ContentHint::AutoCompletion | TextInputInterface::ContentHint::AutoCorrection | TextInputInterface::ContentHint::AutoCapitalization);

    // all has combinations which don't make sense - what's both lowercase and uppercase?
    QTest::newRow("all/v0") << TextInputInterfaceVersion::UnstableV0
                            << (TextInput::ContentHint::AutoCompletion |
                                TextInput::ContentHint::AutoCorrection |
                                TextInput::ContentHint::AutoCapitalization |
                                TextInput::ContentHint::LowerCase |
                                TextInput::ContentHint::UpperCase |
                                TextInput::ContentHint::TitleCase |
                                TextInput::ContentHint::HiddenText |
                                TextInput::ContentHint::SensitiveData |
                                TextInput::ContentHint::Latin |
                                TextInput::ContentHint::MultiLine)
                            << (TextInputInterface::ContentHint::AutoCompletion |
                                TextInputInterface::ContentHint::AutoCorrection |
                                TextInputInterface::ContentHint::AutoCapitalization |
                                TextInputInterface::ContentHint::LowerCase |
                                TextInputInterface::ContentHint::UpperCase |
                                TextInputInterface::ContentHint::TitleCase |
                                TextInputInterface::ContentHint::HiddenText |
                                TextInputInterface::ContentHint::SensitiveData |
                                TextInputInterface::ContentHint::Latin |
                                TextInputInterface::ContentHint::MultiLine);

    // same for version 2

    QTest::newRow("completion/v2")     << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentHints(TextInput::ContentHint::AutoCompletion) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::AutoCompletion);
    QTest::newRow("Correction/v2")     << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentHints(TextInput::ContentHint::AutoCorrection) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::AutoCorrection);
    QTest::newRow("Capitalization/v2") << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentHints(TextInput::ContentHint::AutoCapitalization) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::AutoCapitalization);
    QTest::newRow("Lowercase/v2")      << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentHints(TextInput::ContentHint::LowerCase) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::LowerCase);
    QTest::newRow("Uppercase/v2")      << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentHints(TextInput::ContentHint::UpperCase) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::UpperCase);
    QTest::newRow("Titlecase/v2")      << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentHints(TextInput::ContentHint::TitleCase) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::TitleCase);
    QTest::newRow("HiddenText/v2")     << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentHints(TextInput::ContentHint::HiddenText) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::HiddenText);
    QTest::newRow("SensitiveData/v2")  << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentHints(TextInput::ContentHint::SensitiveData) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::SensitiveData);
    QTest::newRow("Latin/v2")          << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentHints(TextInput::ContentHint::Latin) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::Latin);
    QTest::newRow("Multiline/v2")      << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentHints(TextInput::ContentHint::MultiLine) << TextInputInterface::ContentHints(TextInputInterface::ContentHint::MultiLine);

    QTest::newRow("autos/v2") << TextInputInterfaceVersion::UnstableV2
                              << (TextInput::ContentHint::AutoCompletion | TextInput::ContentHint::AutoCorrection | TextInput::ContentHint::AutoCapitalization)
                              << (TextInputInterface::ContentHint::AutoCompletion | TextInputInterface::ContentHint::AutoCorrection | TextInputInterface::ContentHint::AutoCapitalization);

    // all has combinations which don't make sense - what's both lowercase and uppercase?
    QTest::newRow("all/v2") << TextInputInterfaceVersion::UnstableV2
                            << (TextInput::ContentHint::AutoCompletion |
                                TextInput::ContentHint::AutoCorrection |
                                TextInput::ContentHint::AutoCapitalization |
                                TextInput::ContentHint::LowerCase |
                                TextInput::ContentHint::UpperCase |
                                TextInput::ContentHint::TitleCase |
                                TextInput::ContentHint::HiddenText |
                                TextInput::ContentHint::SensitiveData |
                                TextInput::ContentHint::Latin |
                                TextInput::ContentHint::MultiLine)
                            << (TextInputInterface::ContentHint::AutoCompletion |
                                TextInputInterface::ContentHint::AutoCorrection |
                                TextInputInterface::ContentHint::AutoCapitalization |
                                TextInputInterface::ContentHint::LowerCase |
                                TextInputInterface::ContentHint::UpperCase |
                                TextInputInterface::ContentHint::TitleCase |
                                TextInputInterface::ContentHint::HiddenText |
                                TextInputInterface::ContentHint::SensitiveData |
                                TextInputInterface::ContentHint::Latin |
                                TextInputInterface::ContentHint::MultiLine);
}

void TextInputTest::testContentHints()
{
    // this test verifies that content hints are properly passed from client to server
    QScopedPointer<Surface> surface(m_compositor->createSurface());
    auto serverSurface = waitForSurface();
    QVERIFY(serverSurface);
    QFETCH(TextInputInterfaceVersion, version);
    QScopedPointer<TextInput> textInput(createTextInput(version));
    QVERIFY(!textInput.isNull());
    textInput->enable(surface.data());
    m_connection->flush();
    m_display->dispatchEvents();

    m_seatInterface->setFocusedKeyboardSurface(serverSurface);
    auto ti = m_seatInterface->focusedTextInput();
    QVERIFY(ti);
    QCOMPARE(ti->contentHints(), TextInputInterface::ContentHints());

    QSignalSpy contentTypeChangedSpy(ti, &TextInputInterface::contentTypeChanged);
    QVERIFY(contentTypeChangedSpy.isValid());
    QFETCH(TextInput::ContentHints, clientHints);
    textInput->setContentType(clientHints, TextInput::ContentPurpose::Normal);
    QVERIFY(contentTypeChangedSpy.wait());
    QTEST(ti->contentHints(), "serverHints");

    // setting to same should not trigger an update
    textInput->setContentType(clientHints, TextInput::ContentPurpose::Normal);
    QVERIFY(!contentTypeChangedSpy.wait(100));

    // unsetting should work
    textInput->setContentType(TextInput::ContentHints(), TextInput::ContentPurpose::Normal);
    QVERIFY(contentTypeChangedSpy.wait());
    QCOMPARE(ti->contentHints(), TextInputInterface::ContentHints());
}

void TextInputTest::testContentPurpose_data()
{
    QTest::addColumn<TextInputInterfaceVersion>("version");
    QTest::addColumn<TextInput::ContentPurpose>("clientPurpose");
    QTest::addColumn<TextInputInterface::ContentPurpose>("serverPurpose");

    QTest::newRow("Alpha/v0")    << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentPurpose::Alpha    << TextInputInterface::ContentPurpose::Alpha;
    QTest::newRow("Digits/v0")   << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentPurpose::Digits   << TextInputInterface::ContentPurpose::Digits;
    QTest::newRow("Number/v0")   << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentPurpose::Number   << TextInputInterface::ContentPurpose::Number;
    QTest::newRow("Phone/v0")    << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentPurpose::Phone    << TextInputInterface::ContentPurpose::Phone;
    QTest::newRow("Url/v0")      << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentPurpose::Url      << TextInputInterface::ContentPurpose::Url;
    QTest::newRow("Email/v0")    << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentPurpose::Email    << TextInputInterface::ContentPurpose::Email;
    QTest::newRow("Name/v0")     << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentPurpose::Name     << TextInputInterface::ContentPurpose::Name;
    QTest::newRow("Password/v0") << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentPurpose::Password << TextInputInterface::ContentPurpose::Password;
    QTest::newRow("Date/v0")     << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentPurpose::Date     << TextInputInterface::ContentPurpose::Date;
    QTest::newRow("Time/v0")     << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentPurpose::Time     << TextInputInterface::ContentPurpose::Time;
    QTest::newRow("Datetime/v0") << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentPurpose::DateTime << TextInputInterface::ContentPurpose::DateTime;
    QTest::newRow("Terminal/v0") << TextInputInterfaceVersion::UnstableV0 << TextInput::ContentPurpose::Terminal << TextInputInterface::ContentPurpose::Terminal;

    QTest::newRow("Alpha/v2")    << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentPurpose::Alpha    << TextInputInterface::ContentPurpose::Alpha;
    QTest::newRow("Digits/v2")   << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentPurpose::Digits   << TextInputInterface::ContentPurpose::Digits;
    QTest::newRow("Number/v2")   << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentPurpose::Number   << TextInputInterface::ContentPurpose::Number;
    QTest::newRow("Phone/v2")    << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentPurpose::Phone    << TextInputInterface::ContentPurpose::Phone;
    QTest::newRow("Url/v2")      << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentPurpose::Url      << TextInputInterface::ContentPurpose::Url;
    QTest::newRow("Email/v2")    << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentPurpose::Email    << TextInputInterface::ContentPurpose::Email;
    QTest::newRow("Name/v2")     << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentPurpose::Name     << TextInputInterface::ContentPurpose::Name;
    QTest::newRow("Password/v2") << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentPurpose::Password << TextInputInterface::ContentPurpose::Password;
    QTest::newRow("Date/v2")     << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentPurpose::Date     << TextInputInterface::ContentPurpose::Date;
    QTest::newRow("Time/v2")     << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentPurpose::Time     << TextInputInterface::ContentPurpose::Time;
    QTest::newRow("Datetime/v2") << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentPurpose::DateTime << TextInputInterface::ContentPurpose::DateTime;
    QTest::newRow("Terminal/v2") << TextInputInterfaceVersion::UnstableV2 << TextInput::ContentPurpose::Terminal << TextInputInterface::ContentPurpose::Terminal;
}

void TextInputTest::testContentPurpose()
{
    // this test verifies that content purpose are properly passed from client to server
    QScopedPointer<Surface> surface(m_compositor->createSurface());
    auto serverSurface = waitForSurface();
    QVERIFY(serverSurface);
    QFETCH(TextInputInterfaceVersion, version);
    QScopedPointer<TextInput> textInput(createTextInput(version));
    QVERIFY(!textInput.isNull());
    textInput->enable(surface.data());
    m_connection->flush();
    m_display->dispatchEvents();

    m_seatInterface->setFocusedKeyboardSurface(serverSurface);
    auto ti = m_seatInterface->focusedTextInput();
    QVERIFY(ti);
    QCOMPARE(ti->contentPurpose(), TextInputInterface::ContentPurpose::Normal);

    QSignalSpy contentTypeChangedSpy(ti, &TextInputInterface::contentTypeChanged);
    QVERIFY(contentTypeChangedSpy.isValid());
    QFETCH(TextInput::ContentPurpose, clientPurpose);
    textInput->setContentType(TextInput::ContentHints(), clientPurpose);
    QVERIFY(contentTypeChangedSpy.wait());
    QTEST(ti->contentPurpose(), "serverPurpose");

    // setting to same should not trigger an update
    textInput->setContentType(TextInput::ContentHints(), clientPurpose);
    QVERIFY(!contentTypeChangedSpy.wait(100));

    // unsetting should work
    textInput->setContentType(TextInput::ContentHints(), TextInput::ContentPurpose::Normal);
    QVERIFY(contentTypeChangedSpy.wait());
    QCOMPARE(ti->contentPurpose(), TextInputInterface::ContentPurpose::Normal);
}

void TextInputTest::testTextDirection_data()
{
    QTest::addColumn<TextInputInterfaceVersion>("version");
    QTest::addColumn<Qt::LayoutDirection>("textDirection");

    QTest::newRow("ltr/v0") << TextInputInterfaceVersion::UnstableV0 << Qt::LeftToRight;
    QTest::newRow("rtl/v0") << TextInputInterfaceVersion::UnstableV0 << Qt::RightToLeft;

    QTest::newRow("ltr/v2") << TextInputInterfaceVersion::UnstableV2 << Qt::LeftToRight;
    QTest::newRow("rtl/v2") << TextInputInterfaceVersion::UnstableV2 << Qt::RightToLeft;
}

void TextInputTest::testTextDirection()
{
    // this test verifies that the text direction is sent from server to client
    QScopedPointer<Surface> surface(m_compositor->createSurface());
    auto serverSurface = waitForSurface();
    QVERIFY(serverSurface);
    QFETCH(TextInputInterfaceVersion, version);
    QScopedPointer<TextInput> textInput(createTextInput(version));
    QVERIFY(!textInput.isNull());
    // default should be auto
    QCOMPARE(textInput->textDirection(), Qt::LayoutDirectionAuto);
    textInput->enable(surface.data());
    m_connection->flush();
    m_display->dispatchEvents();

    m_seatInterface->setFocusedKeyboardSurface(serverSurface);
    auto ti = m_seatInterface->focusedTextInput();
    QVERIFY(ti);

    // let's send the new text direction
    QSignalSpy textDirectionChangedSpy(textInput.data(), &TextInput::textDirectionChanged);
    QVERIFY(textDirectionChangedSpy.isValid());
    QFETCH(Qt::LayoutDirection, textDirection);
    ti->setTextDirection(textDirection);
    QVERIFY(textDirectionChangedSpy.wait());
    QCOMPARE(textInput->textDirection(), textDirection);
    // setting again should not change
    ti->setTextDirection(textDirection);
    QVERIFY(!textDirectionChangedSpy.wait(100));

    // setting back to auto
    ti->setTextDirection(Qt::LayoutDirectionAuto);
    QVERIFY(textDirectionChangedSpy.wait());
    QCOMPARE(textInput->textDirection(), Qt::LayoutDirectionAuto);
}

void TextInputTest::testLanguage_data()
{
    QTest::addColumn<TextInputInterfaceVersion>("version");

    QTest::newRow("UnstableV0") << TextInputInterfaceVersion::UnstableV0;
    QTest::newRow("UnstableV2") << TextInputInterfaceVersion::UnstableV2;
}

void TextInputTest::testLanguage()
{
    // this test verifies that language is sent from server to client
    QScopedPointer<Surface> surface(m_compositor->createSurface());
    auto serverSurface = waitForSurface();
    QVERIFY(serverSurface);
    QFETCH(TextInputInterfaceVersion, version);
    QScopedPointer<TextInput> textInput(createTextInput(version));
    QVERIFY(!textInput.isNull());
    // default should be empty
    QVERIFY(textInput->language().isEmpty());
    textInput->enable(surface.data());
    m_connection->flush();
    m_display->dispatchEvents();

    m_seatInterface->setFocusedKeyboardSurface(serverSurface);
    auto ti = m_seatInterface->focusedTextInput();
    QVERIFY(ti);

    // let's send the new language
    QSignalSpy langugageChangedSpy(textInput.data(), &TextInput::languageChanged);
    QVERIFY(langugageChangedSpy.isValid());
    ti->setLanguage(QByteArrayLiteral("foo"));
    QVERIFY(langugageChangedSpy.wait());
    QCOMPARE(textInput->language(), QByteArrayLiteral("foo"));
    // setting to same should not trigger
    ti->setLanguage(QByteArrayLiteral("foo"));
    QVERIFY(!langugageChangedSpy.wait(100));
    // but to something else should trigger again
    ti->setLanguage(QByteArrayLiteral("bar"));
    QVERIFY(langugageChangedSpy.wait());
    QCOMPARE(textInput->language(), QByteArrayLiteral("bar"));
}

void TextInputTest::testKeyEvent_data()
{
    QTest::addColumn<TextInputInterfaceVersion>("version");

    QTest::newRow("UnstableV0") << TextInputInterfaceVersion::UnstableV0;
    QTest::newRow("UnstableV2") << TextInputInterfaceVersion::UnstableV2;
}

void TextInputTest::testKeyEvent()
{
    qRegisterMetaType<Qt::KeyboardModifiers>();
    qRegisterMetaType<TextInput::KeyState>();
    // this test verifies that key events are properly sent to the client
    QScopedPointer<Surface> surface(m_compositor->createSurface());
    auto serverSurface = waitForSurface();
    QVERIFY(serverSurface);
    QFETCH(TextInputInterfaceVersion, version);
    QScopedPointer<TextInput> textInput(createTextInput(version));
    QVERIFY(!textInput.isNull());
    textInput->enable(surface.data());
    m_connection->flush();
    m_display->dispatchEvents();

    m_seatInterface->setFocusedKeyboardSurface(serverSurface);
    auto ti = m_seatInterface->focusedTextInput();
    QVERIFY(ti);

    // TODO: test modifiers
    QSignalSpy keyEventSpy(textInput.data(), &TextInput::keyEvent);
    QVERIFY(keyEventSpy.isValid());
    m_seatInterface->setTimestamp(100);
    ti->keysymPressed(2);
    QVERIFY(keyEventSpy.wait());
    QCOMPARE(keyEventSpy.count(), 1);
    QCOMPARE(keyEventSpy.last().at(0).value<quint32>(), 2u);
    QCOMPARE(keyEventSpy.last().at(1).value<TextInput::KeyState>(), TextInput::KeyState::Pressed);
    QCOMPARE(keyEventSpy.last().at(2).value<Qt::KeyboardModifiers>(), Qt::KeyboardModifiers());
    QCOMPARE(keyEventSpy.last().at(3).value<quint32>(), 100u);
    m_seatInterface->setTimestamp(101);
    ti->keysymReleased(2);
    QVERIFY(keyEventSpy.wait());
    QCOMPARE(keyEventSpy.count(), 2);
    QCOMPARE(keyEventSpy.last().at(0).value<quint32>(), 2u);
    QCOMPARE(keyEventSpy.last().at(1).value<TextInput::KeyState>(), TextInput::KeyState::Released);
    QCOMPARE(keyEventSpy.last().at(2).value<Qt::KeyboardModifiers>(), Qt::KeyboardModifiers());
    QCOMPARE(keyEventSpy.last().at(3).value<quint32>(), 101u);
}

void TextInputTest::testPreEdit_data()
{
    QTest::addColumn<TextInputInterfaceVersion>("version");

    QTest::newRow("UnstableV0") << TextInputInterfaceVersion::UnstableV0;
    QTest::newRow("UnstableV2") << TextInputInterfaceVersion::UnstableV2;
}

void TextInputTest::testPreEdit()
{
    // this test verifies that pre-edit is correctly passed to the client
    QScopedPointer<Surface> surface(m_compositor->createSurface());
    auto serverSurface = waitForSurface();
    QVERIFY(serverSurface);
    QFETCH(TextInputInterfaceVersion, version);
    QScopedPointer<TextInput> textInput(createTextInput(version));
    QVERIFY(!textInput.isNull());
    // verify default values
    QVERIFY(textInput->composingText().isEmpty());
    QVERIFY(textInput->composingFallbackText().isEmpty());
    QCOMPARE(textInput->composingTextCursorPosition(), 0);

    textInput->enable(surface.data());
    m_connection->flush();
    m_display->dispatchEvents();

    m_seatInterface->setFocusedKeyboardSurface(serverSurface);
    auto ti = m_seatInterface->focusedTextInput();
    QVERIFY(ti);

    // now let's pass through some pre-edit events
    QSignalSpy composingTextChangedSpy(textInput.data(), &TextInput::composingTextChanged);
    QVERIFY(composingTextChangedSpy.isValid());
    ti->setPreEditCursor(1);
    ti->preEdit(QByteArrayLiteral("foo"), QByteArrayLiteral("bar"));
    QVERIFY(composingTextChangedSpy.wait());
    QCOMPARE(composingTextChangedSpy.count(), 1);
    QCOMPARE(textInput->composingText(), QByteArrayLiteral("foo"));
    QCOMPARE(textInput->composingFallbackText(), QByteArrayLiteral("bar"));
    QCOMPARE(textInput->composingTextCursorPosition(), 1);

    // when no pre edit cursor is sent, it's at end of text
    ti->preEdit(QByteArrayLiteral("foobar"), QByteArray());
    QVERIFY(composingTextChangedSpy.wait());
    QCOMPARE(composingTextChangedSpy.count(), 2);
    QCOMPARE(textInput->composingText(), QByteArrayLiteral("foobar"));
    QCOMPARE(textInput->composingFallbackText(), QByteArray());
    QCOMPARE(textInput->composingTextCursorPosition(), 6);
}

void TextInputTest::testCommit_data()
{
    QTest::addColumn<TextInputInterfaceVersion>("version");

    QTest::newRow("UnstableV0") << TextInputInterfaceVersion::UnstableV0;
    QTest::newRow("UnstableV2") << TextInputInterfaceVersion::UnstableV2;
}

void TextInputTest::testCommit()
{
    // this test verifies that the commit is handled correctly by the client
    QScopedPointer<Surface> surface(m_compositor->createSurface());
    auto serverSurface = waitForSurface();
    QVERIFY(serverSurface);
    QFETCH(TextInputInterfaceVersion, version);
    QScopedPointer<TextInput> textInput(createTextInput(version));
    QVERIFY(!textInput.isNull());
    // verify default values
    QCOMPARE(textInput->commitText(), QByteArray());
    QCOMPARE(textInput->cursorPosition(), 0);
    QCOMPARE(textInput->anchorPosition(), 0);
    QCOMPARE(textInput->deleteSurroundingText().beforeLength, 0u);
    QCOMPARE(textInput->deleteSurroundingText().afterLength, 0u);

    textInput->enable(surface.data());
    m_connection->flush();
    m_display->dispatchEvents();

    m_seatInterface->setFocusedKeyboardSurface(serverSurface);
    auto ti = m_seatInterface->focusedTextInput();
    QVERIFY(ti);

    // now let's commit
    QSignalSpy committedSpy(textInput.data(), &TextInput::committed);
    QVERIFY(committedSpy.isValid());
    ti->setCursorPosition(3, 4);
    ti->deleteSurroundingText(2, 1);
    ti->commit(QByteArrayLiteral("foo"));

    QVERIFY(committedSpy.wait());
    QCOMPARE(textInput->commitText(), QByteArrayLiteral("foo"));
    QCOMPARE(textInput->cursorPosition(), 3);
    QCOMPARE(textInput->anchorPosition(), 4);
    QCOMPARE(textInput->deleteSurroundingText().beforeLength, 2u);
    QCOMPARE(textInput->deleteSurroundingText().afterLength, 1u);
}

QTEST_GUILESS_MAIN(TextInputTest)
#include "test_text_input.moc"
