#include <QCoreApplication>
#include <QElapsedTimer>
#include <QFileInfo>
#include <QJsonDocument>
#include <QJsonObject>
#include <QProcess>
#include <QStringList>

#include <cstdio>
#include <utility>

#include "rkt_protocol.h"

namespace {

constexpr int protocolVersion = 1;
constexpr int timeoutMs = 10000;

class ProtocolClient
{
public:
    explicit ProtocolClient(QString program)
        : program_(std::move(program))
    {
        process_.setProcessChannelMode(QProcess::SeparateChannels);
    }

    bool start()
    {
        process_.start(program_, {});
        if (!process_.waitForStarted(timeoutMs)) {
            fail(QString("Could not start %1: %2")
                     .arg(program_, process_.errorString()));
            return false;
        }

        const QJsonObject ready = readMessage("ready", -1);
        if (ready.isEmpty()) {
            return false;
        }
        if (ready["protocol"].toInt(-1) != protocolVersion) {
            fail(QString("Unexpected protocol version: %1")
                     .arg(ready["protocol"].toInt(-1)));
            return false;
        }

        std::fprintf(stderr, "PASS: backend ready (protocol %d)\n", protocolVersion);
        return true;
    }

    QJsonObject command(int command, const QJsonObject &data = {})
    {
        const int id = nextId_++;

        QJsonObject request;
        request["type"] = "command";
        request["id"] = id;
        request["command"] = command;
        request["data"] = data;

        const QByteArray json = QJsonDocument(request).toJson(QJsonDocument::Compact) + '\n';
        if (process_.write(json) != json.size() || !process_.waitForBytesWritten(timeoutMs)) {
            fail(QString("Could not write command %1: %2")
                     .arg(command)
                     .arg(process_.errorString()));
            return {};
        }

        return readMessage("result", id);
    }

    bool stop()
    {
        const QJsonObject result = command(CMD_QUIT);
        if (result.isEmpty() || result["result"].toInt() != RESULT_QUIT) {
            fail("QUIT did not return RESULT_QUIT");
            process_.kill();
            process_.waitForFinished(timeoutMs);
            return false;
        }

        process_.closeWriteChannel();
        if (!process_.waitForFinished(timeoutMs)) {
            fail("Backend did not stop after QUIT");
            process_.kill();
            process_.waitForFinished(timeoutMs);
            return false;
        }

        drainStderr();
        std::fprintf(stderr, "PASS: backend stopped cleanly\n");
        return process_.exitStatus() == QProcess::NormalExit && process_.exitCode() == 0;
    }

    void abort()
    {
        if (process_.state() != QProcess::NotRunning) {
            process_.kill();
            process_.waitForFinished(timeoutMs);
        }
        drainStderr();
    }

private:
    QJsonObject readMessage(const QString &expectedType, int expectedId)
    {
        QElapsedTimer timer;
        timer.start();

        while (timer.elapsed() < timeoutMs) {
            while (process_.canReadLine()) {
                const QByteArray line = process_.readLine().trimmed();
                if (line.isEmpty()) {
                    continue;
                }

                QJsonParseError error;
                const QJsonDocument document = QJsonDocument::fromJson(line, &error);
                if (error.error != QJsonParseError::NoError || !document.isObject()) {
                    fail(QString("Invalid JSON from backend: %1").arg(QString::fromUtf8(line)));
                    return {};
                }

                const QJsonObject message = document.object();
                const QString type = message["type"].toString();

                if (type == "event") {
                    std::fprintf(stderr,
                                 "EVENT: webview=%d %s\n",
                                 message["wv"].toInt(),
                                 message["data"].toString().toUtf8().constData());
                    continue;
                }

                if (type == "protocol-error") {
                    fail(QString("Protocol error: %1").arg(message["message"].toString()));
                    return {};
                }

                if (type != expectedType) {
                    fail(QString("Expected message type %1, got %2")
                             .arg(expectedType, type));
                    return {};
                }

                if (expectedId >= 0 && message["id"].toInt(-1) != expectedId) {
                    fail(QString("Expected result id %1, got %2")
                             .arg(expectedId)
                             .arg(message["id"].toInt(-1)));
                    return {};
                }

                return message;
            }

            drainStderr();
            if (process_.state() == QProcess::NotRunning) {
                fail(QString("Backend stopped unexpectedly with exit code %1")
                         .arg(process_.exitCode()));
                return {};
            }

            process_.waitForReadyRead(100);
        }

        fail(QString("Timed out waiting for %1").arg(expectedType));
        return {};
    }

    void drainStderr()
    {
        const QByteArray diagnostics = process_.readAllStandardError();
        if (!diagnostics.isEmpty()) {
            std::fwrite(diagnostics.constData(), 1, diagnostics.size(), stderr);
        }
    }

    static void fail(const QString &message)
    {
        std::fprintf(stderr, "FAIL: %s\n", message.toUtf8().constData());
    }

    QString program_;
    QProcess process_;
    int nextId_ = 1;
};

bool expectResult(const QJsonObject &message, int expected, const char *description)
{
    if (message.isEmpty()) {
        return false;
    }

    const int actual = message["result"].toInt();
    if (actual != expected) {
        std::fprintf(stderr,
                     "FAIL: %s returned %d, expected %d\n",
                     description,
                     actual,
                     expected);
        return false;
    }

    std::fprintf(stderr, "PASS: %s\n", description);
    return true;
}

QString backendProgram(const QCoreApplication &app)
{
    const QString environmentProgram = qEnvironmentVariable("RKT_WEBVIEW_PRG");
    if (!environmentProgram.isEmpty()) {
        return environmentProgram;
    }

#ifdef Q_OS_WIN
    return app.applicationDirPath() + "/rktwebview_prg.exe";
#else
    return app.applicationDirPath() + "/rktwebview_prg";
#endif
}

} // namespace

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);
    const bool protocolOnly = app.arguments().contains("--protocol-only");
    const QString program = backendProgram(app);

    if (!QFileInfo::exists(program)) {
        std::fprintf(stderr,
                     "FAIL: backend executable not found: %s\n",
                     program.toUtf8().constData());
        return 1;
    }

    ProtocolClient client(program);
    if (!client.start()) {
        client.abort();
        return 1;
    }

    bool ok = true;
    ok = expectResult(client.command(CMD_NOOP), 0, "NOOP") && ok;

    const QJsonObject initialInfo = client.command(CMD_INFO);
    ok = expectResult(initialInfo, 0, "INFO reports no open windows") && ok;

    if (!protocolOnly && ok) {
        QJsonObject contextData;
        contextData["boilerplate_js"] = "";
        contextData["has_pem_cert"] = false;
        contextData["pem_cert"] = "";

        const QJsonObject contextResult = client.command(CMD_CONTEXT_NEW, contextData);
        const int context = contextResult["result"].toInt(-1);
        if (context < 0) {
            std::fprintf(stderr, "FAIL: CONTEXT_NEW returned %d\n", context);
            ok = false;
        } else {
            std::fprintf(stderr, "PASS: context created (%d)\n", context);
        }

        QJsonObject createData;
        createData["context"] = context;
        createData["parent"] = 0;
        const QJsonObject createResult = client.command(CMD_CREATE_WV, createData);
        const int webview = createResult["result"].toInt(-1);
        if (webview <= 0) {
            std::fprintf(stderr, "FAIL: CREATE_WV returned %d\n", webview);
            ok = false;
        } else {
            std::fprintf(stderr, "PASS: webview created (%d)\n", webview);
        }

        if (ok) {
            QJsonObject titleData;
            titleData["wv"] = webview;
            titleData["title"] = "rktwebview stdio integration test";
            ok = expectResult(client.command(CMD_SET_TITLE, titleData), 0, "SET_TITLE") && ok;

            QJsonObject htmlData;
            htmlData["wv"] = webview;
            htmlData["html"] = "<html><head><title>stdio test</title></head>"
                               "<body><h1>rktwebview stdio test</h1></body></html>";
            ok = expectResult(client.command(CMD_SET_HTML, htmlData), 0, "SET_HTML") && ok;

            QJsonObject validData;
            validData["wv"] = webview;
            ok = expectResult(client.command(CMD_HANDLE_IS_VALID, validData), 1,
                              "HANDLE_IS_VALID before close") && ok;

            QJsonObject closeData;
            closeData["wv"] = webview;
            ok = expectResult(client.command(CMD_CLOSE_WV, closeData), 0, "CLOSE_WV") && ok;
        }
    }

    const bool stopped = client.stop();
    return ok && stopped ? 0 : 1;
}
