//------------------------------------------------------------------------------
/*
    This file is part of clio: https://github.com/XRPLF/clio
    Copyright (c) 2023, the clio developers.

    Permission to use, copy, modify, and distribute this software for any
    purpose with or without fee is hereby granted, provided that the above
    copyright notice and this permission notice appear in all copies.

    THE  SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
    WITH  REGARD  TO  THIS  SOFTWARE  INCLUDING  ALL  IMPLIED  WARRANTIES  OF
    MERCHANTABILITY  AND  FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
    ANY  SPECIAL,  DIRECT,  INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
    WHATSOEVER  RESULTING  FROM  LOSS  OF USE, DATA OR PROFITS, WHETHER IN AN
    ACTION  OF  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
    OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================

#include "data/LedgerCacheInterface.hpp"
#include "util/AssignRandomPort.hpp"
#include "util/MockLedgerCache.hpp"
#include "util/MockPrometheus.hpp"
#include "util/TestHttpClient.hpp"
#include "util/TestWebSocketClient.hpp"
#include "util/TmpFile.hpp"
#include "util/config/Array.hpp"
#include "util/config/ConfigDefinition.hpp"
#include "util/config/ConfigFileJson.hpp"
#include "util/config/ConfigValue.hpp"
#include "util/config/Types.hpp"
#include "util/prometheus/Label.hpp"
#include "util/prometheus/Prometheus.hpp"
#include "web/AdminVerificationStrategy.hpp"
#include "web/Server.hpp"
#include "web/dosguard/DOSGuard.hpp"
#include "web/dosguard/DOSGuardInterface.hpp"
#include "web/dosguard/IntervalSweepHandler.hpp"
#include "web/dosguard/Weights.hpp"
#include "web/dosguard/WhitelistHandler.hpp"
#include "web/interface/ConnectionBase.hpp"

#include <boost/asio/executor_work_guard.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/beast/core/error.hpp>
#include <boost/beast/http/field.hpp>
#include <boost/beast/http/status.hpp>
#include <boost/beast/websocket/error.hpp>
#include <boost/json/parse.hpp>
#include <boost/json/value.hpp>
#include <boost/system/system_error.hpp>
#include <fmt/format.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <test_data/SslCert.hpp>

#include <cstdint>
#include <functional>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <thread>
#include <tuple>
#include <utility>
#include <vector>

using namespace util;
using namespace util::config;
using namespace web::impl;
using namespace web;

static boost::json::value
generateJSONWithDynamicPort(std::string_view port)
{
    return boost::json::parse(
        fmt::format(
            R"JSON({{
            "server": {{
                "ip": "0.0.0.0",
                "port": {}
            }},
            "dos_guard": {{
                "max_fetches": 100,
                "sweep_interval": 1000,
                "max_connections": 2,
                "max_requests": 3,
                "whitelist": ["127.0.0.1"]
            }}
        }})JSON",
            port
        )
    );
}

static boost::json::value
generateJSONDataOverload(std::string_view port)
{
    return boost::json::parse(
        fmt::format(
            R"JSON({{
            "server": {{
                "ip": "0.0.0.0",
                "port": {}
            }},
            "dos_guard": {{
                "max_fetches": 100,
                "sweep_interval": 1000,
                "max_connections": 2,
                "max_requests": 1
            }}
        }})JSON",
            port
        )
    );
}

inline static ClioConfigDefinition
getParseServerConfig(boost::json::value val)
{
    ConfigFileJson const jsonVal{val.as_object()};
    auto config = ClioConfigDefinition{
        {"server.ip", ConfigValue{ConfigType::String}},
        {"server.port", ConfigValue{ConfigType::Integer}},
        {"server.admin_password", ConfigValue{ConfigType::String}.optional()},
        {"server.local_admin", ConfigValue{ConfigType::Boolean}.optional()},
        {"server.proxy.ips.[]", Array{ConfigValue{ConfigType::String}}},
        {"server.proxy.tokens.[]", Array{ConfigValue{ConfigType::String}}},
        {"server.ws_max_sending_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(1500)},
        {"log.tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")},
        {"dos_guard.max_fetches", ConfigValue{ConfigType::Integer}},
        {"dos_guard.sweep_interval", ConfigValue{ConfigType::Integer}},
        {"dos_guard.max_connections", ConfigValue{ConfigType::Integer}},
        {"dos_guard.max_requests", ConfigValue{ConfigType::Integer}},
        {"dos_guard.whitelist.[]", Array{ConfigValue{ConfigType::String}.optional()}},
        {"ssl_key_file", ConfigValue{ConfigType::String}.optional()},
        {"ssl_cert_file", ConfigValue{ConfigType::String}.optional()},
    };
    auto const errors = config.parse(jsonVal);
    [&]() { ASSERT_FALSE(errors.has_value()); }();
    return config;
};

struct WebServerTest : public virtual ::testing::Test {
    ~WebServerTest() override
    {
        work_.reset();
        ctx.stop();
        if (runner_->joinable())
            runner_->join();
    }

    WebServerTest()
    {
        work_.emplace(boost::asio::make_work_guard(ctx));  // make sure ctx does not stop on its own
        runner_.emplace([this] { ctx.run(); });
    }

    boost::json::value
    addSslConfig(boost::json::value config) const
    {
        config.as_object()["ssl_key_file"] = sslKeyFile.path;
        config.as_object()["ssl_cert_file"] = sslCertFile.path;
        return config;
    }

    // this ctx is for dos timer
    boost::asio::io_context ctxSync;
    std::string const port = std::to_string(tests::util::generateFreePort());
    ClioConfigDefinition cfg{getParseServerConfig(generateJSONWithDynamicPort(port))};
    dosguard::WhitelistHandler whitelistHandler{cfg};
    dosguard::Weights dosguardWeights{1, {}};
    dosguard::DOSGuard dosGuard{cfg, whitelistHandler, dosguardWeights};
    dosguard::IntervalSweepHandler sweepHandler{cfg, ctxSync, dosGuard};

    ClioConfigDefinition cfgOverload{getParseServerConfig(generateJSONDataOverload(port))};
    dosguard::WhitelistHandler whitelistHandlerOverload{cfgOverload};
    dosguard::DOSGuard dosGuardOverload{cfgOverload, whitelistHandlerOverload, dosguardWeights};
    dosguard::IntervalSweepHandler sweepHandlerOverload{cfgOverload, ctxSync, dosGuardOverload};
    // this ctx is for http server
    boost::asio::io_context ctx;

    TmpFile sslCertFile{tests::sslCertFile()};
    TmpFile sslKeyFile{tests::sslKeyFile()};

private:
    std::optional<boost::asio::executor_work_guard<boost::asio::io_context::executor_type>> work_;
    std::optional<std::thread> runner_;
};

class EchoExecutor {
public:
    void
    operator()(std::string const& reqStr, std::shared_ptr<web::ConnectionBase> const& ws)
    {
        ws->send(std::string(reqStr), http::status::ok);
    }

    void
    operator()(boost::beast::error_code /* ec */, std::shared_ptr<web::ConnectionBase> const& /* ws */)
    {
    }
};

class ExceptionExecutor {
public:
    void
    operator()(std::string const& /* req */, std::shared_ptr<web::ConnectionBase> const& /* ws */)
    {
        throw std::runtime_error("MyError");
    }

    void
    operator()(boost::beast::error_code /* ec */, std::shared_ptr<web::ConnectionBase> const& /* ws */)
    {
    }
};

namespace {

template <class Executor>
std::shared_ptr<web::HttpServer<Executor>>
makeServerSync(
    util::config::ClioConfigDefinition const& config,
    boost::asio::io_context& ioc,
    web::dosguard::DOSGuardInterface& dosGuard,
    std::shared_ptr<Executor> const& handler,
    std::reference_wrapper<data::LedgerCacheInterface const> cache
)
{
    return web::makeHttpServer(config, ioc, dosGuard, handler, cache);
}

}  // namespace

TEST_F(WebServerTest, Http)
{
    auto cache = MockLedgerCache();
    auto const e = std::make_shared<EchoExecutor>();
    auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
    auto const [status, res] = HttpSyncClient::post("localhost", port, R"JSON({"Hello":1})JSON");
    EXPECT_EQ(res, R"JSON({"Hello":1})JSON");
    EXPECT_EQ(status, boost::beast::http::status::ok);
}

TEST_F(WebServerTest, Ws)
{
    auto cache = MockLedgerCache();
    auto e = std::make_shared<EchoExecutor>();
    auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
    WebSocketSyncClient wsClient;
    wsClient.connect("localhost", port);
    auto const res = wsClient.syncPost(R"JSON({"Hello":1})JSON");
    EXPECT_EQ(res, R"JSON({"Hello":1})JSON");
    wsClient.disconnect();
}

TEST_F(WebServerTest, HttpInternalError)
{
    auto cache = MockLedgerCache();
    auto const e = std::make_shared<ExceptionExecutor>();
    auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
    auto const [status, res] = HttpSyncClient::post("localhost", port, R"JSON({})JSON");
    EXPECT_EQ(
        res,
        R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response"})JSON"
    );
    EXPECT_EQ(status, boost::beast::http::status::internal_server_error);
}

TEST_F(WebServerTest, WsInternalError)
{
    auto cache = MockLedgerCache();
    auto e = std::make_shared<ExceptionExecutor>();
    auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
    WebSocketSyncClient wsClient;
    wsClient.connect("localhost", port);
    auto const res = wsClient.syncPost(R"JSON({"id":"id1"})JSON");
    wsClient.disconnect();
    EXPECT_EQ(
        res,
        R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","id":"id1","request":{"id":"id1"}})JSON"
    );
}

TEST_F(WebServerTest, WsInternalErrorNotJson)
{
    auto cache = MockLedgerCache();
    auto e = std::make_shared<ExceptionExecutor>();
    auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
    WebSocketSyncClient wsClient;
    wsClient.connect("localhost", port);
    auto const res = wsClient.syncPost("not json");
    wsClient.disconnect();
    EXPECT_EQ(
        res,
        R"JSON({"error":"internal","error_code":73,"error_message":"Internal error.","status":"error","type":"response","request":"not json"})JSON"
    );
}

TEST_F(WebServerTest, IncompleteSslConfig)
{
    auto const e = std::make_shared<EchoExecutor>();

    auto jsonConfig = generateJSONWithDynamicPort(port);
    jsonConfig.as_object()["ssl_key_file"] = sslKeyFile.path;

    auto cache = MockLedgerCache();
    auto const server = makeServerSync(getParseServerConfig(jsonConfig), ctx, dosGuard, e, cache);
    EXPECT_EQ(server, nullptr);
}

TEST_F(WebServerTest, WrongSslConfig)
{
    auto const e = std::make_shared<EchoExecutor>();

    auto jsonConfig = generateJSONWithDynamicPort(port);
    jsonConfig.as_object()["ssl_key_file"] = sslKeyFile.path;
    jsonConfig.as_object()["ssl_cert_file"] = "wrong_path";

    auto cache = MockLedgerCache();
    auto const server = makeServerSync(getParseServerConfig(jsonConfig), ctx, dosGuard, e, cache);
    EXPECT_EQ(server, nullptr);
}

TEST_F(WebServerTest, Https)
{
    auto cache = MockLedgerCache();
    auto const e = std::make_shared<EchoExecutor>();
    cfg = getParseServerConfig(addSslConfig(generateJSONWithDynamicPort(port)));
    auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
    auto const res = HttpsSyncClient::syncPost("localhost", port, R"JSON({"Hello":1})JSON");
    EXPECT_EQ(res, R"JSON({"Hello":1})JSON");
}

TEST_F(WebServerTest, Wss)
{
    auto cache = MockLedgerCache();
    auto e = std::make_shared<EchoExecutor>();
    cfg = getParseServerConfig(addSslConfig(generateJSONWithDynamicPort(port)));
    auto server = makeServerSync(cfg, ctx, dosGuard, e, cache);
    WebServerSslSyncClient wsClient;
    wsClient.connect("localhost", port);
    auto const res = wsClient.syncPost(R"JSON({"Hello":1})JSON");
    EXPECT_EQ(res, R"JSON({"Hello":1})JSON");
    wsClient.disconnect();
}

TEST_F(WebServerTest, HttpPayloadOverload)
{
    std::string const s100(100, 'a');
    auto cache = MockLedgerCache();
    auto const e = std::make_shared<EchoExecutor>();
    auto server = makeServerSync(cfg, ctx, dosGuardOverload, e, cache);
    auto const [status, res] =
        HttpSyncClient::post("localhost", port, fmt::format(R"JSON({{"payload":"{}"}})JSON", s100));
    EXPECT_EQ(
        res,
        R"JSON({"payload":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","warning":"load","warnings":[{"id":2003,"message":"You are about to be rate limited"}]})JSON"
    );
    EXPECT_EQ(status, boost::beast::http::status::ok);
}

TEST_F(WebServerTest, WsPayloadOverload)
{
    std::string const s100(100, 'a');
    auto cache = MockLedgerCache();
    auto const e = std::make_shared<EchoExecutor>();
    auto server = makeServerSync(cfg, ctx, dosGuardOverload, e, cache);
    WebSocketSyncClient wsClient;
    wsClient.connect("localhost", port);
    auto const res = wsClient.syncPost(fmt::format(R"JSON({{"payload":"{}"}})JSON", s100));
    wsClient.disconnect();
    EXPECT_EQ(
        res,
        R"JSON({"payload":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","warning":"load","warnings":[{"id":2003,"message":"You are about to be rate limited"}]})JSON"
    );
}

TEST_F(WebServerTest, WsTooManyConnection)
{
    auto cache = MockLedgerCache();
    auto const e = std::make_shared<EchoExecutor>();
    auto server = makeServerSync(cfg, ctx, dosGuardOverload, e, cache);
    // max connection is 2, exception should happen when the third connection is made
    WebSocketSyncClient wsClient1;
    wsClient1.connect("localhost", port);
    WebSocketSyncClient wsClient2;
    wsClient2.connect("localhost", port);
    bool exceptionThrown = false;
    try {
        WebSocketSyncClient wsClient3;
        wsClient3.connect("localhost", port);
    } catch (boost::system::system_error const& ex) {
        exceptionThrown = true;
        EXPECT_EQ(ex.code(), boost::beast::websocket::error::upgrade_declined);
    }
    wsClient1.disconnect();
    wsClient2.disconnect();
    EXPECT_TRUE(exceptionThrown);
}

TEST_F(WebServerTest, HealthCheck)
{
    auto cache = MockLedgerCache();
    auto e = std::make_shared<ExceptionExecutor>();  // request handled before we get to executor
    auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
    auto const [status, res] = HttpSyncClient::get("localhost", port, "", "/health");

    EXPECT_FALSE(res.empty());
    EXPECT_EQ(status, boost::beast::http::status::ok);
}

TEST_F(WebServerTest, CacheStateCheckWithLoadedCache)
{
    auto cache = MockLedgerCache();
    EXPECT_CALL(cache, isFull()).WillRepeatedly(testing::Return(true));

    auto e = std::make_shared<ExceptionExecutor>();  // request handled before we get to executor
    auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
    auto const [status, res] = HttpSyncClient::get("localhost", port, "", "/cache_state");

    EXPECT_FALSE(res.empty());
    EXPECT_EQ(status, boost::beast::http::status::ok);
}

TEST_F(WebServerTest, CacheStateCheckWithoutLoadedCache)
{
    auto cache = MockLedgerCache();
    EXPECT_CALL(cache, isFull()).WillRepeatedly(testing::Return(false));

    auto e = std::make_shared<ExceptionExecutor>();  // request handled before we get to executor
    auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
    auto const [status, res] = HttpSyncClient::get("localhost", port, "", "/cache_state");

    EXPECT_FALSE(res.empty());
    EXPECT_EQ(status, boost::beast::http::status::service_unavailable);
}

TEST_F(WebServerTest, GetOtherThanHealthCheck)
{
    auto cache = MockLedgerCache();
    auto e = std::make_shared<ExceptionExecutor>();  // request handled before we get to executor
    auto const server = makeServerSync(cfg, ctx, dosGuard, e, cache);
    auto const [status, res] = HttpSyncClient::get("localhost", port, "", "/");

    EXPECT_FALSE(res.empty());
    EXPECT_EQ(status, boost::beast::http::status::bad_request);
}

namespace {

std::string
jsonServerConfigWithAdminPassword(uint32_t const port)
{
    return fmt::format(
        R"JSON({{
            "server": {{
                "ip": "0.0.0.0",
                "port": {},
                "admin_password": "secret"
            }}
        }})JSON",
        port
    );
}

std::string
jsonServerConfigWithLocalAdmin(uint32_t const port)
{
    return fmt::format(
        R"JSON({{
            "server": {{
                "ip": "0.0.0.0",
                "port": {},
                "local_admin": true
            }}
        }})JSON",
        port
    );
}

std::string
jsonServerConfigWithBothAdminPasswordAndLocalAdminFalse(uint32_t const port)
{
    return fmt::format(
        R"JSON({{
            "server": {{
                "ip": "0.0.0.0",
                "port": {},
                "admin_password": "secret",
                "local_admin": false
            }}
        }})JSON",
        port
    );
}

std::string
jsonServerConfigWithNoSpecifiedAdmin(uint32_t const port)
{
    return fmt::format(
        R"JSON({{
            "server": {{
                "ip": "0.0.0.0",
                "port": {}
            }}
        }})JSON",
        port
    );
}

// get this value from online sha256 generator
constexpr auto kSECRET_SHA256 = "2bb80d537b1da3e38bd30361aa855686bde0eacd7162fef6a25fe97bf527a25b";

}  // namespace

class AdminCheckExecutor {
public:
    void
    operator()(std::string const& reqStr, std::shared_ptr<web::ConnectionBase> const& ws)
    {
        auto response = fmt::format("{} {}", reqStr, ws->isAdmin() ? "admin" : "user");
        ws->send(std::move(response), http::status::ok);
    }

    void
    operator()(boost::beast::error_code /* ec */, std::shared_ptr<web::ConnectionBase> const& /* ws */)
    {
    }
};

struct WebServerAdminTestParams {
    std::string config;
    std::vector<WebHeader> headers;
    std::string expectedResponse;
};

inline static ClioConfigDefinition
getParseAdminServerConfig(boost::json::value val)
{
    ConfigFileJson const jsonVal{val.as_object()};
    auto config = ClioConfigDefinition{
        {"server.ip", ConfigValue{ConfigType::String}},
        {"server.port", ConfigValue{ConfigType::Integer}},
        {"server.admin_password", ConfigValue{ConfigType::String}.optional()},
        {"server.local_admin", ConfigValue{ConfigType::Boolean}.optional()},
        {"server.processing_policy", ConfigValue{ConfigType::String}.defaultValue("parallel")},
        {"server.proxy.ips.[]", Array{ConfigValue{ConfigType::String}}},
        {"server.proxy.tokens.[]", Array{ConfigValue{ConfigType::String}}},
        {"server.parallel_requests_limit", ConfigValue{ConfigType::Integer}.optional()},
        {"server.ws_max_sending_queue_size", ConfigValue{ConfigType::Integer}.defaultValue(1500)},
        {"ssl_cert_file", ConfigValue{ConfigType::String}.optional()},
        {"ssl_key_file", ConfigValue{ConfigType::String}.optional()},
        {"prometheus.enabled", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
        {"prometheus.compress_reply", ConfigValue{ConfigType::Boolean}.defaultValue(true)},
        {"log.tag_style", ConfigValue{ConfigType::String}.defaultValue("uint")}
    };
    auto const errors = config.parse(jsonVal);
    [&]() { ASSERT_FALSE(errors.has_value()); }();
    return config;
};

class WebServerAdminTest : public WebServerTest, public ::testing::WithParamInterface<WebServerAdminTestParams> {};

TEST_P(WebServerAdminTest, WsAdminCheck)
{
    auto cache = MockLedgerCache();
    auto e = std::make_shared<AdminCheckExecutor>();
    ClioConfigDefinition const serverConfig{getParseAdminServerConfig(boost::json::parse(GetParam().config))};
    auto server = makeServerSync(serverConfig, ctx, dosGuardOverload, e, cache);
    WebSocketSyncClient wsClient;
    uint32_t const webServerPort = serverConfig.get<uint32_t>("server.port");
    wsClient.connect("localhost", std::to_string(webServerPort), GetParam().headers);
    std::string const request = "Why hello";
    auto const res = wsClient.syncPost(request);
    wsClient.disconnect();
    EXPECT_EQ(res, fmt::format("{} {}", request, GetParam().expectedResponse));
}

TEST_P(WebServerAdminTest, HttpAdminCheck)
{
    auto cache = MockLedgerCache();
    auto const e = std::make_shared<AdminCheckExecutor>();
    ClioConfigDefinition const serverConfig{getParseAdminServerConfig(boost::json::parse(GetParam().config))};
    auto server = makeServerSync(serverConfig, ctx, dosGuardOverload, e, cache);
    std::string const request = "Why hello";
    uint32_t const webServerPort = serverConfig.get<uint32_t>("server.port");
    auto const [status, res] =
        HttpSyncClient::post("localhost", std::to_string(webServerPort), request, GetParam().headers);

    EXPECT_EQ(res, fmt::format("{} {}", request, GetParam().expectedResponse));
    EXPECT_EQ(status, boost::beast::http::status::ok);
}

INSTANTIATE_TEST_CASE_P(
    WebServerAdminTestsSuit,
    WebServerAdminTest,
    ::testing::Values(
        WebServerAdminTestParams{
            .config = jsonServerConfigWithAdminPassword(tests::util::generateFreePort()),
            .headers = {},
            .expectedResponse = "user"
        },
        WebServerAdminTestParams{
            .config = jsonServerConfigWithAdminPassword(tests::util::generateFreePort()),
            .headers = {WebHeader(http::field::authorization, "")},
            .expectedResponse = "user"
        },
        WebServerAdminTestParams{
            .config = jsonServerConfigWithAdminPassword(tests::util::generateFreePort()),
            .headers = {WebHeader(http::field::authorization, "s")},
            .expectedResponse = "user"
        },
        WebServerAdminTestParams{
            .config = jsonServerConfigWithAdminPassword(tests::util::generateFreePort()),
            .headers = {WebHeader(http::field::authorization, kSECRET_SHA256)},
            .expectedResponse = "user"
        },
        WebServerAdminTestParams{
            .config = jsonServerConfigWithAdminPassword(tests::util::generateFreePort()),
            .headers = {WebHeader(
                http::field::authorization,
                fmt::format("{}{}", PasswordAdminVerificationStrategy::kPASSWORD_PREFIX, kSECRET_SHA256)
            )},
            .expectedResponse = "admin"
        },
        WebServerAdminTestParams{
            .config = jsonServerConfigWithBothAdminPasswordAndLocalAdminFalse(tests::util::generateFreePort()),
            .headers = {WebHeader(http::field::authorization, kSECRET_SHA256)},
            .expectedResponse = "user"
        },
        WebServerAdminTestParams{
            .config = jsonServerConfigWithBothAdminPasswordAndLocalAdminFalse(tests::util::generateFreePort()),
            .headers = {WebHeader(
                http::field::authorization,
                fmt::format("{}{}", PasswordAdminVerificationStrategy::kPASSWORD_PREFIX, kSECRET_SHA256)
            )},
            .expectedResponse = "admin"
        },
        WebServerAdminTestParams{
            .config = jsonServerConfigWithAdminPassword(tests::util::generateFreePort()),
            .headers = {WebHeader(
                http::field::authentication_info,
                fmt::format("{}{}", PasswordAdminVerificationStrategy::kPASSWORD_PREFIX, kSECRET_SHA256)
            )},
            .expectedResponse = "user"
        },
        WebServerAdminTestParams{
            .config = jsonServerConfigWithLocalAdmin(tests::util::generateFreePort()),
            .headers = {},
            .expectedResponse = "admin"
        },
        WebServerAdminTestParams{
            .config = jsonServerConfigWithNoSpecifiedAdmin(tests::util::generateFreePort()),
            .headers = {},
            .expectedResponse = "admin"
        }

    )
);

TEST_F(WebServerTest, AdminErrorCfgTestBothAdminPasswordAndLocalAdminSet)
{
    uint32_t webServerPort = tests::util::generateFreePort();
    std::string const jsonServerConfigWithBothAdminPasswordAndLocalAdmin = fmt::format(
        R"JSON({{
        "server":{{
                "ip": "0.0.0.0",
                "port": {},
                "admin_password": "secret",
                "local_admin": true
            }}
    }})JSON",
        webServerPort
    );

    auto const e = std::make_shared<AdminCheckExecutor>();
    ClioConfigDefinition const serverConfig{
        getParseAdminServerConfig(boost::json::parse(jsonServerConfigWithBothAdminPasswordAndLocalAdmin))
    };

    MockLedgerCache cache;
    EXPECT_THROW(web::makeHttpServer(serverConfig, ctx, dosGuardOverload, e, cache), std::logic_error);
}

TEST_F(WebServerTest, AdminErrorCfgTestBothAdminPasswordAndLocalAdminFalse)
{
    uint32_t webServerPort = tests::util::generateFreePort();
    std::string const jsonServerConfigWithNoAdminPasswordAndLocalAdminFalse = fmt::format(
        R"JSON({{
        "server": {{
            "ip": "0.0.0.0",
            "port": {},
            "local_admin": false
        }}
    }})JSON",
        webServerPort
    );

    auto const e = std::make_shared<AdminCheckExecutor>();
    ClioConfigDefinition const serverConfig{
        getParseAdminServerConfig(boost::json::parse(jsonServerConfigWithNoAdminPasswordAndLocalAdminFalse))
    };

    MockLedgerCache cache;
    EXPECT_THROW(web::makeHttpServer(serverConfig, ctx, dosGuardOverload, e, cache), std::logic_error);
}

struct WebServerPrometheusTest : util::prometheus::WithPrometheus, WebServerTest {};

TEST_F(WebServerPrometheusTest, rejectedWithoutAdminPassword)
{
    auto cache = MockLedgerCache();
    auto const e = std::make_shared<EchoExecutor>();
    uint32_t const webServerPort = tests::util::generateFreePort();
    ClioConfigDefinition const serverConfig{
        getParseAdminServerConfig(boost::json::parse(jsonServerConfigWithAdminPassword(webServerPort)))
    };
    auto server = makeServerSync(serverConfig, ctx, dosGuard, e, cache);
    auto const [status, res] = HttpSyncClient::get("localhost", std::to_string(webServerPort), "", "/metrics");

    EXPECT_EQ(res, "Only admin is allowed to collect metrics");
    EXPECT_EQ(status, boost::beast::http::status::unauthorized);
}

struct WebServerPrometheusDisabledTest : util::prometheus::WithPrometheusDisabled, WebServerTest {};

TEST_F(WebServerPrometheusDisabledTest, rejectedIfPrometheusIsDisabled)
{
    uint32_t webServerPort = tests::util::generateFreePort();
    std::string const jsonServerConfigWithDisabledPrometheus = fmt::format(
        R"JSON({{
        "server":{{
                "ip": "0.0.0.0",
                "port": {},
                "admin_password": "secret",
                "ws_max_sending_queue_size": 1500
            }}
    }})JSON",
        webServerPort
    );

    auto cache = MockLedgerCache();
    auto const e = std::make_shared<EchoExecutor>();
    ClioConfigDefinition const serverConfig{
        getParseAdminServerConfig(boost::json::parse(jsonServerConfigWithDisabledPrometheus))
    };
    auto server = makeServerSync(serverConfig, ctx, dosGuard, e, cache);
    auto const [status, res] = HttpSyncClient::get(
        "localhost",
        std::to_string(webServerPort),
        "",
        "/metrics",
        {WebHeader(
            http::field::authorization,
            fmt::format("{}{}", PasswordAdminVerificationStrategy::kPASSWORD_PREFIX, kSECRET_SHA256)
        )}
    );
    EXPECT_EQ(res, "Prometheus is disabled in clio config");
    EXPECT_EQ(status, boost::beast::http::status::forbidden);
}

TEST_F(WebServerPrometheusTest, validResponse)
{
    auto cache = MockLedgerCache();
    uint32_t const webServerPort = tests::util::generateFreePort();
    auto& testCounter = PrometheusService::counterInt("test_counter", util::prometheus::Labels());
    ++testCounter;
    auto const e = std::make_shared<EchoExecutor>();
    ClioConfigDefinition const serverConfig{
        getParseAdminServerConfig(boost::json::parse(jsonServerConfigWithAdminPassword(webServerPort)))
    };
    auto server = makeServerSync(serverConfig, ctx, dosGuard, e, cache);
    auto const [status, res] = HttpSyncClient::get(
        "localhost",
        std::to_string(webServerPort),
        "",
        "/metrics",
        {WebHeader(
            http::field::authorization,
            fmt::format("{}{}", PasswordAdminVerificationStrategy::kPASSWORD_PREFIX, kSECRET_SHA256)
        )}
    );
    EXPECT_EQ(res, "# TYPE test_counter counter\ntest_counter 1\n\n");
    EXPECT_EQ(status, boost::beast::http::status::ok);
}
