/*
    SPDX-FileCopyrightText: 2016 Oleg Chernovskiy <kanedias@xaker.ru>
    SPDX-FileCopyrightText: 2018 Roman Gilg <subdiff@gmail.com>

    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
*/
// Qt
#include <QSignalSpy>
#include <QTemporaryFile>
#include <QTest>
#include <QTimer>
// client
#include "../../src/client/connection_thread.h"
#include "../../src/client/event_queue.h"
#include "../../src/client/output.h"
#include "../../src/client/registry.h"
#include "../../src/client/remote_access.h"
// server
#include "../../src/server/display.h"
#include "../../src/server/output_interface.h"
#include "../../src/server/remote_access_interface.h"

using namespace KWayland::Client;
using namespace KWayland::Server;

Q_DECLARE_METATYPE(const BufferHandle *)
Q_DECLARE_METATYPE(const RemoteBuffer *)
Q_DECLARE_METATYPE(const void *)

class RemoteAccessTest : public QObject
{
    Q_OBJECT
private Q_SLOTS:
    void init();
    void cleanup();

    void testSendReleaseSingle();
    void testSendReleaseMultiple();
    void testSendReleaseCrossScreen();
    void testSendClientGone();
    void testSendReceiveClientGone();

private:
    Display *m_display = nullptr;
    OutputInterface *m_outputInterface[2] = {nullptr};
    RemoteAccessManagerInterface *m_remoteAccessInterface = nullptr;
};

class MockupClient : public QObject
{
    Q_OBJECT
public:
    MockupClient(QObject *parent = nullptr);
    ~MockupClient() override;

    void bindOutput(int index);

    ConnectionThread *connection = nullptr;
    QThread *thread = nullptr;
    EventQueue *queue = nullptr;
    Registry *registry = nullptr;
    RemoteAccessManager *remoteAccess = nullptr;
    Output *outputs[2] = {nullptr};
};

static const QString s_socketName = QStringLiteral("kwayland-test-remote-access-0");

MockupClient::MockupClient(QObject *parent)
    : QObject(parent)
{
    // setup connection
    connection = new KWayland::Client::ConnectionThread;
    QSignalSpy connectedSpy(connection, &ConnectionThread::connected);
    QVERIFY(connectedSpy.isValid());
    connection->setSocketName(s_socketName);

    thread = new QThread(this);
    connection->moveToThread(thread);
    thread->start();

    connection->initConnection();
    QVERIFY(connectedSpy.wait());

    queue = new EventQueue(this);
    queue->setup(connection);

    registry = new Registry(this);
    QSignalSpy interfacesAnnouncedSpy(registry, &Registry::interfacesAnnounced);
    QVERIFY(interfacesAnnouncedSpy.isValid());
    registry->setEventQueue(queue);
    registry->create(connection);
    QVERIFY(registry->isValid());
    registry->setup();
    QVERIFY(interfacesAnnouncedSpy.wait());

    remoteAccess = registry->createRemoteAccessManager(registry->interface(Registry::Interface::RemoteAccessManager).name,
                                                       registry->interface(Registry::Interface::RemoteAccessManager).version,
                                                       this);
    QVERIFY(remoteAccess->isValid());
    connection->flush();
}

MockupClient::~MockupClient()
{
#define CLEANUP(variable)                                                                                                                                      \
    if (variable) {                                                                                                                                            \
        delete variable;                                                                                                                                       \
        variable = nullptr;                                                                                                                                    \
    }
    CLEANUP(outputs[0])
    CLEANUP(outputs[1])
    CLEANUP(remoteAccess)
    CLEANUP(queue)
    CLEANUP(registry)
    if (thread) {
        if (connection) {
            connection->flush();
            connection->deleteLater();
            connection = nullptr;
        }

        thread->quit();
        thread->wait();
        delete thread;
        thread = nullptr;
    }
#undef CLEANUP
}

void MockupClient::bindOutput(int index)
{
    // client-bound output
    outputs[index] = registry->createOutput(registry->interfaces(Registry::Interface::Output)[index].name,
                                            registry->interfaces(Registry::Interface::Output)[index].version,
                                            this);
    QVERIFY(outputs[index]->isValid());
    connection->flush();
}

void RemoteAccessTest::init()
{
    qRegisterMetaType<const BufferHandle *>();
    qRegisterMetaType<const RemoteBuffer *>();
    qRegisterMetaType<const void *>();

    delete m_display;
    m_display = new Display(this);
    m_display->setSocketName(s_socketName);
    m_display->start();
    QVERIFY(m_display->isRunning());
    m_display->createShm();

    auto initOutputIface = [this](int i) {
        m_outputInterface[i] = m_display->createOutput();
        m_outputInterface[i]->create();
    };
    initOutputIface(0);
    initOutputIface(1);

    m_remoteAccessInterface = m_display->createRemoteAccessManager();
    m_remoteAccessInterface->create();
    QSignalSpy bufferReleasedSpy(m_remoteAccessInterface, &RemoteAccessManagerInterface::bufferReleased);
    QVERIFY(bufferReleasedSpy.isValid());
}

void RemoteAccessTest::cleanup()
{
#define CLEANUP(variable)                                                                                                                                      \
    if (variable) {                                                                                                                                            \
        delete variable;                                                                                                                                       \
        variable = nullptr;                                                                                                                                    \
    }
    CLEANUP(m_outputInterface[0])
    CLEANUP(m_outputInterface[1])
    CLEANUP(m_remoteAccessInterface)
    CLEANUP(m_display)
#undef CLEANUP
}

void RemoteAccessTest::testSendReleaseSingle()
{
    // this test verifies that a buffer is sent to client and returned back

    // setup
    QVERIFY(!m_remoteAccessInterface->isBound());
    auto *client = new MockupClient(this);
    client->bindOutput(0);

    m_display->dispatchEvents();

    QVERIFY(m_remoteAccessInterface->isBound()); // we have one client now
    QSignalSpy bufferReadySpy(client->remoteAccess, &RemoteAccessManager::bufferReady);
    QVERIFY(bufferReadySpy.isValid());

    BufferHandle *buf = new BufferHandle();
    QTemporaryFile *tmpFile = new QTemporaryFile(this);
    tmpFile->open();

    buf->setFd(tmpFile->handle());
    buf->setSize(50, 50);
    buf->setFormat(100500);
    buf->setStride(7800);
    m_remoteAccessInterface->sendBufferReady(m_outputInterface[0], buf);

    // receive buffer
    QVERIFY(bufferReadySpy.wait());
    auto rbuf = bufferReadySpy.takeFirst()[1].value<const RemoteBuffer *>();
    QSignalSpy paramsObtainedSpy(rbuf, &RemoteBuffer::parametersObtained);
    QVERIFY(paramsObtainedSpy.isValid());

    // wait for params
    QVERIFY(paramsObtainedSpy.wait());
    // client fd is different, not subject to check
    QCOMPARE(rbuf->width(), 50u);
    QCOMPARE(rbuf->height(), 50u);
    QCOMPARE(rbuf->format(), 100500u);
    QCOMPARE(rbuf->stride(), 7800u);

    // release
    QSignalSpy bufferReleasedSpy(m_remoteAccessInterface, &RemoteAccessManagerInterface::bufferReleased);
    QVERIFY(bufferReleasedSpy.isValid());
    delete rbuf;
    QVERIFY(bufferReleasedSpy.wait());

    // cleanup
    delete buf;
    delete client;
    m_display->dispatchEvents();
    QVERIFY(!m_remoteAccessInterface->isBound());
}

void RemoteAccessTest::testSendReleaseMultiple()
{
    // this test verifies that a buffer is sent to 2 clients and returned back

    // setup
    QVERIFY(!m_remoteAccessInterface->isBound());
    auto *client1 = new MockupClient(this);
    auto *client2 = new MockupClient(this);
    client1->bindOutput(0);
    client2->bindOutput(0);
    m_display->dispatchEvents();
    QVERIFY(m_remoteAccessInterface->isBound()); // now we have 2 clients

    BufferHandle *buf = new BufferHandle();
    QTemporaryFile *tmpFile = new QTemporaryFile(this);
    tmpFile->open();

    buf->setFd(tmpFile->handle());
    buf->setSize(50, 50);
    buf->setFormat(100500);
    buf->setStride(7800);

    bool buffer1ParametersReceived = false;
    bool buffer2ParametersReceived = false;

    const RemoteBuffer *rbuf1 = nullptr;
    const RemoteBuffer *rbuf2 = nullptr;

    client1->connection->flush();
    client2->connection->flush();

    {
        QEventLoop e;
        QTimer::singleShot(1000, &e, &QEventLoop::quit); // safety timer
        connect(client1->remoteAccess, &RemoteAccessManager::bufferReady, [&](const void *, const RemoteBuffer *buffer) {
            rbuf1 = buffer;
            connect(rbuf1, &RemoteBuffer::parametersObtained, [&]() {
                buffer1ParametersReceived = true;
                if (buffer1ParametersReceived && buffer2ParametersReceived) {
                    e.quit();
                }
            });
        });

        connect(client2->remoteAccess, &RemoteAccessManager::bufferReady, [&](const void *, const RemoteBuffer *buffer) {
            rbuf2 = buffer;
            connect(rbuf2, &RemoteBuffer::parametersObtained, [&]() {
                buffer2ParametersReceived = true;
                if (buffer1ParametersReceived && buffer2ParametersReceived) {
                    e.quit();
                }
            });
        });
        m_remoteAccessInterface->sendBufferReady(m_outputInterface[0], buf);

        e.exec();
    }

    QVERIFY(rbuf1);
    QVERIFY(rbuf2);
    QVERIFY(buffer1ParametersReceived);
    QVERIFY(buffer2ParametersReceived);

    // release
    QSignalSpy bufferReleasedSpy(m_remoteAccessInterface, &RemoteAccessManagerInterface::bufferReleased);
    delete rbuf1;
    QVERIFY(!bufferReleasedSpy.wait(1000)); // one client released, second still holds buffer!

    delete rbuf2;
    QVERIFY(bufferReleasedSpy.wait()); // all clients released, buffer should be freed

    // cleanup
    delete buf;
    delete client1;
    delete client2;
    m_display->dispatchEvents();
    QVERIFY(!m_remoteAccessInterface->isBound());
}

void RemoteAccessTest::testSendReleaseCrossScreen()
{
    // This test fails on CI occasionally
    // It is also for a protocol that is now completely unused,
    // So it is not worth investing time in
    QSKIP("Known to be racey");

    // this test verifies that multiple buffers for multiple screens are sent to
    // multiple clients and returned back

    // setup
    QVERIFY(!m_remoteAccessInterface->isBound());
    auto *client1 = new MockupClient(this);
    auto *client2 = new MockupClient(this);
    client1->bindOutput(1);
    client2->bindOutput(0);
    m_display->dispatchEvents();
    QVERIFY(m_remoteAccessInterface->isBound()); // now we have 2 clients

    QSignalSpy bufferReadySpy1(client1->remoteAccess, &RemoteAccessManager::bufferReady);
    QVERIFY(bufferReadySpy1.isValid());
    QSignalSpy bufferReadySpy2(client2->remoteAccess, &RemoteAccessManager::bufferReady);
    QVERIFY(bufferReadySpy2.isValid());

    BufferHandle *buf1 = new BufferHandle();
    QTemporaryFile *tmpFile1 = new QTemporaryFile(this);
    tmpFile1->open();

    BufferHandle *buf2 = new BufferHandle();
    QTemporaryFile *tmpFile2 = new QTemporaryFile(this);
    tmpFile2->open();

    buf1->setFd(tmpFile1->handle());
    buf1->setSize(50, 50);
    buf1->setFormat(100500);
    buf1->setStride(7800);

    buf2->setFd(tmpFile2->handle());
    buf2->setSize(100, 100);
    buf2->setFormat(100500);
    buf2->setStride(7800);

    m_remoteAccessInterface->sendBufferReady(m_outputInterface[0], buf1);
    m_remoteAccessInterface->sendBufferReady(m_outputInterface[1], buf2);

    // wait for event loop
    QVERIFY(bufferReadySpy1.wait());
    if (bufferReadySpy2.size() == 0) {
        QVERIFY(bufferReadySpy2.wait());
    }

    // receive buffer at client 1
    QCOMPARE(bufferReadySpy1.size(), 1);
    auto rbuf1 = bufferReadySpy1.takeFirst()[1].value<const RemoteBuffer *>();
    QSignalSpy paramsObtainedSpy1(rbuf1, &RemoteBuffer::parametersObtained);
    QVERIFY(paramsObtainedSpy1.isValid());

    // receive buffer at client 2
    QCOMPARE(bufferReadySpy2.size(), 1);
    auto rbuf2 = bufferReadySpy2.takeFirst()[1].value<const RemoteBuffer *>();
    QSignalSpy paramsObtainedSpy2(rbuf2, &RemoteBuffer::parametersObtained);
    QVERIFY(paramsObtainedSpy2.isValid());

    // wait for event loop
    if (paramsObtainedSpy1.isEmpty()) {
        QVERIFY(paramsObtainedSpy1.wait());
    }
    if (paramsObtainedSpy2.isEmpty()) {
        QVERIFY(paramsObtainedSpy2.wait());
    }
    QCOMPARE(paramsObtainedSpy1.size(), 1);
    QCOMPARE(paramsObtainedSpy2.size(), 1);

    // release
    QSignalSpy bufferReleasedSpy(m_remoteAccessInterface, &RemoteAccessManagerInterface::bufferReleased);
    QVERIFY(bufferReleasedSpy.isValid());
    delete rbuf1;
    QVERIFY(bufferReleasedSpy.wait());

    delete rbuf2;
    QVERIFY(bufferReleasedSpy.wait());

    QCOMPARE(bufferReleasedSpy.size(), 2);

    // cleanup
    delete buf1;
    delete buf2;
    delete client1;
    delete client2;
    m_display->dispatchEvents();
    QVERIFY(!m_remoteAccessInterface->isBound());
}

void RemoteAccessTest::testSendClientGone()
{
    // this test verifies that when buffer is sent and client is gone, server will release buffer correctly
    QVERIFY(!m_remoteAccessInterface->isBound());
    auto *client = new MockupClient(this);
    client->bindOutput(0);
    m_display->dispatchEvents();

    QVERIFY(m_remoteAccessInterface->isBound()); // we have one client now
    QSignalSpy bufferReadySpy(client->remoteAccess, &RemoteAccessManager::bufferReady);
    QVERIFY(bufferReadySpy.isValid());

    BufferHandle *buf = new BufferHandle();
    QTemporaryFile *tmpFile = new QTemporaryFile(this);
    tmpFile->open();

    buf->setFd(tmpFile->handle());
    buf->setSize(50, 50);
    buf->setFormat(100500);
    buf->setStride(7800);
    m_remoteAccessInterface->sendBufferReady(m_outputInterface[0], buf);

    // release forcefully
    QSignalSpy bufferReleasedSpy(m_remoteAccessInterface, &RemoteAccessManagerInterface::bufferReleased);
    QVERIFY(bufferReleasedSpy.isValid());
    delete client;
    QVERIFY(bufferReleasedSpy.wait());

    // cleanup
    delete buf;
    m_display->dispatchEvents();
    QVERIFY(!m_remoteAccessInterface->isBound());
}

void RemoteAccessTest::testSendReceiveClientGone()
{
    // this test verifies that when buffer is sent, received and client is gone,
    // both client and server will release buffer correctly
    QVERIFY(!m_remoteAccessInterface->isBound());
    auto *client = new MockupClient(this);
    client->bindOutput(0);
    m_display->dispatchEvents();

    QVERIFY(m_remoteAccessInterface->isBound()); // we have one client now
    QSignalSpy bufferReadySpy(client->remoteAccess, &RemoteAccessManager::bufferReady);
    QVERIFY(bufferReadySpy.isValid());

    BufferHandle *buf = new BufferHandle();
    QTemporaryFile *tmpFile = new QTemporaryFile(this);
    tmpFile->open();

    buf->setFd(tmpFile->handle());
    buf->setSize(50, 50);
    buf->setFormat(100500);
    buf->setStride(7800);
    m_remoteAccessInterface->sendBufferReady(m_outputInterface[0], buf);

    // receive buffer
    QVERIFY(bufferReadySpy.wait());
    auto rbuf = bufferReadySpy.takeFirst()[1].value<const RemoteBuffer *>();
    QSignalSpy paramsObtainedSpy(rbuf, &RemoteBuffer::parametersObtained);
    QVERIFY(paramsObtainedSpy.isValid());

    // wait for params
    QVERIFY(paramsObtainedSpy.wait());
    // client fd is different, not subject to check
    QCOMPARE(rbuf->width(), 50u);
    QCOMPARE(rbuf->height(), 50u);
    QCOMPARE(rbuf->format(), 100500u);
    QCOMPARE(rbuf->stride(), 7800u);

    // release forcefully
    QSignalSpy bufferReleasedSpy(m_remoteAccessInterface, &RemoteAccessManagerInterface::bufferReleased);
    QVERIFY(bufferReleasedSpy.isValid());
    delete client;
    QVERIFY(bufferReleasedSpy.wait());

    // cleanup
    delete buf;
    m_display->dispatchEvents();
    QVERIFY(!m_remoteAccessInterface->isBound());
}

QTEST_GUILESS_MAIN(RemoteAccessTest)
#include "test_remote_access.moc"
