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

#include "testserverrunner.h"

#include <QDebug>
#include <QFile>
#include <QString>
#include <QThread>

#include "testservice.grpc.pb.h"

#include <grpc++/grpc++.h>

#include <memory>
#include <charconv>

namespace {

using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::ServerWriter;
using grpc::Status;
using qtgrpc::tests::Empty;
using qtgrpc::tests::BlobMessage;
using qtgrpc::tests::SimpleIntMessage;
using qtgrpc::tests::SimpleStringMessage;
using qtgrpc::tests::TestService;

// Logic and data behind the server's behavior.
class TestServiceServiceImpl final : public qtgrpc::tests::TestService::Service
{
public:
    TestServiceServiceImpl(qint64 latency) : m_latency(latency) { }

private:
    grpc::Status testMethod(grpc::ServerContext *, const SimpleStringMessage *request,
                            SimpleStringMessage *response) override;

    grpc::Status testMethodServerStream(grpc::ServerContext *, const SimpleStringMessage *request,
                                        grpc::ServerWriter<SimpleStringMessage> *writer) override;

    grpc::Status testMethodBlobServerStream(grpc::ServerContext *, const BlobMessage *request,
                                            grpc::ServerWriter<BlobMessage> *writer) override;
    grpc::Status testMethodStatusMessage(grpc::ServerContext *, const SimpleStringMessage *request,
                                         SimpleStringMessage *) override;
    grpc::Status testMethodNonCompatibleArgRet(grpc::ServerContext *,
                                               const SimpleIntMessage *request,
                                               SimpleStringMessage *response) override;
    grpc::Status testMetadata(grpc::ServerContext *,
                                                const Empty *,
                                                qtgrpc::tests::Empty *) override;

    grpc::Status
    testMethodClientStream(::grpc::ServerContext *,
                           ::grpc::ServerReader<::qtgrpc::tests::SimpleStringMessage> *reader,
                           ::qtgrpc::tests::SimpleStringMessage *response) override;

    grpc::Status testMethodBiStream(
            ::grpc::ServerContext *context,
            ::grpc::ServerReaderWriter<::qtgrpc::tests::SimpleStringMessage,
                                       ::qtgrpc::tests::SimpleStringMessage> *stream) override;
    grpc::Status
    testMethodClientStreamWithDone(::grpc::ServerContext *,
                           ::grpc::ServerReader<::qtgrpc::tests::SimpleStringMessage> *reader,
                           ::qtgrpc::tests::SimpleStringMessage *response) override;

    grpc::Status testMethodBiStreamWithDone(
        ::grpc::ServerContext *context,
        ::grpc::ServerReaderWriter<::qtgrpc::tests::SimpleStringMessage,
                                   ::qtgrpc::tests::SimpleStringMessage> *stream) override;

    grpc::Status testMethodSleep(grpc::ServerContext *context,
                                 const qtgrpc::tests::SleepMessage *request,
                                 qtgrpc::tests::Empty * /* response */) override
    {
        if (request->has_sleeptimems())
            QThread::msleep(request->sleeptimems());

        if (context->IsCancelled())
            return { grpc::StatusCode::CANCELLED, "testMethodSleep" };
        return grpc::Status::OK;
    }

    grpc::Status
    testMethodServerStreamSleep(grpc::ServerContext *context,
                                const qtgrpc::tests::ServerStreamSleepMessage *request,
                                grpc::ServerWriter<qtgrpc::tests::Empty> *writer) override
    {
        size_t count = 0;
        while (count < request->amountresponses()) {
            if (request->sleepmessage().has_sleeptimems())
                QThread::msleep(request->sleepmessage().sleeptimems());
            writer->Write(qtgrpc::tests::Empty());
            ++count;
        }

        if (context->IsCancelled())
            return { grpc::StatusCode::CANCELLED, "testMethodServerStreamSleep" };
        return grpc::Status::OK;
    }

    grpc::Status
    testMethodClientStreamSleep(grpc::ServerContext *context,
                                grpc::ServerReader<qtgrpc::tests::SleepMessage> *reader,
                                qtgrpc::tests::Empty * /* response */) override
    {
        qtgrpc::tests::SleepMessage request;
        while (reader->Read(&request)) {
            if (request.has_sleeptimems())
                QThread::msleep(request.sleeptimems());
        }

        if (context->IsCancelled())
            return { grpc::StatusCode::CANCELLED, "testMethodClientStreamSleep" };
        return grpc::Status::OK;
    }

    grpc::Status
    testMethodBiStreamSleep(grpc::ServerContext *context,
                            grpc::ServerReaderWriter<qtgrpc::tests::Empty,
                                                     qtgrpc::tests::SleepMessage> *stream) override
    {
        qtgrpc::tests::SleepMessage request;

        while (stream->Read(&request)) {
            if (request.has_sleeptimems())
                QThread::msleep(request.sleeptimems());
            if (!stream->Write(Empty()))
                return { grpc::StatusCode::ABORTED, "testMethodBiStreamSleep failed to write" };
        }

        if (context->IsCancelled())
            return { grpc::StatusCode::CANCELLED, "testMethodBiStreamSleep" };
        return grpc::Status::OK;
    }

    ::grpc::Status replyWithMetadata(::grpc::ServerContext *ctx, const ::qtgrpc::tests::Empty *,
                                     ::qtgrpc::tests::MetadataMessage *response) override
    {
        qInfo() << "replyWithMetadata called";
        for (const auto &header : ctx->client_metadata()) {
            auto *headerValue = response->mutable_values()->Add();
            headerValue->set_key(std::string(header.first.data(), header.first.size()));
            headerValue->set_value(std::string(header.second.data(), header.second.size()));
        }

        return Status();
    }

    qint64 m_latency;
};
}

Status TestServiceServiceImpl::testMethod(grpc::ServerContext *, const SimpleStringMessage *request,
                                          SimpleStringMessage *response)
{
    qInfo() << "testMethod called with: " << request->testfieldstring();
    response->set_testfieldstring(request->testfieldstring());
    if (request->testfieldstring() == "sleep") {
        QThread::msleep(m_latency);
    }
    return Status();
}

Status TestServiceServiceImpl::testMethodServerStream(grpc::ServerContext *,
                                                      const SimpleStringMessage *request,
                                                      ServerWriter<SimpleStringMessage> *writer)

{
    qInfo() << "testMethodServerStream called with: " << request->testfieldstring();
    SimpleStringMessage msg;

    msg.set_testfieldstring(request->testfieldstring() + "1");
    QThread::msleep(m_latency);
    qInfo() << "send back " << (request->testfieldstring() + "1");
    writer->Write(msg);

    msg.set_testfieldstring(request->testfieldstring() + "2");
    QThread::msleep(m_latency);
    qInfo() << "send back " << (request->testfieldstring() + "2");
    writer->Write(msg);

    msg.set_testfieldstring(request->testfieldstring() + "3");
    QThread::msleep(m_latency);
    qInfo() << "send back " << (request->testfieldstring() + "3");
    writer->Write(msg);

    msg.set_testfieldstring(request->testfieldstring() + "4");
    QThread::msleep(m_latency);
    qInfo() << "send back " << (request->testfieldstring() + "4");
    writer->WriteLast(msg, grpc::WriteOptions());

    return Status();
}

Status TestServiceServiceImpl::testMethodBlobServerStream(grpc::ServerContext *,
                                                          const BlobMessage *request,
                                                          ServerWriter<BlobMessage> *writer)

{
    qInfo() << "testMethodBlobServerStream called with testbytes size: "
            << request->testbytes().size();
    BlobMessage msg;
    msg.set_allocated_testbytes(new std::string(request->testbytes()));
    writer->Write(msg);
    return Status();
}

Status TestServiceServiceImpl::testMethodStatusMessage(grpc::ServerContext *,
                                                       const SimpleStringMessage *request,
                                                       SimpleStringMessage *)
{
    return Status(grpc::StatusCode::UNIMPLEMENTED, request->testfieldstring());
}

Status TestServiceServiceImpl::testMethodNonCompatibleArgRet(grpc::ServerContext *,
                                                             const SimpleIntMessage *request,
                                                             SimpleStringMessage *response)
{
    qInfo() << "testMethodNonCompatibleArgRet called with: " << request->testfield();
    response->set_testfieldstring(std::to_string(request->testfield()));
    return Status();
}

Status TestServiceServiceImpl::testMetadata(grpc::ServerContext *ctx, const Empty *,
                                            qtgrpc::tests::Empty *)
{
    const auto &md = ctx->client_metadata();
    auto initial = md.find("request_initial");
    uint initialCount = 0;
    if (initial != md.end()) {
        std::string v (initial->second.begin(), initial->second.end());
        auto result = std::from_chars(v.data(), v.data() + v.size(), initialCount);
        if (result.ec != std::errc())
            return { grpc::StatusCode::ABORTED, "conversion failed initial" };
    }

    auto trailing = md.find("request_trailing");
    uint trailingCount = 0;
    if (trailing != md.end()) {
        std::string v (trailing->second.begin(), trailing->second.end());
        auto result = std::from_chars(v.data(), v.data() + v.size(), trailingCount);
        if (result.ec != std::errc())
            return { grpc::StatusCode::ABORTED, "conversion failed trailing" };
    }

    auto sum = md.equal_range("request_sum");
    uint sumCount = 0;
    while (sum.first != sum.second) {
        uint temp = 0;
        std::string v (sum.first->second.begin(), sum.first->second.end());
        auto result = std::from_chars(v.data(), v.data() + v.size(), temp);
        if (result.ec != std::errc())
            return { grpc::StatusCode::ABORTED, "conversion failed sum" };
        sumCount += temp;
        ++sum.first;
    }

    for (auto i = 0u; i < initialCount; ++i)
        ctx->AddInitialMetadata("response_initial", std::to_string(i));
    for (auto i = 0u; i < trailingCount; ++i)
        ctx->AddTrailingMetadata("response_trailing", std::to_string(i));
    if (sumCount > 0)
        ctx->AddTrailingMetadata("response_sum", std::to_string(sumCount));

    return Status::OK;
}

grpc::Status TestServiceServiceImpl::testMethodClientStream(
        ::grpc::ServerContext *, ::grpc::ServerReader<::qtgrpc::tests::SimpleStringMessage> *reader,
        ::qtgrpc::tests::SimpleStringMessage *response)
{
    ::qtgrpc::tests::SimpleStringMessage req;
    std::string rspString;
    for (int i = 0; i < 4; ++i) {
        if (!reader->Read(&req)) {
            qInfo() << "Unable to read message from client stream";
            return grpc::Status(grpc::StatusCode::DATA_LOSS, rspString);
        }
        rspString += req.testfieldstring();
        rspString += std::to_string(i + 1);
    }

    response->set_testfieldstring(rspString);
    return {};
}

grpc::Status TestServiceServiceImpl::testMethodBiStream(
        ::grpc::ServerContext *,
        ::grpc::ServerReaderWriter<::qtgrpc::tests::SimpleStringMessage,
                                   ::qtgrpc::tests::SimpleStringMessage> *stream)
{
    ::qtgrpc::tests::SimpleStringMessage req;
    for (int i = 0; i < 4; ++i) {
        if (!stream->Read(&req)) {
            qInfo() << "Unable to read message from bidirectional stream";
            return grpc::Status(grpc::StatusCode::DATA_LOSS, "Read failed");
        }
        std::string rspString = req.testfieldstring() + std::to_string(i + 1);
        ::qtgrpc::tests::SimpleStringMessage rsp;
        rsp.set_testfieldstring(rspString);
        if (!stream->Write(rsp, {})) {
            qInfo() << "Unable to write message to bidirectional stream";
            return grpc::Status(grpc::StatusCode::DATA_LOSS, "Write failed");
        }
        QThread::msleep(m_latency);
    }

    return {};
}

grpc::Status TestServiceServiceImpl::testMethodClientStreamWithDone(
    ::grpc::ServerContext *, ::grpc::ServerReader<::qtgrpc::tests::SimpleStringMessage> *reader,
    ::qtgrpc::tests::SimpleStringMessage *response)
{
    ::qtgrpc::tests::SimpleStringMessage req;
    std::string rspString;
    int i = 0;
    while (reader->Read(&req)) {
        rspString += req.testfieldstring();
        rspString += std::to_string(++i);
    }

    response->set_testfieldstring(rspString);
    return {};
}

grpc::Status TestServiceServiceImpl::testMethodBiStreamWithDone(
    ::grpc::ServerContext *,
    ::grpc::ServerReaderWriter<::qtgrpc::tests::SimpleStringMessage,
                               ::qtgrpc::tests::SimpleStringMessage> *stream)
{
    ::qtgrpc::tests::SimpleStringMessage req;
    int i = 0;
    while (stream->Read(&req)) {
        std::string rspString = req.testfieldstring() + std::to_string(++i);
        ::qtgrpc::tests::SimpleStringMessage rsp;
        rsp.set_testfieldstring(rspString);
        if (!stream->Write(rsp, {})) {
            qInfo() << "Unable to write message to bidirectional stream";
            return grpc::Status(grpc::StatusCode::DATA_LOSS, "Write failed");
        }
        QThread::msleep(m_latency);
    }

    return {};
}

void TestServer::run(qint64 latency)
{
    TestServiceServiceImpl service(latency);

    grpc::ServerBuilder builder;

    QString httpURI("127.0.0.1:50051");
    builder.AddListeningPort(httpURI.toStdString(), grpc::InsecureServerCredentials());

    QString httpsURI("127.0.0.1:50052");
    if (QFile cfile(":/assets/cert.pem"); cfile.open(QFile::ReadOnly)) {
        QString cert = cfile.readAll();

        if (QFile kfile(":/assets/key.pem"); kfile.open(QFile::ReadOnly)) {
            QString key = kfile.readAll();

            grpc::SslServerCredentialsOptions opts(GRPC_SSL_DONT_REQUEST_CLIENT_CERTIFICATE);
            opts.pem_key_cert_pairs.push_back({ key.toStdString(), cert.toStdString() });

            builder.AddListeningPort(httpsURI.toStdString(), grpc::SslServerCredentials(opts));
        } else {
            qDebug() << "Unable to open SSL key. Test lacks resources.";
            httpsURI = "";
        }
    } else {
        qDebug() << "Unable to open SSL cert. Test lacks resources.";
        httpsURI = "";
    }
#ifndef Q_OS_WINDOWS
    QString unixUri("unix:///tmp/qtgrpc_test.sock");
    builder.AddListeningPort(unixUri.toStdString(), grpc::InsecureServerCredentials());
#endif
    builder.RegisterService(&service);

    std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
    if (!server) {
        qDebug() << "Creating grpc::Server failed.";
        return;
    }
#ifdef Q_OS_WINDOWS
    qDebug() << "Server listening on " << httpURI << httpsURI;
#else
    qDebug() << "Server listening on " << httpURI << httpsURI << "and" << unixUri;
#endif
    server->Wait();
}
