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

    SPDX-License-Identifier: LGPL-2.1-or-later
*/

#include "kwindowsystem.h"
#include "kx11extras.h"
#include "nettesthelper.h"
#include "netwm.h"

#include <QSignalSpy>
#include <QWidget>
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
#include <private/qtx11extras_p.h>
#else
#include <QX11Info>
#endif

#include <qtest_widgets.h>
Q_DECLARE_METATYPE(WId)
Q_DECLARE_METATYPE(NET::Properties)
Q_DECLARE_METATYPE(NET::Properties2)
Q_DECLARE_METATYPE(const unsigned long *)

class KWindowSystemX11Test : public QObject
{
    Q_OBJECT
private Q_SLOTS:
    void initTestCase();
    // needs to be first test, would fail if run after others (X11)
    void testActiveWindowChanged();
    void testWindowAdded();
    void testWindowRemoved();
    void testDesktopChanged();
    void testNumberOfDesktopsChanged();
    void testDesktopNamesChanged();
    void testShowingDesktopChanged();
    void testSetShowingDesktop();
    void testWorkAreaChanged();
    void testWindowTitleChanged();
    void testMinimizeWindow();
    void testPlatformX11();
};

void KWindowSystemX11Test::initTestCase()
{
    QCoreApplication::setAttribute(Qt::AA_ForceRasterWidgets);
}

void KWindowSystemX11Test::testActiveWindowChanged()
{
    qRegisterMetaType<WId>("WId");
    QSignalSpy spy(KX11Extras::self(), &KX11Extras::activeWindowChanged);

    std::unique_ptr<QWidget> widget(new QWidget);
    widget->show();

    QVERIFY(spy.wait());
    QCOMPARE(spy.count(), 1);
    QCOMPARE(spy.first().at(0).toULongLong(), widget->winId());
    QCOMPARE(KX11Extras::activeWindow(), widget->winId());
}

void KWindowSystemX11Test::testWindowAdded()
{
    qRegisterMetaType<WId>("WId");
    QSignalSpy spy(KX11Extras::self(), &KX11Extras::windowAdded);
    QSignalSpy stackingOrderSpy(KX11Extras::self(), &KX11Extras::stackingOrderChanged);
    std::unique_ptr<QWidget> widget(new QWidget);
    widget->show();
    QVERIFY(QTest::qWaitForWindowExposed(widget.get()));
    QVERIFY(spy.count() > 0);
    bool hasWId = false;
    for (auto it = spy.constBegin(); it != spy.constEnd(); ++it) {
        if ((*it).isEmpty()) {
            continue;
        }
        QCOMPARE((*it).count(), 1);
        hasWId = (*it).at(0).toULongLong() == widget->winId();
        if (hasWId) {
            break;
        }
    }
    QVERIFY(hasWId);
    QVERIFY(KX11Extras::hasWId(widget->winId()));
    QVERIFY(!stackingOrderSpy.isEmpty());
}

void KWindowSystemX11Test::testWindowRemoved()
{
    qRegisterMetaType<WId>("WId");
    std::unique_ptr<QWidget> widget(new QWidget);
    widget->show();
    QVERIFY(QTest::qWaitForWindowExposed(widget.get()));
    QVERIFY(KX11Extras::hasWId(widget->winId()));

    QSignalSpy spy(KX11Extras::self(), &KX11Extras::windowRemoved);
    widget->hide();
    spy.wait(1000);
    QCOMPARE(spy.first().at(0).toULongLong(), widget->winId());
    QVERIFY(!KX11Extras::hasWId(widget->winId()));
}

void KWindowSystemX11Test::testDesktopChanged()
{
    // This test requires a running NETWM-compliant window manager
    if (KX11Extras::numberOfDesktops() == 1) {
        QSKIP("At least two virtual desktops are required to test desktop changed");
    }
    const int current = KX11Extras::currentDesktop();

    QSignalSpy spy(KX11Extras::self(), &KX11Extras::currentDesktopChanged);
    int newDesktop = current + 1;
    if (newDesktop > KX11Extras::numberOfDesktops()) {
        newDesktop = 1;
    }
    KX11Extras::setCurrentDesktop(newDesktop);
    QVERIFY(spy.wait());
    QCOMPARE(KX11Extras::currentDesktop(), newDesktop);
    QCOMPARE(spy.count(), 1);
    QCOMPARE(spy.first().at(0).toInt(), newDesktop);
    spy.clear();

    // setting to current desktop should not change anything
    KX11Extras::setCurrentDesktop(newDesktop);

    // set back for clean state
    KX11Extras::setCurrentDesktop(current);
    QVERIFY(spy.wait());
    QCOMPARE(KX11Extras::currentDesktop(), current);
    QCOMPARE(spy.count(), 1);
    QCOMPARE(spy.first().at(0).toInt(), current);
}

void KWindowSystemX11Test::testNumberOfDesktopsChanged()
{
    // This test requires a running NETWM-compliant window manager
    const int oldNumber = KX11Extras::numberOfDesktops();
    QSignalSpy spy(KX11Extras::self(), &KX11Extras::numberOfDesktopsChanged);

    // KWin has arbitrary max number of 20 desktops, so don't fail the test if we use +1
    const int newNumber = oldNumber < 20 ? oldNumber + 1 : oldNumber - 1;

    NETRootInfo info(QX11Info::connection(), NET::NumberOfDesktops, NET::Properties2());
    info.setNumberOfDesktops(newNumber);

    QVERIFY(spy.wait());
    QCOMPARE(KX11Extras::numberOfDesktops(), newNumber);
    QCOMPARE(spy.count(), 1);
    QCOMPARE(spy.first().at(0).toInt(), newNumber);
    spy.clear();

    // setting to same number should not change
    info.setNumberOfDesktops(newNumber);

    // set back for clean state
    info.setNumberOfDesktops(oldNumber);
    QVERIFY(spy.wait());
    QCOMPARE(KX11Extras::numberOfDesktops(), oldNumber);
    QCOMPARE(spy.count(), 1);
    QCOMPARE(spy.first().at(0).toInt(), oldNumber);
}

void KWindowSystemX11Test::testDesktopNamesChanged()
{
    // This test requires a running NETWM-compliant window manager
    const QString origName = KX11Extras::desktopName(KX11Extras::currentDesktop());
    QSignalSpy spy(KX11Extras::self(), &KX11Extras::desktopNamesChanged);

    const QString testName = QStringLiteral("testFooBar");

    KX11Extras::setDesktopName(KX11Extras::currentDesktop(), testName);
    QVERIFY(spy.wait());
    QCOMPARE(KX11Extras::desktopName(KX11Extras::currentDesktop()), testName);
    QCOMPARE(spy.count(), 1);
    spy.clear();

    QX11Info::setAppTime(QX11Info::getTimestamp());

    // setting back to clean state
    KX11Extras::setDesktopName(KX11Extras::currentDesktop(), origName);
    QVERIFY(spy.wait());
    QCOMPARE(KX11Extras::desktopName(KX11Extras::currentDesktop()), origName);
    QCOMPARE(spy.count(), 1);
}

void KWindowSystemX11Test::testShowingDesktopChanged()
{
    QX11Info::setAppTime(QX11Info::getTimestamp());
    const bool showingDesktop = KWindowSystem::showingDesktop();

    QSignalSpy spy(KWindowSystem::self(), &KWindowSystem::showingDesktopChanged);

    NETRootInfo info(QX11Info::connection(), NET::Properties(), NET::WM2ShowingDesktop);
    info.setShowingDesktop(!showingDesktop);

    QVERIFY(spy.wait());
    QCOMPARE(spy.count(), 1);
    QCOMPARE(spy.first().at(0).toBool(), !showingDesktop);
    QCOMPARE(KWindowSystem::showingDesktop(), !showingDesktop);
    spy.clear();

    QX11Info::setAppTime(QX11Info::getTimestamp());

    // setting again should not change
    info.setShowingDesktop(!showingDesktop);

    // setting back to clean state
    info.setShowingDesktop(showingDesktop);
    QVERIFY(spy.wait(100));
    QCOMPARE(spy.count(), 1);
    QCOMPARE(spy.first().at(0).toBool(), showingDesktop);
    QCOMPARE(KWindowSystem::showingDesktop(), showingDesktop);
}

void KWindowSystemX11Test::testSetShowingDesktop()
{
    QSignalSpy spy(KWindowSystem::self(), &KWindowSystem::showingDesktopChanged);
    const bool showingDesktop = KWindowSystem::showingDesktop();

    // setting the same state shouldn't change it
    QX11Info::setAppTime(QX11Info::getTimestamp());
    KWindowSystem::setShowingDesktop(showingDesktop);
    QCOMPARE(spy.wait(), false); // spy.wait() waits for 5s
    QCOMPARE(KWindowSystem::showingDesktop(), showingDesktop);
    spy.clear();

    // set opposite state
    QX11Info::setAppTime(QX11Info::getTimestamp());
    KWindowSystem::setShowingDesktop(!showingDesktop);
    QVERIFY(spy.wait());
    QCOMPARE(KWindowSystem::showingDesktop(), !showingDesktop);
    spy.clear();

    // setting back to clean state
    QX11Info::setAppTime(QX11Info::getTimestamp());
    KWindowSystem::setShowingDesktop(showingDesktop);
    QVERIFY(spy.wait());
    QCOMPARE(KWindowSystem::showingDesktop(), showingDesktop);
    spy.clear();
}

void KWindowSystemX11Test::testWorkAreaChanged()
{
    // if there are multiple screens this test can fail as workarea is not multi screen aware
    QSignalSpy spy(KX11Extras::self(), &KX11Extras::workAreaChanged);
    QSignalSpy strutSpy(KX11Extras::self(), &KX11Extras::strutChanged);

    QWidget widget;
    widget.setGeometry(0, 0, 100, 10);
    widget.show();

    KX11Extras::setExtendedStrut(widget.winId(), 10, 0, 10, 0, 0, 0, 100, 0, 100, 0, 0, 0);
    QVERIFY(spy.wait());
    QVERIFY(!spy.isEmpty());
    QVERIFY(!strutSpy.isEmpty());
}

void KWindowSystemX11Test::testWindowTitleChanged()
{
    qRegisterMetaType<WId>("WId");
    qRegisterMetaType<NET::Properties>("NET::Properties");
    qRegisterMetaType<NET::Properties2>("NET::Properties2");
    qRegisterMetaType<const unsigned long *>("const ulong*");
    QWidget widget;
    widget.setWindowTitle(QStringLiteral("foo"));
    widget.show();
    QVERIFY(QTest::qWaitForWindowExposed(&widget));

    // wait till the window is mapped, etc.
    QTest::qWait(200);

    QSignalSpy propertiesChangedSpy(KX11Extras::self(), &KX11Extras::windowChanged);
    QVERIFY(propertiesChangedSpy.isValid());

#if KWINDOWSYSTEM_ENABLE_DEPRECATED_SINCE(5, 0)
    QSignalSpy propertyChangedSpy(KWindowSystem::self(), qOverload<WId, uint>(&KWindowSystem::windowChanged));
    QVERIFY(propertyChangedSpy.isValid());
#endif

#if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 80)
    QSignalSpy windowChangedSpy(KWindowSystem::self(), qOverload<WId>(&KWindowSystem::windowChanged));
    QVERIFY(windowChangedSpy.isValid());
#endif

    widget.setWindowTitle(QStringLiteral("bar"));
    QX11Info::setAppTime(QX11Info::getTimestamp());

    int counter = 0;
    bool gotWMName = false;
    while (propertiesChangedSpy.wait() && counter < 10) {
        for (auto it = propertiesChangedSpy.constBegin(); it != propertiesChangedSpy.constEnd(); ++it) {
            if ((*it).isEmpty()) {
                continue;
            }
            if ((*it).at(0).toULongLong() == widget.winId()) {
                NET::Properties props = (*it).at(1).value<NET::Properties>();
                if (props.testFlag(NET::WMName)) {
                    gotWMName = true;
                }
            }
        }
        if (gotWMName) {
            break;
        }
        propertiesChangedSpy.clear();
        counter++;
    }
    QVERIFY(gotWMName);

#if KWINDOWSYSTEM_ENABLE_DEPRECATED_SINCE(5, 0)
    gotWMName = false;
    QCOMPARE(propertyChangedSpy.isEmpty(), false);
    for (auto it = propertyChangedSpy.constBegin(); it != propertyChangedSpy.constEnd(); ++it) {
        if ((*it).isEmpty()) {
            continue;
        }
        if ((*it).at(0).toULongLong() == widget.winId()) {
            unsigned int props = (*it).at(1).value<unsigned int>();
            if (props & NET::WMName) {
                gotWMName = true;
            }
        }
        if (gotWMName) {
            break;
        }
    }
    QVERIFY(gotWMName);
#endif

#if KWINDOWSYSTEM_BUILD_DEPRECATED_SINCE(5, 80)
    QCOMPARE(windowChangedSpy.isEmpty(), false);
    bool gotWindow = false;
    for (auto it = windowChangedSpy.constBegin(); it != windowChangedSpy.constEnd(); ++it) {
        if ((*it).isEmpty()) {
            continue;
        }
        if ((*it).at(0).toULongLong() == widget.winId()) {
            gotWindow = true;
        }
        if (gotWindow) {
            break;
        }
    }
    QVERIFY(gotWindow);
#endif

    // now let's verify the info in KWindowInfo
    // we wait a little bit more as openbox is updating the visible name
    QTest::qWait(500);
    KWindowInfo info(widget.winId(), NET::WMName | NET::WMVisibleName | NET::WMVisibleIconName | NET::WMIconName, NET::Properties2());
    QVERIFY(info.valid());
    const QString expectedName = QStringLiteral("bar");
    QCOMPARE(info.name(), expectedName);
    QCOMPARE(info.visibleName(), expectedName);
    QCOMPARE(info.visibleIconName(), expectedName);
    QCOMPARE(info.iconName(), expectedName);
}

void KWindowSystemX11Test::testMinimizeWindow()
{
    NETRootInfo rootInfo(QX11Info::connection(), NET::Supported | NET::SupportingWMCheck);
    if (qstrcmp(rootInfo.wmName(), "Openbox") != 0 && qstrcmp(rootInfo.wmName(), "KWin") != 0) {
        QSKIP("Test minimize window might not be supported on the used window manager.");
    }
    QWidget widget;
    widget.show();
    QVERIFY(QTest::qWaitForWindowExposed(&widget));

    KWindowInfo info(widget.winId(), NET::WMState | NET::XAWMState);
    QVERIFY(!info.isMinimized());

    KX11Extras::minimizeWindow(widget.winId());
    // create a roundtrip, updating minimized state is done by the window manager and wait a short time
    QX11Info::setAppTime(QX11Info::getTimestamp());
    QTest::qWait(200);

    KWindowInfo info2(widget.winId(), NET::WMState | NET::XAWMState);
    QVERIFY(info2.isMinimized());

    KX11Extras::unminimizeWindow(widget.winId());
    // create a roundtrip, updating minimized state is done by the window manager and wait a short time
    QX11Info::setAppTime(QX11Info::getTimestamp());
    QTest::qWait(200);

    KWindowInfo info3(widget.winId(), NET::WMState | NET::XAWMState);
    QVERIFY(!info3.isMinimized());
}

void KWindowSystemX11Test::testPlatformX11()
{
    QCOMPARE(KWindowSystem::platform(), KWindowSystem::Platform::X11);
    QCOMPARE(KWindowSystem::isPlatformX11(), true);
    QCOMPARE(KWindowSystem::isPlatformWayland(), false);
}

QTEST_MAIN(KWindowSystemX11Test)

#include "kwindowsystemx11test.moc"
