/****************************************************************************
**
** Copyright (C) 2017 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the test suite of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:GPL-EXCEPT$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "mockcompositor.h"

#include <QBackingStore>
#include <QPainter>
#include <QScreen>
#include <QWindow>
#include <QMimeData>
#include <QPixmap>
#include <QDrag>

#include <QtTest/QtTest>

static const QSize screenSize(1600, 1200);

class TestWindow : public QWindow
{
    Q_OBJECT
public:
    TestWindow()
    {
        setSurfaceType(QSurface::RasterSurface);
        setGeometry(0, 0, 32, 32);
        create();
    }

    bool event(QEvent *event) override
    {
        if (event->type() == QEvent::WindowStateChange)
            emit windowStateChangeEventReceived(static_cast<QWindowStateChangeEvent *>(event)->oldState());
        return QWindow::event(event);
    }

    void exposeEvent(QExposeEvent *event) override
    {
        ++exposeEventCount;
        QWindow::exposeEvent(event);
    }

    int exposeEventCount = 0;

signals:
    void windowStateChangeEventReceived(uint oldState);
};

class tst_WaylandClientXdgShellV6 : public QObject
{
    Q_OBJECT
public:
    tst_WaylandClientXdgShellV6(MockCompositor *c)
        : m_compositor(c)
    {
        qRegisterMetaType<Qt::WindowState>();
        QSocketNotifier *notifier = new QSocketNotifier(m_compositor->waylandFileDescriptor(), QSocketNotifier::Read, this);
        connect(notifier, &QSocketNotifier::activated, this, &tst_WaylandClientXdgShellV6::processWaylandEvents);
        // connect to the event dispatcher to make sure to flush out the outgoing message queue
        connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::awake, this, &tst_WaylandClientXdgShellV6::processWaylandEvents);
        connect(QCoreApplication::eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, &tst_WaylandClientXdgShellV6::processWaylandEvents);
    }

public slots:
    void processWaylandEvents()
    {
        m_compositor->processWaylandEvents();
    }

    void cleanup()
    {
        // make sure the surfaces from the last test are properly cleaned up
        // and don't show up as false positives in the next test
        QTRY_VERIFY(!m_compositor->surface());
        QTRY_VERIFY(!m_compositor->xdgToplevelV6());
    }

private slots:
    void createDestroyWindow();
    void configure();
    void showMinimized();
    void setMinimized();
    void unsetMaximized();
    void focusWindowFollowsConfigure();
    void windowStateChangedEvents();
    void windowGeometrySimple();
    void windowGeometryFixed();
    void flushUnconfiguredXdgSurface();
    void dontSpamExposeEvents();

private:
    MockCompositor *m_compositor = nullptr;
};

void tst_WaylandClientXdgShellV6::createDestroyWindow()
{
    TestWindow window;
    window.show();

    QTRY_VERIFY(m_compositor->surface());

    window.destroy();
    QTRY_VERIFY(!m_compositor->surface());
}

void tst_WaylandClientXdgShellV6::configure()
{
    QSharedPointer<MockOutput> output;
    QTRY_VERIFY(output = m_compositor->output());

    TestWindow window;
    window.show();

    QSharedPointer<MockSurface> surface;
    QTRY_VERIFY(surface = m_compositor->surface());

    m_compositor->processWaylandEvents();
    QTRY_VERIFY(window.isVisible());
    QTRY_VERIFY(!window.isExposed()); //Window should not be exposed before the first configure event

    //TODO: according to xdg-shell protocol, a buffer should not be attached to a the surface
    //until it's configured. Ensure this in the test!

    QSharedPointer<MockXdgToplevelV6> toplevel;
    QTRY_VERIFY(toplevel = m_compositor->xdgToplevelV6());

    const QSize newSize(123, 456);
    m_compositor->sendXdgToplevelV6Configure(toplevel, newSize);
    QTRY_VERIFY(window.isExposed());
    QTRY_COMPARE(window.visibility(), QWindow::Windowed);
    QTRY_COMPARE(window.windowStates(), Qt::WindowNoState);
    QTRY_COMPARE(window.frameGeometry(), QRect(QPoint(), newSize));

    m_compositor->sendXdgToplevelV6Configure(toplevel, screenSize, { ZXDG_TOPLEVEL_V6_STATE_ACTIVATED, ZXDG_TOPLEVEL_V6_STATE_MAXIMIZED });
    QTRY_COMPARE(window.visibility(), QWindow::Maximized);
    QTRY_COMPARE(window.windowStates(), Qt::WindowMaximized);
    QTRY_COMPARE(window.frameGeometry(), QRect(QPoint(), screenSize));

    m_compositor->sendXdgToplevelV6Configure(toplevel, screenSize, { ZXDG_TOPLEVEL_V6_STATE_ACTIVATED, ZXDG_TOPLEVEL_V6_STATE_FULLSCREEN });
    QTRY_COMPARE(window.visibility(), QWindow::FullScreen);
    QTRY_COMPARE(window.windowStates(), Qt::WindowFullScreen);
    QTRY_COMPARE(window.frameGeometry(), QRect(QPoint(), screenSize));

    //The window should remember it's original size
    m_compositor->sendXdgToplevelV6Configure(toplevel, QSize(0, 0), { ZXDG_TOPLEVEL_V6_STATE_ACTIVATED });
    QTRY_COMPARE(window.visibility(), QWindow::Windowed);
    QTRY_COMPARE(window.windowStates(), Qt::WindowNoState);
    QTRY_COMPARE(window.frameGeometry(), QRect(QPoint(), newSize));
}

void tst_WaylandClientXdgShellV6::showMinimized()
{
    // On xdg-shell v6 there's really no way for the compositor to tell the window if it's minimized
    // There are wl_surface.enter events and so on, but there's really no way to differentiate
    // between a window preview and an unminimized window.
    TestWindow window;
    window.showMinimized();
    QCOMPARE(window.windowStates(), Qt::WindowMinimized);   // should return minimized until
    QTRY_COMPARE(window.windowStates(), Qt::WindowNoState); // rejected by handleWindowStateChanged

    // Make sure the window on the compositor side is/was created here, and not after the test
    // finishes, as that may mess up for later tests.
    QTRY_VERIFY(m_compositor->xdgToplevelV6());
}

void tst_WaylandClientXdgShellV6::setMinimized()
{
    TestWindow window;
    window.show();

    QSharedPointer<MockXdgToplevelV6> toplevel;
    QTRY_VERIFY(toplevel = m_compositor->xdgToplevelV6());

    m_compositor->sendXdgToplevelV6Configure(toplevel);
    QTRY_COMPARE(window.visibility(), QWindow::Windowed);
    QTRY_COMPARE(window.windowStates(), Qt::WindowNoState);

    QSignalSpy setMinimizedSpy(toplevel.data(), SIGNAL(setMinimizedRequested()));
    QSignalSpy windowStateChangeSpy(&window, SIGNAL(windowStateChangeEventReceived(uint)));

    window.setVisibility(QWindow::Minimized);
    QCOMPARE(window.visibility(), QWindow::Minimized);
    QCOMPARE(window.windowStates(), Qt::WindowMinimized);
    QTRY_COMPARE(setMinimizedSpy.count(), 1);
    {
        QTRY_VERIFY(windowStateChangeSpy.count() > 0);
        Qt::WindowStates oldStates(windowStateChangeSpy.takeFirst().at(0).toUInt());
        QCOMPARE(oldStates, Qt::WindowNoState);
    }

    // In the meantime the compositor may minimize, do nothing or reshow the window without
    // telling us.

    QTRY_COMPARE(window.visibility(), QWindow::Windowed); // verify that we don't know anything
    QTRY_COMPARE(window.windowStates(), Qt::WindowNoState);
    {
        QTRY_COMPARE(windowStateChangeSpy.count(), 1);
        Qt::WindowStates oldStates(windowStateChangeSpy.takeFirst().at(0).toUInt());
        QCOMPARE(oldStates, Qt::WindowNoState); // because the window never was minimized
    }

    // Setting visibility again should send another set_minimized request
    window.setVisibility(QWindow::Minimized);
    QTRY_COMPARE(setMinimizedSpy.count(), 2);
}

void tst_WaylandClientXdgShellV6::unsetMaximized()
{
    TestWindow window;
    window.show();

    QSharedPointer<MockXdgToplevelV6> toplevel;
    QTRY_VERIFY(toplevel = m_compositor->xdgToplevelV6());

    QSignalSpy unsetMaximizedSpy(toplevel.data(), SIGNAL(unsetMaximizedRequested()));

    QSignalSpy windowStateChangedSpy(&window, SIGNAL(windowStateChanged(Qt::WindowState)));

    m_compositor->sendXdgToplevelV6Configure(toplevel, screenSize, { ZXDG_TOPLEVEL_V6_STATE_MAXIMIZED });

    QTRY_COMPARE(windowStateChangedSpy.count(), 1);
    QCOMPARE(windowStateChangedSpy.takeFirst().at(0).toUInt(), Qt::WindowMaximized);

    window.setWindowStates(Qt::WindowNoState);

    QTRY_COMPARE(unsetMaximizedSpy.count(), 1);
    QTRY_COMPARE(windowStateChangedSpy.count(), 1);
    QCOMPARE(windowStateChangedSpy.takeFirst().at(0).toUInt(), Qt::WindowNoState);

    m_compositor->sendXdgToplevelV6Configure(toplevel, QSize(0, 0), {});

    QTRY_COMPARE(windowStateChangedSpy.count(), 1);
    QCOMPARE(windowStateChangedSpy.takeFirst().at(0).toUInt(), Qt::WindowNoState);
}

void tst_WaylandClientXdgShellV6::focusWindowFollowsConfigure()
{
    TestWindow window;
    window.show();

    QSharedPointer<MockXdgToplevelV6> toplevel;
    QTRY_VERIFY(toplevel = m_compositor->xdgToplevelV6());
    QTRY_VERIFY(!window.isActive());

    QSignalSpy windowStateChangeSpy(&window, SIGNAL(windowStateChangeEventReceived(uint)));

    m_compositor->sendXdgToplevelV6Configure(toplevel, QSize(0, 0), { ZXDG_TOPLEVEL_V6_STATE_ACTIVATED });
    QTRY_VERIFY(window.isActive());

    m_compositor->sendXdgToplevelV6Configure(toplevel, QSize(0, 0), {});
    QTRY_VERIFY(!window.isActive());
}

void tst_WaylandClientXdgShellV6::windowStateChangedEvents()
{
    TestWindow window;
    window.show();

    QSharedPointer<MockXdgToplevelV6> toplevel;
    QTRY_VERIFY(toplevel = m_compositor->xdgToplevelV6());

    QSignalSpy eventSpy(&window, SIGNAL(windowStateChangeEventReceived(uint)));
    QSignalSpy signalSpy(&window, SIGNAL(windowStateChanged(Qt::WindowState)));

    m_compositor->sendXdgToplevelV6Configure(toplevel, screenSize, { ZXDG_TOPLEVEL_V6_STATE_MAXIMIZED });

    QTRY_COMPARE(window.windowStates(), Qt::WindowMaximized);
    QTRY_COMPARE(window.windowState(), Qt::WindowMaximized);
    {
        QTRY_COMPARE(eventSpy.count(), 1);
        Qt::WindowStates oldStates(eventSpy.takeFirst().at(0).toUInt());
        QCOMPARE(oldStates, Qt::WindowNoState);

        QTRY_COMPARE(signalSpy.count(), 1);
        uint newState = signalSpy.takeFirst().at(0).toUInt();
        QCOMPARE(newState, Qt::WindowMaximized);
    }

    m_compositor->sendXdgToplevelV6Configure(toplevel, screenSize, { ZXDG_TOPLEVEL_V6_STATE_FULLSCREEN });

    QTRY_COMPARE(window.windowStates(), Qt::WindowFullScreen);
    QTRY_COMPARE(window.windowState(), Qt::WindowFullScreen);
    {
        QTRY_COMPARE(eventSpy.count(), 1);
        Qt::WindowStates oldStates(eventSpy.takeFirst().at(0).toUInt());
        QCOMPARE(oldStates, Qt::WindowMaximized);

        QTRY_COMPARE(signalSpy.count(), 1);
        uint newState = signalSpy.takeFirst().at(0).toUInt();
        QCOMPARE(newState, Qt::WindowFullScreen);
    }

    m_compositor->sendXdgToplevelV6Configure(toplevel, QSize(0, 0), {});

    QTRY_COMPARE(window.windowStates(), Qt::WindowNoState);
    QTRY_COMPARE(window.windowState(), Qt::WindowNoState);
    {
        QTRY_COMPARE(eventSpy.count(), 1);
        Qt::WindowStates oldStates(eventSpy.takeFirst().at(0).toUInt());
        QCOMPARE(oldStates, Qt::WindowFullScreen);

        QTRY_COMPARE(signalSpy.count(), 1);
        uint newState = signalSpy.takeFirst().at(0).toUInt();
        QCOMPARE(newState, Qt::WindowNoState);
    }
}

void tst_WaylandClientXdgShellV6::windowGeometrySimple()
{
    QSKIP("TODO: This test is flaky, figure out why.");
    QWindow window;
    window.show();

    QSharedPointer<MockXdgToplevelV6> toplevel;
    QTRY_VERIFY(toplevel = m_compositor->xdgToplevelV6());
    QSignalSpy geometrySpy(toplevel.data(), SIGNAL(windowGeometryRequested(QRect)));

    m_compositor->sendXdgToplevelV6Configure(toplevel);
    QTRY_COMPARE(geometrySpy.count(), 1);
    QCOMPARE(geometrySpy.takeFirst().at(0).toRect().size(), window.frameGeometry().size());

    m_compositor->sendXdgToplevelV6Configure(toplevel, QSize(123, 456));
    QTRY_COMPARE(geometrySpy.count(), 1);
    QCOMPARE(geometrySpy.takeFirst().at(0).toRect().size(), QSize(123, 456));
}

void tst_WaylandClientXdgShellV6::windowGeometryFixed()
{
    QSKIP("TODO: This test is flaky, figure out why.");
    QWindow window;
    window.resize(QSize(1337, 137));
    window.setMaximumSize(window.size());
    window.setMinimumSize(window.size());
    window.show();

    QSharedPointer<MockXdgToplevelV6> toplevel;
    QTRY_VERIFY(toplevel = m_compositor->xdgToplevelV6());
    QSignalSpy geometrySpy(toplevel.data(), SIGNAL(windowGeometryRequested(QRect)));

    m_compositor->sendXdgToplevelV6Configure(toplevel);
    QTRY_COMPARE(geometrySpy.count(), 1);
    QRect initialWindowGeometry = geometrySpy.takeFirst().at(0).toRect();
    QCOMPARE(initialWindowGeometry.size(), window.frameGeometry().size());

    m_compositor->sendXdgToplevelV6Configure(toplevel, QSize(123, 456));
    QTRY_COMPARE(geometrySpy.count(), 1);
    // Configuring the window should not change the window geometry
    QCOMPARE(geometrySpy.takeFirst().at(0).toRect().size(), initialWindowGeometry.size());
}

void tst_WaylandClientXdgShellV6::flushUnconfiguredXdgSurface()
{
    TestWindow window;
    window.show();

    QSharedPointer<MockSurface> surface;
    QTRY_VERIFY(surface = m_compositor->surface());

    // Paint and flush some magenta
    QBackingStore backingStore(&window);
    QRect rect(QPoint(), window.size());
    backingStore.resize(rect.size());
    backingStore.beginPaint(rect);
    QColor color = Qt::magenta;
    QPainter p(backingStore.paintDevice());
    p.fillRect(rect, color);
    p.end();
    backingStore.endPaint();
    backingStore.flush(rect);

    // We're not allowed to send buffer on this surface since it isn't yet configured.
    // So, from the compositor's point of view there should be no buffer data yet.
    m_compositor->processWaylandEvents();
    QVERIFY(surface->image.isNull());
    QVERIFY(!window.isExposed());

    // Finally sending the configure should trigger an attach and commit with the
    // right buffer.
    m_compositor->sendShellSurfaceConfigure(surface);
    QTRY_COMPARE(surface->image.size(), window.frameGeometry().size());
    QTRY_COMPARE(surface->image.pixel(window.frameMargins().left(), window.frameMargins().top()), color.rgba());
    QVERIFY(window.isExposed());
}

void tst_WaylandClientXdgShellV6::dontSpamExposeEvents()
{
    TestWindow window;
    window.show();

    QSharedPointer<MockSurface> surface;
    QTRY_VERIFY(surface = m_compositor->surface());
    QTRY_COMPARE(window.exposeEventCount, 0);

    m_compositor->sendShellSurfaceConfigure(surface);
    QTRY_VERIFY(window.isExposed());
    QTRY_COMPARE(window.exposeEventCount, 1);
}

int main(int argc, char **argv)
{
    QTemporaryDir tmpRuntimeDir;
    setenv("XDG_RUNTIME_DIR", tmpRuntimeDir.path().toLocal8Bit(), 1);
    setenv("QT_QPA_PLATFORM", "wayland", 1); // force QGuiApplication to use wayland plugin
    setenv("QT_WAYLAND_SHELL_INTEGRATION", "xdg-shell-v6", 1);

    // wayland-egl hangs in the test setup when we try to initialize. Until it gets
    // figured out, avoid clientBufferIntegration() from being called in
    // QWaylandWindow::createDecorations().
    setenv("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1", 1);

    MockCompositor compositor;
    compositor.setOutputMode(screenSize);

    QGuiApplication app(argc, argv);
    compositor.applicationInitialized();

    tst_WaylandClientXdgShellV6 tc(&compositor);
    return QTest::qExec(&tc, argc, argv);
}

#include <tst_xdgshellv6.moc>
