// Copyright (C) 2019 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "mockcompositor.h"
#include <QtGui/QScreen>
#include <QtGui/QRasterWindow>

using namespace MockCompositor;

class tst_output : public QObject, private DefaultCompositor
{
    Q_OBJECT
private slots:
    void initTestCase()
    {
        m_config.autoConfigure = true;
        m_config.autoEnter = false;
    }
    void cleanup()
    {
        QCOMPOSITOR_COMPARE(getAll<Output>().size(), 1); // Only the default output should be left
        QTRY_COMPARE(QGuiApplication::screens().size(), 1);
        QTRY_VERIFY2(isClean(), qPrintable(dirtyMessage()));
    }
    void primaryScreen();
    void secondaryHiDpiScreen();
    void addScreenWithGeometryChange();
    void windowScreens();
    void removePrimaryScreen();
    void screenOrder();
    void removeAllScreens();
};

void tst_output::primaryScreen()
{
    // Verify that the client has bound to the output global
    QCOMPOSITOR_TRY_COMPARE(output()->resourceMap().size(), 1);
    QTRY_VERIFY(QGuiApplication::primaryScreen());
    QScreen *screen = QGuiApplication::primaryScreen();
    QCOMPARE(screen->manufacturer(), "Make");
    QCOMPARE(screen->model(), "Model");
    QCOMPARE(screen->size(), QSize(1920, 1080));
    QCOMPARE(screen->refreshRate(), 60);
    QCOMPARE(qRound(screen->physicalDotsPerInch()), 96 / screen->devicePixelRatio());
    QCOMPARE(screen->devicePixelRatio(), 1);
    QCOMPARE(screen->logicalDotsPerInch(), 96);
}

void tst_output::secondaryHiDpiScreen()
{
    exec([&] {
        OutputData d;
        d.position = {1920, 0}; // in global compositor space (not pixels)
        d.mode.resolution = {800, 640};
        d.physicalSize = d.mode.physicalSizeForDpi(200);
        d.scale = 2;
        add<Output>(d);
    });

    // Verify that the client has bound to the output global
    QCOMPOSITOR_TRY_VERIFY(output(1) && output(1)->resourceMap().size() == 1);

    QTRY_COMPARE(QGuiApplication::screens().size(), 2);
    QScreen *screen = QGuiApplication::screens()[1];
    QCOMPARE(screen->refreshRate(), 60);
    QCOMPARE(screen->devicePixelRatio(), 2);
    QCOMPARE(screen->logicalDotsPerInch(), 96);

    // Dots currently means device pixels, not actual pixels (see QTBUG-62649)
    QCOMPARE(qRound(screen->physicalDotsPerInch() * screen->devicePixelRatio()), 200);

    // Size is in logical pixel coordinates
    QCOMPARE(screen->size(), QSize(800, 640) / 2);
    QCOMPARE(screen->geometry(), QRect(QPoint(1920, 0), QSize(400, 320)));
    QCOMPARE(screen->virtualGeometry(), QRect(QPoint(0, 0), QSize(1920 + 800 / 2, 1080)));

    exec([&] { remove(output(1)); });
}

// QTBUG-62044
void tst_output::addScreenWithGeometryChange()
{
    const QPoint initialPosition = exec([&] { return output(0)->m_data.position; });

    exec([&] {
        auto *oldOutput = output(0);
        auto *newOutput = add<Output>();
        newOutput->m_data.mode.resolution = {1280, 720};
        // Move the primary output to the right
        QPoint newPosition(newOutput->m_data.mode.resolution.width(), 0);
        Q_ASSERT(newPosition != initialPosition);
        oldOutput->m_data.position = newPosition;
        oldOutput->sendGeometry();
        oldOutput->sendDone();
    });

    QTRY_COMPARE(QGuiApplication::screens().size(), 2);
    QTRY_COMPARE(QGuiApplication::primaryScreen()->geometry(), QRect(QPoint(1280, 0), QSize(1920, 1080)));

    // Remove the extra output and move the old one back
    exec([&] {
        remove(output(1));
        output()->m_data.position = initialPosition;
        output()->sendGeometry();
        output()->sendDone();
    });
    QTRY_COMPARE(QGuiApplication::screens().size(), 1);
    QTRY_COMPARE(QGuiApplication::primaryScreen()->geometry(), QRect(QPoint(0, 0), QSize(1920, 1080)));
}

void tst_output::windowScreens()
{
    QRasterWindow window;
    window.resize(400, 320);
    window.show();
    QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);

    QTRY_COMPARE(QGuiApplication::screens().size(), 1);
    QScreen *primaryScreen = QGuiApplication::screens().first();
    QCOMPARE(window.screen(), primaryScreen);

    exec([&] { add<Output>(); });

    QTRY_COMPARE(QGuiApplication::screens().size(), 2);
    QScreen *secondaryScreen = QGuiApplication::screens().at(1);
    QVERIFY(secondaryScreen);

    window.setScreen(secondaryScreen);
    QCOMPARE(window.screen(), secondaryScreen);

    exec([&] {
        xdgToplevel()->surface()->sendEnter(output(0));
        xdgToplevel()->surface()->sendEnter(output(1));
    });

    QTRY_COMPARE(window.screen(), primaryScreen);

    exec([&] {
        xdgToplevel()->surface()->sendLeave(output(0));
    });
    QTRY_COMPARE(window.screen(), secondaryScreen);

    exec([&] {
        remove(output(1));
    });
    QTRY_COMPARE(QGuiApplication::screens().size(), 1);
    QCOMPARE(window.screen(), primaryScreen);
}

void tst_output::removePrimaryScreen()
{
    QRasterWindow window;
    window.resize(400, 320);
    window.show();
    QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial);

    QTRY_COMPARE(QGuiApplication::screens().size(), 1);
    QScreen *primaryScreen = QGuiApplication::screens().first();
    QCOMPARE(window.screen(), primaryScreen);

    // Add a clone of the primary output
    exec([&] { add<Output>(output()->m_data); });

    QTRY_COMPARE(QGuiApplication::screens().size(), 2);
    QTRY_COMPARE(QGuiApplication::primaryScreen()->virtualSiblings().size(), 2);
    QScreen *secondaryScreen = QGuiApplication::screens().at(1);
    QVERIFY(secondaryScreen);

    exec([&] { remove(output()); });
    QTRY_COMPARE(QGuiApplication::screens().size(), 1);

    exec([&] {
        auto *surface = xdgToplevel()->surface();
        pointer()->sendEnter(surface, {32, 32});
        pointer()->sendFrame(client());
        pointer()->sendButton(client(), BTN_LEFT, 1);
        pointer()->sendFrame(client());
        pointer()->sendButton(client(), BTN_LEFT, 0);
        pointer()->sendFrame(client());
    });

    // Wait to make sure mouse events dont't cause a crash now that the screen has changed
    xdgPingAndWaitForPong();
}

// QTBUG-72828
void tst_output::screenOrder()
{
    exec([&] {
        add<Output>()->m_data.model = "Screen 1";
        add<Output>()->m_data.model = "Screen 2";
    });

    QTRY_COMPARE(QGuiApplication::screens().size(), 3);
    const auto screens = QGuiApplication::screens();

    QCOMPARE(screens[1]->model(), "Screen 1");
    QCOMPARE(screens[2]->model(), "Screen 2");

    exec([&] {
        remove(output(2));
        remove(output(1));
    });
}

// This is different from tst_nooutput::noScreens because here we have a screen at platform
// integration initialization, which we then remove.
void tst_output::removeAllScreens()
{
    QRasterWindow window1;
    window1.resize(400, 320);
    window1.show();
    QCOMPOSITOR_TRY_VERIFY(xdgSurface(0) && xdgSurface(0)->m_committedConfigureSerial);

    const QString wlOutputPrimaryScreenModel = QGuiApplication::primaryScreen()->model();

    // Get screen info so we can restore it after
    auto screenInfo = exec([&] { return output()->m_data; });
    exec([&] { remove(output()); });

    // Make sure the wl_output is actually removed before we continue
    QTRY_VERIFY(!QGuiApplication::primaryScreen() || QGuiApplication::primaryScreen()->model() != wlOutputPrimaryScreenModel);

    // Adding a window while there are no screens should also work
    QRasterWindow window2;
    window2.resize(400, 320);
    window2.show();

    exec([&] { add<Output>(screenInfo); });

    // Things should be back to normal
    QTRY_VERIFY(QGuiApplication::primaryScreen());
    QTRY_COMPARE(QGuiApplication::primaryScreen()->model(), wlOutputPrimaryScreenModel);

    // Test that we don't leave any fake screens around after we get a wl_output back.
    QTRY_COMPARE(QGuiApplication::screens().size(), 1);

    // Qt may choose to recreate/hide windows in response to changing screens, so give the client
    // some time to potentially mess up before we verify that the windows are visible.
    xdgPingAndWaitForPong();

    // Windows should be visible after we've reconnected the screen
    QCOMPOSITOR_TRY_VERIFY(xdgToplevel(0) && xdgToplevel(0)->m_xdgSurface->m_committedConfigureSerial);
    QCOMPOSITOR_TRY_VERIFY(xdgToplevel(1) && xdgToplevel(1)->m_xdgSurface->m_committedConfigureSerial);

}

QCOMPOSITOR_TEST_MAIN(tst_output)
#include "tst_output.moc"
