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

    SPDX-FileCopyrightText: 2018 Vlad Zahorodnii <vlad.zahorodnii@kde.org>

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

#include "kwin_wayland_test.h"

#include "abstract_client.h"
#include "atoms.h"
#include "x11client.h"
#include "deleted.h"
#include "main.h"
#include "platform.h"
#include "wayland_server.h"
#include "workspace.h"

#include <KWayland/Client/compositor.h>
#include <KWayland/Client/surface.h>

#include <xcb/xcb.h>
#include <xcb/xcb_icccm.h>

using namespace KWin;

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

class StackingOrderTest : public QObject
{
    Q_OBJECT

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

    void testTransientIsAboveParent();
    void testRaiseTransient();
    void testDeletedTransient();

    void testGroupTransientIsAboveWindowGroup();
    void testRaiseGroupTransient();
    void testDeletedGroupTransient();
    void testDontKeepAboveNonModalDialogGroupTransients();

    void testKeepAbove();
    void testKeepBelow();

};

void StackingOrderTest::initTestCase()
{
    qRegisterMetaType<KWin::AbstractClient *>();
    qRegisterMetaType<KWin::Deleted *>();

    QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
    QVERIFY(applicationStartedSpy.isValid());
    kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024));
    QVERIFY(waylandServer()->init(s_socketName));

    kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig));

    kwinApp()->start();
    QVERIFY(applicationStartedSpy.wait());
    Test::initWaylandWorkspace();
}

void StackingOrderTest::init()
{
    QVERIFY(Test::setupWaylandConnection());
}

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

void StackingOrderTest::testTransientIsAboveParent()
{
    // This test verifies that transients are always above their parents.

    // Create the parent.
    KWayland::Client::Surface *parentSurface =
        Test::createSurface(Test::waylandCompositor());
    QVERIFY(parentSurface);
    Test::XdgToplevel *parentShellSurface =
        Test::createXdgToplevelSurface(parentSurface, parentSurface);
    QVERIFY(parentShellSurface);
    AbstractClient *parent = Test::renderAndWaitForShown(parentSurface, QSize(256, 256), Qt::blue);
    QVERIFY(parent);
    QVERIFY(parent->isActive());
    QVERIFY(!parent->isTransient());

    // Initially, the stacking order should contain only the parent window.
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{parent}));

    // Create the transient.
    KWayland::Client::Surface *transientSurface =
        Test::createSurface(Test::waylandCompositor());
    QVERIFY(transientSurface);
    Test::XdgToplevel *transientShellSurface =
        Test::createXdgToplevelSurface(transientSurface, transientSurface);
    QVERIFY(transientShellSurface);
    transientShellSurface->set_parent(parentShellSurface->object());
    AbstractClient *transient = Test::renderAndWaitForShown(
        transientSurface, QSize(128, 128), Qt::red);
    QVERIFY(transient);
    QVERIFY(transient->isActive());
    QVERIFY(transient->isTransient());

    // The transient should be above the parent.
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{parent, transient}));

    // The transient still stays above the parent if we activate the latter.
    workspace()->activateClient(parent);
    QTRY_VERIFY(parent->isActive());
    QTRY_VERIFY(!transient->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{parent, transient}));
}

void StackingOrderTest::testRaiseTransient()
{
    // This test verifies that both the parent and the transient will be
    // raised if either one of them is activated.

    // Create the parent.
    KWayland::Client::Surface *parentSurface =
        Test::createSurface(Test::waylandCompositor());
    QVERIFY(parentSurface);
    Test::XdgToplevel *parentShellSurface =
        Test::createXdgToplevelSurface(parentSurface, parentSurface);
    QVERIFY(parentShellSurface);
    AbstractClient *parent = Test::renderAndWaitForShown(parentSurface, QSize(256, 256), Qt::blue);
    QVERIFY(parent);
    QVERIFY(parent->isActive());
    QVERIFY(!parent->isTransient());

    // Initially, the stacking order should contain only the parent window.
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{parent}));

    // Create the transient.
    KWayland::Client::Surface *transientSurface =
        Test::createSurface(Test::waylandCompositor());
    QVERIFY(transientSurface);
    Test::XdgToplevel *transientShellSurface =
        Test::createXdgToplevelSurface(transientSurface, transientSurface);
    QVERIFY(transientShellSurface);
    transientShellSurface->set_parent(parentShellSurface->object());
    AbstractClient *transient = Test::renderAndWaitForShown(
        transientSurface, QSize(128, 128), Qt::red);
    QVERIFY(transient);
    QTRY_VERIFY(transient->isActive());
    QVERIFY(transient->isTransient());

    // The transient should be above the parent.
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{parent, transient}));

    // Create a window that doesn't have any relationship to the parent or the transient.
    KWayland::Client::Surface *anotherSurface =
        Test::createSurface(Test::waylandCompositor());
    QVERIFY(anotherSurface);
    Test::XdgToplevel *anotherShellSurface =
        Test::createXdgToplevelSurface(anotherSurface, anotherSurface);
    QVERIFY(anotherShellSurface);
    AbstractClient *anotherClient = Test::renderAndWaitForShown(anotherSurface, QSize(128, 128), Qt::green);
    QVERIFY(anotherClient);
    QVERIFY(anotherClient->isActive());
    QVERIFY(!anotherClient->isTransient());

    // The newly created surface has to be above both the parent and the transient.
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{parent, transient, anotherClient}));

    // If we activate the parent, the transient should be raised too.
    workspace()->activateClient(parent);
    QTRY_VERIFY(parent->isActive());
    QTRY_VERIFY(!transient->isActive());
    QTRY_VERIFY(!anotherClient->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{anotherClient, parent, transient}));

    // Go back to the initial setup.
    workspace()->activateClient(anotherClient);
    QTRY_VERIFY(!parent->isActive());
    QTRY_VERIFY(!transient->isActive());
    QTRY_VERIFY(anotherClient->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{parent, transient, anotherClient}));

    // If we activate the transient, the parent should be raised too.
    workspace()->activateClient(transient);
    QTRY_VERIFY(!parent->isActive());
    QTRY_VERIFY(transient->isActive());
    QTRY_VERIFY(!anotherClient->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{anotherClient, parent, transient}));
}

struct WindowUnrefDeleter
{
    static inline void cleanup(Deleted *d) {
        if (d != nullptr) {
            d->unrefWindow();
        }
    }
};

void StackingOrderTest::testDeletedTransient()
{
    // This test verifies that deleted transients are kept above their
    // old parents.

    // Create the parent.
    KWayland::Client::Surface *parentSurface =
        Test::createSurface(Test::waylandCompositor());
    QVERIFY(parentSurface);
    Test::XdgToplevel *parentShellSurface =
        Test::createXdgToplevelSurface(parentSurface, parentSurface);
    QVERIFY(parentShellSurface);
    AbstractClient *parent = Test::renderAndWaitForShown(parentSurface, QSize(256, 256), Qt::blue);
    QVERIFY(parent);
    QVERIFY(parent->isActive());
    QVERIFY(!parent->isTransient());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{parent}));

    // Create the first transient.
    KWayland::Client::Surface *transient1Surface =
        Test::createSurface(Test::waylandCompositor());
    QVERIFY(transient1Surface);
    Test::XdgToplevel *transient1ShellSurface =
        Test::createXdgToplevelSurface(transient1Surface, transient1Surface);
    QVERIFY(transient1ShellSurface);
    transient1ShellSurface->set_parent(parentShellSurface->object());
    AbstractClient *transient1 = Test::renderAndWaitForShown(
        transient1Surface, QSize(128, 128), Qt::red);
    QVERIFY(transient1);
    QTRY_VERIFY(transient1->isActive());
    QVERIFY(transient1->isTransient());
    QCOMPARE(transient1->transientFor(), parent);

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{parent, transient1}));

    // Create the second transient.
    KWayland::Client::Surface *transient2Surface =
        Test::createSurface(Test::waylandCompositor());
    QVERIFY(transient2Surface);
    Test::XdgToplevel *transient2ShellSurface =
        Test::createXdgToplevelSurface(transient2Surface, transient2Surface);
    QVERIFY(transient2ShellSurface);
    transient2ShellSurface->set_parent(transient1ShellSurface->object());
    AbstractClient *transient2 = Test::renderAndWaitForShown(
        transient2Surface, QSize(128, 128), Qt::red);
    QVERIFY(transient2);
    QTRY_VERIFY(transient2->isActive());
    QVERIFY(transient2->isTransient());
    QCOMPARE(transient2->transientFor(), transient1);

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{parent, transient1, transient2}));

    // Activate the parent, both transients have to be above it.
    workspace()->activateClient(parent);
    QTRY_VERIFY(parent->isActive());
    QTRY_VERIFY(!transient1->isActive());
    QTRY_VERIFY(!transient2->isActive());

    // Close the top-most transient.
    connect(transient2, &AbstractClient::windowClosed, this,
        [](Toplevel *toplevel, Deleted *deleted) {
            Q_UNUSED(toplevel)
            deleted->refWindow();
        }
    );

    QSignalSpy windowClosedSpy(transient2, &AbstractClient::windowClosed);
    QVERIFY(windowClosedSpy.isValid());
    delete transient2ShellSurface;
    delete transient2Surface;
    QVERIFY(windowClosedSpy.wait());

    QScopedPointer<Deleted, WindowUnrefDeleter> deletedTransient(
        windowClosedSpy.first().at(1).value<Deleted *>());
    QVERIFY(deletedTransient.data());

    // The deleted transient still has to be above its old parent (transient1).
    QTRY_VERIFY(parent->isActive());
    QTRY_VERIFY(!transient1->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{parent, transient1, deletedTransient.data()}));
}

static xcb_window_t createGroupWindow(xcb_connection_t *conn,
                                      const QRect &geometry,
                                      xcb_window_t leaderWid = XCB_WINDOW_NONE)
{
    xcb_window_t wid = xcb_generate_id(conn);
    xcb_create_window(
        conn,                          // c
        XCB_COPY_FROM_PARENT,          // depth
        wid,                           // wid
        rootWindow(),                  // parent
        geometry.x(),                  // x
        geometry.y(),                  // y
        geometry.width(),              // width
        geometry.height(),             // height
        0,                             // border_width
        XCB_WINDOW_CLASS_INPUT_OUTPUT, // _class
        XCB_COPY_FROM_PARENT,          // visual
        0,                             // value_mask
        nullptr                        // value_list
    );

    xcb_size_hints_t sizeHints = {};
    xcb_icccm_size_hints_set_position(&sizeHints, 1, geometry.x(), geometry.y());
    xcb_icccm_size_hints_set_size(&sizeHints, 1, geometry.width(), geometry.height());
    xcb_icccm_set_wm_normal_hints(conn, wid, &sizeHints);

    if (leaderWid == XCB_WINDOW_NONE) {
        leaderWid = wid;
    }

    xcb_change_property(
        conn,                    // c
        XCB_PROP_MODE_REPLACE,   // mode
        wid,                     // window
        atoms->wm_client_leader, // property
        XCB_ATOM_WINDOW,         // type
        32,                      // format
        1,                       // data_len
        &leaderWid               // data
    );

    return wid;
}

struct XcbConnectionDeleter
{
    static inline void cleanup(xcb_connection_t *c) {
        xcb_disconnect(c);
    }
};

void StackingOrderTest::testGroupTransientIsAboveWindowGroup()
{
    // This test verifies that group transients are always above other
    // window group members.

    const QRect geometry = QRect(0, 0, 128, 128);

    QScopedPointer<xcb_connection_t, XcbConnectionDeleter> conn(
        xcb_connect(nullptr, nullptr));

    QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded);
    QVERIFY(windowCreatedSpy.isValid());

    // Create the group leader.
    xcb_window_t leaderWid = createGroupWindow(conn.data(), geometry);
    xcb_map_window(conn.data(), leaderWid);
    xcb_flush(conn.data());

    QVERIFY(windowCreatedSpy.wait());
    X11Client *leader = windowCreatedSpy.first().first().value<X11Client *>();
    QVERIFY(leader);
    QVERIFY(leader->isActive());
    QCOMPARE(leader->window(), leaderWid);
    QVERIFY(!leader->isTransient());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader}));

    // Create another group member.
    windowCreatedSpy.clear();
    xcb_window_t member1Wid = createGroupWindow(conn.data(), geometry, leaderWid);
    xcb_map_window(conn.data(), member1Wid);
    xcb_flush(conn.data());

    QVERIFY(windowCreatedSpy.wait());
    X11Client *member1 = windowCreatedSpy.first().first().value<X11Client *>();
    QVERIFY(member1);
    QVERIFY(member1->isActive());
    QCOMPARE(member1->window(), member1Wid);
    QCOMPARE(member1->group(), leader->group());
    QVERIFY(!member1->isTransient());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1}));

    // Create yet another group member.
    windowCreatedSpy.clear();
    xcb_window_t member2Wid = createGroupWindow(conn.data(), geometry, leaderWid);
    xcb_map_window(conn.data(), member2Wid);
    xcb_flush(conn.data());

    QVERIFY(windowCreatedSpy.wait());
    X11Client *member2 = windowCreatedSpy.first().first().value<X11Client *>();
    QVERIFY(member2);
    QVERIFY(member2->isActive());
    QCOMPARE(member2->window(), member2Wid);
    QCOMPARE(member2->group(), leader->group());
    QVERIFY(!member2->isTransient());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1, member2}));

    // Create a group transient.
    windowCreatedSpy.clear();
    xcb_window_t transientWid = createGroupWindow(conn.data(), geometry, leaderWid);
    xcb_icccm_set_wm_transient_for(conn.data(), transientWid, rootWindow());

    // Currently, we have some weird bug workaround: if a group transient
    // is a non-modal dialog, then it won't be kept above its window group.
    // We need to explicitly specify window type, otherwise the window type
    // will be deduced to _NET_WM_WINDOW_TYPE_DIALOG because we set transient
    // for before (the EWMH spec says to do that).
    xcb_atom_t net_wm_window_type = Xcb::Atom(
        QByteArrayLiteral("_NET_WM_WINDOW_TYPE"), false, conn.data());
    xcb_atom_t net_wm_window_type_normal = Xcb::Atom(
        QByteArrayLiteral("_NET_WM_WINDOW_TYPE_NORMAL"), false, conn.data());
    xcb_change_property(
        conn.data(),               // c
        XCB_PROP_MODE_REPLACE,     // mode
        transientWid,              // window
        net_wm_window_type,        // property
        XCB_ATOM_ATOM,             // type
        32,                        // format
        1,                         // data_len
        &net_wm_window_type_normal // data
    );

    xcb_map_window(conn.data(), transientWid);
    xcb_flush(conn.data());

    QVERIFY(windowCreatedSpy.wait());
    X11Client *transient = windowCreatedSpy.first().first().value<X11Client *>();
    QVERIFY(transient);
    QVERIFY(transient->isActive());
    QCOMPARE(transient->window(), transientWid);
    QCOMPARE(transient->group(), leader->group());
    QVERIFY(transient->isTransient());
    QVERIFY(transient->groupTransient());
    QVERIFY(!transient->isDialog()); // See above why

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1, member2, transient}));

    // If we activate any member of the window group, the transient will be above it.
    workspace()->activateClient(leader);
    QTRY_VERIFY(leader->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{member1, member2, leader, transient}));

    workspace()->activateClient(member1);
    QTRY_VERIFY(member1->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{member2, leader, member1, transient}));

    workspace()->activateClient(member2);
    QTRY_VERIFY(member2->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1, member2, transient}));

    workspace()->activateClient(transient);
    QTRY_VERIFY(transient->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1, member2, transient}));
}

void StackingOrderTest::testRaiseGroupTransient()
{
    const QRect geometry = QRect(0, 0, 128, 128);

    QScopedPointer<xcb_connection_t, XcbConnectionDeleter> conn(
        xcb_connect(nullptr, nullptr));

    QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded);
    QVERIFY(windowCreatedSpy.isValid());

    // Create the group leader.
    xcb_window_t leaderWid = createGroupWindow(conn.data(), geometry);
    xcb_map_window(conn.data(), leaderWid);
    xcb_flush(conn.data());

    QVERIFY(windowCreatedSpy.wait());
    X11Client *leader = windowCreatedSpy.first().first().value<X11Client *>();
    QVERIFY(leader);
    QVERIFY(leader->isActive());
    QCOMPARE(leader->window(), leaderWid);
    QVERIFY(!leader->isTransient());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader}));

    // Create another group member.
    windowCreatedSpy.clear();
    xcb_window_t member1Wid = createGroupWindow(conn.data(), geometry, leaderWid);
    xcb_map_window(conn.data(), member1Wid);
    xcb_flush(conn.data());

    QVERIFY(windowCreatedSpy.wait());
    X11Client *member1 = windowCreatedSpy.first().first().value<X11Client *>();
    QVERIFY(member1);
    QVERIFY(member1->isActive());
    QCOMPARE(member1->window(), member1Wid);
    QCOMPARE(member1->group(), leader->group());
    QVERIFY(!member1->isTransient());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1}));

    // Create yet another group member.
    windowCreatedSpy.clear();
    xcb_window_t member2Wid = createGroupWindow(conn.data(), geometry, leaderWid);
    xcb_map_window(conn.data(), member2Wid);
    xcb_flush(conn.data());

    QVERIFY(windowCreatedSpy.wait());
    X11Client *member2 = windowCreatedSpy.first().first().value<X11Client *>();
    QVERIFY(member2);
    QVERIFY(member2->isActive());
    QCOMPARE(member2->window(), member2Wid);
    QCOMPARE(member2->group(), leader->group());
    QVERIFY(!member2->isTransient());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1, member2}));

    // Create a group transient.
    windowCreatedSpy.clear();
    xcb_window_t transientWid = createGroupWindow(conn.data(), geometry, leaderWid);
    xcb_icccm_set_wm_transient_for(conn.data(), transientWid, rootWindow());

    // Currently, we have some weird bug workaround: if a group transient
    // is a non-modal dialog, then it won't be kept above its window group.
    // We need to explicitly specify window type, otherwise the window type
    // will be deduced to _NET_WM_WINDOW_TYPE_DIALOG because we set transient
    // for before (the EWMH spec says to do that).
    xcb_atom_t net_wm_window_type = Xcb::Atom(
        QByteArrayLiteral("_NET_WM_WINDOW_TYPE"), false, conn.data());
    xcb_atom_t net_wm_window_type_normal = Xcb::Atom(
        QByteArrayLiteral("_NET_WM_WINDOW_TYPE_NORMAL"), false, conn.data());
    xcb_change_property(
        conn.data(),               // c
        XCB_PROP_MODE_REPLACE,     // mode
        transientWid,              // window
        net_wm_window_type,        // property
        XCB_ATOM_ATOM,             // type
        32,                        // format
        1,                         // data_len
        &net_wm_window_type_normal // data
    );

    xcb_map_window(conn.data(), transientWid);
    xcb_flush(conn.data());

    QVERIFY(windowCreatedSpy.wait());
    X11Client *transient = windowCreatedSpy.first().first().value<X11Client *>();
    QVERIFY(transient);
    QVERIFY(transient->isActive());
    QCOMPARE(transient->window(), transientWid);
    QCOMPARE(transient->group(), leader->group());
    QVERIFY(transient->isTransient());
    QVERIFY(transient->groupTransient());
    QVERIFY(!transient->isDialog()); // See above why

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1, member2, transient}));

    // Create a Wayland client that is not a member of the window group.
    KWayland::Client::Surface *anotherSurface =
        Test::createSurface(Test::waylandCompositor());
    QVERIFY(anotherSurface);
    Test::XdgToplevel *anotherShellSurface =
        Test::createXdgToplevelSurface(anotherSurface, anotherSurface);
    QVERIFY(anotherShellSurface);
    AbstractClient *anotherClient = Test::renderAndWaitForShown(anotherSurface, QSize(128, 128), Qt::green);
    QVERIFY(anotherClient);
    QVERIFY(anotherClient->isActive());
    QVERIFY(!anotherClient->isTransient());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1, member2, transient, anotherClient}));

    // If we activate the leader, then only it and the transient have to be raised.
    workspace()->activateClient(leader);
    QTRY_VERIFY(leader->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{member1, member2, anotherClient, leader, transient}));

    // If another member of the window group is activated, then the transient will
    // be above that member and the leader.
    workspace()->activateClient(member2);
    QTRY_VERIFY(member2->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{member1, anotherClient, leader, member2, transient}));

    // FIXME: If we activate the transient, only it will be raised.
    workspace()->activateClient(anotherClient);
    QTRY_VERIFY(anotherClient->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{member1, leader, member2, transient, anotherClient}));

    workspace()->activateClient(transient);
    QTRY_VERIFY(transient->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{member1, leader, member2, anotherClient, transient}));
}

void StackingOrderTest::testDeletedGroupTransient()
{
    // This test verifies that deleted group transients are kept above their
    // old window groups.

    const QRect geometry = QRect(0, 0, 128, 128);

    QScopedPointer<xcb_connection_t, XcbConnectionDeleter> conn(
        xcb_connect(nullptr, nullptr));

    QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded);
    QVERIFY(windowCreatedSpy.isValid());

    // Create the group leader.
    xcb_window_t leaderWid = createGroupWindow(conn.data(), geometry);
    xcb_map_window(conn.data(), leaderWid);
    xcb_flush(conn.data());

    QVERIFY(windowCreatedSpy.wait());
    X11Client *leader = windowCreatedSpy.first().first().value<X11Client *>();
    QVERIFY(leader);
    QVERIFY(leader->isActive());
    QCOMPARE(leader->window(), leaderWid);
    QVERIFY(!leader->isTransient());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader}));

    // Create another group member.
    windowCreatedSpy.clear();
    xcb_window_t member1Wid = createGroupWindow(conn.data(), geometry, leaderWid);
    xcb_map_window(conn.data(), member1Wid);
    xcb_flush(conn.data());

    QVERIFY(windowCreatedSpy.wait());
    X11Client *member1 = windowCreatedSpy.first().first().value<X11Client *>();
    QVERIFY(member1);
    QVERIFY(member1->isActive());
    QCOMPARE(member1->window(), member1Wid);
    QCOMPARE(member1->group(), leader->group());
    QVERIFY(!member1->isTransient());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1}));

    // Create yet another group member.
    windowCreatedSpy.clear();
    xcb_window_t member2Wid = createGroupWindow(conn.data(), geometry, leaderWid);
    xcb_map_window(conn.data(), member2Wid);
    xcb_flush(conn.data());

    QVERIFY(windowCreatedSpy.wait());
    X11Client *member2 = windowCreatedSpy.first().first().value<X11Client *>();
    QVERIFY(member2);
    QVERIFY(member2->isActive());
    QCOMPARE(member2->window(), member2Wid);
    QCOMPARE(member2->group(), leader->group());
    QVERIFY(!member2->isTransient());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1, member2}));

    // Create a group transient.
    windowCreatedSpy.clear();
    xcb_window_t transientWid = createGroupWindow(conn.data(), geometry, leaderWid);
    xcb_icccm_set_wm_transient_for(conn.data(), transientWid, rootWindow());

    // Currently, we have some weird bug workaround: if a group transient
    // is a non-modal dialog, then it won't be kept above its window group.
    // We need to explicitly specify window type, otherwise the window type
    // will be deduced to _NET_WM_WINDOW_TYPE_DIALOG because we set transient
    // for before (the EWMH spec says to do that).
    xcb_atom_t net_wm_window_type = Xcb::Atom(
        QByteArrayLiteral("_NET_WM_WINDOW_TYPE"), false, conn.data());
    xcb_atom_t net_wm_window_type_normal = Xcb::Atom(
        QByteArrayLiteral("_NET_WM_WINDOW_TYPE_NORMAL"), false, conn.data());
    xcb_change_property(
        conn.data(),               // c
        XCB_PROP_MODE_REPLACE,     // mode
        transientWid,              // window
        net_wm_window_type,        // property
        XCB_ATOM_ATOM,             // type
        32,                        // format
        1,                         // data_len
        &net_wm_window_type_normal // data
    );

    xcb_map_window(conn.data(), transientWid);
    xcb_flush(conn.data());

    QVERIFY(windowCreatedSpy.wait());
    X11Client *transient = windowCreatedSpy.first().first().value<X11Client *>();
    QVERIFY(transient);
    QVERIFY(transient->isActive());
    QCOMPARE(transient->window(), transientWid);
    QCOMPARE(transient->group(), leader->group());
    QVERIFY(transient->isTransient());
    QVERIFY(transient->groupTransient());
    QVERIFY(!transient->isDialog()); // See above why

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1, member2, transient}));

    // Unmap the transient.
    connect(transient, &X11Client::windowClosed, this,
        [](Toplevel *toplevel, Deleted *deleted) {
            Q_UNUSED(toplevel)
            deleted->refWindow();
        }
    );

    QSignalSpy windowClosedSpy(transient, &X11Client::windowClosed);
    QVERIFY(windowClosedSpy.isValid());
    xcb_unmap_window(conn.data(), transientWid);
    xcb_flush(conn.data());
    QVERIFY(windowClosedSpy.wait());

    QScopedPointer<Deleted, WindowUnrefDeleter> deletedTransient(
        windowClosedSpy.first().at(1).value<Deleted *>());
    QVERIFY(deletedTransient.data());

    // The transient has to be above each member of the window group.
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1, member2, deletedTransient.data()}));
}

void StackingOrderTest::testDontKeepAboveNonModalDialogGroupTransients()
{
    // Bug 76026

    const QRect geometry = QRect(0, 0, 128, 128);

    QScopedPointer<xcb_connection_t, XcbConnectionDeleter> conn(
        xcb_connect(nullptr, nullptr));

    QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded);
    QVERIFY(windowCreatedSpy.isValid());

    // Create the group leader.
    xcb_window_t leaderWid = createGroupWindow(conn.data(), geometry);
    xcb_map_window(conn.data(), leaderWid);
    xcb_flush(conn.data());

    QVERIFY(windowCreatedSpy.wait());
    X11Client *leader = windowCreatedSpy.first().first().value<X11Client *>();
    QVERIFY(leader);
    QVERIFY(leader->isActive());
    QCOMPARE(leader->window(), leaderWid);
    QVERIFY(!leader->isTransient());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader}));

    // Create another group member.
    windowCreatedSpy.clear();
    xcb_window_t member1Wid = createGroupWindow(conn.data(), geometry, leaderWid);
    xcb_map_window(conn.data(), member1Wid);
    xcb_flush(conn.data());

    QVERIFY(windowCreatedSpy.wait());
    X11Client *member1 = windowCreatedSpy.first().first().value<X11Client *>();
    QVERIFY(member1);
    QVERIFY(member1->isActive());
    QCOMPARE(member1->window(), member1Wid);
    QCOMPARE(member1->group(), leader->group());
    QVERIFY(!member1->isTransient());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1}));

    // Create yet another group member.
    windowCreatedSpy.clear();
    xcb_window_t member2Wid = createGroupWindow(conn.data(), geometry, leaderWid);
    xcb_map_window(conn.data(), member2Wid);
    xcb_flush(conn.data());

    QVERIFY(windowCreatedSpy.wait());
    X11Client *member2 = windowCreatedSpy.first().first().value<X11Client *>();
    QVERIFY(member2);
    QVERIFY(member2->isActive());
    QCOMPARE(member2->window(), member2Wid);
    QCOMPARE(member2->group(), leader->group());
    QVERIFY(!member2->isTransient());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1, member2}));

    // Create a group transient.
    windowCreatedSpy.clear();
    xcb_window_t transientWid = createGroupWindow(conn.data(), geometry, leaderWid);
    xcb_icccm_set_wm_transient_for(conn.data(), transientWid, rootWindow());
    xcb_map_window(conn.data(), transientWid);
    xcb_flush(conn.data());

    QVERIFY(windowCreatedSpy.wait());
    X11Client *transient = windowCreatedSpy.first().first().value<X11Client *>();
    QVERIFY(transient);
    QVERIFY(transient->isActive());
    QCOMPARE(transient->window(), transientWid);
    QCOMPARE(transient->group(), leader->group());
    QVERIFY(transient->isTransient());
    QVERIFY(transient->groupTransient());
    QVERIFY(transient->isDialog());
    QVERIFY(!transient->isModal());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1, member2, transient}));

    workspace()->activateClient(leader);
    QTRY_VERIFY(leader->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{member1, member2, transient, leader}));

    workspace()->activateClient(member1);
    QTRY_VERIFY(member1->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{member2, transient, leader, member1}));

    workspace()->activateClient(member2);
    QTRY_VERIFY(member2->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{transient, leader, member1, member2}));

    workspace()->activateClient(transient);
    QTRY_VERIFY(transient->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{leader, member1, member2, transient}));
}

void StackingOrderTest::testKeepAbove()
{
    // This test verifies that "keep-above" windows are kept above other windows.

    // Create the first client.
    KWayland::Client::Surface *clientASurface =
        Test::createSurface(Test::waylandCompositor());
    QVERIFY(clientASurface);
    Test::XdgToplevel *clientAShellSurface =
        Test::createXdgToplevelSurface(clientASurface, clientASurface);
    QVERIFY(clientAShellSurface);
    AbstractClient *clientA = Test::renderAndWaitForShown(clientASurface, QSize(128, 128), Qt::green);
    QVERIFY(clientA);
    QVERIFY(clientA->isActive());
    QVERIFY(!clientA->keepAbove());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{clientA}));

    // Create the second client.
    KWayland::Client::Surface *clientBSurface =
        Test::createSurface(Test::waylandCompositor());
    QVERIFY(clientBSurface);
    Test::XdgToplevel *clientBShellSurface =
        Test::createXdgToplevelSurface(clientBSurface, clientBSurface);
    QVERIFY(clientBShellSurface);
    AbstractClient *clientB = Test::renderAndWaitForShown(clientBSurface, QSize(128, 128), Qt::green);
    QVERIFY(clientB);
    QVERIFY(clientB->isActive());
    QVERIFY(!clientB->keepAbove());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{clientA, clientB}));

    // Go to the initial test position.
    workspace()->activateClient(clientA);
    QTRY_VERIFY(clientA->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{clientB, clientA}));

    // Set the "keep-above" flag on the client B, it should go above other clients.
    {
        StackingUpdatesBlocker blocker(workspace());
        clientB->setKeepAbove(true);
    }

    QVERIFY(clientB->keepAbove());
    QVERIFY(!clientB->isActive());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{clientA, clientB}));
}

void StackingOrderTest::testKeepBelow()
{
    // This test verifies that "keep-below" windows are kept below other windows.

    // Create the first client.
    KWayland::Client::Surface *clientASurface =
        Test::createSurface(Test::waylandCompositor());
    QVERIFY(clientASurface);
    Test::XdgToplevel *clientAShellSurface =
        Test::createXdgToplevelSurface(clientASurface, clientASurface);
    QVERIFY(clientAShellSurface);
    AbstractClient *clientA = Test::renderAndWaitForShown(clientASurface, QSize(128, 128), Qt::green);
    QVERIFY(clientA);
    QVERIFY(clientA->isActive());
    QVERIFY(!clientA->keepBelow());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{clientA}));

    // Create the second client.
    KWayland::Client::Surface *clientBSurface =
        Test::createSurface(Test::waylandCompositor());
    QVERIFY(clientBSurface);
    Test::XdgToplevel *clientBShellSurface =
        Test::createXdgToplevelSurface(clientBSurface, clientBSurface);
    QVERIFY(clientBShellSurface);
    AbstractClient *clientB = Test::renderAndWaitForShown(clientBSurface, QSize(128, 128), Qt::green);
    QVERIFY(clientB);
    QVERIFY(clientB->isActive());
    QVERIFY(!clientB->keepBelow());

    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{clientA, clientB}));

    // Set the "keep-below" flag on the client B, it should go below other clients.
    {
        StackingUpdatesBlocker blocker(workspace());
        clientB->setKeepBelow(true);
    }

    QVERIFY(clientB->isActive());
    QVERIFY(clientB->keepBelow());
    QCOMPARE(workspace()->stackingOrder(), (QList<Toplevel *>{clientB, clientA}));
}

WAYLANDTEST_MAIN(StackingOrderTest)
#include "stacking_order_test.moc"
