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

    SPDX-FileCopyrightText: 2017 Martin Flöser <mgraesslin@kde.org>
    SPDX-FileCopyrightText: 2019 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
    SPDX-FileCopyrightText: 2022 Ismael Asensio <isma.af@gmail.com>

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

#include "kwin_wayland_test.h"

#include "core/output.h"
#include "core/outputbackend.h"
#include "core/outputconfiguration.h"
#include "cursor.h"
#include "rules.h"
#include "scene/windowitem.h"
#include "virtualdesktops.h"
#include "wayland_server.h"
#include "window.h"
#include "workspace.h"

#include <KWayland/Client/surface.h>

#include <linux/input.h>

using namespace KWin;

class TestXdgShellWindowRules : public QObject
{
    Q_OBJECT

    enum ClientFlag {
        None = 0,
        ClientShouldBeInactive = 1 << 0, // Window should be inactive. Used on Minimize tests
        ServerSideDecoration = 1 << 1, // Create window with server side decoration. Used on noBorder tests
        ReturnAfterSurfaceConfiguration = 1 << 2, // Do not create the window now, but return after surface configuration.
    };
    Q_DECLARE_FLAGS(ClientFlags, ClientFlag)

private Q_SLOTS:
    void initTestCase();
    void init();
    void cleanup();

    void testPositionDontAffect();
    void testPositionApply();
    void testPositionRemember();
    void testPositionForce();
    void testPositionApplyNow();
    void testPositionForceTemporarily();

    void testSizeDontAffect();
    void testSizeApply();
    void testSizeRemember();
    void testSizeForce();
    void testSizeApplyNow();
    void testSizeForceTemporarily();

    void testMaximizeDontAffect();
    void testMaximizeApply();
    void testMaximizeRemember();
    void testMaximizeForce();
    void testMaximizeApplyNow();
    void testMaximizeForceTemporarily();

    void testDesktopsDontAffect();
    void testDesktopsApply();
    void testDesktopsRemember();
    void testDesktopsForce();
    void testDesktopsForceWithPerOutputDesktopsAndInteractiveMove();
    void testDesktopsApplyNow();
    void testDesktopsForceTemporarily();

    void testMinimizeDontAffect();
    void testMinimizeApply();
    void testMinimizeRemember();
    void testMinimizeForce();
    void testMinimizeApplyNow();
    void testMinimizeForceTemporarily();

    void testSkipTaskbarDontAffect();
    void testSkipTaskbarApply();
    void testSkipTaskbarRemember();
    void testSkipTaskbarForce();
    void testSkipTaskbarApplyNow();
    void testSkipTaskbarForceTemporarily();

    void testSkipPagerDontAffect();
    void testSkipPagerApply();
    void testSkipPagerRemember();
    void testSkipPagerForce();
    void testSkipPagerApplyNow();
    void testSkipPagerForceTemporarily();

    void testSkipSwitcherDontAffect();
    void testSkipSwitcherApply();
    void testSkipSwitcherRemember();
    void testSkipSwitcherForce();
    void testSkipSwitcherApplyNow();
    void testSkipSwitcherForceTemporarily();

    void testKeepAboveDontAffect();
    void testKeepAboveApply();
    void testKeepAboveRemember();
    void testKeepAboveForce();
    void testKeepAboveApplyNow();
    void testKeepAboveForceTemporarily();

    void testKeepBelowDontAffect();
    void testKeepBelowApply();
    void testKeepBelowRemember();
    void testKeepBelowForce();
    void testKeepBelowApplyNow();
    void testKeepBelowForceTemporarily();

    void testShortcutDontAffect();
    void testShortcutApply();
    void testShortcutRemember();
    void testShortcutForce();
    void testShortcutApplyNow();
    void testShortcutForceTemporarily();

    void testDesktopFileDontAffect();
    void testDesktopFileApply();
    void testDesktopFileRemember();
    void testDesktopFileForce();
    void testDesktopFileApplyNow();
    void testDesktopFileForceTemporarily();

    void testActiveOpacityDontAffect();
    void testActiveOpacityForce();
    void testActiveOpacityForceTemporarily();

    void testInactiveOpacityDontAffect();
    void testInactiveOpacityForce();
    void testInactiveOpacityForceTemporarily();

    void testNoBorderDontAffect();
    void testNoBorderApply();
    void testNoBorderRemember();
    void testNoBorderForce();
    void testNoBorderApplyNow();
    void testNoBorderForceTemporarily();

    void testScreenDontAffect();
    void testScreenApply();
    void testScreenRemember();
    void testScreenForce();
    void testScreenApplyNow();
    void testScreenForceTemporarily();

    void testLayerDontAffect();
    void testLayerForce();
    void testLayerForceTemporarily();

    void testCloseableDontAffect();
    void testCloseableForce();
    void testCloseableForceTemporarily();

    void testMatchAfterNameChange();
    void testNotEnabled();

    void testMatchTransientParent_data();
    void testMatchTransientParent();

private:
    void createTestWindow(ClientFlags flags = None);
    void mapClientToSurface(QSize clientSize, ClientFlags flags = None);
    void destroyTestWindow();

    template<typename T>
    void setWindowRule(const QString &property, const T &value, int policy);

private:
    KSharedConfig::Ptr m_config;

    Window *m_window;
    std::unique_ptr<KWayland::Client::Surface> m_surface;
    std::unique_ptr<Test::XdgToplevel> m_shellSurface;
    std::unique_ptr<Test::XdgToplevelDecorationV1> m_decoration;

    std::unique_ptr<QSignalSpy> m_toplevelConfigureRequestedSpy;
    std::unique_ptr<QSignalSpy> m_surfaceConfigureRequestedSpy;
};

void TestXdgShellWindowRules::initTestCase()
{
    qRegisterMetaType<KWin::Window *>();

    QVERIFY(waylandServer()->init(qAppName()));

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

    m_config = KSharedConfig::openConfig(QStringLiteral("kwinrulesrc"), KConfig::SimpleConfig);
    workspace()->rulebook()->setConfig(m_config);
}

void TestXdgShellWindowRules::init()
{
    VirtualDesktopManager::self()->setCurrent(VirtualDesktopManager::self()->desktops().first());
    QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::XdgDecorationV1));

    workspace()->setActiveOutput(QPoint(640, 512));
}

void TestXdgShellWindowRules::cleanup()
{
    if (m_shellSurface) {
        destroyTestWindow();
    }

    Test::destroyWaylandConnection();

    // Wipe the window rule config clean.
    for (const QString &group : m_config->groupList()) {
        m_config->deleteGroup(group);
    }
    workspace()->slotReconfigure();

    // Restore virtual desktops to the initial state.
    VirtualDesktopManager::self()->setCount(1);
    QCOMPARE(VirtualDesktopManager::self()->count(), 1u);
    VirtualDesktopManager::self()->setPerOutputVirtualDesktops(false);
    QCOMPARE(VirtualDesktopManager::self()->isPerOutputVirtualDesktops(), false);
}

void TestXdgShellWindowRules::createTestWindow(ClientFlags flags)
{
    // Apply flags for special windows and rules
    const bool createClient = !(flags & ReturnAfterSurfaceConfiguration);
    const auto decorationMode = (flags & ServerSideDecoration) ? Test::XdgToplevelDecorationV1::mode_server_side
                                                               : Test::XdgToplevelDecorationV1::mode_client_side;
    // Create an xdg surface.
    m_surface = Test::createSurface();
    m_shellSurface = Test::createXdgToplevelSurface(m_surface.get(), Test::CreationSetup::CreateOnly);
    m_decoration = Test::createXdgToplevelDecorationV1(m_shellSurface.get());

    // Add signal watchers
    m_toplevelConfigureRequestedSpy = std::make_unique<QSignalSpy>(m_shellSurface.get(), &Test::XdgToplevel::configureRequested);
    m_surfaceConfigureRequestedSpy = std::make_unique<QSignalSpy>(m_shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested);

    m_shellSurface->set_app_id(QStringLiteral("org.kde.foo"));
    m_decoration->set_mode(decorationMode);

    // Wait for the initial configure event
    m_surface->commit(KWayland::Client::Surface::CommitFlag::None);
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());

    if (createClient) {
        mapClientToSurface(QSize(100, 50), flags);
    }
}

void TestXdgShellWindowRules::mapClientToSurface(QSize clientSize, ClientFlags flags)
{
    const bool clientShouldBeActive = !(flags & ClientShouldBeInactive);

    QVERIFY(m_surface != nullptr);
    QVERIFY(m_shellSurface != nullptr);
    QVERIFY(m_surfaceConfigureRequestedSpy != nullptr);

    // Draw content of the surface.
    m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value<quint32>());

    // Create the window
    m_window = Test::renderAndWaitForShown(m_surface.get(), clientSize, Qt::blue);
    QVERIFY(m_window);
    QCOMPARE(m_window->isActive(), clientShouldBeActive);
}

void TestXdgShellWindowRules::destroyTestWindow()
{
    m_surfaceConfigureRequestedSpy.reset();
    m_toplevelConfigureRequestedSpy.reset();
    m_decoration.reset();
    m_shellSurface.reset();
    m_surface.reset();
    QVERIFY(Test::waitForWindowClosed(m_window));
}

template<typename T>
void TestXdgShellWindowRules::setWindowRule(const QString &property, const T &value, int policy)
{
    // Initialize RuleBook with the test rule.
    const QString ruleGroupName = QStringLiteral("test-rule");
    m_config->group(QStringLiteral("General")).writeEntry("rules", QStringList{ruleGroupName});
    KConfigGroup group = m_config->group(ruleGroupName);

    group.writeEntry(property, value);
    group.writeEntry(QStringLiteral("%1rule").arg(property), policy);

    group.writeEntry("wmclass", "org.kde.foo");
    group.writeEntry("wmclasscomplete", false);
    group.writeEntry("wmclassmatch", int(Rules::ExactMatch));
    group.sync();

    workspace()->slotReconfigure();
}

void TestXdgShellWindowRules::testPositionDontAffect()
{
    setWindowRule("position", QPoint(42, 42), int(Rules::DontAffect));

    createTestWindow();

    // The position of the window should not be affected by the rule. The default
    // placement policy will put the window in the top-left corner of the screen.
    QVERIFY(m_window->isMovable());
    QVERIFY(m_window->isMovableAcrossScreens());
    QCOMPARE(m_window->pos(), QPoint(0, 0));

    destroyTestWindow();
}

void TestXdgShellWindowRules::testPositionApply()
{
    setWindowRule("position", QPoint(42, 42), int(Rules::Apply));

    createTestWindow();

    // The window should be moved to the position specified by the rule.
    QVERIFY(m_window->isMovable());
    QVERIFY(m_window->isMovableAcrossScreens());
    QCOMPARE(m_window->pos(), QPoint(42, 42));

    // One should still be able to move the window around.
    QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted);
    QSignalSpy interactiveMoveResizeSteppedSpy(m_window, &Window::interactiveMoveResizeStepped);
    QSignalSpy interactiveMoveResizeFinishedSpy(m_window, &Window::interactiveMoveResizeFinished);

    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());
    workspace()->slotWindowMove();
    QCOMPARE(workspace()->moveResizeWindow(), m_window);
    QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1);
    QVERIFY(m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());

    const QPointF cursorPos = KWin::Cursors::self()->mouse()->pos();
    m_window->keyPressEvent(Qt::Key_Right);
    m_window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers());
    QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0));
    QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1);
    QCOMPARE(m_window->pos(), QPoint(50, 42));

    m_window->keyPressEvent(Qt::Key_Enter);
    QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1);
    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());
    QCOMPARE(m_window->pos(), QPoint(50, 42));

    // The rule should be applied again if the window appears after it's been closed.
    destroyTestWindow();
    createTestWindow();

    QVERIFY(m_window->isMovable());
    QVERIFY(m_window->isMovableAcrossScreens());
    QCOMPARE(m_window->pos(), QPoint(42, 42));

    destroyTestWindow();
}

void TestXdgShellWindowRules::testPositionRemember()
{
    setWindowRule("position", QPoint(42, 42), int(Rules::Remember));
    createTestWindow();

    // The window should be moved to the position specified by the rule.
    QVERIFY(m_window->isMovable());
    QVERIFY(m_window->isMovableAcrossScreens());
    QCOMPARE(m_window->pos(), QPoint(42, 42));

    // One should still be able to move the window around.
    QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted);
    QSignalSpy interactiveMoveResizeSteppedSpy(m_window, &Window::interactiveMoveResizeStepped);
    QSignalSpy interactiveMoveResizeFinishedSpy(m_window, &Window::interactiveMoveResizeFinished);

    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());
    workspace()->slotWindowMove();
    QCOMPARE(workspace()->moveResizeWindow(), m_window);
    QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1);
    QVERIFY(m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());

    const QPointF cursorPos = KWin::Cursors::self()->mouse()->pos();
    m_window->keyPressEvent(Qt::Key_Right);
    m_window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers());
    QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0));
    QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1);
    QCOMPARE(m_window->pos(), QPoint(50, 42));

    m_window->keyPressEvent(Qt::Key_Enter);
    QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1);
    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());
    QCOMPARE(m_window->pos(), QPoint(50, 42));

    // The window should be placed at the last know position if we reopen it.
    destroyTestWindow();
    createTestWindow();

    QVERIFY(m_window->isMovable());
    QVERIFY(m_window->isMovableAcrossScreens());
    QCOMPARE(m_window->pos(), QPoint(50, 42));

    destroyTestWindow();
}

void TestXdgShellWindowRules::testPositionForce()
{
    setWindowRule("position", QPoint(42, 42), int(Rules::Force));

    createTestWindow();

    // The window should be moved to the position specified by the rule.
    QVERIFY(!m_window->isMovable());
    QVERIFY(!m_window->isMovableAcrossScreens());
    QCOMPARE(m_window->pos(), QPoint(42, 42));

    // User should not be able to move the window.
    QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted);
    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());
    workspace()->slotWindowMove();
    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QCOMPARE(interactiveMoveResizeStartedSpy.count(), 0);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());

    // The position should still be forced if we reopen the window.
    destroyTestWindow();
    createTestWindow();

    QVERIFY(!m_window->isMovable());
    QVERIFY(!m_window->isMovableAcrossScreens());
    QCOMPARE(m_window->pos(), QPoint(42, 42));

    destroyTestWindow();
}

void TestXdgShellWindowRules::testPositionApplyNow()
{
    createTestWindow();

    // The position of the window isn't set by any rule, thus the default placement
    // policy will try to put the window in the top-left corner of the screen.
    QVERIFY(m_window->isMovable());
    QVERIFY(m_window->isMovableAcrossScreens());
    QCOMPARE(m_window->pos(), QPoint(0, 0));

    QSignalSpy frameGeometryChangedSpy(m_window, &Window::frameGeometryChanged);

    setWindowRule("position", QPoint(42, 42), int(Rules::ApplyNow));

    // The window should be moved to the position specified by the rule.
    QCOMPARE(frameGeometryChangedSpy.count(), 1);
    QCOMPARE(m_window->pos(), QPoint(42, 42));

    // We still have to be able to move the window around.
    QVERIFY(m_window->isMovable());
    QVERIFY(m_window->isMovableAcrossScreens());
    QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted);
    QSignalSpy interactiveMoveResizeSteppedSpy(m_window, &Window::interactiveMoveResizeStepped);
    QSignalSpy interactiveMoveResizeFinishedSpy(m_window, &Window::interactiveMoveResizeFinished);

    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());
    workspace()->slotWindowMove();
    QCOMPARE(workspace()->moveResizeWindow(), m_window);
    QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1);
    QVERIFY(m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());

    const QPointF cursorPos = KWin::Cursors::self()->mouse()->pos();
    m_window->keyPressEvent(Qt::Key_Right);
    m_window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers());
    QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0));
    QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1);
    QCOMPARE(m_window->pos(), QPoint(50, 42));

    m_window->keyPressEvent(Qt::Key_Enter);
    QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1);
    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());
    QCOMPARE(m_window->pos(), QPoint(50, 42));

    // The rule should not be applied again.
    m_window->evaluateWindowRules();
    QCOMPARE(m_window->pos(), QPoint(50, 42));

    destroyTestWindow();
}

void TestXdgShellWindowRules::testPositionForceTemporarily()
{
    setWindowRule("position", QPoint(42, 42), int(Rules::ForceTemporarily));

    createTestWindow();

    // The window should be moved to the position specified by the rule.
    QVERIFY(!m_window->isMovable());
    QVERIFY(!m_window->isMovableAcrossScreens());
    QCOMPARE(m_window->pos(), QPoint(42, 42));

    // User should not be able to move the window.
    QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted);
    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());
    workspace()->slotWindowMove();
    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QCOMPARE(interactiveMoveResizeStartedSpy.count(), 0);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());

    // The rule should be discarded if we close the window.
    destroyTestWindow();
    createTestWindow();

    QVERIFY(m_window->isMovable());
    QVERIFY(m_window->isMovableAcrossScreens());
    QCOMPARE(m_window->pos(), QPoint(0, 0));

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSizeDontAffect()
{
    setWindowRule("size", QSize(480, 640), int(Rules::DontAffect));

    createTestWindow(ReturnAfterSurfaceConfiguration);

    // The window size shouldn't be enforced by the rule.
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(0, 0));

    // Map the window.
    mapClientToSurface(QSize(100, 50));
    QVERIFY(m_window->isResizable());
    QCOMPARE(m_window->size(), QSize(100, 50));

    // We should receive a configure event when the window becomes active.
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSizeApply()
{
    setWindowRule("size", QSize(480, 640), int(Rules::Apply));

    createTestWindow(ReturnAfterSurfaceConfiguration);

    // The initial configure event should contain size hint set by the rule.
    Test::XdgToplevel::States states;
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(480, 640));
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Resizing));

    // Map the window.
    mapClientToSurface(QSize(480, 640));
    QVERIFY(m_window->isResizable());
    QCOMPARE(m_window->size(), QSize(480, 640));

    // We should receive a configure event when the window becomes active.
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Resizing));

    // One still should be able to resize the window.
    QSignalSpy frameGeometryChangedSpy(m_window, &Window::frameGeometryChanged);
    QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted);
    QSignalSpy interactiveMoveResizeSteppedSpy(m_window, &Window::interactiveMoveResizeStepped);
    QSignalSpy interactiveMoveResizeFinishedSpy(m_window, &Window::interactiveMoveResizeFinished);

    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());
    workspace()->slotWindowResize();
    QCOMPARE(workspace()->moveResizeWindow(), m_window);
    QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(m_window->isInteractiveResize());
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 3);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 3);
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing));
    m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value<quint32>());

    const QPointF cursorPos = KWin::Cursors::self()->mouse()->pos();
    m_window->keyPressEvent(Qt::Key_Right);
    m_window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers());
    QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0));
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 4);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 4);
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing));
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(488, 640));
    QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1);
    m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value<quint32>());
    Test::render(m_surface.get(), QSize(488, 640), Qt::blue);
    QVERIFY(frameGeometryChangedSpy.wait());
    QCOMPARE(m_window->size(), QSize(488, 640));
    QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1);

    m_window->keyPressEvent(Qt::Key_Enter);
    QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1);
    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());

    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 5);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 5);

    // The rule should be applied again if the window appears after it's been closed.
    destroyTestWindow();
    createTestWindow(ReturnAfterSurfaceConfiguration);

    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(480, 640));

    mapClientToSurface(QSize(480, 640));
    QVERIFY(m_window->isResizable());
    QCOMPARE(m_window->size(), QSize(480, 640));

    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSizeRemember()
{
    setWindowRule("size", QSize(480, 640), int(Rules::Remember));

    createTestWindow(ReturnAfterSurfaceConfiguration);

    // The initial configure event should contain size hint set by the rule.
    Test::XdgToplevel::States states;
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(480, 640));
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Resizing));

    // Map the window.
    mapClientToSurface(QSize(480, 640));
    QVERIFY(m_window->isResizable());
    QCOMPARE(m_window->size(), QSize(480, 640));

    // We should receive a configure event when the window becomes active.
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Resizing));

    // One should still be able to resize the window.
    QSignalSpy frameGeometryChangedSpy(m_window, &Window::frameGeometryChanged);
    QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted);
    QSignalSpy interactiveMoveResizeSteppedSpy(m_window, &Window::interactiveMoveResizeStepped);
    QSignalSpy interactiveMoveResizeFinishedSpy(m_window, &Window::interactiveMoveResizeFinished);

    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());
    workspace()->slotWindowResize();
    QCOMPARE(workspace()->moveResizeWindow(), m_window);
    QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(m_window->isInteractiveResize());
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 3);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 3);
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing));
    m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value<quint32>());

    const QPointF cursorPos = KWin::Cursors::self()->mouse()->pos();
    m_window->keyPressEvent(Qt::Key_Right);
    m_window->updateInteractiveMoveResize(KWin::Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers());
    QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0));
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 4);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 4);
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Resizing));
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(488, 640));
    QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1);
    m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value<quint32>());
    Test::render(m_surface.get(), QSize(488, 640), Qt::blue);
    QVERIFY(frameGeometryChangedSpy.wait());
    QCOMPARE(m_window->size(), QSize(488, 640));
    QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 1);

    m_window->keyPressEvent(Qt::Key_Enter);
    QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1);
    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());

    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 5);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 5);

    // If the window appears again, it should have the last known size.
    destroyTestWindow();
    createTestWindow(ReturnAfterSurfaceConfiguration);

    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(488, 640));

    mapClientToSurface(QSize(488, 640));
    QVERIFY(m_window->isResizable());
    QCOMPARE(m_window->size(), QSize(488, 640));

    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSizeForce()
{
    setWindowRule("size", QSize(480, 640), int(Rules::Force));

    createTestWindow(ReturnAfterSurfaceConfiguration);

    // The initial configure event should contain size hint set by the rule.
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(480, 640));

    // Map the window.
    mapClientToSurface(QSize(480, 640));
    QVERIFY(!m_window->isResizable());
    QCOMPARE(m_window->size(), QSize(480, 640));

    // We should receive a configure event when the window becomes active.
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);

    // Any attempt to resize the window should not succeed.
    QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted);
    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());
    workspace()->slotWindowResize();
    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QCOMPARE(interactiveMoveResizeStartedSpy.count(), 0);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());
    QVERIFY(!m_surfaceConfigureRequestedSpy->wait(100));

    // If the window appears again, the size should still be forced.
    destroyTestWindow();
    createTestWindow(ReturnAfterSurfaceConfiguration);

    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(480, 640));

    mapClientToSurface(QSize(480, 640));
    QVERIFY(!m_window->isResizable());
    QCOMPARE(m_window->size(), QSize(480, 640));

    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSizeApplyNow()
{
    createTestWindow(ReturnAfterSurfaceConfiguration);

    // The expected surface dimensions should be set by the rule.
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(0, 0));

    // Map the window.
    mapClientToSurface(QSize(100, 50));
    QVERIFY(m_window->isResizable());
    QCOMPARE(m_window->size(), QSize(100, 50));

    // We should receive a configure event when the window becomes active.
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);

    setWindowRule("size", QSize(480, 640), int(Rules::ApplyNow));

    // The compositor should send a configure event with a new size.
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 3);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 3);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(480, 640));

    // Draw the surface with the new size.
    QSignalSpy frameGeometryChangedSpy(m_window, &Window::frameGeometryChanged);
    m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value<quint32>());
    Test::render(m_surface.get(), QSize(480, 640), Qt::blue);
    QVERIFY(frameGeometryChangedSpy.wait());
    QCOMPARE(m_window->size(), QSize(480, 640));
    QVERIFY(!m_surfaceConfigureRequestedSpy->wait(100));

    // The rule should not be applied again.
    m_window->evaluateWindowRules();
    QVERIFY(!m_surfaceConfigureRequestedSpy->wait(100));

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSizeForceTemporarily()
{
    setWindowRule("size", QSize(480, 640), int(Rules::ForceTemporarily));

    createTestWindow(ReturnAfterSurfaceConfiguration);

    // The initial configure event should contain size hint set by the rule.
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(480, 640));

    // Map the window.
    mapClientToSurface(QSize(480, 640));
    QVERIFY(!m_window->isResizable());
    QCOMPARE(m_window->size(), QSize(480, 640));

    // We should receive a configure event when the window becomes active.
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);

    // Any attempt to resize the window should not succeed.
    QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted);
    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());
    workspace()->slotWindowResize();
    QCOMPARE(workspace()->moveResizeWindow(), nullptr);
    QCOMPARE(interactiveMoveResizeStartedSpy.count(), 0);
    QVERIFY(!m_window->isInteractiveMove());
    QVERIFY(!m_window->isInteractiveResize());
    QVERIFY(!m_surfaceConfigureRequestedSpy->wait(100));

    // The rule should be discarded when the window is closed.
    destroyTestWindow();
    createTestWindow(ReturnAfterSurfaceConfiguration);

    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().first().toSize(), QSize(0, 0));

    mapClientToSurface(QSize(100, 50));
    QVERIFY(m_window->isResizable());
    QCOMPARE(m_window->size(), QSize(100, 50));

    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testMaximizeDontAffect()
{
    setWindowRule("maximizehoriz", true, int(Rules::DontAffect));
    setWindowRule("maximizevert", true, int(Rules::DontAffect));

    createTestWindow(ReturnAfterSurfaceConfiguration);

    // Wait for the initial configure event.
    Test::XdgToplevel::States states;
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(0, 0));
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized));

    // Map the window.
    mapClientToSurface(QSize(100, 50));

    QVERIFY(m_window->isMaximizable());
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeRestore);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore);
    QCOMPARE(m_window->size(), QSize(100, 50));

    // We should receive a configure event when the window becomes active.
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized));

    destroyTestWindow();
}

void TestXdgShellWindowRules::testMaximizeApply()
{
    setWindowRule("maximizehoriz", true, int(Rules::Apply));
    setWindowRule("maximizevert", true, int(Rules::Apply));

    createTestWindow(ReturnAfterSurfaceConfiguration);

    // Wait for the initial configure event.
    Test::XdgToplevel::States states;
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024));
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized));

    // Map the window.
    mapClientToSurface(QSize(1280, 1024));

    QVERIFY(m_window->isMaximizable());
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->size(), QSize(1280, 1024));

    // We should receive a configure event when the window becomes active.
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized));

    // One should still be able to change the maximized state of the window.
    workspace()->slotWindowMaximize();
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 3);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 3);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(0, 0));
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized));

    QSignalSpy frameGeometryChangedSpy(m_window, &Window::frameGeometryChanged);
    m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value<quint32>());
    Test::render(m_surface.get(), QSize(100, 50), Qt::blue);
    QVERIFY(frameGeometryChangedSpy.wait());
    QCOMPARE(m_window->size(), QSize(100, 50));
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeRestore);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore);

    // If we create the window again, it should be initially maximized.
    destroyTestWindow();
    createTestWindow(ReturnAfterSurfaceConfiguration);

    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024));
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized));

    mapClientToSurface(QSize(1280, 1024));
    QVERIFY(m_window->isMaximizable());
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->size(), QSize(1280, 1024));

    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized));

    destroyTestWindow();
}

void TestXdgShellWindowRules::testMaximizeRemember()
{
    setWindowRule("maximizehoriz", true, int(Rules::Remember));
    setWindowRule("maximizevert", true, int(Rules::Remember));

    createTestWindow(ReturnAfterSurfaceConfiguration);

    // Wait for the initial configure event.
    Test::XdgToplevel::States states;
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024));
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized));

    // Map the window.
    mapClientToSurface(QSize(1280, 1024));

    QVERIFY(m_window->isMaximizable());
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->size(), QSize(1280, 1024));

    // We should receive a configure event when the window becomes active.
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized));

    // One should still be able to change the maximized state of the window.
    workspace()->slotWindowMaximize();
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 3);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 3);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(0, 0));
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized));

    QSignalSpy frameGeometryChangedSpy(m_window, &Window::frameGeometryChanged);
    m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value<quint32>());
    Test::render(m_surface.get(), QSize(100, 50), Qt::blue);
    QVERIFY(frameGeometryChangedSpy.wait());
    QCOMPARE(m_window->size(), QSize(100, 50));
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeRestore);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore);

    // If we create the window again, it should not be maximized (because last time it wasn't).
    destroyTestWindow();
    createTestWindow(ReturnAfterSurfaceConfiguration);

    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(0, 0));
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized));

    mapClientToSurface(QSize(100, 50));

    QVERIFY(m_window->isMaximizable());
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeRestore);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore);
    QCOMPARE(m_window->size(), QSize(100, 50));

    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized));

    destroyTestWindow();
}

void TestXdgShellWindowRules::testMaximizeForce()
{
    setWindowRule("maximizehoriz", true, int(Rules::Force));
    setWindowRule("maximizevert", true, int(Rules::Force));

    createTestWindow(ReturnAfterSurfaceConfiguration);

    // Wait for the initial configure event.
    Test::XdgToplevel::States states;
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024));
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized));

    // Map the window.
    mapClientToSurface(QSize(1280, 1024));

    QVERIFY(!m_window->isMaximizable());
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->size(), QSize(1280, 1024));

    // We should receive a configure event when the window becomes active.
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized));

    // Any attempt to change the maximized state should not succeed.
    const RectF oldGeometry = m_window->frameGeometry();
    workspace()->slotWindowMaximize();
    QVERIFY(!m_surfaceConfigureRequestedSpy->wait(100));
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->frameGeometry(), oldGeometry);

    // If we create the window again, the maximized state should still be forced.
    destroyTestWindow();
    createTestWindow(ReturnAfterSurfaceConfiguration);

    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024));
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized));

    mapClientToSurface(QSize(1280, 1024));

    QVERIFY(!m_window->isMaximizable());
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->size(), QSize(1280, 1024));

    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized));

    destroyTestWindow();
}

void TestXdgShellWindowRules::testMaximizeApplyNow()
{
    createTestWindow(ReturnAfterSurfaceConfiguration);

    // Wait for the initial configure event.
    Test::XdgToplevel::States states;
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(0, 0));
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized));

    // Map the window.
    mapClientToSurface(QSize(100, 50));

    QVERIFY(m_window->isMaximizable());
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeRestore);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore);
    QCOMPARE(m_window->size(), QSize(100, 50));

    // We should receive a configure event when the window becomes active.
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized));

    setWindowRule("maximizehoriz", true, int(Rules::ApplyNow));
    setWindowRule("maximizevert", true, int(Rules::ApplyNow));

    // We should receive a configure event with a new surface size.
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 3);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 3);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024));
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized));

    // Draw contents of the maximized client.
    QSignalSpy frameGeometryChangedSpy(m_window, &Window::frameGeometryChanged);
    m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value<quint32>());
    Test::render(m_surface.get(), QSize(1280, 1024), Qt::blue);
    QVERIFY(frameGeometryChangedSpy.wait());
    QCOMPARE(m_window->size(), QSize(1280, 1024));
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull);

    // The window still has to be maximizeable.
    QVERIFY(m_window->isMaximizable());

    // Restore the window.
    workspace()->slotWindowMaximize();
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 4);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 4);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(100, 50));
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized));

    m_shellSurface->xdgSurface()->ack_configure(m_surfaceConfigureRequestedSpy->last().at(0).value<quint32>());
    Test::render(m_surface.get(), QSize(100, 50), Qt::blue);
    QVERIFY(frameGeometryChangedSpy.wait());
    QCOMPARE(m_window->size(), QSize(100, 50));
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeRestore);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore);

    // The rule should be discarded after it's been applied.
    const RectF oldGeometry = m_window->frameGeometry();
    m_window->evaluateWindowRules();
    QVERIFY(!m_surfaceConfigureRequestedSpy->wait(100));
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeRestore);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore);
    QCOMPARE(m_window->frameGeometry(), oldGeometry);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testMaximizeForceTemporarily()
{
    setWindowRule("maximizehoriz", true, int(Rules::ForceTemporarily));
    setWindowRule("maximizevert", true, int(Rules::ForceTemporarily));

    createTestWindow(ReturnAfterSurfaceConfiguration);

    // Wait for the initial configure event.
    Test::XdgToplevel::States states;
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024));
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized));

    // Map the window.
    mapClientToSurface(QSize(1280, 1024));

    QVERIFY(!m_window->isMaximizable());
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->size(), QSize(1280, 1024));

    // We should receive a configure event when the window becomes active.
    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized));

    // Any attempt to change the maximized state should not succeed.
    const RectF oldGeometry = m_window->frameGeometry();
    workspace()->slotWindowMaximize();
    QVERIFY(!m_surfaceConfigureRequestedSpy->wait(100));
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeFull);
    QCOMPARE(m_window->frameGeometry(), oldGeometry);

    // The rule should be discarded if we close the window.
    destroyTestWindow();
    createTestWindow(ReturnAfterSurfaceConfiguration);

    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 1);
    QCOMPARE(m_toplevelConfigureRequestedSpy->last().at(0).toSize(), QSize(0, 0));
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized));

    mapClientToSurface(QSize(100, 50));

    QVERIFY(m_window->isMaximizable());
    QCOMPARE(m_window->maximizeMode(), MaximizeMode::MaximizeRestore);
    QCOMPARE(m_window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore);
    QCOMPARE(m_window->size(), QSize(100, 50));

    QVERIFY(m_surfaceConfigureRequestedSpy->wait());
    QCOMPARE(m_surfaceConfigureRequestedSpy->count(), 2);
    QCOMPARE(m_toplevelConfigureRequestedSpy->count(), 2);
    states = m_toplevelConfigureRequestedSpy->last().at(1).value<Test::XdgToplevel::States>();
    QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
    QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized));

    destroyTestWindow();
}

void TestXdgShellWindowRules::testDesktopsDontAffect()
{
    // We need at least two virtual desktop for this test.
    VirtualDesktopManager::self()->setCount(2);
    QCOMPARE(VirtualDesktopManager::self()->count(), 2u);
    VirtualDesktop *vd1 = VirtualDesktopManager::self()->desktops().at(0);
    VirtualDesktop *vd2 = VirtualDesktopManager::self()->desktops().at(1);

    VirtualDesktopManager::self()->setCurrent(vd1);
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);

    setWindowRule("desktops", QStringList{vd2->id()}, int(Rules::DontAffect));

    createTestWindow();

    // The window should appear on the current virtual desktop.
    QCOMPARE(m_window->desktops(), {vd1});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testDesktopsApply()
{
    // We need at least two virtual desktop for this test.
    VirtualDesktopManager::self()->setCount(2);
    QCOMPARE(VirtualDesktopManager::self()->count(), 2u);
    VirtualDesktop *vd1 = VirtualDesktopManager::self()->desktops().at(0);
    VirtualDesktop *vd2 = VirtualDesktopManager::self()->desktops().at(1);

    VirtualDesktopManager::self()->setCurrent(vd1);
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);

    setWindowRule("desktops", QStringList{vd2->id()}, int(Rules::Apply));

    createTestWindow();

    // The window should appear on the second virtual desktop.
    QCOMPARE(m_window->desktops(), {vd2});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2);

    // We still should be able to move the window between desktops.
    m_window->setDesktops({vd1});
    QCOMPARE(m_window->desktops(), {vd1});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2);

    // If we re-open the window, it should appear on the second virtual desktop again.
    destroyTestWindow();
    VirtualDesktopManager::self()->setCurrent(vd1);
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);
    createTestWindow();

    QCOMPARE(m_window->desktops(), {vd2});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testDesktopsRemember()
{
    // We need at least two virtual desktop for this test.
    VirtualDesktopManager::self()->setCount(2);
    QCOMPARE(VirtualDesktopManager::self()->count(), 2u);
    VirtualDesktop *vd1 = VirtualDesktopManager::self()->desktops().at(0);
    VirtualDesktop *vd2 = VirtualDesktopManager::self()->desktops().at(1);

    VirtualDesktopManager::self()->setCurrent(vd1);
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);

    setWindowRule("desktops", QStringList{vd2->id()}, int(Rules::Remember));

    createTestWindow();

    QCOMPARE(m_window->desktops(), {vd2});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2);

    // Move the window to the first virtual desktop.
    m_window->setDesktops({vd1});
    QCOMPARE(m_window->desktops(), {vd1});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2);

    // If we create the window again, it should appear on the first virtual desktop.
    destroyTestWindow();
    createTestWindow();

    QCOMPARE(m_window->desktops(), {vd1});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testDesktopsForce()
{
    // We need at least two virtual desktop for this test.
    VirtualDesktopManager::self()->setCount(2);
    QCOMPARE(VirtualDesktopManager::self()->count(), 2u);
    VirtualDesktop *vd1 = VirtualDesktopManager::self()->desktops().at(0);
    VirtualDesktop *vd2 = VirtualDesktopManager::self()->desktops().at(1);

    VirtualDesktopManager::self()->setCurrent(vd1);
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);

    setWindowRule("desktops", QStringList{vd2->id()}, int(Rules::Force));

    createTestWindow();

    // The window should appear on the second virtual desktop.
    QCOMPARE(m_window->desktops(), {vd2});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2);

    // Any attempt to move the window to another virtual desktop should fail.
    m_window->setDesktops({vd1});
    QCOMPARE(m_window->desktops(), {vd2});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2);

    // If we re-open the window, it should appear on the second virtual desktop again.
    destroyTestWindow();
    VirtualDesktopManager::self()->setCurrent(vd1);
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);
    createTestWindow();

    QCOMPARE(m_window->desktops(), {vd2});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testDesktopsForceWithPerOutputDesktopsAndInteractiveMove()
{
    // We need at least two virtual desktop for this test.
    VirtualDesktopManager::self()->setCount(2);
    QCOMPARE(VirtualDesktopManager::self()->count(), 2u);
    VirtualDesktopManager::self()->setPerOutputVirtualDesktops(true);
    QVERIFY(VirtualDesktopManager::self()->isPerOutputVirtualDesktops());
    VirtualDesktop *vd1 = VirtualDesktopManager::self()->desktops().at(0);
    VirtualDesktop *vd2 = VirtualDesktopManager::self()->desktops().at(1);

    setWindowRule("desktops", QStringList{vd2->id()}, int(Rules::Force));

    const auto outputs = workspace()->outputs();
    QCOMPARE_GE(outputs.size(), 2);
    LogicalOutput *startOutput = outputs[0];
    LogicalOutput *endOutput = outputs[1];
    VirtualDesktopManager::self()->setCurrent(vd2, startOutput);
    VirtualDesktopManager::self()->setCurrent(vd1, endOutput);
    workspace()->setActiveOutput(startOutput);

    createTestWindow();

    QVERIFY(m_window);
    QCOMPARE(workspace()->activeWindow(), m_window);
    QCOMPARE(m_window->frameGeometry(), RectF(0, 0, 100, 50));
    m_window->sendToOutput(startOutput);
    QCOMPARE(m_window->output(), startOutput);
    workspace()->setActiveWindow(m_window);

    QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted);
    QSignalSpy moveResizedChangedSpy(m_window, &Window::moveResizedChanged);
    QSignalSpy interactiveMoveResizeSteppedSpy(m_window, &Window::interactiveMoveResizeStepped);
    QSignalSpy interactiveMoveResizeFinishedSpy(m_window, &Window::interactiveMoveResizeFinished);

    // begin move
    QVERIFY(workspace()->moveResizeWindow() == nullptr);
    QVERIFY(m_window->isActive());
    QVERIFY(m_window->windowItem()->isVisible());
    QCOMPARE(m_window->isInteractiveMove(), false);
    workspace()->slotWindowMove();
    QCOMPARE(workspace()->moveResizeWindow(), m_window);
    QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1);
    QCOMPARE(moveResizedChangedSpy.count(), 1);
    QCOMPARE(m_window->isInteractiveMove(), true);
    QCOMPARE(m_window->geometryRestore(), RectF());

    // move the window to endOutput
    const QPointF cursorPos = Cursors::self()->mouse()->pos();

    for (int i = 0; i < 40; i++) {
        m_window->keyPressEvent(Qt::Key_Right | Qt::ALT);
        m_window->updateInteractiveMoveResize(Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers());
    }

    QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(40 * 32, 0));
    QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 40);
    QVERIFY(m_window->isActive());
    QVERIFY(m_window->windowItem()->isVisible());
    interactiveMoveResizeSteppedSpy.clear();

    // end move
    QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 0);
    m_window->keyPressEvent(Qt::Key_Enter);
    QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1);
    QCOMPARE(moveResizedChangedSpy.count(), 2);
    QCOMPARE(m_window->isInteractiveMove(), false);
    QVERIFY(workspace()->moveResizeWindow() == nullptr);

    // The window should appear on the second screen. It should keep its desktop, so it should disappear.
    QCOMPARE(m_window->desktops(), {vd2});
    QCOMPARE(m_window->output(), endOutput);
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(endOutput), vd1);
    QCOMPARE(m_window->isOnCurrentDesktop(), false);
    // It's not immediate due to scale effect.
    QTRY_COMPARE_WITH_TIMEOUT(m_window->windowItem()->isVisible(), false, 1000);
    QCOMPARE(m_window->isActive(), false);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testDesktopsApplyNow()
{
    // We need at least two virtual desktop for this test.
    VirtualDesktopManager::self()->setCount(2);
    QCOMPARE(VirtualDesktopManager::self()->count(), 2u);
    VirtualDesktop *vd1 = VirtualDesktopManager::self()->desktops().at(0);
    VirtualDesktop *vd2 = VirtualDesktopManager::self()->desktops().at(1);

    VirtualDesktopManager::self()->setCurrent(vd1);
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);

    createTestWindow();

    QCOMPARE(m_window->desktops(), {vd1});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);

    setWindowRule("desktops", QStringList{vd2->id()}, int(Rules::ApplyNow));

    // The window should have been moved to the second virtual desktop.
    QCOMPARE(m_window->desktops(), {vd2});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);

    // One should still be able to move the window between desktops.
    m_window->setDesktops({vd1});
    QCOMPARE(m_window->desktops(), {vd1});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);

    // The rule should not be applied again.
    m_window->evaluateWindowRules();
    QCOMPARE(m_window->desktops(), {vd1});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testDesktopsForceTemporarily()
{
    // We need at least two virtual desktop for this test.
    VirtualDesktopManager::self()->setCount(2);
    QCOMPARE(VirtualDesktopManager::self()->count(), 2u);
    VirtualDesktop *vd1 = VirtualDesktopManager::self()->desktops().at(0);
    VirtualDesktop *vd2 = VirtualDesktopManager::self()->desktops().at(1);

    VirtualDesktopManager::self()->setCurrent(vd1);
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);

    setWindowRule("desktops", QStringList{vd2->id()}, int(Rules::ForceTemporarily));

    createTestWindow();

    // The window should appear on the second virtual desktop.
    QCOMPARE(m_window->desktops(), {vd2});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2);

    // Any attempt to move the window to another virtual desktop should fail.
    m_window->setDesktops({vd1});
    QCOMPARE(m_window->desktops(), {vd2});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd2);

    // The rule should be discarded when the window is withdrawn.
    destroyTestWindow();
    VirtualDesktopManager::self()->setCurrent(vd1);
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);
    createTestWindow();

    QCOMPARE(m_window->desktops(), {vd1});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);

    // One should be able to move the window between desktops.
    m_window->setDesktops({vd2});
    QCOMPARE(m_window->desktops(), {vd2});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);

    m_window->setDesktops({vd1});
    QCOMPARE(m_window->desktops(), {vd1});
    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(), vd1);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testMinimizeDontAffect()
{
    setWindowRule("minimize", true, int(Rules::DontAffect));

    createTestWindow();
    QVERIFY(m_window->isMinimizable());

    // The window should not be minimized.
    QVERIFY(!m_window->isMinimized());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testMinimizeApply()
{
    setWindowRule("minimize", true, int(Rules::Apply));

    createTestWindow(ClientShouldBeInactive);
    QVERIFY(m_window->isMinimizable());

    // The window should be minimized.
    QVERIFY(m_window->isMinimized());

    // We should still be able to unminimize the window.
    m_window->setMinimized(false);
    QVERIFY(!m_window->isMinimized());

    // If we re-open the window, it should be minimized back again.
    destroyTestWindow();
    createTestWindow(ClientShouldBeInactive);
    QVERIFY(m_window->isMinimizable());
    QVERIFY(m_window->isMinimized());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testMinimizeRemember()
{
    setWindowRule("minimize", false, int(Rules::Remember));

    createTestWindow();
    QVERIFY(m_window->isMinimizable());
    QVERIFY(!m_window->isMinimized());

    // Minimize the window.
    m_window->setMinimized(true);
    QVERIFY(m_window->isMinimized());

    // If we open the window again, it should be minimized.
    destroyTestWindow();
    createTestWindow(ClientShouldBeInactive);
    QVERIFY(m_window->isMinimizable());
    QVERIFY(m_window->isMinimized());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testMinimizeForce()
{
    setWindowRule("minimize", false, int(Rules::Force));

    createTestWindow();
    QVERIFY(!m_window->isMinimizable());
    QVERIFY(!m_window->isMinimized());

    // Any attempt to minimize the window should fail.
    m_window->setMinimized(true);
    QVERIFY(!m_window->isMinimized());

    // If we re-open the window, the minimized state should still be forced.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(!m_window->isMinimizable());
    QVERIFY(!m_window->isMinimized());
    m_window->setMinimized(true);
    QVERIFY(!m_window->isMinimized());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testMinimizeApplyNow()
{
    createTestWindow();
    QVERIFY(m_window->isMinimizable());
    QVERIFY(!m_window->isMinimized());

    setWindowRule("minimize", true, int(Rules::ApplyNow));

    // The window should be minimized now.
    QVERIFY(m_window->isMinimizable());
    QVERIFY(m_window->isMinimized());

    // One is still able to unminimize the window.
    m_window->setMinimized(false);
    QVERIFY(!m_window->isMinimized());

    // The rule should not be applied again.
    m_window->evaluateWindowRules();
    QVERIFY(m_window->isMinimizable());
    QVERIFY(!m_window->isMinimized());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testMinimizeForceTemporarily()
{
    setWindowRule("minimize", false, int(Rules::ForceTemporarily));

    createTestWindow();
    QVERIFY(!m_window->isMinimizable());
    QVERIFY(!m_window->isMinimized());

    // Any attempt to minimize the window should fail until the window is closed.
    m_window->setMinimized(true);
    QVERIFY(!m_window->isMinimized());

    // The rule should be discarded when the window is closed.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(m_window->isMinimizable());
    QVERIFY(!m_window->isMinimized());
    m_window->setMinimized(true);
    QVERIFY(m_window->isMinimized());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipTaskbarDontAffect()
{
    setWindowRule("skiptaskbar", true, int(Rules::DontAffect));

    createTestWindow();

    // The window should not be affected by the rule.
    QVERIFY(!m_window->skipTaskbar());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipTaskbarApply()
{
    setWindowRule("skiptaskbar", true, int(Rules::Apply));

    createTestWindow();

    // The window should not be included on a taskbar.
    QVERIFY(m_window->skipTaskbar());

    // Though one can change that.
    m_window->setOriginalSkipTaskbar(false);
    QVERIFY(!m_window->skipTaskbar());

    // Reopen the window, the rule should be applied again.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(m_window->skipTaskbar());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipTaskbarRemember()
{
    setWindowRule("skiptaskbar", true, int(Rules::Remember));

    createTestWindow();

    // The window should not be included on a taskbar.
    QVERIFY(m_window->skipTaskbar());

    // Change the skip-taskbar state.
    m_window->setOriginalSkipTaskbar(false);
    QVERIFY(!m_window->skipTaskbar());

    // Reopen the window.
    destroyTestWindow();
    createTestWindow();

    // The window should be included on a taskbar.
    QVERIFY(!m_window->skipTaskbar());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipTaskbarForce()
{
    setWindowRule("skiptaskbar", true, int(Rules::Force));

    createTestWindow();

    // The window should not be included on a taskbar.
    QVERIFY(m_window->skipTaskbar());

    // Any attempt to change the skip-taskbar state should not succeed.
    m_window->setOriginalSkipTaskbar(false);
    QVERIFY(m_window->skipTaskbar());

    // Reopen the window.
    destroyTestWindow();
    createTestWindow();

    // The skip-taskbar state should be still forced.
    QVERIFY(m_window->skipTaskbar());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipTaskbarApplyNow()
{
    createTestWindow();
    QVERIFY(!m_window->skipTaskbar());

    setWindowRule("skiptaskbar", true, int(Rules::ApplyNow));

    // The window should not be on a taskbar now.
    QVERIFY(m_window->skipTaskbar());

    // Also, one change the skip-taskbar state.
    m_window->setOriginalSkipTaskbar(false);
    QVERIFY(!m_window->skipTaskbar());

    // The rule should not be applied again.
    m_window->evaluateWindowRules();
    QVERIFY(!m_window->skipTaskbar());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipTaskbarForceTemporarily()
{
    setWindowRule("skiptaskbar", true, int(Rules::ForceTemporarily));

    createTestWindow();

    // The window should not be included on a taskbar.
    QVERIFY(m_window->skipTaskbar());

    // Any attempt to change the skip-taskbar state should not succeed.
    m_window->setOriginalSkipTaskbar(false);
    QVERIFY(m_window->skipTaskbar());

    // The rule should be discarded when the window is closed.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(!m_window->skipTaskbar());

    // The skip-taskbar state is no longer forced.
    m_window->setOriginalSkipTaskbar(true);
    QVERIFY(m_window->skipTaskbar());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipPagerDontAffect()
{
    setWindowRule("skippager", true, int(Rules::DontAffect));

    createTestWindow();

    // The window should not be affected by the rule.
    QVERIFY(!m_window->skipPager());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipPagerApply()
{
    setWindowRule("skippager", true, int(Rules::Apply));

    createTestWindow();

    // The window should not be included on a pager.
    QVERIFY(m_window->skipPager());

    // Though one can change that.
    m_window->setSkipPager(false);
    QVERIFY(!m_window->skipPager());

    // Reopen the window, the rule should be applied again.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(m_window->skipPager());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipPagerRemember()
{
    setWindowRule("skippager", true, int(Rules::Remember));

    createTestWindow();

    // The window should not be included on a pager.
    QVERIFY(m_window->skipPager());

    // Change the skip-pager state.
    m_window->setSkipPager(false);
    QVERIFY(!m_window->skipPager());

    // Reopen the window.
    destroyTestWindow();
    createTestWindow();

    // The window should be included on a pager.
    QVERIFY(!m_window->skipPager());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipPagerForce()
{
    setWindowRule("skippager", true, int(Rules::Force));

    createTestWindow();

    // The window should not be included on a pager.
    QVERIFY(m_window->skipPager());

    // Any attempt to change the skip-pager state should not succeed.
    m_window->setSkipPager(false);
    QVERIFY(m_window->skipPager());

    // Reopen the window.
    destroyTestWindow();
    createTestWindow();

    // The skip-pager state should be still forced.
    QVERIFY(m_window->skipPager());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipPagerApplyNow()
{
    createTestWindow();
    QVERIFY(!m_window->skipPager());

    setWindowRule("skippager", true, int(Rules::ApplyNow));

    // The window should not be on a pager now.
    QVERIFY(m_window->skipPager());

    // Also, one change the skip-pager state.
    m_window->setSkipPager(false);
    QVERIFY(!m_window->skipPager());

    // The rule should not be applied again.
    m_window->evaluateWindowRules();
    QVERIFY(!m_window->skipPager());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipPagerForceTemporarily()
{
    setWindowRule("skippager", true, int(Rules::ForceTemporarily));

    createTestWindow();

    // The window should not be included on a pager.
    QVERIFY(m_window->skipPager());

    // Any attempt to change the skip-pager state should not succeed.
    m_window->setSkipPager(false);
    QVERIFY(m_window->skipPager());

    // The rule should be discarded when the window is closed.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(!m_window->skipPager());

    // The skip-pager state is no longer forced.
    m_window->setSkipPager(true);
    QVERIFY(m_window->skipPager());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipSwitcherDontAffect()
{
    setWindowRule("skipswitcher", true, int(Rules::DontAffect));

    createTestWindow();

    // The window should not be affected by the rule.
    QVERIFY(!m_window->skipSwitcher());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipSwitcherApply()
{
    setWindowRule("skipswitcher", true, int(Rules::Apply));

    createTestWindow();

    // The window should be excluded from window switching effects.
    QVERIFY(m_window->skipSwitcher());

    // Though one can change that.
    m_window->setSkipSwitcher(false);
    QVERIFY(!m_window->skipSwitcher());

    // Reopen the window, the rule should be applied again.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(m_window->skipSwitcher());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipSwitcherRemember()
{
    setWindowRule("skipswitcher", true, int(Rules::Remember));

    createTestWindow();

    // The window should be excluded from window switching effects.
    QVERIFY(m_window->skipSwitcher());

    // Change the skip-switcher state.
    m_window->setSkipSwitcher(false);
    QVERIFY(!m_window->skipSwitcher());

    // Reopen the window.
    destroyTestWindow();
    createTestWindow();

    // The window should be included in window switching effects.
    QVERIFY(!m_window->skipSwitcher());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipSwitcherForce()
{
    setWindowRule("skipswitcher", true, int(Rules::Force));

    createTestWindow();

    // The window should be excluded from window switching effects.
    QVERIFY(m_window->skipSwitcher());

    // Any attempt to change the skip-switcher state should not succeed.
    m_window->setSkipSwitcher(false);
    QVERIFY(m_window->skipSwitcher());

    // Reopen the window.
    destroyTestWindow();
    createTestWindow();

    // The skip-switcher state should be still forced.
    QVERIFY(m_window->skipSwitcher());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipSwitcherApplyNow()
{
    createTestWindow();
    QVERIFY(!m_window->skipSwitcher());

    setWindowRule("skipswitcher", true, int(Rules::ApplyNow));

    // The window should be excluded from window switching effects now.
    QVERIFY(m_window->skipSwitcher());

    // Also, one change the skip-switcher state.
    m_window->setSkipSwitcher(false);
    QVERIFY(!m_window->skipSwitcher());

    // The rule should not be applied again.
    m_window->evaluateWindowRules();
    QVERIFY(!m_window->skipSwitcher());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testSkipSwitcherForceTemporarily()
{
    setWindowRule("skipswitcher", true, int(Rules::ForceTemporarily));

    createTestWindow();

    // The window should be excluded from window switching effects.
    QVERIFY(m_window->skipSwitcher());

    // Any attempt to change the skip-switcher state should not succeed.
    m_window->setSkipSwitcher(false);
    QVERIFY(m_window->skipSwitcher());

    // The rule should be discarded when the window is closed.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(!m_window->skipSwitcher());

    // The skip-switcher state is no longer forced.
    m_window->setSkipSwitcher(true);
    QVERIFY(m_window->skipSwitcher());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testKeepAboveDontAffect()
{
    setWindowRule("above", true, int(Rules::DontAffect));

    createTestWindow();

    // The keep-above state of the window should not be affected by the rule.
    QVERIFY(!m_window->keepAbove());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testKeepAboveApply()
{
    setWindowRule("above", true, int(Rules::Apply));

    createTestWindow();

    // Initially, the window should be kept above.
    QVERIFY(m_window->keepAbove());

    // One should also be able to alter the keep-above state.
    m_window->setKeepAbove(false);
    QVERIFY(!m_window->keepAbove());

    // If one re-opens the window, it should be kept above back again.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(m_window->keepAbove());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testKeepAboveRemember()
{
    setWindowRule("above", true, int(Rules::Remember));

    createTestWindow();

    // Initially, the window should be kept above.
    QVERIFY(m_window->keepAbove());

    // Unset the keep-above state.
    m_window->setKeepAbove(false);
    QVERIFY(!m_window->keepAbove());
    destroyTestWindow();

    // Re-open the window, it should not be kept above.
    createTestWindow();
    QVERIFY(!m_window->keepAbove());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testKeepAboveForce()
{
    setWindowRule("above", true, int(Rules::Force));

    createTestWindow();

    // Initially, the window should be kept above.
    QVERIFY(m_window->keepAbove());

    // Any attempt to unset the keep-above should not succeed.
    m_window->setKeepAbove(false);
    QVERIFY(m_window->keepAbove());

    // If we re-open the window, it should still be kept above.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(m_window->keepAbove());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testKeepAboveApplyNow()
{
    createTestWindow();
    QVERIFY(!m_window->keepAbove());

    setWindowRule("above", true, int(Rules::ApplyNow));

    // The window should now be kept above other windows.
    QVERIFY(m_window->keepAbove());

    // One is still able to change the keep-above state of the window.
    m_window->setKeepAbove(false);
    QVERIFY(!m_window->keepAbove());

    // The rule should not be applied again.
    m_window->evaluateWindowRules();
    QVERIFY(!m_window->keepAbove());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testKeepAboveForceTemporarily()
{
    setWindowRule("above", true, int(Rules::ForceTemporarily));

    createTestWindow();

    // Initially, the window should be kept above.
    QVERIFY(m_window->keepAbove());

    // Any attempt to alter the keep-above state should not succeed.
    m_window->setKeepAbove(false);
    QVERIFY(m_window->keepAbove());

    // The rule should be discarded when the window is closed.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(!m_window->keepAbove());

    // The keep-above state is no longer forced.
    m_window->setKeepAbove(true);
    QVERIFY(m_window->keepAbove());
    m_window->setKeepAbove(false);
    QVERIFY(!m_window->keepAbove());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testKeepBelowDontAffect()
{
    setWindowRule("below", true, int(Rules::DontAffect));

    createTestWindow();

    // The keep-below state of the window should not be affected by the rule.
    QVERIFY(!m_window->keepBelow());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testKeepBelowApply()
{
    setWindowRule("below", true, int(Rules::Apply));

    createTestWindow();

    // Initially, the window should be kept below.
    QVERIFY(m_window->keepBelow());

    // One should also be able to alter the keep-below state.
    m_window->setKeepBelow(false);
    QVERIFY(!m_window->keepBelow());

    // If one re-opens the window, it should be kept above back again.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(m_window->keepBelow());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testKeepBelowRemember()
{
    setWindowRule("below", true, int(Rules::Remember));

    createTestWindow();

    // Initially, the window should be kept below.
    QVERIFY(m_window->keepBelow());

    // Unset the keep-below state.
    m_window->setKeepBelow(false);
    QVERIFY(!m_window->keepBelow());
    destroyTestWindow();

    // Re-open the window, it should not be kept below.
    createTestWindow();
    QVERIFY(!m_window->keepBelow());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testKeepBelowForce()
{
    setWindowRule("below", true, int(Rules::Force));

    createTestWindow();

    // Initially, the window should be kept below.
    QVERIFY(m_window->keepBelow());

    // Any attempt to unset the keep-below should not succeed.
    m_window->setKeepBelow(false);
    QVERIFY(m_window->keepBelow());

    // If we re-open the window, it should still be kept below.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(m_window->keepBelow());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testKeepBelowApplyNow()
{
    createTestWindow();
    QVERIFY(!m_window->keepBelow());

    setWindowRule("below", true, int(Rules::ApplyNow));

    // The window should now be kept below other windows.
    QVERIFY(m_window->keepBelow());

    // One is still able to change the keep-below state of the window.
    m_window->setKeepBelow(false);
    QVERIFY(!m_window->keepBelow());

    // The rule should not be applied again.
    m_window->evaluateWindowRules();
    QVERIFY(!m_window->keepBelow());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testKeepBelowForceTemporarily()
{
    setWindowRule("below", true, int(Rules::ForceTemporarily));

    createTestWindow();

    // Initially, the window should be kept below.
    QVERIFY(m_window->keepBelow());

    // Any attempt to alter the keep-below state should not succeed.
    m_window->setKeepBelow(false);
    QVERIFY(m_window->keepBelow());

    // The rule should be discarded when the window is closed.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(!m_window->keepBelow());

    // The keep-below state is no longer forced.
    m_window->setKeepBelow(true);
    QVERIFY(m_window->keepBelow());
    m_window->setKeepBelow(false);
    QVERIFY(!m_window->keepBelow());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testShortcutDontAffect()
{
#if !KWIN_BUILD_GLOBALSHORTCUTS
    QSKIP("Can't test shortcuts without shortcuts");
    return;
#endif

    setWindowRule("shortcut", "Ctrl+Alt+1", int(Rules::DontAffect));

    createTestWindow();
    QCOMPARE(m_window->shortcut(), QKeySequence());
    m_window->setMinimized(true);
    QVERIFY(m_window->isMinimized());

    // If we press the window shortcut, nothing should happen.
    QSignalSpy minimizedChangedSpy(m_window, &Window::minimizedChanged);
    quint32 timestamp = 1;
    Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++);
    Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyPressed(KEY_1, timestamp++);
    Test::keyboardKeyReleased(KEY_1, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++);
    QVERIFY(!minimizedChangedSpy.wait(100));
    QVERIFY(m_window->isMinimized());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testShortcutApply()
{
#if !KWIN_BUILD_GLOBALSHORTCUTS
    QSKIP("Can't test shortcuts without shortcuts");
    return;
#endif
    setWindowRule("shortcut", "Ctrl+Alt+1", int(Rules::Apply));

    createTestWindow();

    // If we press the window shortcut, the window should be brought back to user.
    QSignalSpy minimizedChangedSpy(m_window, &Window::minimizedChanged);
    quint32 timestamp = 1;
    QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1}));
    m_window->setMinimized(true);
    QVERIFY(m_window->isMinimized());
    Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++);
    Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyPressed(KEY_1, timestamp++);
    Test::keyboardKeyReleased(KEY_1, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++);
    QVERIFY(minimizedChangedSpy.wait());
    QVERIFY(!m_window->isMinimized());

    // One can also change the shortcut.
    m_window->setShortcut(QStringLiteral("Ctrl+Alt+2"));
    QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_2}));
    m_window->setMinimized(true);
    QVERIFY(m_window->isMinimized());
    Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++);
    Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyPressed(KEY_2, timestamp++);
    Test::keyboardKeyReleased(KEY_2, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++);
    QVERIFY(minimizedChangedSpy.wait());
    QVERIFY(!m_window->isMinimized());

    // The old shortcut should do nothing.
    m_window->setMinimized(true);
    QVERIFY(m_window->isMinimized());
    Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++);
    Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyPressed(KEY_1, timestamp++);
    Test::keyboardKeyReleased(KEY_1, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++);
    QVERIFY(!minimizedChangedSpy.wait(100));
    QVERIFY(m_window->isMinimized());

    // Reopen the window.
    destroyTestWindow();
    createTestWindow();

    // The window shortcut should be set back to Ctrl+Alt+1.
    QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1}));

    destroyTestWindow();
}

void TestXdgShellWindowRules::testShortcutRemember()
{
    QSKIP("KWin core doesn't try to save the last used window shortcut");

    setWindowRule("shortcut", "Ctrl+Alt+1", int(Rules::Remember));

    createTestWindow();

    // If we press the window shortcut, the window should be brought back to user.
    QSignalSpy minimizedChangedSpy(m_window, &Window::minimizedChanged);
    quint32 timestamp = 1;
    QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1}));
    m_window->setMinimized(true);
    QVERIFY(m_window->isMinimized());
    Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++);
    Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyPressed(KEY_1, timestamp++);
    Test::keyboardKeyReleased(KEY_1, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++);
    QVERIFY(minimizedChangedSpy.wait());
    QVERIFY(!m_window->isMinimized());

    // Change the window shortcut to Ctrl+Alt+2.
    m_window->setShortcut(QStringLiteral("Ctrl+Alt+2"));
    QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_2}));
    m_window->setMinimized(true);
    QVERIFY(m_window->isMinimized());
    Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++);
    Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyPressed(KEY_2, timestamp++);
    Test::keyboardKeyReleased(KEY_2, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++);
    QVERIFY(minimizedChangedSpy.wait());
    QVERIFY(!m_window->isMinimized());

    // Reopen the window.
    destroyTestWindow();
    createTestWindow();

    // The window shortcut should be set to the last known value.
    QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_2}));

    destroyTestWindow();
}

void TestXdgShellWindowRules::testShortcutForce()
{
    QSKIP("KWin core can't release forced window shortcuts");

    setWindowRule("shortcut", "Ctrl+Alt+1", int(Rules::Force));

    createTestWindow();

    // If we press the window shortcut, the window should be brought back to user.
    QSignalSpy minimizedChangedSpy(m_window, &Window::minimizedChanged);
    quint32 timestamp = 1;
    QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1}));
    m_window->setMinimized(true);
    QVERIFY(m_window->isMinimized());
    Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++);
    Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyPressed(KEY_1, timestamp++);
    Test::keyboardKeyReleased(KEY_1, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++);
    QVERIFY(minimizedChangedSpy.wait());
    QVERIFY(!m_window->isMinimized());

    // Any attempt to change the window shortcut should not succeed.
    m_window->setShortcut(QStringLiteral("Ctrl+Alt+2"));
    QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1}));
    m_window->setMinimized(true);
    QVERIFY(m_window->isMinimized());
    Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++);
    Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyPressed(KEY_2, timestamp++);
    Test::keyboardKeyReleased(KEY_2, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++);
    QVERIFY(!minimizedChangedSpy.wait(100));
    QVERIFY(m_window->isMinimized());

    // Reopen the window.
    destroyTestWindow();
    createTestWindow();

    // The window shortcut should still be forced.
    QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1}));

    destroyTestWindow();
}

void TestXdgShellWindowRules::testShortcutApplyNow()
{
#if !KWIN_BUILD_GLOBALSHORTCUTS
    QSKIP("Can't test shortcuts without shortcuts");
    return;
#endif

    createTestWindow();
    QVERIFY(m_window->shortcut().isEmpty());

    setWindowRule("shortcut", "Ctrl+Alt+1", int(Rules::ApplyNow));

    // The window should now have a window shortcut assigned.
    QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1}));
    QSignalSpy minimizedChangedSpy(m_window, &Window::minimizedChanged);
    quint32 timestamp = 1;
    m_window->setMinimized(true);
    QVERIFY(m_window->isMinimized());
    Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++);
    Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyPressed(KEY_1, timestamp++);
    Test::keyboardKeyReleased(KEY_1, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++);
    QVERIFY(minimizedChangedSpy.wait());
    QVERIFY(!m_window->isMinimized());

    // Assign a different shortcut.
    m_window->setShortcut(QStringLiteral("Ctrl+Alt+2"));
    QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_2}));
    m_window->setMinimized(true);
    QVERIFY(m_window->isMinimized());
    Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++);
    Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyPressed(KEY_2, timestamp++);
    Test::keyboardKeyReleased(KEY_2, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++);
    QVERIFY(minimizedChangedSpy.wait());
    QVERIFY(!m_window->isMinimized());

    // The rule should not be applied again.
    m_window->evaluateWindowRules();
    QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_2}));

    destroyTestWindow();
}

void TestXdgShellWindowRules::testShortcutForceTemporarily()
{
    QSKIP("KWin core can't release forced window shortcuts");

    setWindowRule("shortcut", "Ctrl+Alt+1", int(Rules::ForceTemporarily));

    createTestWindow();

    // If we press the window shortcut, the window should be brought back to user.
    QSignalSpy minimizedChangedSpy(m_window, &Window::minimizedChanged);
    quint32 timestamp = 1;
    QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1}));
    m_window->setMinimized(true);
    QVERIFY(m_window->isMinimized());
    Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++);
    Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyPressed(KEY_1, timestamp++);
    Test::keyboardKeyReleased(KEY_1, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++);
    QVERIFY(minimizedChangedSpy.wait());
    QVERIFY(!m_window->isMinimized());

    // Any attempt to change the window shortcut should not succeed.
    m_window->setShortcut(QStringLiteral("Ctrl+Alt+2"));
    QCOMPARE(m_window->shortcut(), (QKeySequence{Qt::CTRL | Qt::ALT | Qt::Key_1}));
    m_window->setMinimized(true);
    QVERIFY(m_window->isMinimized());
    Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++);
    Test::keyboardKeyPressed(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyPressed(KEY_2, timestamp++);
    Test::keyboardKeyReleased(KEY_2, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTALT, timestamp++);
    Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++);
    QVERIFY(!minimizedChangedSpy.wait(100));
    QVERIFY(m_window->isMinimized());

    // The rule should be discarded when the window is closed.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(m_window->shortcut().isEmpty());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testDesktopFileDontAffect()
{
    // Currently, the desktop file name is derived from the app id. If the app id is
    // changed, then the old rules will be lost. Either setDesktopFileName should
    // be exposed or the desktop file name rule should be removed for wayland windows.
    QSKIP("Needs changes in KWin core to pass");
}

void TestXdgShellWindowRules::testDesktopFileApply()
{
    // Currently, the desktop file name is derived from the app id. If the app id is
    // changed, then the old rules will be lost. Either setDesktopFileName should
    // be exposed or the desktop file name rule should be removed for wayland windows.
    QSKIP("Needs changes in KWin core to pass");
}

void TestXdgShellWindowRules::testDesktopFileRemember()
{
    // Currently, the desktop file name is derived from the app id. If the app id is
    // changed, then the old rules will be lost. Either setDesktopFileName should
    // be exposed or the desktop file name rule should be removed for wayland windows.
    QSKIP("Needs changes in KWin core to pass");
}

void TestXdgShellWindowRules::testDesktopFileForce()
{
    // Currently, the desktop file name is derived from the app id. If the app id is
    // changed, then the old rules will be lost. Either setDesktopFileName should
    // be exposed or the desktop file name rule should be removed for wayland windows.
    QSKIP("Needs changes in KWin core to pass");
}

void TestXdgShellWindowRules::testDesktopFileApplyNow()
{
    // Currently, the desktop file name is derived from the app id. If the app id is
    // changed, then the old rules will be lost. Either setDesktopFileName should
    // be exposed or the desktop file name rule should be removed for wayland windows.
    QSKIP("Needs changes in KWin core to pass");
}

void TestXdgShellWindowRules::testDesktopFileForceTemporarily()
{
    // Currently, the desktop file name is derived from the app id. If the app id is
    // changed, then the old rules will be lost. Either setDesktopFileName should
    // be exposed or the desktop file name rule should be removed for wayland windows.
    QSKIP("Needs changes in KWin core to pass");
}

void TestXdgShellWindowRules::testActiveOpacityDontAffect()
{
    setWindowRule("opacityactive", 90, int(Rules::DontAffect));

    createTestWindow();
    QVERIFY(m_window->isActive());

    // The opacity should not be affected by the rule.
    QCOMPARE(m_window->opacity(), 1.0);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testActiveOpacityForce()
{
    setWindowRule("opacityactive", 90, int(Rules::Force));

    createTestWindow();
    QVERIFY(m_window->isActive());
    QCOMPARE(m_window->opacity(), 0.9);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testActiveOpacityForceTemporarily()
{
    setWindowRule("opacityactive", 90, int(Rules::ForceTemporarily));

    createTestWindow();
    QVERIFY(m_window->isActive());
    QCOMPARE(m_window->opacity(), 0.9);

    // The rule should be discarded when the window is closed.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(m_window->isActive());
    QCOMPARE(m_window->opacity(), 1.0);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testInactiveOpacityDontAffect()
{
    setWindowRule("opacityinactive", 80, int(Rules::DontAffect));

    createTestWindow();
    QVERIFY(m_window->isActive());

    // Make the window inactive.
    workspace()->activateWindow(nullptr);
    QVERIFY(!m_window->isActive());

    // The opacity of the window should not be affected by the rule.
    QCOMPARE(m_window->opacity(), 1.0);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testInactiveOpacityForce()
{
    setWindowRule("opacityinactive", 80, int(Rules::Force));

    createTestWindow();
    QVERIFY(m_window->isActive());
    QCOMPARE(m_window->opacity(), 1.0);

    // Make the window inactive.
    workspace()->activateWindow(nullptr);
    QVERIFY(!m_window->isActive());

    // The opacity should be forced by the rule.
    QCOMPARE(m_window->opacity(), 0.8);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testInactiveOpacityForceTemporarily()
{
    setWindowRule("opacityinactive", 80, int(Rules::ForceTemporarily));

    createTestWindow();
    QVERIFY(m_window->isActive());
    QCOMPARE(m_window->opacity(), 1.0);

    // Make the window inactive.
    workspace()->activateWindow(nullptr);
    QVERIFY(!m_window->isActive());

    // The opacity should be forced by the rule.
    QCOMPARE(m_window->opacity(), 0.8);

    // The rule should be discarded when the window is closed.
    destroyTestWindow();
    createTestWindow();

    QVERIFY(m_window->isActive());
    QCOMPARE(m_window->opacity(), 1.0);
    workspace()->activateWindow(nullptr);
    QVERIFY(!m_window->isActive());
    QCOMPARE(m_window->opacity(), 1.0);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testNoBorderDontAffect()
{
    setWindowRule("noborder", true, int(Rules::DontAffect));
    createTestWindow(ServerSideDecoration);

    // The window should not be affected by the rule.
    QVERIFY(!m_window->noBorder());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testNoBorderApply()
{
    setWindowRule("noborder", true, int(Rules::Apply));
    createTestWindow(ServerSideDecoration);

    // Initially, the window should not be decorated.
    QVERIFY(m_window->noBorder());
    QVERIFY(!m_window->isDecorated());

    // But you should be able to change "no border" property afterwards.
    QVERIFY(m_window->userCanSetNoBorder());
    m_window->setNoBorder(false);
    QVERIFY(!m_window->noBorder());

    // If one re-opens the window, it should have no border again.
    destroyTestWindow();
    createTestWindow(ServerSideDecoration);
    QVERIFY(m_window->noBorder());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testNoBorderRemember()
{
    setWindowRule("noborder", true, int(Rules::Remember));
    createTestWindow(ServerSideDecoration);

    // Initially, the window should not be decorated.
    QVERIFY(m_window->noBorder());
    QVERIFY(!m_window->isDecorated());

    // Unset the "no border" property.
    QVERIFY(m_window->userCanSetNoBorder());
    m_window->setNoBorder(false);
    QVERIFY(!m_window->noBorder());

    // Re-open the window, it should be decorated.
    destroyTestWindow();
    createTestWindow(ServerSideDecoration);
    QVERIFY(m_window->isDecorated());
    QVERIFY(!m_window->noBorder());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testNoBorderForce()
{
    setWindowRule("noborder", true, int(Rules::Force));
    createTestWindow(ServerSideDecoration);

    // The window should not be decorated.
    QVERIFY(m_window->noBorder());
    QVERIFY(!m_window->isDecorated());

    // And the user should not be able to change the "no border" property.
    m_window->setNoBorder(false);
    QVERIFY(m_window->noBorder());

    // Reopen the window.
    destroyTestWindow();
    createTestWindow(ServerSideDecoration);

    // The "no border" property should be still forced.
    QVERIFY(m_window->noBorder());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testNoBorderApplyNow()
{
    createTestWindow(ServerSideDecoration);
    QVERIFY(!m_window->noBorder());

    // Initialize RuleBook with the test rule.
    setWindowRule("noborder", true, int(Rules::ApplyNow));

    // The "no border" property should be set now.
    QVERIFY(m_window->noBorder());

    // One should be still able to change the "no border" property.
    m_window->setNoBorder(false);
    QVERIFY(!m_window->noBorder());

    // The rule should not be applied again.
    m_window->evaluateWindowRules();
    QVERIFY(!m_window->noBorder());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testNoBorderForceTemporarily()
{
    setWindowRule("noborder", true, int(Rules::ForceTemporarily));
    createTestWindow(ServerSideDecoration);

    // The "no border" property should be set.
    QVERIFY(m_window->noBorder());

    // And you should not be able to change it.
    m_window->setNoBorder(false);
    QVERIFY(m_window->noBorder());

    // The rule should be discarded when the window is closed.
    destroyTestWindow();
    createTestWindow(ServerSideDecoration);
    QVERIFY(!m_window->noBorder());

    // The "no border" property is no longer forced.
    m_window->setNoBorder(true);
    QVERIFY(m_window->noBorder());
    m_window->setNoBorder(false);
    QVERIFY(!m_window->noBorder());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testScreenDontAffect()
{
    const QList<KWin::LogicalOutput *> outputs = workspace()->outputs();

    setWindowRule("screen", int(1), int(Rules::DontAffect));

    createTestWindow();

    // The window should not be affected by the rule.
    QCOMPARE(m_window->output()->name(), outputs.at(0)->name());

    // The user can still move the window to another screen.
    m_window->sendToOutput(outputs.at(1));
    QCOMPARE(m_window->output()->name(), outputs.at(1)->name());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testScreenApply()
{
    const QList<KWin::LogicalOutput *> outputs = workspace()->outputs();

    setWindowRule("screen", int(1), int(Rules::Apply));

    createTestWindow();

    // The window should be in the screen specified by the rule.
    QEXPECT_FAIL("", "Applying a screen rule on a new client fails on Wayland", Continue);
    QCOMPARE(m_window->output()->name(), outputs.at(1)->name());

    // The user can move the window to another screen.
    m_window->sendToOutput(outputs.at(0));
    QCOMPARE(m_window->output()->name(), outputs.at(0)->name());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testScreenRemember()
{
    const QList<KWin::LogicalOutput *> outputs = workspace()->outputs();

    setWindowRule("screen", int(1), int(Rules::Remember));

    createTestWindow();

    // Initially, the window should be in the first screen
    QCOMPARE(m_window->output()->name(), outputs.at(0)->name());

    // Move the window to the second screen.
    m_window->sendToOutput(outputs.at(1));
    QCOMPARE(m_window->output()->name(), outputs.at(1)->name());

    // Close and reopen the window.
    destroyTestWindow();
    workspace()->setActiveOutput(outputs.at(0));
    createTestWindow();

    QEXPECT_FAIL("", "Applying a screen rule on a new client fails on Wayland", Continue);
    QCOMPARE(m_window->output()->name(), outputs.at(1)->name());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testScreenForce()
{
    const QList<KWin::BackendOutput *> outputs = kwinApp()->outputBackend()->outputs();

    createTestWindow();
    QVERIFY(m_window->isActive());

    setWindowRule("screen", int(1), int(Rules::Force));

    // The window should be forced to the screen specified by the rule.
    QCOMPARE(m_window->output()->name(), outputs.at(1)->name());

    // User should not be able to move the window to another screen.
    m_window->sendToOutput(workspace()->findOutput(outputs.at(0)));
    QCOMPARE(m_window->output()->name(), outputs.at(1)->name());

    // Disable the output where the window is on, so the window is moved the other screen
    OutputConfiguration config;
    auto changeSet = config.changeSet(outputs.at(1));
    changeSet->enabled = false;
    workspace()->applyOutputConfiguration(config);

    QVERIFY(!outputs.at(1)->isEnabled());
    QCOMPARE(m_window->output()->name(), outputs.at(0)->name());

    // Enable the output and check that the window is moved there again
    changeSet->enabled = true;
    workspace()->applyOutputConfiguration(config);

    QVERIFY(outputs.at(1)->isEnabled());
    QCOMPARE(m_window->output()->name(), outputs.at(1)->name());

    // Close and reopen the window.
    destroyTestWindow();
    createTestWindow();

    QEXPECT_FAIL("", "Applying a screen rule on a new client fails on Wayland", Continue);
    QCOMPARE(m_window->output()->name(), outputs.at(1)->name());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testScreenApplyNow()
{
    const QList<KWin::LogicalOutput *> outputs = workspace()->outputs();

    createTestWindow();

    QCOMPARE(m_window->output()->name(), outputs.at(0)->name());

    // Set the rule so the window should move to the screen specified by the rule.
    setWindowRule("screen", int(1), int(Rules::ApplyNow));
    QCOMPARE(m_window->output()->name(), outputs.at(1)->name());

    // The user can move the window to another screen.
    m_window->sendToOutput(outputs.at(0));
    QCOMPARE(m_window->output()->name(), outputs.at(0)->name());

    // The rule should not be applied again.
    m_window->evaluateWindowRules();
    QCOMPARE(m_window->output()->name(), outputs.at(0)->name());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testScreenForceTemporarily()
{
    const QList<KWin::LogicalOutput *> outputs = workspace()->outputs();

    createTestWindow();

    setWindowRule("screen", int(1), int(Rules::ForceTemporarily));

    // The window should be forced the second screen
    QCOMPARE(m_window->output()->name(), outputs.at(1)->name());

    // User is not allowed to move it
    m_window->sendToOutput(outputs.at(0));
    QCOMPARE(m_window->output()->name(), outputs.at(1)->name());

    // Close and reopen the window.
    destroyTestWindow();
    workspace()->setActiveOutput(outputs.at(0));
    createTestWindow();

    // The rule should be discarded now
    QCOMPARE(m_window->output()->name(), outputs.at(0)->name());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testMatchAfterNameChange()
{
    setWindowRule("above", true, int(Rules::Force));

    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);
    QVERIFY(window->isActive());
    QCOMPARE(window->keepAbove(), false);

    QSignalSpy desktopFileNameSpy(window, &Window::desktopFileNameChanged);

    shellSurface->set_app_id(QStringLiteral("org.kde.foo"));
    QVERIFY(desktopFileNameSpy.wait());
    QCOMPARE(window->keepAbove(), true);
}

void TestXdgShellWindowRules::testNotEnabled()
{
    setWindowRule("above", true, int(Rules::Force));
    setWindowRule("Enabled", false, int(Rules::Unused));

    createTestWindow();

    // The rule should not be applied as it is not enabled
    QCOMPARE(m_window->keepAbove(), false);

    // Now enable it and check again
    setWindowRule("Enabled", true, int(Rules::Unused));
    QCOMPARE(m_window->keepAbove(), true);
}

void TestXdgShellWindowRules::testLayerDontAffect()
{
    setWindowRule("layer", QStringLiteral("overlay"), int(Rules::DontAffect));

    createTestWindow();

    // The layer should not be affected by the rule.
    QCOMPARE(m_window->layer(), NormalLayer);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testLayerForce()
{
    setWindowRule("layer", QStringLiteral("overlay"), int(Rules::Force));

    createTestWindow();
    QCOMPARE(m_window->layer(), OverlayLayer);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testLayerForceTemporarily()
{
    setWindowRule("layer", QStringLiteral("overlay"), int(Rules::ForceTemporarily));

    createTestWindow();
    QCOMPARE(m_window->layer(), OverlayLayer);

    // The rule should be discarded when the window is closed.
    destroyTestWindow();
    createTestWindow();
    QCOMPARE(m_window->layer(), NormalLayer);

    destroyTestWindow();
}

void TestXdgShellWindowRules::testCloseableDontAffect()
{
    setWindowRule("closeable", false, int(Rules::DontAffect));

    createTestWindow();

    QVERIFY(m_window->isCloseable());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testCloseableForce()
{
    setWindowRule("closeable", false, int(Rules::Force));

    createTestWindow();
    QVERIFY(!m_window->isCloseable());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testCloseableForceTemporarily()
{
    setWindowRule("closeable", false, int(Rules::ForceTemporarily));

    createTestWindow();
    QVERIFY(!m_window->isCloseable());

    // The rule should be discarded when the window is closed.
    destroyTestWindow();
    createTestWindow();
    QVERIFY(m_window->isCloseable());

    destroyTestWindow();
}

void TestXdgShellWindowRules::testMatchTransientParent_data()
{
    QTest::addColumn<bool>("hasTransientParent");

    QTest::newRow("has transient") << true;
    QTest::newRow("does not have transient") << false;
}

void TestXdgShellWindowRules::testMatchTransientParent()
{
    QFETCH(bool, hasTransientParent);

    const QString ruleGroupName = QStringLiteral("transient-test-rule");
    m_config->group(QStringLiteral("General")).writeEntry("rules", QStringList{ruleGroupName});
    KConfigGroup group = m_config->group(ruleGroupName);

    group.writeEntry("above", true);
    group.writeEntry("aboverule", int(Rules::Force));

    group.writeEntry("wmclass", "org.kde.foo");
    group.writeEntry("wmclasscomplete", false);
    group.writeEntry("wmclassmatch", int(Rules::ExactMatch));
    group.writeEntry("hastransientparent", hasTransientParent);
    group.writeEntry("hastransientparentmatch", int(Rules::ExactBoolMatch));
    group.sync();

    workspace()->slotReconfigure();

    std::unique_ptr<KWayland::Client::Surface> parentSurface(Test::createSurface());
    std::unique_ptr<Test::XdgToplevel> parentToplevel(Test::createXdgToplevelSurface(parentSurface.get()));

    auto surfaceWithParent = Test::createSurface();
    auto toplevelWithParent = Test::createXdgToplevelSurface(surfaceWithParent.get(), [&parentToplevel](Test::XdgToplevel *toplevel) {
        toplevel->set_app_id(QStringLiteral("org.kde.foo"));
        toplevel->set_parent(parentToplevel->object());
    });

    auto windowWithParent = Test::renderAndWaitForShown(surfaceWithParent.get(), QSize(100, 50), Qt::blue);
    QVERIFY(windowWithParent);
    QVERIFY(windowWithParent->isActive());
    QCOMPARE(windowWithParent->keepAbove(), hasTransientParent);

    auto surfaceWithOutParent = Test::createSurface();
    auto toplevelWithOutParent = Test::createXdgToplevelSurface(surfaceWithOutParent.get(), [](Test::XdgToplevel *toplevel) {
        toplevel->set_app_id(QStringLiteral("org.kde.foo"));
    });

    auto windowWithOutParent = Test::renderAndWaitForShown(surfaceWithOutParent.get(), QSize(100, 50), Qt::green);
    QVERIFY(windowWithOutParent);
    QVERIFY(windowWithOutParent->isActive());
    QCOMPARE(windowWithOutParent->keepAbove(), !hasTransientParent);
}

WAYLANDTEST_MAIN(TestXdgShellWindowRules)
#include "xdgshellwindow_rules_test.moc"
