#include <QDBusConnection>
#include <QDBusError>
#include <QDesktopServices>
#include <QGuiApplication>
#include <QProcess>
#include <QSignalSpy>
#include <QStandardPaths>
#include <QtTest>

#include "ukui/app-launcher.h"

using namespace UkuiQuick;

namespace {

constexpr auto LauncherService = "com.kylin.ProcessManager";
constexpr auto LauncherPath = "/com/kylin/ProcessManager/AppLauncher";

class ProcessManagerReceiver : public QObject
{
    Q_OBJECT
    Q_CLASSINFO("D-Bus Interface", "com.kylin.ProcessManager.AppLauncher")

public slots:
    void LaunchApp(const QString &desktopFile)
    {
        Q_EMIT launchAppCalled(desktopFile);
    }

    void LaunchAppWithArguments(const QString &desktopFile, const QStringList &args)
    {
        Q_EMIT launchAppWithArgumentsCalled(desktopFile, args);
    }

    void RunCommand(const QString &cmd)
    {
        Q_EMIT runCommandCalled(cmd);
    }

signals:
    void launchAppCalled(const QString &desktopFile);
    void launchAppWithArgumentsCalled(const QString &desktopFile, const QStringList &args);
    void runCommandCalled(const QString &cmd);
};

class UrlHandlerReceiver : public QObject
{
    Q_OBJECT

public slots:
    void handle(const QUrl &url)
    {
        Q_EMIT handled(url);
    }

signals:
    void handled(const QUrl &url);
};

class RegisteredServiceGuard
{
public:
    RegisteredServiceGuard(QDBusConnection &bus, const QString &service, const QString &path)
        : m_bus(bus)
        , m_service(service)
        , m_path(path)
    {
    }

    ~RegisteredServiceGuard()
    {
        m_bus.unregisterObject(m_path);
        m_bus.unregisterService(m_service);
    }

private:
    QDBusConnection &m_bus;
    QString m_service;
    QString m_path;
};

class PrivateSessionBus
{
public:
    PrivateSessionBus()
        : m_previousAddress(qgetenv("DBUS_SESSION_BUS_ADDRESS"))
    {
        const QString dbusDaemon = QStandardPaths::findExecutable(QStringLiteral("dbus-daemon"));
        if (dbusDaemon.isEmpty()) {
            m_error = QStringLiteral("dbus-daemon is not available");
            return;
        }

        m_daemon.start(dbusDaemon, {QStringLiteral("--session"),
                                    QStringLiteral("--print-address=1"),
                                    QStringLiteral("--nofork")});
        if (!m_daemon.waitForStarted(3000)) {
            m_error = QStringLiteral("failed to start dbus-daemon");
            return;
        }
        if (!m_daemon.waitForReadyRead(3000)) {
            m_error = QStringLiteral("dbus-daemon did not print an address");
            return;
        }

        const QByteArray address = m_daemon.readLine().trimmed();
        if (address.isEmpty()) {
            m_error = QStringLiteral("dbus-daemon printed an empty address");
            return;
        }

        qputenv("DBUS_SESSION_BUS_ADDRESS", address);
        m_ready = true;
    }

    ~PrivateSessionBus()
    {
        if (m_ready) {
            const QString connectionName = QDBusConnection::sessionBus().name();
            QDBusConnection::disconnectFromBus(connectionName);
        }

        if (m_previousAddress.isEmpty()) {
            qunsetenv("DBUS_SESSION_BUS_ADDRESS");
        } else {
            qputenv("DBUS_SESSION_BUS_ADDRESS", m_previousAddress);
        }

        if (m_daemon.state() != QProcess::NotRunning) {
            m_daemon.terminate();
            if (!m_daemon.waitForFinished(3000)) {
                m_daemon.kill();
                m_daemon.waitForFinished(3000);
            }
        }
    }

    bool isReady() const
    {
        return m_ready;
    }

    QString errorString() const
    {
        return m_error;
    }

private:
    bool m_ready = false;
    QByteArray m_previousAddress;
    QString m_error;
    QProcess m_daemon;
};

} // namespace

class app_launcher_test : public QObject
{
    Q_OBJECT

private slots:
    void cleanupTestCase();
    void singletonReturnsSharedInstance();
    void dbusMethodsAreForwardedToProcessManager();
    void emptyInputsAreIgnoredAndOpenUriUsesDesktopHandler();
};

void app_launcher_test::cleanupTestCase()
{
    delete AppLauncher::instance();
}

void app_launcher_test::singletonReturnsSharedInstance()
{
    auto *launcher = AppLauncher::instance();
    QVERIFY(launcher != nullptr);
    QCOMPARE(AppLauncher::instance(), launcher);
}

void app_launcher_test::dbusMethodsAreForwardedToProcessManager()
{
    PrivateSessionBus privateBus;
    QDBusConnection bus = QDBusConnection::sessionBus();
    if (!privateBus.isReady() && !bus.isConnected()) {
        const QByteArray reason = privateBus.errorString().toUtf8();
        QSKIP(reason.constData());
    }
    if (!bus.isConnected()) {
        QSKIP("session bus is not connected");
    }

    if (!bus.registerService(QString::fromLatin1(LauncherService))) {
        const QByteArray reason = bus.lastError().message().toUtf8();
        QSKIP(reason.constData());
    }
    RegisteredServiceGuard guard(bus,
                                 QString::fromLatin1(LauncherService),
                                 QString::fromLatin1(LauncherPath));

    ProcessManagerReceiver receiver;
    QVERIFY2(bus.registerObject(QString::fromLatin1(LauncherPath), &receiver, QDBusConnection::ExportAllSlots),
             qPrintable(bus.lastError().message()));

    QSignalSpy launchSpy(&receiver, &ProcessManagerReceiver::launchAppCalled);
    QSignalSpy launchArgsSpy(&receiver, &ProcessManagerReceiver::launchAppWithArgumentsCalled);
    QSignalSpy runCommandSpy(&receiver, &ProcessManagerReceiver::runCommandCalled);

    auto *launcher = AppLauncher::instance();
    const QString desktopFile = QStringLiteral("/tmp/codex-launch.desktop");
    const QStringList args{QStringLiteral("--flag"), QStringLiteral("value")};
    const QString command = QStringLiteral("printf codex");

    launcher->launchApp(desktopFile);
    QTRY_COMPARE(launchSpy.count(), 1);
    QCOMPARE(launchSpy.takeFirst().at(0).toString(), desktopFile);

    launcher->launchAppWithArguments(desktopFile, args);
    QTRY_COMPARE(launchArgsSpy.count(), 1);
    const auto launchArgs = launchArgsSpy.takeFirst();
    QCOMPARE(launchArgs.at(0).toString(), desktopFile);
    QCOMPARE(launchArgs.at(1).toStringList(), args);

    launcher->runCommand(command);
    QTRY_COMPARE(runCommandSpy.count(), 1);
    QCOMPARE(runCommandSpy.takeFirst().at(0).toString(), command);

    delete launcher;
}

void app_launcher_test::emptyInputsAreIgnoredAndOpenUriUsesDesktopHandler()
{
    UrlHandlerReceiver receiver;
    QSignalSpy handledSpy(&receiver, &UrlHandlerReceiver::handled);
    const QString scheme = QStringLiteral("codex-open");
    const QString uri = QStringLiteral("codex-open:test-target");

    QDesktopServices::setUrlHandler(scheme, &receiver, "handle");

    auto *launcher = AppLauncher::instance();
    launcher->launchApp(QString());
    launcher->launchAppWithArguments(QString(), {QStringLiteral("--ignored")});
    launcher->runCommand(QString());
    launcher->openUri(QString());
    QCOMPARE(handledSpy.count(), 0);

    launcher->openUri(uri);
    QTRY_COMPARE(handledSpy.count(), 1);
    QCOMPARE(handledSpy.takeFirst().at(0).toUrl(), QUrl(uri));

    QDesktopServices::unsetUrlHandler(scheme);
}

int main(int argc, char *argv[])
{
    qputenv("QT_QPA_PLATFORM", "offscreen");
    qputenv("QT_QPA_PLATFORMTHEME", "generic");

    QGuiApplication app(argc, argv);
    app_launcher_test testCase;
    return QTest::qExec(&testCase, argc, argv);
}

#include "test-app-launcher.moc"
