BareGit
#include "app.h"
#include "data_source_sqlite.h"
#include "embedded_assets.h"

#include <chrono>
#include <string>
#include <thread>
#include <utility>

#include <gtest/gtest.h>

namespace
{

class TestApp : public App
{
public:
    TestApp(Configuration configuration, DataSourceInterface& data_source)
        : App(mw::IPSocketInfo{"127.0.0.1", 0}, std::move(configuration),
               data_source)
    {}

    int listen()
    {
        setup();
        const int port = server.bind_to_any_port("127.0.0.1");
        if(port >= 0)
        {
            worker = std::thread(&TestApp::serve, this);
            server.wait_until_ready();
        }
        return port;
    }

    ~TestApp()
    {
        server.stop();
        if(worker.joinable())
        {
            worker.join();
        }
    }

private:
    void serve()
    {
        server.listen_after_bind();
    }

    std::thread worker;
};

Configuration emptyConfiguration()
{
    return {};
}

Configuration serviceConfiguration()
{
    return Configuration{
        .worker_count = 1,
        .database_path = ":memory:",
        .groups = {{
            .name = "Websites",
            .services = {{
                .id = "blog",
                .name = "Blog",
                .description = "Personal website",
                .endpoint = HttpEndpoint{"https://example.test"},
                .timeout = std::chrono::seconds(5),
                .interval = std::chrono::hours(1),
                .url = "https://example.test"
            }}
        }}
    };
}

std::unique_ptr<DataSourceSqlite> openTestSource()
{
    auto source = DataSourceSqlite::open(":memory:");
    EXPECT_TRUE(source);
    return source ? std::move(*source) : nullptr;
}

TEST(App, ServesEmbeddedAssets)
{
    auto source = openTestSource();
    ASSERT_TRUE(source);
    TestApp app(emptyConfiguration(), *source);
    const int port = app.listen();
    ASSERT_GT(port, 0);
    httplib::Client client("127.0.0.1", port);
    for(const auto& asset : embeddedAssets())
    {
        if(asset.path == "/")
        {
            continue;
        }
        auto response = client.Get(std::string(asset.path) + "?v=1");
        ASSERT_TRUE(response);
        EXPECT_EQ(response->status, 200);
        EXPECT_EQ(response->body, asset.content);
        EXPECT_EQ(response->get_header_value("Content-Type"),
                  asset.content_type);
        auto head = client.Head(std::string(asset.path));
        ASSERT_TRUE(head);
        EXPECT_EQ(head->status, 200);
        EXPECT_TRUE(head->body.empty());
        EXPECT_EQ(head->get_header_value("Content-Length"),
                  std::to_string(asset.content.size()));
    }
    auto index = client.Get("/");
    ASSERT_TRUE(index);
    EXPECT_EQ(index->status, 200);
    EXPECT_NE(index->body.find("No services are configured"),
              std::string::npos);
}

TEST(App, RejectsUnknownPathsAndWrites)
{
    auto source = openTestSource();
    ASSERT_TRUE(source);
    TestApp app(emptyConfiguration(), *source);
    const int port = app.listen();
    ASSERT_GT(port, 0);
    httplib::Client client("127.0.0.1", port);
    for(const auto* path : {"/missing", "/static/missing.css", "/prd.md"})
    {
        auto response = client.Get(path);
        ASSERT_TRUE(response);
        EXPECT_EQ(response->status, 404);
    }
    auto response = client.Post("/static/style.css", "replace", "text/plain");
    ASSERT_TRUE(response);
    EXPECT_NE(response->status, 200);
}

TEST(App, RendersConfiguredStatuses)
{
    auto source = openTestSource();
    ASSERT_TRUE(source);
    const auto now = std::chrono::duration_cast<std::chrono::seconds>(
        std::chrono::system_clock::now().time_since_epoch()).count();
    ASSERT_TRUE(source->save({"blog", now, 1200, ProbeStatus::GOOD}));
    TestApp app(serviceConfiguration(), *source);
    const int port = app.listen();
    ASSERT_GT(port, 0);
    httplib::Client client("127.0.0.1", port);
    auto response = client.Get("/");
    ASSERT_TRUE(response);
    EXPECT_EQ(response->status, 200);
    EXPECT_NE(response->body.find("Websites"), std::string::npos);
    EXPECT_NE(response->body.find("Blog"), std::string::npos);
    EXPECT_NE(response->body.find("Personal website"), std::string::npos);
    EXPECT_NE(response->body.find("led-good"), std::string::npos);
    EXPECT_NE(response->body.find("Good"), std::string::npos);
}

TEST(App, RendersMissingStatusAsNa)
{
    auto source = openTestSource();
    ASSERT_TRUE(source);
    TestApp app(serviceConfiguration(), *source);
    const int port = app.listen();
    ASSERT_GT(port, 0);
    httplib::Client client("127.0.0.1", port);
    auto response = client.Get("/");
    ASSERT_TRUE(response);
    EXPECT_EQ(response->status, 200);
    EXPECT_NE(response->body.find("led-na"), std::string::npos);
    EXPECT_NE(response->body.find("N/A"), std::string::npos);
}

TEST(App, DerivesNaForStaleStatus)
{
    auto source = openTestSource();
    ASSERT_TRUE(source);
    const auto now = std::chrono::duration_cast<std::chrono::seconds>(
        std::chrono::system_clock::now().time_since_epoch()).count();
    ASSERT_TRUE(source->save({"blog", now - 7201, 1200, ProbeStatus::GOOD}));
    TestApp app(serviceConfiguration(), *source);
    const int port = app.listen();
    ASSERT_GT(port, 0);
    httplib::Client client("127.0.0.1", port);
    auto response = client.Get("/");
    ASSERT_TRUE(response);
    EXPECT_EQ(response->status, 200);
    EXPECT_NE(response->body.find("led-na"), std::string::npos);
}

}