BareGit
#include <filesystem>
#include <format>
#include <chrono>
#include <iostream>
#include <netinet/in.h>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>

#include <httplib.h>
#include <sys/socket.h>
#include <unistd.h>

#include "api_server.hpp"
#include "database.hpp"
#include "delivery_store.hpp"
#include "key_store.hpp"
#include "subscription_store.hpp"
#include "telegram_client.hpp"
#include "service_error.hpp"

namespace
{

std::filesystem::path makeDirectory()
{
    std::string pattern = "/tmp/telegrammer-test-XXXXXX";
    std::vector<char> buffer(pattern.begin(), pattern.end());
    buffer.push_back('\0');
    char* result = ::mkdtemp(buffer.data());
    if(result == nullptr)
    {
        throw std::runtime_error("mkdtemp failed");
    }
    return result;
}

void require(bool condition, std::string_view message)
{
    if(!condition)
    {
        throw std::runtime_error(std::string(message));
    }
}

int freePort()
{
    int socket_fd = ::socket(AF_INET, SOCK_STREAM, 0);
    require(socket_fd >= 0, "socket failed");
    sockaddr_in address{};
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
    address.sin_port = 0;
    require(::bind(socket_fd, reinterpret_cast<sockaddr*>(&address),
                   sizeof(address)) == 0,
            "bind failed");
    socklen_t length = sizeof(address);
    require(::getsockname(socket_fd, reinterpret_cast<sockaddr*>(&address),
                          &length) == 0,
            "getsockname failed");
    int port = ntohs(address.sin_port);
    ::close(socket_fd);
    return port;
}

class FakeTelegram final : public telegrammer::TelegramApi
{
public:
    mutable int64_t sent_chat_id = 0;
    mutable std::string sent_text;

    mw::E<telegrammer::json> getMe() const override
    {
        return telegrammer::json{
            {"ok", true}, {"result", {{"id", 77}, {"is_bot", true}}}};
    }

    mw::E<telegrammer::json> sendMessage(
        int64_t chat_id, const std::string& text) const override
    {
        sent_chat_id = chat_id;
        sent_text = text;
        return telegrammer::json{
            {"ok", true},
            {"result", {{"message_id", 42}, {"chat", {{"id", chat_id}}}}}};
    }

    mw::E<telegrammer::json> getUpdates(
        int64_t, int) const override
    {
        return telegrammer::json{
            {"ok", true}, {"result", telegrammer::json::array()}};
    }
};

void requireJsonResponse(const httplib::Result& response, int status,
                         std::string_view message)
{
    require(response != nullptr, message);
    require(response->status == status, message);
}

} // namespace

int main()
{
    auto directory = makeDirectory();
    try
    {
        auto database = directory / "state.db";
        std::string database_path = database.string();

    auto initialized = telegrammer::initializeDatabase(database_path);
    require(initialized.has_value(), "database initialization failed");
    auto database_connection = telegrammer::openDatabase(database_path);
    require(database_connection.has_value(), "database could not be reopened");
    auto user_version = (*database_connection)->evalToValue<int64_t>(
        "PRAGMA user_version;");
    require(user_version.has_value() && *user_version == 0,
            "development schema unexpectedly changed user_version");

    telegrammer::DatabaseLock first_lock(database_path);
    bool second_lock_rejected = false;
    try
    {
        telegrammer::DatabaseLock second_lock(database_path);
    }
    catch(const std::exception&)
    {
        second_lock_rejected = true;
    }
    require(second_lock_rejected, "database lock was not exclusive");

    telegrammer::KeyStore keys(database_path);
    auto token = keys.addKey("test-client");
    require(token.has_value(), "key generation failed");
    require(token->size() == 64, "generated key has the wrong length");

    auto identity = keys.authenticate(*token);
    require(identity.has_value() && identity->has_value(),
            "generated key did not authenticate");
    require(identity->value().name == "test-client", "wrong key identity");

    auto listed_keys = keys.listKeys();
    require(listed_keys.has_value() && listed_keys->size() == 1,
            "key listing failed");
    require(listed_keys->at(0).name == "test-client",
            "key listing returned the wrong name");

    telegrammer::SubscriptionStore subscriptions(database_path);
    auto subscription = subscriptions.add(identity->value().id, 123,
                                           "http://127.0.0.1/hook");
    require(subscription.has_value(), "subscription creation failed");
    auto duplicate = subscriptions.add(identity->value().id, 123,
                                        "http://127.0.0.1/hook");
    require(duplicate.has_value(), "duplicate subscription failed");
    require(duplicate->id == subscription->id,
            "duplicate subscription was not idempotent");

    telegrammer::DeliveryStore deliveries(database_path);
    require(deliveries.ensureBot(77).has_value(), "bot binding failed");
    telegrammer::json updates = telegrammer::json::array({telegrammer::json{
        {"update_id", 1},
        {"message", {
            {"message_id", 5},
            {"from", {{"username", "Test_User"}}},
            {"chat", {{"id", 123}, {"type", "private"}}},
            {"text", "hello"}
        }}}});
    require(deliveries.ingest(77, updates).has_value(),
            "update ingestion failed");

    auto resolved = deliveries.resolveUsername("test_user");
    require(resolved.has_value() && resolved->has_value(),
            "username was not observed");
    require(resolved->value() == 123, "username resolved to the wrong chat");

    auto job = deliveries.claimNext();
    require(job.has_value() && job->has_value(), "delivery was not queued");
    require(job->value().subscription_id == subscription->id,
            "delivery has the wrong subscription");
    require(job->value().payload.find("hello") != std::string::npos,
            "delivery payload was not preserved");
    require(deliveries.complete(job->value().id).has_value(),
            "delivery completion failed");

    auto failed_update = telegrammer::json::array({telegrammer::json{
        {"update_id", 2},
        {"message", {
            {"message_id", 6},
            {"chat", {{"id", 123}, {"type", "private"}}},
            {"text", "retry me"}
        }}}});
    require(deliveries.ingest(77, failed_update).has_value(),
            "second update ingestion failed");
    auto failed_job = deliveries.claimNext();
    require(failed_job.has_value() && failed_job->has_value(),
            "second delivery was not queued");
    require(deliveries.fail(failed_job->value().id, "HTTP 400", false,
                            std::nullopt)
                .has_value(),
            "dead-letter transition failed");
    auto dead_jobs = deliveries.listDead();
    require(dead_jobs.has_value() && dead_jobs->size() == 1,
            "dead-letter listing failed");
    require(dead_jobs->at(0).last_error == "HTTP 400",
            "dead-letter reason was not retained");
    require(deliveries.retry(failed_job->value().id).has_value(),
            "dead-letter retry command failed");
    auto retried_job = deliveries.claimNext();
    require(retried_job.has_value() && retried_job->has_value(),
            "retried delivery was not queued");
    require(deliveries.complete(retried_job->value().id).has_value(),
            "retried delivery completion failed");

    auto legacy_path = (directory / "legacy.db").string();
    auto legacy_connection = mw::SQLite::connectFile(legacy_path);
    require(legacy_connection.has_value(), "legacy database could not be made");
    require((*legacy_connection)
                ->execute("CREATE TABLE api_keys (name TEXT PRIMARY KEY, "
                          "key TEXT NOT NULL, created_at INTEGER);")
                .has_value(),
            "legacy schema could not be made");
    auto legacy_initialization =
        telegrammer::initializeDatabase(legacy_path);
    require(!legacy_initialization.has_value(),
            "legacy schema was accepted");
    const telegrammer::ServiceError* schema_error =
        telegrammer::asServiceError(legacy_initialization.error());
    require(schema_error != nullptr && schema_error->code == "DATABASE_SCHEMA",
            "legacy schema returned the wrong error");

    FakeTelegram telegram;
    telegrammer::RuntimeState state;
    state.polling_ready = true;
    state.degraded = false;
    int port = freePort();
    telegrammer::ApiServer server(
        {"127.0.0.1", port}, keys, subscriptions, deliveries, telegram, state);
    require(server.start().has_value(), "API server did not start");
    httplib::Client client("127.0.0.1", port);
    client.set_connection_timeout(std::chrono::seconds(2));
    auto unauthorized = client.Get("/health");
    requireJsonResponse(unauthorized, 401, "missing auth was not rejected");
    require(unauthorized->get_header_value("WWW-Authenticate") == "Bearer",
            "auth challenge was missing");
    auto auth_header = httplib::Headers{
        {"Authorization", "Bearer " + *token}};
    auto health = client.Get("/health", auth_header);
    requireJsonResponse(health, 200, "health endpoint failed");
    require(telegrammer::json::parse(health->body)["ok"] == true,
            "health response was not an object success envelope");
    telegrammer::ApiServer conflicting_server(
        {"127.0.0.1", port}, keys, subscriptions, deliveries, telegram, state);
    require(!conflicting_server.start().has_value(),
            "occupied port was reported as available");
    auto wrong_method = client.Post("/health", auth_header, "{}",
                                   "application/json");
    requireJsonResponse(wrong_method, 405,
                        "unsupported method did not return 405");
    require(wrong_method->get_header_value("Allow") == "GET",
            "unsupported method did not return Allow");
    auto bad_media = client.Post("/subscribe", auth_header, "{}",
                                 "text/plain");
    requireJsonResponse(bad_media, 415, "media type validation failed");
    auto subscribe_response = client.Post(
        "/subscribe", auth_header,
        R"({"chat_id":123,"callback_url":"http://127.0.0.1/hook"})",
        "application/json");
    requireJsonResponse(subscribe_response, 200, "HTTP subscription failed");
    auto subscribe_body = telegrammer::json::parse(subscribe_response->body);
    require(subscribe_body.is_object() && subscribe_body["ok"] == true,
            "subscription response was not a JSON object");
    int64_t subscription_id = subscribe_body["subscription_id"].get<int64_t>();
    auto duplicate_response = client.Post(
        "/subscribe", auth_header,
        R"({"chat_id":123,"callback_url":"http://127.0.0.1/hook"})",
        "application/json");
    requireJsonResponse(duplicate_response, 200,
                        "duplicate HTTP subscription failed");
    require(telegrammer::json::parse(duplicate_response->body)
                ["subscription_id"] == subscription_id,
            "duplicate HTTP subscription was not idempotent");
    auto send_response = client.Post(
        "/send", auth_header,
        R"({"chat_id":123,"text":"hello from api"})", "application/json");
    requireJsonResponse(send_response, 200, "HTTP send failed");
    require(telegram.sent_chat_id == 123 && telegram.sent_text == "hello from api",
            "HTTP send did not reach Telegram client");
    auto both_destinations = client.Post(
        "/send", auth_header,
        R"({"chat_id":123,"username":"alice","text":"bad"})",
        "application/json");
    requireJsonResponse(both_destinations, 400,
                        "ambiguous send destination was accepted");
    auto subscriptions_response = client.Get("/subscriptions", auth_header);
    requireJsonResponse(subscriptions_response, 200,
                        "subscription listing failed");
    require(telegrammer::json::parse(subscriptions_response->body)
                ["subscriptions"].is_array(),
            "subscription listing was not an array");
    auto malformed_delete = client.Delete(
        std::format("/subscriptions/{}junk", subscription_id), auth_header);
    requireJsonResponse(malformed_delete, 400,
                        "malformed subscription ID was accepted");
    auto delete_response = client.Delete(
        std::format("/subscriptions/{}", subscription_id), auth_header);
    requireJsonResponse(delete_response, 204, "subscription deletion failed");
    server.stop();
    server.wait();

    auto removed = subscriptions.remove(identity->value().id,
                                        subscription->id);
    require(removed.has_value() && !*removed,
            "HTTP subscription deletion was not persisted");
    auto deleted_key = keys.deleteKey("test-client");
    require(deleted_key.has_value() && *deleted_key, "key deletion failed");
    auto revoked = keys.authenticate(*token);
    require(revoked.has_value() && !revoked->has_value(),
            "deleted key still authenticated");

        std::filesystem::remove_all(directory);
        return 0;
    }
    catch(const std::exception& error)
    {
        std::filesystem::remove_all(directory);
        std::cerr << error.what() << std::endl;
        return 1;
    }
}